spaceship/world/WorldMap.gd
Loïc Wisniewski 2f161621c3 #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>
2026-08-21 19:44:14 +02:00

129 lines
4.4 KiB
GDScript

extends Control
@export var min_zoom: float = 0.3
@export var max_zoom: float = 3.0
@onready var content: Node2D = $MapContent
@onready var open_button: IconButton = get_node("../MapButton")
@onready var recenter_button: IconButton = $RecenterButton
@onready var close_button: IconButton = $CloseButton
@onready var fleet: Node2D = get_node("../../Fleet")
var touches: Dictionary = {}
var pinch_active: bool = false
var pinch_start_distance: float = 0.0
var pinch_start_zoom: float = 1.0
func _ready() -> void:
open_button.pressed.connect(open_map)
recenter_button.pressed.connect(recenter_view)
close_button.pressed.connect(close_map)
func open_map() -> void:
visible = true
open_button.visible = false
recenter_view()
func close_map() -> void:
visible = false
open_button.visible = true
touches.clear()
pinch_active = false
func recenter_view() -> void:
var fleet_local: Vector2 = fleet.get_centroid() * content.WORLD_TO_LOCAL
var viewport_center: Vector2 = get_viewport_rect().size / 2.0
content.position = viewport_center - fleet_local * content.scale.x
# The map takes over touch input while it is open, so a drag pans the map
# instead of ordering the fleet somewhere. What it must NOT take over is a
# touch aimed at its own buttons.
#
# _input() runs BEFORE the GUI system, so set_input_as_handled() here stopped
# the touch ever reaching CloseButton._gui_input: the map could be opened and
# then not closed. On Android and desktop that stayed hidden, because Godot
# also emits an emulated MOUSE event for each touch and this only ever
# swallowed touch events - the button still got the mouse one, a second
# chance a phone browser does not give. So it surfaced on the web build,
# while being a real routing bug on every platform.
#
# Letting those events through is safe: IconButton consumes them with
# accept_event(), which marks them handled before GameController's
# _unhandled_input runs - so tapping Close still cannot also move the fleet.
func _input(event: InputEvent) -> void:
if not visible:
return
if event is InputEventScreenTouch:
# A press over one of the map's own buttons is never adopted, and
# because it is never adopted, neither is anything else that touch
# goes on to do - the index simply never enters `touches`.
if event.pressed and _over_own_button(event.position):
return
if not event.pressed and not touches.has(event.index):
return
_handle_touch(event)
get_viewport().set_input_as_handled()
elif event is InputEventScreenDrag:
if not touches.has(event.index):
return
_handle_drag(event)
get_viewport().set_input_as_handled()
elif event is InputEventMagnifyGesture:
_handle_magnify(event)
get_viewport().set_input_as_handled()
# Asked of the children rather than listed by name, so a button added to the
# map panel later is covered without anyone having to remember this exists.
# Keyed on having a `pressed` signal: the backdrop is a full-screen ColorRect
# and would otherwise match every point on the display.
func _over_own_button(at: Vector2) -> bool:
for child in get_children():
if child is Control and child.visible and child.has_signal("pressed") \
and (child as Control).get_global_rect().has_point(at):
return true
return false
func _handle_touch(event: InputEventScreenTouch) -> void:
if event.pressed:
touches[event.index] = event.position
if touches.size() == 2:
pinch_active = true
var positions: Array = touches.values()
pinch_start_distance = positions[0].distance_to(positions[1])
pinch_start_zoom = content.scale.x
else:
touches.erase(event.index)
if touches.size() < 2:
pinch_active = false
func _handle_drag(event: InputEventScreenDrag) -> void:
if not touches.has(event.index):
return
var previous_pos: Vector2 = touches[event.index]
touches[event.index] = event.position
if pinch_active and touches.size() == 2 and pinch_start_distance > 0.0:
var positions: Array = touches.values()
var current_distance: float = positions[0].distance_to(positions[1])
var new_zoom: float = pinch_start_zoom * (current_distance / max(pinch_start_distance, 1.0))
_set_zoom(new_zoom)
elif touches.size() == 1:
content.position += event.position - previous_pos
func _handle_magnify(event: InputEventMagnifyGesture) -> void:
_set_zoom(content.scale.x * event.factor)
func _set_zoom(z: float) -> void:
var clamped: float = clamp(z, min_zoom, max_zoom)
content.scale = Vector2(clamped, clamped)