spaceship/tests/test_ability_keys.gd

121 lines
4.7 KiB
GDScript3
Raw Normal View History

#231, #230: a key per ability, and an upkeep sheet that adds up #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
2026-09-15 22:27:00 -04:00
extends SceneTree
# Forgejo #231: on a computer each ability has a key that fires it outright.
#
# Selecting a weapon with a number key and then clicking is two acts for one
# shot, and the selection is invisible until you look at the buttons - which
# is what made the abilities hard to use with a keyboard. Q, E and space now
# fire the gas bomb, the laser and the dash where the mouse already points.
#
# Covers:
# A. the three keys map to the three abilities, in the documented order
# B. pressing one fires THAT ability and no other
# C. it fires at the mouse, so pointing is the aim
# D. Q and E no longer pivot, which is what they used to do
# E. the number keys still choose what a CLICK does - #231 replaces the
# selection as a way to FIRE, not the click path itself
# F. touch is untouched: on a phone these keys do nothing
var scene: Node
var mouse: Node
var controller: Node
var game_state: Node
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.reset_new_game()
game_state.intro_active = false
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
mouse = scene.get_node("MouseCombatController")
# GameController is the ROOT of Main.tscn, not a child of it - the same
# script roots the planet surface too.
controller = scene
func _key(code: int) -> InputEventKey:
var event := InputEventKey.new()
event.keycode = code
event.pressed = true
return event
func _process(delta: float) -> bool:
elapsed += delta
if elapsed < 0.4:
return false
# --- A. the mapping is the documented one ----------------------------
if mouse.ABILITY_ORDER.size() != 3:
return _fail("expected 3 abilities, found %d" % mouse.ABILITY_ORDER.size())
var expected: Dictionary = {KEY_Q: "GasBombController", KEY_E: "LaserController",
KEY_SPACE: "DashController"}
for code in expected:
if not mouse.ABILITY_KEYS.has(code):
return _fail("key %d is not bound to an ability" % code)
var slot: int = int(mouse.ABILITY_KEYS[code])
if String(mouse.ABILITY_ORDER[slot]) != String(expected[code]):
return _fail("key %d fires %s, expected %s" \
% [code, mouse.ABILITY_ORDER[slot], expected[code]])
print("Q, E and space map to the bomb, the laser and the dash")
# --- D. and they no longer pivot --------------------------------------
if controller.KEY_PIVOT_CW in [KEY_Q, KEY_E, KEY_SPACE] \
or controller.KEY_PIVOT_CCW in [KEY_Q, KEY_E, KEY_SPACE]:
return _fail("pivot still uses an ability key (cw=%d ccw=%d)" \
% [controller.KEY_PIVOT_CW, controller.KEY_PIVOT_CCW])
print("pivot moved to %d/%d, clear of the ability keys" \
% [controller.KEY_PIVOT_CCW, controller.KEY_PIVOT_CW])
# --- B/C. the KEY itself reaches that ability, aimed at the mouse -----
# Driven through _unhandled_input rather than by calling fire_at(), or
# this would prove the abilities work and nothing about the binding.
#
# Observed through aim_world: the ability branch overwrites it with the
# live cursor before firing, so a value that survives means the branch
# never ran. The abilities themselves are locked in a fresh game, so
# fire_at() correctly refuses - the wiring is what is under test here.
mouse.pointer_mode = 1 # PointerMode.ON - a headless display reports no mouse
var sentinel := Vector2(98765.0, -4321.0)
for code in [KEY_Q, KEY_E, KEY_SPACE]:
mouse.aim_world = sentinel
mouse._unhandled_input(_key(code))
if mouse.aim_world == sentinel:
var slot: int = int(mouse.ABILITY_KEYS[code])
return _fail("key %d never reached %s" % [code, mouse.ABILITY_ORDER[slot]])
print("Q, E and space each reach their ability through the input path")
# A key bound to nothing must fall through to the number-key branch
# rather than being swallowed by the ability one.
mouse.aim_world = sentinel
mouse._unhandled_input(_key(KEY_K))
if mouse.aim_world != sentinel:
return _fail("an unbound key was treated as an ability trigger")
print("an unbound key is left alone")
# --- E. the number keys still select for the click path ---------------
if not mouse.select_weapon(1):
return _fail("the number keys no longer select a weapon for a click")
if mouse.selected_weapon != 1:
return _fail("selecting weapon 1 left selected_weapon at %d" % mouse.selected_weapon)
print("the number keys still choose what a click fires")
# --- F. and none of this reaches a phone ------------------------------
mouse.pointer_mode = 2 # PointerMode.OFF
if mouse.is_enabled():
return _fail("mouse combat is live with the pointer mode off")
print("with a touch pointer the keys are inert")
print("TEST_ABILITY_KEYS: PASS")
quit(0)
return true
func _fail(message: String) -> bool:
print("TEST_ABILITY_KEYS: FAIL (%s)" % message)
quit(1)
return true