## BombCarrier — Client-side bomb status UI and prompts. ## ## Attached to the local T player who carries the bomb. ## Provides visual feedback for: ## - Bomb status (planted/defusing/timer) ## - Direction indicator to planted bomb ## - "Hold E to defuse" prompt (CT, near bomb) ## - "Hold E to plant" prompt (T, in bombsite, LIVE phase) ## ## This script is a Node that should be added as a child of the local ## player's character node (FPSCharacterController) on the client side. ## It communicates with the server BombObjective via RPC or state sync. extends Node class_name BombCarrier # --------------------------------------------------------------------------- # Signals # --------------------------------------------------------------------------- ## Emitted when the nearby interaction prompt changes. signal prompt_changed(prompt_text: String, visible: bool) ## Emitted when bomb status UI should update. signal bomb_status_updated(planted: bool, timer: float, defusing: bool, defuse_progress: float) ## Emitted when the bomb direction indicator should update. signal bomb_direction_updated(bearing_degrees: float, distance: float) # --------------------------------------------------------------------------- # Exports # --------------------------------------------------------------------------- ## The "Use/Interact" action name (matched in Input Map). @export var interact_action: String = "interact" ## Interaction distance for planting (in Godot units). @export var plant_distance: float = 2.0 ## Interaction distance for defusing (in Godot units). @export var defuse_distance: float = 3.0 # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- ## Whether this player has the bomb (T-side bomb carrier). var has_bomb: bool = false ## Whether the bomb is currently planted. var _bomb_planted: bool = false ## Position of the planted bomb (server-authoritative). var _bomb_position: Vector3 = Vector3.ZERO ## Remaining bomb timer (received from server). var _bomb_timer: float = 0.0 ## Whether the local player is currently defusing. var _is_defusing: bool = false ## Current defuse progress (0.0 to 1.0). var _defuse_progress: float = 0.0 ## Current prompt text to show. var _current_prompt: String = "" ## Whether the prompt is currently visible. var _prompt_visible: bool = false ## Whether we are inside a bombsite. var _in_bombsite: bool = false ## Whether we are near the planted bomb. var _near_planted_bomb: bool = false ## Whether the interact key is being held. var _interact_held: bool = false ## Hold-down timer for plant/defuse (E key held long enough). var _interact_hold_time: float = 0.0 ## Time required to hold E to start action (for UX consistency). var _interact_hold_duration: float = 0.0 # Immediate on press ## Reference to the local player's parent/character controller. var _character_controller: Node = null ## Reference to the bomb objective on the server (for RPC calls). ## Set by the game manager after instantiation. var bomb_objective_path: NodePath = NodePath() # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: _character_controller = get_parent() if _character_controller == null: push_warning("[BombCarrier] No parent node — prompts will not work") # Try to set up the interact action if it doesn't exist if not InputMap.has_action(interact_action): var event := InputEventKey.new() event.keycode = KEY_E InputMap.add_action(interact_action) InputMap.action_add_event(interact_action, event) print("[BombCarrier] Initialised — has_bomb=%s" % has_bomb) func _process(delta: float) -> void: _update_interaction(delta) _emit_status_updates() # --------------------------------------------------------------------------- # Public API — Server state sync # --------------------------------------------------------------------------- ## Called by the game manager or network sync to update bomb state. func sync_bomb_state(planted: bool, position: Vector3, timer: float, defusing: bool, defuse_progress: float) -> void: _bomb_planted = planted _bomb_position = position _bomb_timer = timer _is_defusing = defusing _defuse_progress = defuse_progress ## Set whether this player has the bomb (assigned by server). func set_has_bomb(value: bool) -> void: has_bomb = value print("[BombCarrier] Has bomb: %s" % value) ## Set whether the local player is inside a bombsite Area3D. ## Called by the bombsite Area3D signal handlers on the client. func set_in_bombsite(value: bool) -> void: _in_bombsite = value ## Set whether the local player is near the planted bomb. ## Called by the bomb area proximity check. func set_near_planted_bomb(value: bool) -> void: _near_planted_bomb = value # --------------------------------------------------------------------------- # Public API — RPC calls to server # --------------------------------------------------------------------------- ## Request to plant the bomb at the current position. ## Called when the T player presses E inside a bombsite with the bomb. func request_plant() -> void: if not has_bomb or not _in_bombsite: return if _bomb_planted: return # Send plant request to server # In real implementation, this would be an RPC call: # _send_plant_request.rpc_id(1, _character_controller.global_position) _send_plant_request(_character_controller.global_position) ## Request to start defusing the bomb. ## Called when the CT player presses E near the planted bomb. func request_defuse() -> void: if not _near_planted_bomb or not _bomb_planted: return if _is_defusing: return # Send defuse request to server # _send_defuse_request.rpc_id(1) _send_defuse_request() ## Request to cancel defuse (when player moves or is interrupted). func request_cancel_defuse() -> void: if not _is_defusing: return _send_cancel_defuse() # --------------------------------------------------------------------------- # RPC stubs (to be replaced with actual RPC calls) # --------------------------------------------------------------------------- func _send_plant_request(position: Vector3) -> void: # Placeholder: in real implementation, send RPC to server print("[BombCarrier] Plant request at (%.1f, %.1f, %.1f)" % [position.x, position.y, position.z]) func _send_defuse_request() -> void: # Placeholder: in real implementation, send RPC to server print("[BombCarrier] Defuse request") func _send_cancel_defuse() -> void: # Placeholder: in real implementation, send RPC to server print("[BombCarrier] Cancel defuse request") # --------------------------------------------------------------------------- # Internal — Interaction logic # --------------------------------------------------------------------------- func _update_interaction(delta: float) -> void: var interact_pressed: bool = Input.is_action_pressed(interact_action) var prompt_text: String = "" var show_prompt: bool = false # Determine what prompt to show if _bomb_planted and _near_planted_bomb and not _is_defusing: # CT player near planted bomb — defuse prompt var team: int = _get_local_player_team() if team == TeamData.Team.COUNTER_TERRORIST: prompt_text = "Hold E to defuse" show_prompt = true elif has_bomb and _in_bombsite and not _bomb_planted: # T player with bomb inside bombsite — plant prompt if _is_round_live(): prompt_text = "Hold E to plant" show_prompt = true elif _bomb_planted and _is_defusing: # Currently defusing — show progress var progress_pct: int = int(_defuse_progress * 100.0) prompt_text = "Defusing... %d%%" % progress_pct show_prompt = true # Handle interact hold if interact_pressed and show_prompt: if not _interact_held: _interact_held = true _interact_hold_time = 0.0 _interact_hold_time += delta # Check if we should trigger the action if _interact_hold_time >= _interact_hold_duration: _interact_hold_time = 0.0 if prompt_text.contains("defuse"): request_defuse() elif prompt_text.contains("plant"): request_plant() else: # Cancel any pending interaction if _interact_held: _interact_held = false _interact_hold_time = 0.0 # If we were defusing and released, cancel if _is_defusing: request_cancel_defuse() # Update prompt if changed if prompt_text != _current_prompt or show_prompt != _prompt_visible: _current_prompt = prompt_text _prompt_visible = show_prompt prompt_changed.emit(prompt_text, show_prompt) func _emit_status_updates() -> void: # Emit bomb status updates (rate-limited is handled by engine) bomb_status_updated.emit(_bomb_planted, _bomb_timer, _is_defusing, _defuse_progress) # Emit direction indicator if bomb is planted if _bomb_planted and _character_controller != null: var player_pos: Vector3 = _character_controller.global_position var direction: Vector3 = _bomb_position - player_pos var distance: float = direction.length() if distance > 0.01: var bearing: float = atan2(direction.x, -direction.z) # Relative to forward (Z) bomb_direction_updated.emit(rad_to_deg(bearing), distance) else: bomb_direction_updated.emit(0.0, 0.0) # --------------------------------------------------------------------------- # Internal — Helpers # --------------------------------------------------------------------------- ## Get the local player's team (from TeamManager singleton if available). func _get_local_player_team() -> int: var team_manager: TeamManager = get_node_or_null("/root/TeamManager") if team_manager: var peer_id: int = multiplayer.get_unique_id() if multiplayer.has_method(&\"get_unique_id\") else -1 if peer_id >= 0: return team_manager.get_player_team(peer_id) return -1 ## Check if the current round is in LIVE phase. func _is_round_live() -> bool: var round_manager: RoundManager = get_node_or_null("/root/RoundManager") if round_manager: return round_manager.get_phase() == RoundManager.RoundPhase.LIVE return false ## Get the multiplayer API (stub for compatibility). func get_multiplayer() -> MultiplayerAPI: if multiplayer and multiplayer.has_method(&\"get_unique_id\"): return multiplayer return null