max_fleet_size() clamped itself to DEFAULT_FORMATION.size(), so the ceiling on the fleet was a property of the LAYOUT - the wedge ran out of authored positions at twenty - rather than a rule anyone decided on. Capacity is the supplied Fleet Offices and nothing else now. Dropping the clamp alone would index off the end of the formation array, so the wedge continues by rule instead of stopping: formation_slot() grows it on demand, each further pair stepping 30 further out and 40 further back, which is what the authored rows already do - the first twenty slots come back unchanged. A grown slot is rotated by formation_rotation so a fleet that outgrows the array after the player has pivoted does not sprout hulls facing the original heading, and that angle is saved with the layout. Two "fleet is full" messages pointed at the vanished ceiling. Both now say what actually buys a berth, and the hangar's stops claiming it is research, which it has not been since #219. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QR2Mdk42ghEpeyXPe4UFyn
221 lines
7.2 KiB
GDScript
221 lines
7.2 KiB
GDScript
extends Control
|
|
|
|
# Drag-and-drop editor for the fleet's formation, opened at a command
|
|
# centre. Each slot is a draggable marker; dropping one writes the new
|
|
# offset straight back into GameState.formation, so the change survives
|
|
# scene swaps and applies to any fleet rebuilt later.
|
|
#
|
|
# Works in "formation space": the panel centre represents the fleet centre,
|
|
# and one formation unit maps to `scale_factor` screen pixels.
|
|
|
|
signal closed
|
|
signal formation_changed
|
|
|
|
@export var scale_factor: float = 1.4
|
|
@export var min_scale: float = 0.5
|
|
@export var max_scale: float = 4.0
|
|
@export var marker_radius: float = 26.0
|
|
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
|
|
|
var dragging_index: int = -1
|
|
var drag_touch_index: int = -1
|
|
|
|
# Second finger = pinch to zoom the layout view, so a spread-out formation
|
|
# can be inspected close up or seen whole. Zooming only changes how the
|
|
# formation is DISPLAYED - the stored offsets are untouched.
|
|
var touches: Dictionary = {}
|
|
var pinch_active: bool = false
|
|
var pinch_start_distance: float = 0.0
|
|
var pinch_start_scale: float = 1.0
|
|
|
|
|
|
func _ready() -> void:
|
|
queue_redraw()
|
|
|
|
|
|
func _centre() -> Vector2:
|
|
return size / 2.0
|
|
|
|
|
|
func _slot_screen_pos(i: int) -> Vector2:
|
|
return _centre() + GameState.formation_slot(i) * scale_factor
|
|
|
|
|
|
func _screen_to_formation(p: Vector2) -> Vector2:
|
|
return (p - _centre()) / scale_factor
|
|
|
|
|
|
# Returns true when the event was consumed. Called from BuildController,
|
|
# which forwards input while the editor is open.
|
|
# #121: the formation could not be rearranged on a desktop at all. Like the
|
|
# research tree (#93) and the speed fader (#118), this control reads raw
|
|
# touches, and the project no longer turns clicks into them. Wrappers over
|
|
# the same press/drag logic, never a second implementation.
|
|
#
|
|
# The wheel replaces the pinch: a mouse has no second finger, and zoom is
|
|
# the other half of what the panel does.
|
|
const MOUSE_TOUCH_INDEX: int = -2
|
|
const WHEEL_ZOOM_STEP: float = 1.12
|
|
|
|
|
|
func handle_mouse_button(event: InputEventMouseButton) -> bool:
|
|
if not visible:
|
|
return false
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
|
return _zoom_by(WHEEL_ZOOM_STEP)
|
|
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
|
|
return _zoom_by(1.0 / WHEEL_ZOOM_STEP)
|
|
if event.button_index != MOUSE_BUTTON_LEFT:
|
|
return false
|
|
if event.pressed:
|
|
return _grab(event.position, MOUSE_TOUCH_INDEX)
|
|
if drag_touch_index != MOUSE_TOUCH_INDEX:
|
|
return false
|
|
dragging_index = -1
|
|
drag_touch_index = -1
|
|
queue_redraw()
|
|
return true
|
|
|
|
|
|
func handle_mouse_motion(event: InputEventMouseMotion) -> bool:
|
|
if drag_touch_index != MOUSE_TOUCH_INDEX or dragging_index == -1:
|
|
return false
|
|
_move_slot(event.position)
|
|
return true
|
|
|
|
|
|
func _zoom_by(factor: float) -> bool:
|
|
scale_factor = clamp(scale_factor * factor, min_scale, max_scale)
|
|
queue_redraw()
|
|
return true
|
|
|
|
|
|
func _grab(at: Vector2, index: int) -> bool:
|
|
if dragging_index != -1:
|
|
return false
|
|
var local: Vector2 = at - global_position
|
|
for i in range(GameState.max_fleet_size()):
|
|
if local.distance_to(_slot_screen_pos(i)) <= marker_radius * 1.4:
|
|
dragging_index = i
|
|
drag_touch_index = index
|
|
queue_redraw()
|
|
return true
|
|
return false
|
|
|
|
|
|
func _move_slot(at: Vector2) -> void:
|
|
var local: Vector2 = at - global_position
|
|
# Clamped so a slot can't be dragged off the panel and become
|
|
# unreachable (and so the fleet can't be spread absurdly far apart).
|
|
var f: Vector2 = _screen_to_formation(local)
|
|
var limit: float = min(size.x, size.y) / (2.0 * scale_factor) - marker_radius / scale_factor
|
|
GameState.formation[dragging_index] = Vector2(
|
|
clamp(f.x, -limit, limit), clamp(f.y, -limit, limit)
|
|
)
|
|
queue_redraw()
|
|
formation_changed.emit()
|
|
|
|
|
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
|
if not visible:
|
|
return false
|
|
if event.pressed:
|
|
touches[event.index] = event.position
|
|
if touches.size() == 2:
|
|
# A second finger turns the gesture into a pinch: drop any slot
|
|
# being dragged so it isn't yanked around while zooming.
|
|
dragging_index = -1
|
|
drag_touch_index = -1
|
|
var positions: Array = touches.values()
|
|
pinch_start_distance = positions[0].distance_to(positions[1])
|
|
pinch_start_scale = scale_factor
|
|
pinch_active = true
|
|
queue_redraw()
|
|
return true
|
|
return _grab(event.position, event.index)
|
|
|
|
touches.erase(event.index)
|
|
if touches.size() < 2:
|
|
pinch_active = false
|
|
if event.index != drag_touch_index:
|
|
return false
|
|
dragging_index = -1
|
|
drag_touch_index = -1
|
|
queue_redraw()
|
|
return true
|
|
|
|
|
|
func handle_drag(event: InputEventScreenDrag) -> bool:
|
|
if not visible:
|
|
return false
|
|
|
|
if touches.has(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: float = positions[0].distance_to(positions[1])
|
|
# Spreading apart zooms in, matching the world pinch.
|
|
scale_factor = clamp(
|
|
pinch_start_scale * (current / max(pinch_start_distance, 1.0)),
|
|
min_scale, max_scale
|
|
)
|
|
queue_redraw()
|
|
return true
|
|
|
|
if event.index != drag_touch_index or dragging_index == -1:
|
|
return false
|
|
_move_slot(event.position)
|
|
return true
|
|
|
|
|
|
func _draw() -> void:
|
|
var centre: Vector2 = _centre()
|
|
|
|
# Reference cross for the fleet centre.
|
|
var tick: float = 14.0
|
|
NeonDraw.draw_glowing_line(self, centre + Vector2(-tick, 0), centre + Vector2(tick, 0), Color(0.4, 0.45, 0.5), 0.5, 0.3)
|
|
NeonDraw.draw_glowing_line(self, centre + Vector2(0, -tick), centre + Vector2(0, tick), Color(0.4, 0.45, 0.5), 0.5, 0.3)
|
|
|
|
# #119: each slot is drawn as the vessel actually standing in it, in its
|
|
# own colour and its own hull, rather than as an identical placeholder
|
|
# dart. Arranging a formation means deciding where the shield sits
|
|
# relative to the guns, and that is impossible to see when every marker
|
|
# is the same triangle.
|
|
var crewed: PackedStringArray = GameState.fleet_vessel_types
|
|
for i in range(GameState.max_fleet_size()):
|
|
var p: Vector2 = _slot_screen_pos(i)
|
|
var held: bool = i == dragging_index
|
|
var vessel_type: String = String(crewed[i]) if i < crewed.size() else ""
|
|
var definition: Resource = GameState.vessel_definition(vessel_type) \
|
|
if vessel_type != "" else null
|
|
if definition == null:
|
|
# An empty slot: the outline of a berth, not a ship. A hollow
|
|
# marker is how the panel says "nothing is standing here yet".
|
|
_draw_empty_slot(p, held)
|
|
continue
|
|
_draw_vessel_slot(p, definition, held)
|
|
|
|
|
|
# Scaled so the biggest hull in the catalog still fits the marker, and drawn
|
|
# from the definition's own points so a vessel reshaped in its .tres is
|
|
# reshaped here too.
|
|
func _draw_vessel_slot(at: Vector2, definition: Resource, held: bool) -> void:
|
|
var points: PackedVector2Array = definition.hull_points
|
|
var extent: float = 1.0
|
|
for point in points:
|
|
extent = maxf(extent, point.length())
|
|
var factor: float = marker_radius / extent
|
|
var shaped: PackedVector2Array = PackedVector2Array()
|
|
for point in points:
|
|
shaped.append(at + point * factor)
|
|
NeonDraw.draw_glowing_polygon(self, shaped, definition.glow_color,
|
|
1.0 if held else 0.75)
|
|
|
|
|
|
func _draw_empty_slot(at: Vector2, held: bool) -> void:
|
|
var ring: PackedVector2Array = PackedVector2Array()
|
|
for i in range(12):
|
|
var a: float = (float(i) / 12.0) * TAU
|
|
ring.append(at + Vector2(cos(a), sin(a)) * marker_radius * 0.78)
|
|
NeonDraw.draw_glowing_polygon(self, ring, Color(0.42, 0.47, 0.58),
|
|
0.8 if held else 0.4)
|