extends Node2D # Forgejo #80: on a computer the mouse aims and the left button fires, and the # number keys choose which weapon the click uses - 1 the fleet's own guns, 2 # and up the special abilities. # # The twin sticks are built for thumbs. On a desktop they mean dragging a # virtual stick with a pointer that could simply BE the crosshair, so this # offers the shape a mouse actually wants: point, click, hold to keep firing. # # Touch is unaffected, and deliberately so: Godot emulates a mouse button from # every touch, so without a guard a tap anywhere on a phone would fire the # guns. # # #171 changed what that guard asks. It used to be the presence of a # touchscreen, which fails safe but fails often: a desktop browser reporting a # touch-capable panel switched mouse aiming off on exactly the machine it was # written for. It now asks what the player last actually used # (GameState.pointer_is_touch), which still fails safe - a tap makes the # reading "touch" before the emulated click arrives, so a phone can never fire # through this path - and stops being wrong about desktops. # A literal, not PackedStringArray(...): a constructor call is not a constant # expression, and the script fails to PARSE rather than to run - which shows up # as "nonexistent function" on a node that looks fine in the scene. const ABILITY_ORDER: PackedStringArray = ["GasBombController", "LaserController", "DashController"] @onready var fleet: Node2D = get_node("../Fleet") @onready var camera: Camera2D = get_node("../Camera2D") @onready var fire_stick_controller: Node = get_node_or_null("../FireStickController") # 0 is the fleet's own guns; 1.. index into `abilities` below. var selected_weapon: int = 0 var firing: bool = false var aim_world: Vector2 = Vector2.ZERO var abilities: Array = [] func _ready() -> void: # Resolved by NAME in a fixed order, so "2" is the gas bomb on every # scene and in every session. Discovering them by scanning children would # make the number keys depend on scene node order, which is not something # a player can see or rely on. for node_name in ABILITY_ORDER: var controller: Node = get_node_or_null("../%s" % node_name) if controller != null: abilities.append(controller) # AUTO follows the last input the player actually used, which is what every # scene ships with. The explicit settings exist because that reading is seeded # from the display in an environment - headless - where the answer varies by # platform, so with AUTO alone the aiming below could never be exercised by a # test at all, and the only covered behaviour would be the guard itself. enum PointerMode { AUTO, ON, OFF } @export var pointer_mode: PointerMode = PointerMode.AUTO func is_enabled() -> bool: match pointer_mode: PointerMode.ON: return true PointerMode.OFF: return false return not GameState.pointer_is_touch func weapon_count() -> int: return abilities.size() + 1 # Which weapon a given number key selects. Public so a test can ask rather # than hardcode the mapping a second time. func select_weapon(index: int) -> bool: if index < 0 or index >= weapon_count(): return false selected_weapon = index _refresh_highlight() return true func _refresh_highlight() -> void: # The chosen ability's own button carries the selection, so the answer to # "what will a click do" is on screen rather than remembered. for i in range(abilities.size()): var button: Node = abilities[i].get("button") if button != null: button.highlighted = selected_weapon == i + 1 button.queue_redraw() func _unhandled_input(event: InputEvent) -> void: if not is_enabled(): return if event is InputEventMouseMotion: aim_world = get_global_mouse_position() return if event is InputEventKey and event.pressed and not event.echo: var index: int = event.keycode - KEY_1 if index >= 0 and index < weapon_count() and select_weapon(index): get_viewport().set_input_as_handled() return if not (event is InputEventMouseButton) or event.button_index != MOUSE_BUTTON_LEFT: return aim_world = get_global_mouse_position() firing = event.pressed if not event.pressed: return get_viewport().set_input_as_handled() if selected_weapon > 0: # One shot per click. Abilities have their own cooldowns and costs; # holding the button down must not queue them up. abilities[selected_weapon - 1].fire_at(aim_world) func _process(_delta: float) -> void: if fire_stick_controller == null: return # Handed to the stick controller rather than driving the weapons here, so # there is still exactly one thing in the scene that aims a gun. The stick # wins when both are live - a finger on it is a deliberate act, while a # mouse merely resting somewhere is not. # # #172: the cursor's world position goes across as a PLACE. It always was # one; it used to be flattened into a heading from the fleet's centre on # the way out, which threw away the distance and made every hull fire # parallel past the thing being clicked on. fire_stick_controller.has_external_target = is_enabled() and firing \ and selected_weapon == 0 and not fleet.ships.is_empty() fire_stick_controller.external_target = aim_world