#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>
123 lines
4.6 KiB
GDScript
123 lines
4.6 KiB
GDScript
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. The guard is the presence of a touchscreen rather than a "has used a
|
|
# mouse" flag, because the wrong answer has to fail safe - a desktop wrongly
|
|
# reported as touch-capable loses a convenience, while a phone wrongly treated
|
|
# as a desktop fires its weapons at every tap.
|
|
|
|
# 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"]
|
|
|
|
@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 decides from the display, which is what every scene ships with. The
|
|
# explicit settings exist because "is there a touchscreen" is not a question
|
|
# that can be answered honestly in every environment - headless reports one,
|
|
# 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 DisplayServer.is_touchscreen_available()
|
|
|
|
|
|
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.
|
|
var heading: Vector2 = Vector2.ZERO
|
|
if is_enabled() and firing and selected_weapon == 0 and not fleet.ships.is_empty():
|
|
heading = aim_world - fleet.get_centroid()
|
|
fire_stick_controller.external_direction = heading
|