While a line is on the banner the whole tree is paused, which freezes the fleet, the enemies and GameState's own production and wave clocks, and stops input reaching any node but the banner. So a press while a dialog is up acknowledges it and does nothing else - it cannot also steer the fleet or open a menu on the way through. The pause is owned by NarrationBanner rather than by its callers: the thing that must not get out of step is "a line is showing" and "the game is stopped", and the banner is the only code that knows the first. It releases on the last line, on clear(), and on leaving the tree - a scene swapped out mid-line must not leave the game stopped with nothing left to start it. Two consequences worth naming. A beat's deadline in the prologue is now game time, so it can no longer run out from under an unread line; it remains the way past a beat the player will not finish. And "the prologue leaves you movement and nothing else" was applied by _process on the first frame, which no longer comes - the prologue opens ON a dialog - so BuildController and WorldMap apply it at _ready too.
152 lines
5.1 KiB
GDScript
152 lines
5.1 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)
|
|
# #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)
|