#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>
101 lines
3.8 KiB
GDScript
101 lines
3.8 KiB
GDScript
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
|
|
# 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
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
var direction: Vector2 = stick.direction if stick != null else Vector2.ZERO
|
|
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
|
|
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
|