spaceship/controllers/FireStickController.gd

101 lines
3.8 KiB
GDScript3
Raw Normal View History

Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring The fleet's guns picked their own targets and fired on their own, so combat happened whether or not the player did anything - there was nothing to aim and nothing to decide, and the right half of the screen held nothing at all. Firing is now an action. VesselWeaponComponent no longer seeks a target; it shoots along a heading, and the only thing that supplies one is the new right-hand stick via FireStickController. A fleet arriving from another scene starts with its guns silent, which is the correct resting state. A subtle dashed guide runs from the fleet along the aimed heading while the stick is held, out to the guns' OWN range - read from a weapon that is actually aboard, so the line can never promise reach the fleet does not have. Layout: the two sticks take the bottom corners, the pivot pair is centred between them at the bottom edge, and the laser and gas bomb sit on an arc up and left of the fire stick where a thumb already is. Those four are now round - IconButton grew a round_frame mode, and the in-world action controls use it while panel and menu buttons stay square, so the two kinds of button read as different kinds of thing. Worth recording, because it cost a screenshot to find: a script-declared property set BEFORE the `script = ExtResource(...)` line in a .tscn is silently dropped. Godot's parser has no idea the property exists yet. The round frames were all correctly set in both scenes and none of them applied. Also fixed researtch_lab_garrison.tres, which was authored under the void profile - that is the black hole surface, so a lab garrison was spawning on black holes. Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds the real stick toward the enemy rather than poking weapon components, so it exercises the path the player uses. test_fire_stick is new and covers the change itself: an idle stick leaves a target parked well inside range untouched, which is precisely what the old gun would never have done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
extends Node2D
# Points every armed vessel's gun wherever the right-hand stick is pushed.
#
# The fleet's guns used to seek their own targets and fire on their own. That
# made combat something that happened TO the player rather than something they
# did, and left the right half of the screen with nothing on it. Aiming is now
# the second stick, and this is the only thing that tells a weapon to fire.
#
# Deliberately a scene node rather than state on the fleet or in GameState:
# it is input plumbing, it belongs to whichever scene is showing the stick,
# and a fleet that moves between scenes should arrive with its guns silent
# until the local stick says otherwise.
# The aim guide: a dotted line from the fleet along the heading being fired,
# out to the guns' own range. Deliberately faint and broken rather than a
# solid beam - it has to answer "where will this go and how far" at a glance
# without competing with the shots themselves for attention.
const GUIDE_DASH: float = 14.0
const GUIDE_GAP: float = 16.0
const GUIDE_WIDTH: float = 2.0
const GUIDE_COLOR: Color = Color(1.0, 0.55, 0.38, 0.28)
@onready var fleet: Node2D = get_node("../Fleet")
# Optional so a scene without a fire stick (a preview, a test harness) still
# runs - the guns simply stay quiet, which is the correct resting state.
@onready var stick: Node = get_node_or_null("../UI/FireStick")
var aim_direction: Vector2 = Vector2.ZERO
Implement #80, #81, #82 and #83 #80 - desktop combat. The mouse aims, the left button fires, and the number keys pick the weapon: 1 the fleet's guns, 2 and up the 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. The pointer path hands its heading to FireStickController rather than driving weapons itself, so exactly one thing in the scene aims a gun, and a held stick wins - a finger on it is a deliberate act, a mouse resting somewhere is not. Abilities gained fire_at(), which aims and fires in one call instead of pretending to be a press-drag-release touch. It is gated on the absence of a touchscreen, because Godot emulates a mouse button from every touch and without that a tap on a phone would fire the guns. The gate has an explicit override, and not for tidiness: headless reports a touchscreen, so with the automatic answer alone the aiming could never be exercised by a test and the only covered behaviour would be the guard. #81 - the bottom-left control hint is gone, from both scenes and from GameController. #82 - a chaser is destroyed by the impact when it reaches a vessel. It used to pass through the wreck untouched and line up another pass, which made a ramming enemy strictly better than any other. Data-driven per enemy (dies_on_contact), because a sniper that happens to drift into a ship has not spent itself doing it - and routed through take_damage, so it dies exactly as it dies to gunfire. The enemy is told about the impact BEFORE the ship resolves its own damage, since resolving it can free the ship. #83 - the research tree's buy button is gone; tapping a node twice buys it. Deliberately "the same node twice" rather than a timed double-tap: no window to miss on a slow phone, no accidental purchase while browsing, and the first tap always does something visible, so the second is never a blind commitment. The status line now says "Tap again to research" - with the button gone it is the only thing distinguishing an affordable node from an unaffordable one. test_enemy_collision asserted the exact behaviour #82 reversed, and now checks the ram costs both. Suite 117/117. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:49:47 +02:00
# Set by MouseCombatController (#80) when a desktop player holds the left
# button. Kept as a second input rather than a second aimer, so there is still
# exactly one thing in the scene that points a gun.
var external_direction: Vector2 = Vector2.ZERO
Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring The fleet's guns picked their own targets and fired on their own, so combat happened whether or not the player did anything - there was nothing to aim and nothing to decide, and the right half of the screen held nothing at all. Firing is now an action. VesselWeaponComponent no longer seeks a target; it shoots along a heading, and the only thing that supplies one is the new right-hand stick via FireStickController. A fleet arriving from another scene starts with its guns silent, which is the correct resting state. A subtle dashed guide runs from the fleet along the aimed heading while the stick is held, out to the guns' OWN range - read from a weapon that is actually aboard, so the line can never promise reach the fleet does not have. Layout: the two sticks take the bottom corners, the pivot pair is centred between them at the bottom edge, and the laser and gas bomb sit on an arc up and left of the fire stick where a thumb already is. Those four are now round - IconButton grew a round_frame mode, and the in-world action controls use it while panel and menu buttons stay square, so the two kinds of button read as different kinds of thing. Worth recording, because it cost a screenshot to find: a script-declared property set BEFORE the `script = ExtResource(...)` line in a .tscn is silently dropped. Godot's parser has no idea the property exists yet. The round frames were all correctly set in both scenes and none of them applied. Also fixed researtch_lab_garrison.tres, which was authored under the void profile - that is the black hole surface, so a lab garrison was spawning on black holes. Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds the real stick toward the enemy rather than poking weapon components, so it exercises the path the player uses. test_fire_stick is new and covers the change itself: an idle stick leaves a target parked well inside range untouched, which is precisely what the old gun would never have done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
func _process(_delta: float) -> void:
var direction: Vector2 = stick.direction if stick != null else Vector2.ZERO
Implement #80, #81, #82 and #83 #80 - desktop combat. The mouse aims, the left button fires, and the number keys pick the weapon: 1 the fleet's guns, 2 and up the 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. The pointer path hands its heading to FireStickController rather than driving weapons itself, so exactly one thing in the scene aims a gun, and a held stick wins - a finger on it is a deliberate act, a mouse resting somewhere is not. Abilities gained fire_at(), which aims and fires in one call instead of pretending to be a press-drag-release touch. It is gated on the absence of a touchscreen, because Godot emulates a mouse button from every touch and without that a tap on a phone would fire the guns. The gate has an explicit override, and not for tidiness: headless reports a touchscreen, so with the automatic answer alone the aiming could never be exercised by a test and the only covered behaviour would be the guard. #81 - the bottom-left control hint is gone, from both scenes and from GameController. #82 - a chaser is destroyed by the impact when it reaches a vessel. It used to pass through the wreck untouched and line up another pass, which made a ramming enemy strictly better than any other. Data-driven per enemy (dies_on_contact), because a sniper that happens to drift into a ship has not spent itself doing it - and routed through take_damage, so it dies exactly as it dies to gunfire. The enemy is told about the impact BEFORE the ship resolves its own damage, since resolving it can free the ship. #83 - the research tree's buy button is gone; tapping a node twice buys it. Deliberately "the same node twice" rather than a timed double-tap: no window to miss on a slow phone, no accidental purchase while browsing, and the first tap always does something visible, so the second is never a blind commitment. The status line now says "Tap again to research" - with the button gone it is the only thing distinguishing an affordable node from an unaffordable one. test_enemy_collision asserted the exact behaviour #82 reversed, and now checks the ram costs both. Suite 117/117. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:49:47 +02:00
if direction == Vector2.ZERO:
# The stick wins when both are live: a finger on it is a deliberate
# act, a mouse resting somewhere is not.
direction = external_direction
Twin-stick combat: aim and fire with a second stick, MOBA-style ability ring The fleet's guns picked their own targets and fired on their own, so combat happened whether or not the player did anything - there was nothing to aim and nothing to decide, and the right half of the screen held nothing at all. Firing is now an action. VesselWeaponComponent no longer seeks a target; it shoots along a heading, and the only thing that supplies one is the new right-hand stick via FireStickController. A fleet arriving from another scene starts with its guns silent, which is the correct resting state. A subtle dashed guide runs from the fleet along the aimed heading while the stick is held, out to the guns' OWN range - read from a weapon that is actually aboard, so the line can never promise reach the fleet does not have. Layout: the two sticks take the bottom corners, the pivot pair is centred between them at the bottom edge, and the laser and gas bomb sit on an arc up and left of the fire stick where a thumb already is. Those four are now round - IconButton grew a round_frame mode, and the in-world action controls use it while panel and menu buttons stay square, so the two kinds of button read as different kinds of thing. Worth recording, because it cost a screenshot to find: a script-declared property set BEFORE the `script = ExtResource(...)` line in a .tscn is silently dropped. Godot's parser has no idea the property exists yet. The round frames were all correctly set in both scenes and none of them applied. Also fixed researtch_lab_garrison.tres, which was authored under the void profile - that is the black hole surface, so a lab garrison was spawning on black holes. Suite 110/110. test_enemy_combat's whole premise was auto-fire; it now holds the real stick toward the enemy rather than poking weapon components, so it exercises the path the player uses. test_fire_stick is new and covers the change itself: an idle stick leaves a target parked well inside range untouched, which is precisely what the old gun would never have done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:29:21 +02:00
aim(direction)
# The guide starts at the fleet, which is usually moving, so this redraws
# while aiming rather than only when the heading changes. Once the stick is
# released it redraws once more to clear the line, then stops.
var was_aiming: bool = aim_direction != Vector2.ZERO
aim_direction = direction
if direction != Vector2.ZERO or was_aiming:
queue_redraw()
# Also the entry point for anything driving the guns without a stick - tests
# aim through this rather than reaching into each weapon component, so what
# they exercise is the same path the stick uses.
func aim(direction: Vector2) -> void:
if fleet == null:
return
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null:
weapon.set_fire_direction(direction)
# How far the guide reaches: the actual range of a gun that is actually
# aboard, so the line never promises reach the fleet does not have.
func guide_range() -> float:
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null:
return float(weapon.fire_range)
return 0.0
func _draw() -> void:
if aim_direction == Vector2.ZERO or fleet == null or fleet.ships.is_empty():
return
var reach: float = guide_range()
if reach <= 0.0:
return
var origin: Vector2 = fleet.get_centroid()
var heading: Vector2 = aim_direction.normalized()
var travelled: float = GUIDE_GAP
while travelled < reach:
var dash_end: float = minf(travelled + GUIDE_DASH, reach)
draw_line(origin + heading * travelled, origin + heading * dash_end,
GUIDE_COLOR, GUIDE_WIDTH, true)
travelled = dash_end + GUIDE_GAP
func is_firing() -> bool:
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null and weapon.is_firing():
return true
return false