Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. 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
|