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) # #128: a dialog stops the tree, and the prologue opens on one - so # _process's first frame never comes and the button below would sit on # screen behind the opening line. Applied once here too, where it does # not depend on the game running. _apply_intro_lock() func _process(_delta: float) -> void: _apply_intro_lock() # #122: the prologue leaves movement and nothing else, and the map is a # button like any other. Owned here rather than by BuildController, which # hides the rest - this button belongs to the map. func _apply_intro_lock() -> void: if GameState.intro_active: if open_button.visible: open_button.visible = false if visible: close_map() open_button.visible = false elif not open_button.visible and not visible: open_button.visible = true 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)