156 lines
5.2 KiB
GDScript
156 lines
5.2 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:
|
|
focus_world_position(fleet.get_centroid())
|
|
|
|
|
|
func focus_world_position(world_position: Vector2) -> void:
|
|
var fleet_local: Vector2 = world_position * 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)
|