#84: move 215 root files into a folder hierarchy
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>
This commit is contained in:
parent
baac11ae8b
commit
2f161621c3
495 changed files with 571 additions and 567 deletions
244
controllers/AimedFleetAbilityController.gd
Normal file
244
controllers/AimedFleetAbilityController.gd
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
class_name AimedFleetAbilityController
|
||||
extends Node2D
|
||||
|
||||
# Shared press-drag-release input and camera behavior for manually aimed fleet
|
||||
# abilities. Subclasses provide the component, ammunition, target calculation,
|
||||
# shot effect, and effect visuals.
|
||||
|
||||
const AIM_DEADZONE: float = 22.0
|
||||
|
||||
@export var minimum_zoom: float = 0.4
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
@onready var camera: Camera2D = get_node("../Camera2D")
|
||||
|
||||
var button: IconButton
|
||||
var status: Label
|
||||
var status_message_until: int = 0
|
||||
var aiming: bool = false
|
||||
var aim_touch: int = -1
|
||||
var aim_start_screen: Vector2 = Vector2.ZERO
|
||||
var aim_screen: Vector2 = Vector2.ZERO
|
||||
var aim_target: Vector2 = Vector2.ZERO
|
||||
var saved_zoom: float = 1.0
|
||||
var restoring_zoom: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
button = get_node(_button_path())
|
||||
status = get_node(_status_path())
|
||||
button.visible = false
|
||||
status.text = ""
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var had_visuals: bool = aiming or _has_effect_visuals()
|
||||
button.visible = fleet.has_capability(_component_id())
|
||||
button.enabled = _next_ready_ship() != null and _can_afford_shot()
|
||||
if not button.visible and aiming:
|
||||
_cancel_aim()
|
||||
if status.text != "" and Time.get_ticks_msec() >= status_message_until:
|
||||
status.text = ""
|
||||
|
||||
if restoring_zoom and not aiming:
|
||||
var zoom: float = move_toward(camera.zoom.x, saved_zoom,
|
||||
maxf(saved_zoom, 0.1) * delta * 5.0)
|
||||
camera.zoom = Vector2(zoom, zoom)
|
||||
if is_equal_approx(zoom, saved_zoom):
|
||||
restoring_zoom = false
|
||||
|
||||
_advance_effects(delta)
|
||||
if aiming or _has_effect_visuals() or had_visuals:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func can_begin_aim() -> bool:
|
||||
# An unaffordable shot still claims the press so the status label can
|
||||
# explain what is missing instead of silently passing the touch through.
|
||||
return button.visible and not button._blocked_by_modal() \
|
||||
and not aiming and _next_ready_ship() != null
|
||||
|
||||
|
||||
# Fires immediately at a world point, with no press-drag-release. The mouse
|
||||
# path (#80) has a crosshair already - it does not need to be dragged into
|
||||
# existence - so it aims and fires in one call rather than pretending to be
|
||||
# a touch.
|
||||
func fire_at(world_target: Vector2) -> bool:
|
||||
if not button.visible or button._blocked_by_modal() or _next_ready_ship() == null:
|
||||
return false
|
||||
if not _can_afford_shot():
|
||||
_say(_unaffordable_message())
|
||||
return false
|
||||
# Recorded before firing because _fire() ends by restoring it, and a stale
|
||||
# value from an earlier aim would snap the camera somewhere unrelated.
|
||||
saved_zoom = camera.zoom.x
|
||||
restoring_zoom = false
|
||||
aim_target = world_target
|
||||
_fire()
|
||||
return true
|
||||
|
||||
|
||||
func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||
if event.pressed:
|
||||
if not can_begin_aim() \
|
||||
or not Rect2(button.global_position, button.size).has_point(event.position):
|
||||
return false
|
||||
if not _can_afford_shot():
|
||||
_say(_unaffordable_message())
|
||||
return true
|
||||
aiming = true
|
||||
aim_touch = event.index
|
||||
aim_start_screen = event.position
|
||||
aim_screen = event.position
|
||||
saved_zoom = camera.zoom.x
|
||||
restoring_zoom = false
|
||||
button.pressed_down = true
|
||||
button.queue_redraw()
|
||||
_update_aim()
|
||||
return true
|
||||
if event.index != aim_touch:
|
||||
return false
|
||||
if aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE:
|
||||
_fire()
|
||||
elif _aim_at_nearest_enemy():
|
||||
# Tapping the button used to do nothing at all - the press was claimed,
|
||||
# the aim began, and releasing without dragging simply threw it away.
|
||||
# A tap now fires at the nearest enemy in range; dragging remains the
|
||||
# way to place a shot precisely.
|
||||
_fire()
|
||||
else:
|
||||
_say("Nothing in range - drag from the button to aim")
|
||||
_cancel_aim()
|
||||
return true
|
||||
|
||||
|
||||
# Points aim_target at the closest enemy within reach, or reports that there
|
||||
# is none. Both aimed abilities read only the aim_target when they fire - the
|
||||
# laser takes its DIRECTION and always extends to full range, the gas bomb
|
||||
# takes the point itself - so setting it here is all auto-aim has to do, and
|
||||
# each ability keeps its own meaning for it.
|
||||
func _aim_at_nearest_enemy() -> bool:
|
||||
var origin: Vector2 = _aim_origin()
|
||||
var reach: float = _aim_max_range()
|
||||
var nearest: Node2D = null
|
||||
var nearest_distance: float = INF
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if not is_instance_valid(enemy) or not (enemy is Node2D):
|
||||
continue
|
||||
var distance: float = origin.distance_to((enemy as Node2D).global_position)
|
||||
if distance > reach or distance >= nearest_distance:
|
||||
continue
|
||||
nearest_distance = distance
|
||||
nearest = enemy
|
||||
if nearest == null:
|
||||
return false
|
||||
aim_target = nearest.global_position
|
||||
queue_redraw()
|
||||
return true
|
||||
|
||||
|
||||
func handle_drag(event: InputEventScreenDrag) -> bool:
|
||||
if not aiming or event.index != aim_touch:
|
||||
return false
|
||||
aim_screen = event.position
|
||||
_update_aim()
|
||||
return true
|
||||
|
||||
|
||||
func _update_aim() -> void:
|
||||
var origin: Vector2 = _aim_origin()
|
||||
aim_target = _calculate_aim_target(aim_screen - aim_start_screen, origin)
|
||||
_update_aim_zoom(origin, aim_target)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _update_aim_zoom(origin: Vector2, target: Vector2) -> void:
|
||||
var displacement: Vector2 = target - origin
|
||||
var half_screen: Vector2 = get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0)
|
||||
var padding: float = _aim_padding()
|
||||
var fit_x: float = saved_zoom if absf(displacement.x) < 1.0 \
|
||||
else half_screen.x / (absf(displacement.x) + padding)
|
||||
var fit_y: float = saved_zoom if absf(displacement.y) < 1.0 \
|
||||
else half_screen.y / (absf(displacement.y) + padding)
|
||||
var desired: float = clampf(minf(saved_zoom, minf(fit_x, fit_y)),
|
||||
minimum_zoom, saved_zoom)
|
||||
camera.zoom = Vector2(desired, desired)
|
||||
|
||||
|
||||
func _finish_aim() -> void:
|
||||
aiming = false
|
||||
aim_touch = -1
|
||||
button.pressed_down = false
|
||||
button.queue_redraw()
|
||||
restoring_zoom = true
|
||||
|
||||
|
||||
func _cancel_aim() -> void:
|
||||
_finish_aim()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _fire() -> void:
|
||||
var ship: Node2D = _next_ready_ship()
|
||||
if ship == null:
|
||||
_cancel_aim()
|
||||
return
|
||||
if not _can_afford_shot():
|
||||
_say(_unaffordable_message())
|
||||
_cancel_aim()
|
||||
return
|
||||
var component: Node = ship.get_component(_component_id())
|
||||
if component == null or not _consume_shot(component):
|
||||
_cancel_aim()
|
||||
return
|
||||
GameState.withdraw_fleet_resource(_ammo_resource(), _ammo_cost())
|
||||
_perform_shot(ship, component)
|
||||
_finish_aim()
|
||||
button.enabled = _next_ready_ship() != null and _can_afford_shot()
|
||||
|
||||
|
||||
func _next_ready_ship() -> Node2D:
|
||||
for ship in fleet.ships:
|
||||
if not is_instance_valid(ship):
|
||||
continue
|
||||
var component: Node = ship.get_component(_component_id())
|
||||
if component != null and component.is_ready():
|
||||
return ship
|
||||
return null
|
||||
|
||||
|
||||
func _can_afford_shot() -> bool:
|
||||
return GameState.fleet_resource(_ammo_resource()) >= _ammo_cost()
|
||||
|
||||
|
||||
func _unaffordable_message() -> String:
|
||||
var resource_type: String = _ammo_resource()
|
||||
return "Need %d %s in the fleet hold to fire (have %d)" % [
|
||||
_ammo_cost(), GameState.resource_name(resource_type).to_lower(),
|
||||
GameState.fleet_resource(resource_type),
|
||||
]
|
||||
|
||||
|
||||
func _say(message: String) -> void:
|
||||
status.text = message
|
||||
status_message_until = Time.get_ticks_msec() + 3000
|
||||
|
||||
|
||||
func _aim_origin() -> Vector2:
|
||||
return fleet.get_centroid()
|
||||
|
||||
|
||||
# Subclass hooks.
|
||||
func _button_path() -> NodePath: return NodePath()
|
||||
func _status_path() -> NodePath: return NodePath()
|
||||
func _component_id() -> String: return ""
|
||||
func _ammo_resource() -> String: return ""
|
||||
func _ammo_cost() -> int: return 0
|
||||
func _aim_padding() -> float: return 0.0
|
||||
# How far this ability can reach, for auto-aim and for the aim guide.
|
||||
func _aim_max_range() -> float: return 0.0
|
||||
func _has_effect_visuals() -> bool: return false
|
||||
func _advance_effects(_delta: float) -> void: pass
|
||||
func _consume_shot(_component: Node) -> bool: return false
|
||||
func _perform_shot(_ship: Node2D, _component: Node) -> void: pass
|
||||
func _calculate_aim_target(_drag: Vector2, origin: Vector2) -> Vector2: return origin
|
||||
Loading…
Reference in a new issue