#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
157 lines
6.1 KiB
GDScript
157 lines
6.1 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.
|
|
#
|
|
# #171 changed what that guard asks. It used to be the presence of a
|
|
# touchscreen, which fails safe but fails often: a desktop browser reporting a
|
|
# touch-capable panel switched mouse aiming off on exactly the machine it was
|
|
# written for. It now asks what the player last actually used
|
|
# (GameState.pointer_is_touch), which still fails safe - a tap makes the
|
|
# reading "touch" before the emulated click arrives, so a phone can never fire
|
|
# through this path - and stops being wrong about desktops.
|
|
|
|
# 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", "DashController"]
|
|
|
|
# Forgejo #231. Selecting a weapon and then clicking is two acts for one
|
|
# shot, and on a keyboard the selection is invisible until you look at the
|
|
# buttons - which is what made the abilities "hard to use". Each one now has
|
|
# a key that fires it outright, at wherever the mouse already is.
|
|
#
|
|
# Indices into ABILITY_ORDER above, so these stay in step with it.
|
|
const ABILITY_KEYS: Dictionary = {
|
|
KEY_Q: 0,
|
|
KEY_E: 1,
|
|
KEY_SPACE: 2,
|
|
}
|
|
|
|
@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 follows the last input the player actually used, which is what every
|
|
# scene ships with. The explicit settings exist because that reading is seeded
|
|
# from the display in an environment - headless - where the answer varies by
|
|
# platform, 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 GameState.pointer_is_touch
|
|
|
|
|
|
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:
|
|
# Fired where the mouse already points, so pressing the key is the
|
|
# whole action. fire_at() carries every guard the click path uses -
|
|
# no hull ready, cannot afford it, a panel covering the button.
|
|
if ABILITY_KEYS.has(event.keycode):
|
|
var slot: int = int(ABILITY_KEYS[event.keycode])
|
|
if slot < abilities.size():
|
|
aim_world = get_global_mouse_position()
|
|
abilities[slot].fire_at(aim_world)
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
# The number keys still choose what a CLICK does. They are not what
|
|
# #231 is about, and removing them would take the click path's
|
|
# abilities with them.
|
|
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.
|
|
#
|
|
# #172: the cursor's world position goes across as a PLACE. It always was
|
|
# one; it used to be flattened into a heading from the fleet's centre on
|
|
# the way out, which threw away the distance and made every hull fire
|
|
# parallel past the thing being clicked on.
|
|
fire_stick_controller.has_external_target = is_enabled() and firing \
|
|
and selected_weapon == 0 and not fleet.ships.is_empty()
|
|
fire_stick_controller.external_target = aim_world
|