Compare commits
11 changed files with 14 additions and 451 deletions
|
|
@ -63,7 +63,6 @@ var context_actions_expanded: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
get_viewport().size_changed.connect(_on_viewport_resized)
|
|
||||||
_seat_hud_columns()
|
_seat_hud_columns()
|
||||||
build_button.pressed.connect(_on_build_pressed)
|
build_button.pressed.connect(_on_build_pressed)
|
||||||
action_button.pressed.connect(_on_context_action_pressed)
|
action_button.pressed.connect(_on_context_action_pressed)
|
||||||
|
|
@ -98,22 +97,6 @@ func _ready() -> void:
|
||||||
_layout_context_buttons()
|
_layout_context_buttons()
|
||||||
|
|
||||||
|
|
||||||
# HudScale reapplies each control's authored offsets whenever the window
|
|
||||||
# changes, which wipes the two layouts this controller writes at runtime.
|
|
||||||
# Re-running them after a resize is what keeps a desktop window that gets
|
|
||||||
# dragged to another size from leaving the context row where it was.
|
|
||||||
func _on_viewport_resized() -> void:
|
|
||||||
# Deferred: HudScale reapplies on the same signal, and which of two
|
|
||||||
# siblings connected first is not a thing to depend on. By the end of the
|
|
||||||
# frame its offsets are in, and these two can seat against them.
|
|
||||||
_relayout_hud.call_deferred()
|
|
||||||
|
|
||||||
|
|
||||||
func _relayout_hud() -> void:
|
|
||||||
_seat_hud_columns()
|
|
||||||
_layout_context_buttons()
|
|
||||||
|
|
||||||
|
|
||||||
func _seat_hud_columns() -> void:
|
func _seat_hud_columns() -> void:
|
||||||
var readout: ResourceReadout = get_node_or_null("../UI/ResourceReadout")
|
var readout: ResourceReadout = get_node_or_null("../UI/ResourceReadout")
|
||||||
if readout == null:
|
if readout == null:
|
||||||
|
|
@ -126,10 +109,7 @@ func _seat_hud_columns() -> void:
|
||||||
highest = minf(highest, button.position.y)
|
highest = minf(highest, button.position.y)
|
||||||
if is_inf(highest):
|
if is_inf(highest):
|
||||||
return
|
return
|
||||||
# The margin is authored in the same canvas units as everything else, so
|
var shift: float = readout.content_bottom() + HUD_COLUMN_MARGIN - highest
|
||||||
# it scales with the HUD (#228) or it would be the one gap that stayed
|
|
||||||
# phone-sized on a desktop.
|
|
||||||
var shift: float = readout.content_bottom() + HUD_COLUMN_MARGIN * readout.scale.y - highest
|
|
||||||
if shift <= 0.0:
|
if shift <= 0.0:
|
||||||
return
|
return
|
||||||
for button in buttons:
|
for button in buttons:
|
||||||
|
|
@ -242,14 +222,7 @@ func _clear_detected_context() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _layout_context_buttons() -> void:
|
func _layout_context_buttons() -> void:
|
||||||
# position is in the UI layer's space and size is in the button's own,
|
var row_top: float = build_button.position.y + build_button.size.y + HUD_ROW_GAP
|
||||||
# and since #228 those differ by the HUD scale on a short canvas. Every
|
|
||||||
# gap and stride below is therefore scaled, while the RECTS keep their
|
|
||||||
# authored size - HudScale renders them down rather than shrinking them.
|
|
||||||
# Mixing the two is what this function got wrong twice before (see
|
|
||||||
# test_hud_layout), and a desktop canvas makes it wrong again.
|
|
||||||
var k: float = build_button.scale.y
|
|
||||||
var row_top: float = build_button.position.y + build_button.size.y * k + HUD_ROW_GAP * k
|
|
||||||
var row_height: float = build_button.size.y
|
var row_height: float = build_button.size.y
|
||||||
action_button.offset_top = row_top
|
action_button.offset_top = row_top
|
||||||
action_button.offset_bottom = row_top + row_height
|
action_button.offset_bottom = row_top + row_height
|
||||||
|
|
@ -261,8 +234,8 @@ func _layout_context_buttons() -> void:
|
||||||
for i in range(buttons.size()):
|
for i in range(buttons.size()):
|
||||||
var button: Control = buttons[i]
|
var button: Control = buttons[i]
|
||||||
button.visible = true
|
button.visible = true
|
||||||
button.offset_left = (188.0 + float(i) * 164.0) * k
|
button.offset_left = 188 + i * 164
|
||||||
button.offset_right = button.offset_left + 148.0
|
button.offset_right = button.offset_left + 148
|
||||||
button.offset_top = row_top
|
button.offset_top = row_top
|
||||||
button.offset_bottom = row_top + row_height
|
button.offset_bottom = row_top + row_height
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,6 @@ const CATALOG_CHECK = preload("res://core/CatalogCheck.gd")
|
||||||
# survive the trip has to live here instead.
|
# survive the trip has to live here instead.
|
||||||
|
|
||||||
var has_return_position: bool = false
|
var has_return_position: bool = false
|
||||||
# Set by the menu when a save has just been loaded, cleared by whoever acts
|
|
||||||
# on it - the same one-shot handoff as has_return_position above, and
|
|
||||||
# deliberately NOT saved: it describes this launch, not the run.
|
|
||||||
var resumed_from_save: bool = false
|
|
||||||
var surviving_ship_count: int = STARTING_SHIP_COUNT
|
var surviving_ship_count: int = STARTING_SHIP_COUNT
|
||||||
var surviving_vessel_types: PackedStringArray = PackedStringArray()
|
var surviving_vessel_types: PackedStringArray = PackedStringArray()
|
||||||
# #106: a mirror of what is flying, pushed by Fleet._sync_composition_state()
|
# #106: a mirror of what is flying, pushed by Fleet._sync_composition_state()
|
||||||
|
|
|
||||||
|
|
@ -39,23 +39,6 @@ func _ready() -> void:
|
||||||
camera.position = GameState.return_world_position
|
camera.position = GameState.return_world_position
|
||||||
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
|
||||||
GameState.has_return_position = false
|
GameState.has_return_position = false
|
||||||
elif GameState.resumed_from_save:
|
|
||||||
# #227: a resumed run used to fall through to the brand-new-game
|
|
||||||
# branch below, which put the STARTING fleet at the world origin -
|
|
||||||
# so loading a save stranded the player in empty space with the
|
|
||||||
# wrong ships while their base sat wherever they had left it.
|
|
||||||
#
|
|
||||||
# The mothership is where a wiped-out fleet is already rebuilt (see
|
|
||||||
# BuildController._check_fleet_wipe), so it is the run's home whether
|
|
||||||
# or not this is a resume. Falls back to the position the save was
|
|
||||||
# written at, and only then to the origin, because a run can have no
|
|
||||||
# mothership standing.
|
|
||||||
var home: Variant = GameState.get_mothership_position()
|
|
||||||
var at: Vector2 = home if home != null else GameState.fleet_position
|
|
||||||
fleet.spawn_ships(GameState.fleet_vessel_types.size(), at,
|
|
||||||
GameState.fleet_vessel_types)
|
|
||||||
camera.position = at
|
|
||||||
GameState.resumed_from_save = false
|
|
||||||
elif GameState.intro_active:
|
elif GameState.intro_active:
|
||||||
# #108: the prologue opens with a convoy, and crews it itself. This
|
# #108: the prologue opens with a convoy, and crews it itself. This
|
||||||
# node still runs first, so spawning the ordinary opening fleet here
|
# node still runs first, so spawning the ordinary opening fleet here
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,8 @@ func _process(delta: float) -> bool:
|
||||||
controller.transfer_button.visible = controller._has_transfer_context()
|
controller.transfer_button.visible = controller._has_transfer_context()
|
||||||
controller._layout_context_buttons()
|
controller._layout_context_buttons()
|
||||||
var buttons: Array = [controller.building_action_button, controller.hangar_button]
|
var buttons: Array = [controller.building_action_button, controller.hangar_button]
|
||||||
# Rendered width, not the authored rect: #228 shrinks the HUD on a
|
|
||||||
# short canvas by scaling the node and leaving its rect alone, so
|
|
||||||
# offset_right overstates where a button actually ends.
|
|
||||||
for i in range(buttons.size() - 1):
|
for i in range(buttons.size() - 1):
|
||||||
var ends: float = buttons[i].offset_left + buttons[i].size.x * buttons[i].scale.x
|
if buttons[i].offset_right > buttons[i + 1].offset_left:
|
||||||
if ends > buttons[i + 1].offset_left:
|
|
||||||
_fail("context buttons %d and %d overlap" % [i, i + 1])
|
_fail("context buttons %d and %d overlap" % [i, i + 1])
|
||||||
return true
|
return true
|
||||||
controller._collapse_context_actions()
|
controller._collapse_context_actions()
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@ var controller: Node
|
||||||
var readout: Node
|
var readout: Node
|
||||||
var stage: int = 0
|
var stage: int = 0
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
# A desktop canvas (1080 units) and a phone one (3413), where the HUD
|
|
||||||
# scale is 0.45 and 1.0 respectively.
|
|
||||||
var shapes: Array = [Vector2i(1920, 1080), Vector2i(720, 1280)]
|
|
||||||
|
|
||||||
# Deliberately share a row: a building either offers its own action or a
|
# Deliberately share a row: a building either offers its own action or a
|
||||||
# hangar, never both, so their rects coinciding is the design.
|
# hangar, never both, so their rects coinciding is the design.
|
||||||
|
|
@ -55,40 +52,23 @@ func _process(delta: float) -> bool:
|
||||||
return false
|
return false
|
||||||
scene.get_node("AsteroidField").set_process(false)
|
scene.get_node("AsteroidField").set_process(false)
|
||||||
scene.get_node("EnemySpawner").set_process(false)
|
scene.get_node("EnemySpawner").set_process(false)
|
||||||
# --- E. at BOTH shapes ---------------------------------------
|
if not _check("as laid out at load"):
|
||||||
# #228 shrinks the HUD on a short canvas, so every relationship
|
|
||||||
# here now has a scale in it. A desktop canvas is what the
|
|
||||||
# default viewport gives, and the phone is where that scale is
|
|
||||||
# 1.0 - the arithmetic has to hold at both or one of them is
|
|
||||||
# being checked by accident.
|
|
||||||
root.size = shapes.pop_front()
|
|
||||||
stage = 1
|
|
||||||
return false
|
|
||||||
1:
|
|
||||||
if elapsed < 0.2:
|
|
||||||
return false
|
|
||||||
elapsed = 0.0
|
|
||||||
var label: String = "%.0f-unit canvas" % readout.get_viewport_rect().size.y
|
|
||||||
if not _check("as laid out at load, %s" % label):
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
# --- C. the path that broke it ------------------------------
|
# --- C. the path that broke it ------------------------------
|
||||||
controller.context_actions_expanded = false
|
controller.context_actions_expanded = false
|
||||||
controller._layout_context_buttons()
|
controller._layout_context_buttons()
|
||||||
if not _check("collapsed, %s" % label):
|
if not _check("after _layout_context_buttons() collapsed"):
|
||||||
return true
|
return true
|
||||||
|
|
||||||
controller.context_actions_expanded = true
|
controller.context_actions_expanded = true
|
||||||
controller._layout_context_buttons()
|
controller._layout_context_buttons()
|
||||||
if not _check("expanded, %s" % label):
|
if not _check("after _layout_context_buttons() expanded"):
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if shapes.is_empty():
|
|
||||||
print("TEST_HUD_LAYOUT: PASS")
|
print("TEST_HUD_LAYOUT: PASS")
|
||||||
quit(0)
|
quit(0)
|
||||||
return true
|
return true
|
||||||
root.size = shapes.pop_front()
|
|
||||||
return false
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -130,21 +110,13 @@ func _check(context: String) -> bool:
|
||||||
var b: Control = buttons[b_name]
|
var b: Control = buttons[b_name]
|
||||||
if a == null or b == null:
|
if a == null or b == null:
|
||||||
continue
|
continue
|
||||||
if _rendered(a).intersects(_rendered(b)):
|
if a.get_rect().intersects(b.get_rect()):
|
||||||
return _fail("%s %s overlaps %s %s (%s)" \
|
return _fail("%s %s overlaps %s %s (%s)" \
|
||||||
% [a_name, _rendered(a), b_name, _rendered(b), context])
|
% [a_name, a.get_rect(), b_name, b.get_rect(), context])
|
||||||
print("clear of the readout (reaches %.0f) and of each other - %s" % [readout_bottom, context])
|
print("clear of the readout (reaches %.0f) and of each other - %s" % [readout_bottom, context])
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
# Where a control actually lands, which since #228 is not its layout rect:
|
|
||||||
# the HUD shrinks on a short canvas by scaling the node and leaving the rect
|
|
||||||
# at its authored size, so position is in scaled parent units while size is
|
|
||||||
# in unscaled local ones.
|
|
||||||
func _rendered(c: Control) -> Rect2:
|
|
||||||
return Rect2(c.position, c.size * c.scale)
|
|
||||||
|
|
||||||
|
|
||||||
func _is_exclusive(a: String, b: String) -> bool:
|
func _is_exclusive(a: String, b: String) -> bool:
|
||||||
for pair in EXCLUSIVE_PAIRS:
|
for pair in EXCLUSIVE_PAIRS:
|
||||||
if (a == pair[0] and b == pair[1]) or (a == pair[1] and b == pair[0]):
|
if (a == pair[0] and b == pair[1]) or (a == pair[1] and b == pair[0]):
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
extends SceneTree
|
|
||||||
|
|
||||||
# Forgejo #228: the HUD must not be three times larger on a PC than on a
|
|
||||||
# phone.
|
|
||||||
#
|
|
||||||
# The layout is authored in CANVAS units. With stretch "canvas_items" and
|
|
||||||
# aspect "expand" over a 1920x1080 base, a 720x1280 phone scales by 0.375 and
|
|
||||||
# gets 1920x3413 canvas units, while ANY 16:9-or-wider screen lands on
|
|
||||||
# 1920x1080 units however many pixels it really has. The canvas is three
|
|
||||||
# times shorter, so an unscaled 148-unit button goes from 4.3% of the
|
|
||||||
# screen's height to 13.7% of it.
|
|
||||||
#
|
|
||||||
# Nothing collides - that was checked pair by pair on both canvases and the
|
|
||||||
# same six pairs overlap on each, all of them same-slot alternatives. The
|
|
||||||
# fault is purely that everything is enormous.
|
|
||||||
#
|
|
||||||
# Covers:
|
|
||||||
# A. on a phone the HUD is left exactly as authored
|
|
||||||
# B. on a desktop canvas a control takes a similar FRACTION of the screen
|
|
||||||
# rather than three times more
|
|
||||||
# C. the shrink is bounded, so a landscape phone keeps thumb-sized targets
|
|
||||||
# D. it follows a resize rather than only applying once at startup
|
|
||||||
|
|
||||||
const PHONE: Vector2i = Vector2i(720, 1280)
|
|
||||||
const DESKTOP: Vector2i = Vector2i(1920, 1080)
|
|
||||||
const BIG_DESKTOP: Vector2i = Vector2i(2560, 1440)
|
|
||||||
|
|
||||||
var scene: Node
|
|
||||||
var hud: Node
|
|
||||||
var button: Control
|
|
||||||
var frames: int = 0
|
|
||||||
var stage: int = 0
|
|
||||||
var phone_fraction: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
|
||||||
var gs: Node = root.get_node("GameState")
|
|
||||||
gs.buildings.clear()
|
|
||||||
gs.reset_new_game()
|
|
||||||
gs.intro_active = false
|
|
||||||
scene = load("res://world/Main.tscn").instantiate()
|
|
||||||
root.add_child(scene)
|
|
||||||
current_scene = scene
|
|
||||||
hud = scene.get_node("UI/HudScale")
|
|
||||||
button = scene.get_node("UI/MapButton")
|
|
||||||
|
|
||||||
|
|
||||||
func _fraction() -> float:
|
|
||||||
# Height of a HUD control as a share of the screen's height - the number
|
|
||||||
# that actually differs between the two, and the one a player sees.
|
|
||||||
# Rendered height, not the layout rect: the rect keeps its authored size
|
|
||||||
# and the node is scaled, so scale is where the shrink actually lives.
|
|
||||||
return (button.size.y * button.scale.y) / button.get_viewport_rect().size.y
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> bool:
|
|
||||||
frames += 1
|
|
||||||
if frames < 10:
|
|
||||||
return false
|
|
||||||
frames = 0
|
|
||||||
match stage:
|
|
||||||
0:
|
|
||||||
root.size = PHONE
|
|
||||||
stage = 1
|
|
||||||
return false
|
|
||||||
1:
|
|
||||||
# --- A. authored size on the shape it was authored for -------
|
|
||||||
var k: float = hud.hud_scale()
|
|
||||||
if not is_equal_approx(k, 1.0):
|
|
||||||
return _fail("the phone HUD is being scaled by %.3f, but it is the authored shape" % k)
|
|
||||||
phone_fraction = _fraction()
|
|
||||||
print("phone %s: canvas %.0f tall, scale %.2f, control is %.1f%% of the screen" % [
|
|
||||||
PHONE, button.get_viewport_rect().size.y, k, phone_fraction * 100.0])
|
|
||||||
root.size = DESKTOP
|
|
||||||
stage = 2
|
|
||||||
return false
|
|
||||||
2:
|
|
||||||
# --- B. a similar share of a desktop screen ------------------
|
|
||||||
var pc_fraction: float = _fraction()
|
|
||||||
var ratio: float = pc_fraction / maxf(phone_fraction, 0.0001)
|
|
||||||
print("desktop %s: canvas %.0f tall, scale %.2f, control is %.1f%% of the screen (%.2fx the phone)" % [
|
|
||||||
DESKTOP, button.get_viewport_rect().size.y, hud.hud_scale(),
|
|
||||||
pc_fraction * 100.0, ratio])
|
|
||||||
if ratio > 1.75:
|
|
||||||
return _fail("a HUD control is %.2fx its phone share on a desktop screen - that is the bug" % ratio)
|
|
||||||
# --- C. and not shrunk into uselessness ----------------------
|
|
||||||
if hud.hud_scale() < 0.4:
|
|
||||||
return _fail("the HUD shrank to %.2f, past anything pressable" % hud.hud_scale())
|
|
||||||
root.size = BIG_DESKTOP
|
|
||||||
stage = 3
|
|
||||||
return false
|
|
||||||
3:
|
|
||||||
# --- D. follows the resize -----------------------------------
|
|
||||||
var big: float = _fraction()
|
|
||||||
print("big desktop %s: control is %.1f%% of the screen" % [BIG_DESKTOP, big * 100.0])
|
|
||||||
if absf(big - phone_fraction) / maxf(phone_fraction, 0.0001) > 0.75:
|
|
||||||
return _fail("a bigger desktop screen did not keep the control proportional (%.1f%% vs %.1f%%)" \
|
|
||||||
% [big * 100.0, phone_fraction * 100.0])
|
|
||||||
# Back to the phone: the HUD must return, not stay shrunk.
|
|
||||||
root.size = PHONE
|
|
||||||
stage = 4
|
|
||||||
return false
|
|
||||||
4:
|
|
||||||
if not is_equal_approx(hud.hud_scale(), 1.0):
|
|
||||||
return _fail("returning to the phone shape left the HUD at %.3f" % hud.hud_scale())
|
|
||||||
if absf(_fraction() - phone_fraction) > 0.001:
|
|
||||||
return _fail("the HUD did not come back to its authored size on resize")
|
|
||||||
print("resizing back to the phone restored the authored size")
|
|
||||||
print("TEST_HUD_SCALES_TO_SCREEN: PASS")
|
|
||||||
quit(0)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _fail(message: String) -> bool:
|
|
||||||
print("TEST_HUD_SCALES_TO_SCREEN: FAIL (%s)" % message)
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
extends SceneTree
|
|
||||||
|
|
||||||
# Forgejo #227: a resumed run comes back at its own mothership.
|
|
||||||
#
|
|
||||||
# Loading a save used to fall through to the brand-new-game branch in
|
|
||||||
# PlanetAtmosphereController, which spawns STARTING_SHIP_COUNT interceptors
|
|
||||||
# at the world origin. So a resume stranded the player in empty space with
|
|
||||||
# the wrong ships, while their base sat wherever they had left it - which on
|
|
||||||
# a charted map is thousands of units away.
|
|
||||||
#
|
|
||||||
# Covers:
|
|
||||||
# A. resuming puts the fleet at the mothership, not at the origin
|
|
||||||
# B. and crews it with the ships the save actually had
|
|
||||||
# C. with no mothership standing, it falls back to where the save was made
|
|
||||||
# D. a NEW game is untouched - still the starting fleet at the origin
|
|
||||||
|
|
||||||
const HOME: Vector2 = Vector2(7400.0, -3100.0)
|
|
||||||
const AWAY: Vector2 = Vector2(-5200.0, 900.0)
|
|
||||||
|
|
||||||
var game_state: Node
|
|
||||||
var scene: Node
|
|
||||||
var elapsed: float = 0.0
|
|
||||||
var stage: int = 0
|
|
||||||
var expected_crew: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
|
||||||
game_state = root.get_node("GameState")
|
|
||||||
SaveGame.clear()
|
|
||||||
|
|
||||||
|
|
||||||
func _fresh_state() -> void:
|
|
||||||
game_state.buildings.clear()
|
|
||||||
game_state.extractors.clear()
|
|
||||||
game_state.reset_progression()
|
|
||||||
game_state.reset_new_game()
|
|
||||||
game_state.reset_quests()
|
|
||||||
game_state.intro_active = false
|
|
||||||
|
|
||||||
|
|
||||||
func _load_scene() -> void:
|
|
||||||
if scene != null and is_instance_valid(scene):
|
|
||||||
scene.free()
|
|
||||||
scene = load("res://world/Main.tscn").instantiate()
|
|
||||||
root.add_child(scene)
|
|
||||||
current_scene = scene
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> bool:
|
|
||||||
elapsed += delta
|
|
||||||
match stage:
|
|
||||||
0:
|
|
||||||
if elapsed < 0.3:
|
|
||||||
return false
|
|
||||||
# A run with a base at HOME and the fleet parked far from it.
|
|
||||||
_fresh_state()
|
|
||||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, HOME)
|
|
||||||
# Capacity for the crew below. Since #219 the fleet holds
|
|
||||||
# BASE_FLEET_SIZE (1) plus one berth per supplied fleet office,
|
|
||||||
# so a save with three ships flying is a save that had two
|
|
||||||
# offices standing - without them spawn_ships clamps to 1 and
|
|
||||||
# this would assert nothing.
|
|
||||||
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, HOME + Vector2(400.0, 0.0))
|
|
||||||
game_state.add_building(GameState.BUILDING_FLEET_OFFICE, HOME + Vector2(800.0, 0.0))
|
|
||||||
game_state.fleet_vessel_types = PackedStringArray([
|
|
||||||
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
|
|
||||||
GameState.VESSEL_INTERCEPTOR])
|
|
||||||
game_state.fleet_position = AWAY
|
|
||||||
expected_crew = game_state.fleet_vessel_types.size()
|
|
||||||
if game_state.max_fleet_size() < expected_crew:
|
|
||||||
return _fail("the fleet cannot hold %d, so a resumed crew would prove nothing (cap %d)" \
|
|
||||||
% [expected_crew, game_state.max_fleet_size()])
|
|
||||||
if not SaveGame.save(game_state):
|
|
||||||
return _fail("could not write the save this test resumes from")
|
|
||||||
_fresh_state()
|
|
||||||
var result: Dictionary = SaveGame.load_into(game_state)
|
|
||||||
if not bool(result.get("ok", false)):
|
|
||||||
return _fail("the save would not load: %s" % result.get("reason", ""))
|
|
||||||
game_state.resumed_from_save = true
|
|
||||||
_load_scene()
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 1
|
|
||||||
return false
|
|
||||||
1:
|
|
||||||
if elapsed < 0.6:
|
|
||||||
return false
|
|
||||||
var fleet: Node2D = scene.get_node("Fleet")
|
|
||||||
# --- A. at the mothership, not the origin -------------------
|
|
||||||
var at: Vector2 = fleet.get_centroid()
|
|
||||||
if at.distance_to(HOME) > 400.0:
|
|
||||||
return _fail("resumed at %s, but the mothership is at %s" % [at, HOME])
|
|
||||||
if at.distance_to(Vector2.ZERO) < 400.0:
|
|
||||||
return _fail("resumed at the world origin, which is the new-game behaviour")
|
|
||||||
print("resumed at the mothership (%.0f units from it)" % at.distance_to(HOME))
|
|
||||||
|
|
||||||
# --- B. with the crew the save had --------------------------
|
|
||||||
if fleet.ships.size() != expected_crew:
|
|
||||||
return _fail("resumed with %d ships, the save had %d" \
|
|
||||||
% [fleet.ships.size(), expected_crew])
|
|
||||||
print("resumed with the saved crew of %d" % fleet.ships.size())
|
|
||||||
if game_state.resumed_from_save:
|
|
||||||
return _fail("the resume flag was left raised, so the next scene would re-use it")
|
|
||||||
|
|
||||||
# --- C. no mothership: fall back to where the save was made --
|
|
||||||
_fresh_state()
|
|
||||||
game_state.fleet_vessel_types = PackedStringArray([GameState.VESSEL_INTERCEPTOR])
|
|
||||||
game_state.fleet_position = AWAY
|
|
||||||
game_state.resumed_from_save = true
|
|
||||||
_load_scene()
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 2
|
|
||||||
return false
|
|
||||||
2:
|
|
||||||
if elapsed < 0.6:
|
|
||||||
return false
|
|
||||||
var fleet2: Node2D = scene.get_node("Fleet")
|
|
||||||
var at2: Vector2 = fleet2.get_centroid()
|
|
||||||
if at2.distance_to(AWAY) > 400.0:
|
|
||||||
return _fail("with no mothership, resumed at %s rather than the saved %s" % [at2, AWAY])
|
|
||||||
print("with no mothership, resumed where the save was written")
|
|
||||||
|
|
||||||
# --- D. a new game is untouched -----------------------------
|
|
||||||
_fresh_state()
|
|
||||||
game_state.resumed_from_save = false
|
|
||||||
_load_scene()
|
|
||||||
elapsed = 0.0
|
|
||||||
stage = 3
|
|
||||||
return false
|
|
||||||
3:
|
|
||||||
if elapsed < 0.6:
|
|
||||||
return false
|
|
||||||
var fleet3: Node2D = scene.get_node("Fleet")
|
|
||||||
if fleet3.get_centroid().distance_to(Vector2.ZERO) > 400.0:
|
|
||||||
return _fail("a new game no longer starts at the origin (%s)" % fleet3.get_centroid())
|
|
||||||
if fleet3.ships.size() != GameState.STARTING_SHIP_COUNT:
|
|
||||||
return _fail("a new game starts with %d ships, expected %d" \
|
|
||||||
% [fleet3.ships.size(), GameState.STARTING_SHIP_COUNT])
|
|
||||||
print("a new game still starts with %d ships at the origin" % fleet3.ships.size())
|
|
||||||
|
|
||||||
SaveGame.clear()
|
|
||||||
print("TEST_RESUME_SPAWN: PASS")
|
|
||||||
quit(0)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _fail(message: String) -> bool:
|
|
||||||
print("TEST_RESUME_SPAWN: FAIL (%s)" % message)
|
|
||||||
quit(1)
|
|
||||||
return true
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
extends Node
|
|
||||||
|
|
||||||
# Forgejo #228: the HUD is proportionally three times too big on a PC.
|
|
||||||
#
|
|
||||||
# The layout is authored in CANVAS units against a portrait phone. With
|
|
||||||
# stretch "canvas_items"/"expand" and a 1920x1080 base, a 720x1280 phone
|
|
||||||
# scales by 0.375 and so gets a canvas 1920x3413 units tall - while ANY
|
|
||||||
# 16:9-or-wider screen lands on 1920x1080 units whatever its pixel count.
|
|
||||||
# The canvas is three times shorter, so a 148-unit button goes from 4.3% of
|
|
||||||
# the screen's height to 13.7% of it. Nothing collides; everything is just
|
|
||||||
# enormous, and the thumb sticks in particular.
|
|
||||||
#
|
|
||||||
# Each HUD element's offsets are measured FROM ITS OWN ANCHOR, so scaling
|
|
||||||
# them scales the element's size and its distance from the edge it hangs off
|
|
||||||
# together. That is the whole trick: one multiplier per control, no layout
|
|
||||||
# rules to re-derive, and anchoring keeps working untouched.
|
|
||||||
|
|
||||||
# The canvas the HUD was drawn for. Not a magic number: 1280 / (720 / 1920).
|
|
||||||
const AUTHORED_CANVAS_HEIGHT: float = 3413.0
|
|
||||||
|
|
||||||
# Below this the controls stop being worth pressing. A landscape phone gets
|
|
||||||
# the same short canvas as a desktop, and there the sticks are still thumb
|
|
||||||
# targets - so the shrink is bounded rather than purely proportional.
|
|
||||||
const MIN_SCALE: float = 0.45
|
|
||||||
|
|
||||||
# Authored offsets and sizes, captured before anything is touched.
|
|
||||||
# Re-reading them after a resize would measure a previous scaling and let the
|
|
||||||
# HUD ratchet away to nothing - the same trap NeonPanel documents.
|
|
||||||
var _authored: Dictionary = {}
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
var layer: Node = get_parent()
|
|
||||||
for child in layer.get_children():
|
|
||||||
if child == self or not (child is Control):
|
|
||||||
continue
|
|
||||||
var c: Control = child
|
|
||||||
# Full-bleed things - the modal menus, the breach overlay - hang off
|
|
||||||
# every edge at once and have nothing to shrink.
|
|
||||||
if is_equal_approx(c.anchor_left, 0.0) and is_equal_approx(c.anchor_right, 1.0) \
|
|
||||||
and is_equal_approx(c.anchor_top, 0.0) and is_equal_approx(c.anchor_bottom, 1.0):
|
|
||||||
continue
|
|
||||||
_authored[c] = [c.offset_left, c.offset_top,
|
|
||||||
c.offset_right - c.offset_left, c.offset_bottom - c.offset_top]
|
|
||||||
get_viewport().size_changed.connect(_apply)
|
|
||||||
_apply()
|
|
||||||
|
|
||||||
|
|
||||||
func hud_scale() -> float:
|
|
||||||
var height: float = get_viewport().get_visible_rect().size.y
|
|
||||||
if height <= 0.0:
|
|
||||||
return 1.0
|
|
||||||
# Only ever shrinks. A canvas TALLER than the phone's is not a reason to
|
|
||||||
# inflate the HUD over the game.
|
|
||||||
return clampf(height / AUTHORED_CANVAS_HEIGHT, MIN_SCALE, 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _apply() -> void:
|
|
||||||
var k: float = hud_scale()
|
|
||||||
for control in _authored:
|
|
||||||
if not is_instance_valid(control):
|
|
||||||
continue
|
|
||||||
var a: Array = _authored[control]
|
|
||||||
var c: Control = control
|
|
||||||
# Position moves toward the edge the element hangs off; the RECT keeps
|
|
||||||
# its authored size and the node is scaled instead.
|
|
||||||
#
|
|
||||||
# Scaling the rect alone was wrong: half the HUD draws its own
|
|
||||||
# contents at fixed canvas sizes - ResourceReadout lays its rows out
|
|
||||||
# in code - so a shrunken rect kept full-size content inside it, and
|
|
||||||
# the build button slid up underneath the resource list. Scaling the
|
|
||||||
# node takes the drawn content with it.
|
|
||||||
c.pivot_offset = Vector2.ZERO
|
|
||||||
c.offset_left = float(a[0]) * k
|
|
||||||
c.offset_top = float(a[1]) * k
|
|
||||||
c.offset_right = c.offset_left + float(a[2])
|
|
||||||
c.offset_bottom = c.offset_top + float(a[3])
|
|
||||||
c.scale = Vector2(k, k)
|
|
||||||
|
|
@ -140,7 +140,6 @@ func _on_continue_pressed() -> void:
|
||||||
# A resumed run never replays the prologue, whatever it was doing when
|
# A resumed run never replays the prologue, whatever it was doing when
|
||||||
# it was written.
|
# it was written.
|
||||||
GameState.intro_active = false
|
GameState.intro_active = false
|
||||||
GameState.resumed_from_save = true
|
|
||||||
_start("res://world/Main.tscn")
|
_start("res://world/Main.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,7 @@ func rows_height() -> float:
|
||||||
# Bottom edge of the rows in this Control's parent space - the line anything
|
# Bottom edge of the rows in this Control's parent space - the line anything
|
||||||
# stacked below the readout has to clear.
|
# stacked below the readout has to clear.
|
||||||
func content_bottom() -> float:
|
func content_bottom() -> float:
|
||||||
# Rendered, not laid out. #228 scales the HUD down on a short canvas by
|
return position.y + rows_height()
|
||||||
# scaling the NODE while leaving its rect at the authored size, so
|
|
||||||
# rows_height() is in unscaled local units while position is already in
|
|
||||||
# scaled parent ones. Mixing the two overstated this by more than a
|
|
||||||
# button's height and pushed the columns down to clear space that was
|
|
||||||
# not occupied.
|
|
||||||
return position.y + rows_height() * scale.y
|
|
||||||
|
|
||||||
|
|
||||||
func _add_glyph(resource_type: String, at: Vector2) -> void:
|
func _add_glyph(resource_type: String, at: Vector2) -> void:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/Starfield.gd" id="1"]
|
[ext_resource type="Script" path="res://world/Starfield.gd" id="1"]
|
||||||
[ext_resource type="Script" path="res://ui/HudScale.gd" id="hud_scale"]
|
|
||||||
[ext_resource type="Script" path="res://vessels/Fleet.gd" id="2"]
|
[ext_resource type="Script" path="res://vessels/Fleet.gd" id="2"]
|
||||||
[ext_resource type="Script" path="res://controllers/GameController.gd" id="3"]
|
[ext_resource type="Script" path="res://controllers/GameController.gd" id="3"]
|
||||||
[ext_resource type="Script" path="res://enemies/EnemySpawner.gd" id="4"]
|
[ext_resource type="Script" path="res://enemies/EnemySpawner.gd" id="4"]
|
||||||
|
|
@ -152,9 +151,6 @@ script = ExtResource("12")
|
||||||
[node name="UI" type="CanvasLayer" parent="."]
|
[node name="UI" type="CanvasLayer" parent="."]
|
||||||
layer = 1
|
layer = 1
|
||||||
|
|
||||||
[node name="HudScale" type="Node" parent="UI"]
|
|
||||||
script = ExtResource("hud_scale")
|
|
||||||
|
|
||||||
[node name="ResourceReadout" type="Control" parent="UI"]
|
[node name="ResourceReadout" type="Control" parent="UI"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
offset_left = 28
|
offset_left = 28
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue