#231 - the abilities were hard to use on a keyboard. "The currently selected weapons" names a real mechanism: MouseCombatController (#80) has number keys that SELECT which weapon a click fires. That is two acts for one shot, and the selection is invisible unless you look at the buttons. Q, E and space now fire the gas bomb, the laser and the dash outright, wherever the mouse already points. It goes through fire_at(), the same public entry the click path uses, so every guard comes with it - no hull ready, cannot afford the shot, a panel over the button. The number keys stay: they are not what this ticket is about, and removing them would take the click path's abilities with them. Q and E were already taken - they pivot the formation - so the pivot pair moved to Z and C, one row under the movement keys and still left-hand. That is a change to a control the ticket did not mention, and the one thing here worth arguing with. test_keyboard_input spelled Q and E out and so reported a broken pivot when the pivot was fine. It reads KEY_PIVOT_CW/CCW off the controller now, and additionally asserts the pair is not bound to an ability key - the failure that would actually matter. #230 - the upkeep menu never added anything up. Two faults, and the second is invisible from the code. There was no TOTAL, so the bill had to be done in the player's head. And the rows were one Label padded with %-16s, which aligns only in a monospace font - the theme's is CarroisGothicSC, which is proportional - with autowrap on top, so a long row folded and the table lost its shape. A real GridContainer now: header, a row per paying berth, and a TOTAL. The headline carries both figures, because they answer different questions - the per-charge bill is what the player watches happen, the per-minute rate is what they plan against. Amounts are listed in the game's resource order rather than the dictionary's. The screenshot showed "2 ore 1 energy" on one row above "2 gas 2 ore" on the next, which defeats the point of a column. The sheet is rebuilt only when the numbers change. BuildController refreshes this menu every frame while it is open, and rebuilding a dozen Labels per frame for figures that move every 25 seconds is what #6 and #10 are about. The test refreshes twice and asserts the same cell objects come back. Every figure still comes from fleet_upkeep_breakdown(), the calculation the charging loop itself runs - the new total sums those same per-berth costs rather than recomputing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
162 lines
5.8 KiB
GDScript
162 lines
5.8 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_keyboard_input.gd
|
|
# Desktop/web controls. The phone build is driven entirely by touch, but the
|
|
# web build is played with a keyboard and mouse, so WASD (and the arrows)
|
|
# must steer the fleet, the pivot pair must turn the formation, and releasing
|
|
# must stop - the stick has a centred-again signal to hook, the keyboard has
|
|
# nothing equivalent, so a missed release would leave the fleet flying off
|
|
# forever.
|
|
#
|
|
# Verifies:
|
|
# 1. W/A/S/D and the arrow keys each produce the right heading, and
|
|
# diagonals combine and normalise
|
|
# 2. holding a key actually moves the fleet that way
|
|
# 3. releasing every key stops it
|
|
# 4. Q and E turn the formation, in opposite directions
|
|
# 5. the on-screen stick is not taken away on a touch device
|
|
|
|
var scene: Node
|
|
var controller: Node
|
|
var fleet: Node
|
|
var joystick: Control
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
var start_centroid: Vector2 = Vector2.ZERO
|
|
var moving_centroid: Vector2 = Vector2.ZERO
|
|
|
|
|
|
func _initialize() -> void:
|
|
root.get_node("GameState").reset_formation()
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
controller = scene
|
|
fleet = scene.get_node("Fleet")
|
|
joystick = scene.get_node("UI/Joystick")
|
|
|
|
|
|
func _key(code: int, pressed: bool) -> void:
|
|
var event: InputEventKey = InputEventKey.new()
|
|
event.keycode = code
|
|
event.pressed = pressed
|
|
controller._handle_key(event)
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
|
|
# --- headings -------------------------------------------------
|
|
var cases: Array = [
|
|
{"keys": [KEY_W], "expect": Vector2.UP},
|
|
{"keys": [KEY_S], "expect": Vector2.DOWN},
|
|
{"keys": [KEY_A], "expect": Vector2.LEFT},
|
|
{"keys": [KEY_D], "expect": Vector2.RIGHT},
|
|
{"keys": [KEY_UP], "expect": Vector2.UP},
|
|
{"keys": [KEY_RIGHT], "expect": Vector2.RIGHT},
|
|
{"keys": [KEY_W, KEY_D], "expect": Vector2(1, -1).normalized()},
|
|
{"keys": [KEY_W, KEY_S], "expect": Vector2.ZERO},
|
|
]
|
|
for case in cases:
|
|
for k in case.keys:
|
|
_key(k, true)
|
|
var got: Vector2 = controller.keyboard_direction()
|
|
var ok: bool = got.distance_to(case.expect) < 0.001
|
|
if not ok:
|
|
print("keys %s -> %s, expected %s" % [case.keys, got, case.expect])
|
|
print("TEST_KEYBOARD_INPUT: FAIL (wrong heading)")
|
|
quit(1)
|
|
return true
|
|
for k in case.keys:
|
|
_key(k, false)
|
|
print("all %d key combinations produced the right heading" % cases.size())
|
|
|
|
# #171: the stick follows the last input actually used, rather
|
|
# than the display's claim about what could arrive. Using the
|
|
# keyboard takes it away - on any device - and a touch brings it
|
|
# straight back, which is what keeps that safe.
|
|
if joystick.visible:
|
|
print("TEST_KEYBOARD_INPUT: FAIL (the stick survived keyboard use)")
|
|
quit(1)
|
|
return true
|
|
controller._note_touch()
|
|
if not joystick.visible:
|
|
print("TEST_KEYBOARD_INPUT: FAIL (a touch did not bring the stick back)")
|
|
quit(1)
|
|
return true
|
|
print("keyboard use hides the stick; a touch brings it back")
|
|
root.get_node("GameState").last_touch_msec = -1
|
|
|
|
# --- holding a key flies the fleet ----------------------------
|
|
start_centroid = fleet.get_centroid()
|
|
_key(KEY_D, true)
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 0.6:
|
|
return false
|
|
moving_centroid = fleet.get_centroid()
|
|
var travelled: float = moving_centroid.x - start_centroid.x
|
|
var sideways: float = absf(moving_centroid.y - start_centroid.y)
|
|
print("held D for 0.6s: moved x=%.1f y-drift=%.1f" % [travelled, sideways])
|
|
if travelled < 50.0 or sideways > 30.0:
|
|
print("TEST_KEYBOARD_INPUT: FAIL (holding a key did not fly the fleet that way)")
|
|
quit(1)
|
|
return true
|
|
|
|
# --- releasing stops ------------------------------------------
|
|
_key(KEY_D, false)
|
|
elapsed = 0.0
|
|
stage = 2
|
|
return false
|
|
2:
|
|
if elapsed < 0.9:
|
|
return false
|
|
var coasted: float = fleet.get_centroid().x - moving_centroid.x
|
|
var still_steering: bool = fleet.steer_direction != Vector2.ZERO
|
|
print("0.9s after release: drifted a further %.1f, still steering=%s" % [
|
|
coasted, still_steering
|
|
])
|
|
if still_steering:
|
|
print("TEST_KEYBOARD_INPUT: FAIL (fleet kept flying after the key was released)")
|
|
quit(1)
|
|
return true
|
|
|
|
# --- the pivot pair -------------------------------------------
|
|
# Read off the controller's own constants rather than naming the
|
|
# keys again here. #231 moved this pair from Q/E to Z/C because
|
|
# the abilities took Q, E and space, and a test that spelled the
|
|
# old keys out reported a broken pivot when the pivot was fine.
|
|
var game_state: Node = root.get_node("GameState")
|
|
var cw_key: int = controller.KEY_PIVOT_CW
|
|
var ccw_key: int = controller.KEY_PIVOT_CCW
|
|
if cw_key in [KEY_Q, KEY_E, KEY_SPACE] or ccw_key in [KEY_Q, KEY_E, KEY_SPACE]:
|
|
print("TEST_KEYBOARD_INPUT: FAIL (pivot is bound to an ability key)")
|
|
quit(1)
|
|
return true
|
|
var before: float = game_state.formation[1].angle()
|
|
_key(cw_key, true)
|
|
_key(cw_key, false)
|
|
var after_cw: float = game_state.formation[1].angle()
|
|
_key(ccw_key, true)
|
|
_key(ccw_key, false)
|
|
var after_ccw: float = game_state.formation[1].angle()
|
|
|
|
var cw_turn: float = angle_difference(before, after_cw)
|
|
var ccw_turn: float = angle_difference(after_cw, after_ccw)
|
|
print("key %d turned %.3f rad, key %d turned %.3f rad" \
|
|
% [cw_key, cw_turn, ccw_key, ccw_turn])
|
|
if absf(cw_turn) < 0.01 or absf(ccw_turn) < 0.01 or signf(cw_turn) == signf(ccw_turn):
|
|
print("TEST_KEYBOARD_INPUT: FAIL (the pivot pair did not turn in opposite directions)")
|
|
quit(1)
|
|
return true
|
|
|
|
print("TEST_KEYBOARD_INPUT: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|