spaceship/tests/test_desktop_combat.gd

174 lines
6.9 KiB
GDScript3
Raw Normal View History

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
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_desktop_combat.gd
# Forgejo #80: on a computer the mouse aims and the left button fires, and the
# number keys pick the weapon - 1 the fleet's guns, 2 and up the abilities.
#
# Covers:
# A. the weapon numbering is FIXED and named, so "2" is the gas bomb in
# every scene rather than whatever node order happens to produce
# B. holding the button with weapon 1 selected points the fleet's guns at
# the cursor, through the same controller the stick uses - not a second
# thing that aims
# C. releasing stops it
# D. the stick WINS while it is held, so a mouse resting somewhere cannot
# override a finger
# E. selecting an ability highlights its button, so what a click will do is
# on screen rather than remembered
# F. it is inert on a touch device. Godot emulates a mouse button from every
# touch, so without that guard a tap on a phone would fire the guns
var game_state: Node
var main: Node
var fleet: Node
var mouse: Node
var fire_stick: Node
var stick: Control
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
main = load("res://Main.tscn").instantiate()
root.add_child(main)
current_scene = main
fleet = main.get_node("Fleet")
mouse = main.get_node("MouseCombatController")
# Checked here rather than at first use: a script that fails to PARSE
# leaves a bare Node2D in the scene, and every call against it becomes a
# per-frame "nonexistent function" error that runs until the harness
# times the test out three minutes later instead of failing in one line.
if not mouse.has_method("weapon_count"):
print("TEST_DESKTOP_COMBAT: FAIL (MouseCombatController.gd did not load - see the parse error above)")
quit(1)
return
fire_stick = main.get_node("FireStickController")
stick = main.get_node("UI/FireStick")
func _weapon_heading() -> Vector2:
return fire_stick.aim_direction
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.3:
return false
main.get_node("EnemySpawner").set_process(false)
main.get_node("AsteroidField").set_process(false)
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
if is_instance_valid(hazard):
hazard.queue_free()
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
# --- A. fixed, named ordering ---------------------------------
if mouse.weapon_count() != mouse.abilities.size() + 1:
return _fail("weapon count %d does not match 1 + %d abilities" \
% [mouse.weapon_count(), mouse.abilities.size()])
if mouse.abilities.is_empty():
return _fail("no ability controllers were resolved, so the number keys address nothing")
if mouse.abilities[0] != main.get_node("GasBombController"):
return _fail("weapon 2 is not the gas bomb - the ticket names it explicitly")
if not mouse.select_weapon(0):
return _fail("could not select the default weapon")
if mouse.select_weapon(mouse.weapon_count()):
return _fail("selecting a weapon past the end was accepted")
print("weapon 1 is the fleet's guns, weapon 2 is the gas bomb, %d in total" \
% mouse.weapon_count())
# --- F. inert where a touchscreen exists -----------------------
# Checked against the real capability rather than forced, so this
# reports what will actually happen on the machine it runs on.
print("touchscreen present: %s -> mouse combat enabled: %s" \
% [DisplayServer.is_touchscreen_available(), mouse.is_enabled()])
if mouse.pointer_mode != mouse.PointerMode.AUTO:
return _fail("the scene ships with an overridden pointer mode, so what a real device does is not what was tested")
if mouse.is_enabled() == DisplayServer.is_touchscreen_available():
return _fail("mouse combat is not gated on the absence of a touchscreen")
print("gated correctly on the display's own answer")
# Everything below is about the AIMING, not the guard. Headless
# reports a touchscreen, so the mode is forced on here - otherwise
# these cases would silently never run and this file would test
# nothing but the line above.
mouse.pointer_mode = mouse.PointerMode.ON
if not mouse.is_enabled():
return _fail("forcing the pointer mode on did not enable it")
stage = 1
elapsed = 0.0
return false
1:
if elapsed < 0.1:
return false
# --- B. holding the button aims the guns at the cursor ---------
mouse.select_weapon(0)
mouse.aim_world = fleet.get_centroid() + Vector2(0.0, -500.0)
mouse.firing = true
mouse._process(0.0)
fire_stick._process(0.0)
var heading: Vector2 = _weapon_heading().normalized()
if heading.distance_to(Vector2(0.0, -1.0)) > 0.05:
return _fail("holding the button aimed along %s, but the cursor was straight up" % heading)
print("holding the button aims the guns at the cursor")
# --- C. releasing stops -----------------------------------------
mouse.firing = false
mouse._process(0.0)
fire_stick._process(0.0)
if _weapon_heading() != Vector2.ZERO:
return _fail("releasing the button left the guns aimed at %s" % _weapon_heading())
print("releasing stops the guns")
# --- D. a held stick wins ---------------------------------------
var centre: Vector2 = stick.global_position + stick.size * 0.5
var press := InputEventScreenTouch.new()
press.index = 9
press.pressed = true
press.position = centre
stick.handle_touch(press)
var drag := InputEventScreenDrag.new()
drag.index = 9
drag.position = centre + Vector2(stick.max_radius, 0.0)
stick.handle_drag(drag)
mouse.aim_world = fleet.get_centroid() + Vector2(0.0, -500.0)
mouse.firing = true
mouse._process(0.0)
fire_stick._process(0.0)
var contested: Vector2 = _weapon_heading().normalized()
if contested.distance_to(Vector2(1.0, 0.0)) > 0.05:
return _fail("with the stick pushed right and the mouse aiming up, the guns went %s - the stick must win" % contested)
print("a held stick overrides the mouse")
var release := InputEventScreenTouch.new()
release.index = 9
release.pressed = false
release.position = centre
stick.handle_touch(release)
mouse.firing = false
# --- E. the chosen ability is shown ----------------------------
mouse.select_weapon(1)
var bomb_button: Node = mouse.abilities[0].button
if not bomb_button.highlighted:
return _fail("selecting the gas bomb did not highlight its button")
mouse.select_weapon(0)
if bomb_button.highlighted:
return _fail("going back to the guns left the gas bomb still highlighted")
print("the selected ability's own button carries the selection")
print("TEST_DESKTOP_COMBAT: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_DESKTOP_COMBAT: FAIL (%s)" % message)
quit(1)
return true