Compare commits

..
15 changed files with 41 additions and 196 deletions

View file

@ -839,7 +839,7 @@ func _on_hangar_crew_type(selection: Variant) -> void:
if vessel_type == "": if vessel_type == "":
return return
if fleet.ships.size() >= GameState.max_fleet_size(): if fleet.ships.size() >= GameState.max_fleet_size():
hangar_menu.set_status("Fleet is full (%d/%d) - each further berth is a supplied Fleet Office" \ hangar_menu.set_status("Fleet is full (%d/%d) - research capacity at the mothership" \
% [fleet.ships.size(), GameState.max_fleet_size()]) % [fleet.ships.size(), GameState.max_fleet_size()])
return return
var stored_index: int = GameState.stored_vessels.find(vessel_type) var stored_index: int = GameState.stored_vessels.find(vessel_type)

View file

@ -383,14 +383,14 @@ func _unhandled_input(event: InputEvent) -> void:
_handle_key(event) _handle_key(event)
elif event is InputEventMouseMotion: elif event is InputEventMouseMotion:
_note_keyboard() _note_keyboard()
if speed_fader != null:
speed_fader.handle_mouse_motion(event)
elif event is InputEventMouseButton: elif event is InputEventMouseButton:
_note_keyboard() _note_keyboard()
# The speed fader used to be driven from here, and could not work: # #118: the fader gets first refusal, so dragging the boost does not
# MouseCombatController is dispatched before this node (it is a # also pan or zoom the world behind it.
# child, and this is the root) and marks every left press handled to if speed_fader != null and speed_fader.handle_mouse_button(event):
# fire the guns, so the press never arrived. It listens for its own return
# mouse events now - see SpeedFader._unhandled_input - and claims
# them from the UI layer, ahead of the combat controllers.
if not event.pressed: if not event.pressed:
return return
# Wheel zoom, the desktop equivalent of the pinch. # Wheel zoom, the desktop equivalent of the pinch.

View file

@ -951,16 +951,11 @@ func _advance_fleet_office_upkeep(delta: float) -> void:
fleet_upkeep_failed.emit(i + BASE_FLEET_SIZE, vessel_type) fleet_upkeep_failed.emit(i + BASE_FLEET_SIZE, vessel_type)
# However many berths the fleet offices are keeping supplied, with no
# ceiling above them: a player who builds and feeds fifty offices flies
# fifty vessels. There used to be one at DEFAULT_FORMATION.size(), which was
# a limit of the LAYOUT rather than a rule of the game - the wedge simply ran
# out of authored positions at twenty. It now continues by rule instead (see
# formation_slot), so there is nothing left for the cap to protect.
func max_fleet_size() -> int: func max_fleet_size() -> int:
if intro_fleet_size > 0: if intro_fleet_size > 0:
return intro_fleet_size return mini(intro_fleet_size, DEFAULT_FORMATION.size())
return BASE_FLEET_SIZE + supplied_fleet_office_count() return mini(BASE_FLEET_SIZE + supplied_fleet_office_count(),
DEFAULT_FORMATION.size())
func reset_progression() -> void: func reset_progression() -> void:
@ -2803,11 +2798,11 @@ func get_mothership_position() -> Variant:
# capacity upgrade is researched, but they are defined up front so the wedge # capacity upgrade is researched, but they are defined up front so the wedge
# keeps a deliberate shape as it grows rather than having positions invented # keeps a deliberate shape as it grows rather than having positions invented
# at unlock time. # at unlock time.
# Twenty slots are AUTHORED. They are not a cap: the fleet grows as far as # Twenty slots, because max_fleet_size() caps itself on this array's length -
# the offices supply it, and formation_slot() continues the wedge past the # so the wedge has to be long enough for every doctrine to be worth buying.
# end of this array by the same rule the rows here already follow - each # Each further pair continues the same V rather than starting a new shape:
# further pair steps 30 further out and 40 further back, so the wedge that # the rows step 30 out and 40 back, which is the rule the first ten already
# arrives at fifty is the one that left at three. # followed, so the wedge that arrives at twenty is the one that left at three.
const DEFAULT_FORMATION: Array = [ const DEFAULT_FORMATION: Array = [
Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32), Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32),
Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56), Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56),
@ -2819,43 +2814,9 @@ const DEFAULT_FORMATION: Array = [
var formation: Array = DEFAULT_FORMATION.duplicate() var formation: Array = DEFAULT_FORMATION.duplicate()
# Which way the wedge currently faces. Kept because slots are grown on
# demand: one that appears after the player has pivoted has to arrive
# already turned, or a fleet that outgrows the authored twenty would sprout
# hulls pointing back along the formation's original heading.
var formation_rotation: float = 0.0
# The rule the wedge continues by past the authored slots. Pairs start at
# index 8 - the point where the hand-placed rows settle into a regular
# V - with the even index on the left, and each pair after that steps out
# and back by FORMATION_PAIR_STEP.
const FORMATION_PAIR_FIRST: int = 8
const FORMATION_PAIR_ORIGIN: Vector2 = Vector2(120.0, 112.0)
const FORMATION_PAIR_STEP: Vector2 = Vector2(30.0, 40.0)
func reset_formation() -> void: func reset_formation() -> void:
formation = DEFAULT_FORMATION.duplicate() formation = DEFAULT_FORMATION.duplicate()
formation_rotation = 0.0
# Where one slot sits, growing the wedge if the fleet has outgrown the
# positions stored so far. Every reader asks through this rather than
# indexing `formation` directly, because with no cap on fleet size there is
# no index the array is guaranteed to already hold.
func formation_slot(index: int) -> Vector2:
if index < 0:
return Vector2.ZERO
while formation.size() <= index:
formation.append(_grown_formation_slot(formation.size()).rotated(formation_rotation))
return formation[index]
func _grown_formation_slot(index: int) -> Vector2:
var pair: int = (index - FORMATION_PAIR_FIRST) / 2
var out: float = FORMATION_PAIR_ORIGIN.x + FORMATION_PAIR_STEP.x * float(pair)
return Vector2(out if index % 2 == 1 else -out,
FORMATION_PAIR_ORIGIN.y + FORMATION_PAIR_STEP.y * float(pair))
func rotate_formation(radians: float) -> void: func rotate_formation(radians: float) -> void:
@ -2863,7 +2824,6 @@ func rotate_formation(radians: float) -> void:
# and only changing which way it faces. # and only changing which way it faces.
for i in range(formation.size()): for i in range(formation.size()):
formation[i] = formation[i].rotated(radians) formation[i] = formation[i].rotated(radians)
formation_rotation = wrapf(formation_rotation + radians, -PI, PI)
# --- Extractors ------------------------------------------------------------ # --- Extractors ------------------------------------------------------------

View file

@ -47,7 +47,7 @@ const FIELDS: Array = [
"unlocked", "research_points", "vessel_levels", "unlocked", "research_points", "vessel_levels",
# The fleet itself. # The fleet itself.
"fleet_position", "fleet_vessel_types", "surviving_ship_count", "fleet_position", "fleet_vessel_types", "surviving_ship_count",
"surviving_vessel_types", "formation", "formation_rotation", "stored_vessels", "surviving_vessel_types", "formation", "stored_vessels",
"vessel_targets", "vessel_deliveries", "repair_drones", "vessel_targets", "vessel_deliveries", "repair_drones",
"max_fleet_speed_multiplier", "fleet_speed_multiplier", "build_time_scale", "max_fleet_speed_multiplier", "fleet_speed_multiplier", "build_time_scale",
# The world as the player has seen it. # The world as the player has seen it.

View file

@ -1 +0,0 @@
uid://bk3grd8nn8pj1

View file

@ -1 +0,0 @@
uid://ckdulc4563wt5

View file

@ -10,19 +10,16 @@ extends SceneTree
# The boost was unreachable on a desktop and the formation could not be # The boost was unreachable on a desktop and the formation could not be
# rearranged at all. # rearranged at all.
# #
# Driven through the REAL routing in each case - the viewport itself for the # Driven through the REAL routing in each case (GameController for the
# fader, BuildController for the editor - rather than by calling the # fader, BuildController for the editor) rather than by calling the controls
# controls directly, because the routing is what was missing: a test that # directly, because the routing is what was missing - a test that called
# called handle_mouse_button() itself would have passed against both # handle_mouse_button() itself would have passed against both reports.
# reports. It did, in fact, and hid a second one: the fader was dispatched
# from Main, which is the LAST node to see an unhandled press, so
# MouseCombatController fired the guns with it and the knob never moved.
# #
# Covers: # Covers:
# A. #118 - a click on the fader takes hold of it, dragging moves the # A. #118 - a click on the fader takes hold of it, dragging moves the
# boost, and releasing lets go # boost, and releasing lets go
# B. the press stops at the fader: not the camera behind it, and not the # B. the fader claims the press, so dragging the boost does not also zoom
# guns, which claim every other left press on a desktop # the world behind it
# C. #121 - a click grabs a formation slot, moving drags it, and the # C. #121 - a click grabs a formation slot, moving drags it, and the
# formation really changes # formation really changes
# D. releasing drops the slot, and moving afterwards does not keep dragging # D. releasing drops the slot, and moving afterwards does not keep dragging
@ -70,31 +67,6 @@ func _fail(message: String) -> bool:
return true return true
# Pushed through the viewport, not handed to a node's own _unhandled_input:
# WHICH node sees a press first is the thing under test. #118 shipped a fader
# that only worked when a test called it directly - MouseCombatController
# claims every left press to fire the guns, and it is dispatched well before
# Main's own handler, so on a real desktop the click never arrived.
# `true` keeps the coordinates in viewport space, which is the space the
# controls are laid out in.
func _push(event: InputEvent) -> void:
root.push_input(event, true)
func _push_click(at: Vector2, pressed: bool) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.position = at
_push(event)
func _push_move(to: Vector2) -> void:
var event: InputEventMouseMotion = InputEventMouseMotion.new()
event.position = to
_push(event)
func _click(at: Vector2, pressed: bool, target: Node) -> void: func _click(at: Vector2, pressed: bool, target: Node) -> void:
var event: InputEventMouseButton = InputEventMouseButton.new() var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT event.button_index = MOUSE_BUTTON_LEFT
@ -140,29 +112,22 @@ func _process(delta: float) -> bool:
return _fail("the boost has no range to move through") return _fail("the boost has no range to move through")
# --- A. click, drag, release ----------------------------- # --- A. click, drag, release -----------------------------
# With mouse aiming live, which is what a desktop always has:
# the guns are the other thing a left press means, and they used
# to take every one of them.
var mouse_combat: Node = scene.get_node("MouseCombatController")
mouse_combat.pointer_mode = mouse_combat.PointerMode.ON
var top: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5, var top: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5,
fader.track_margin + 2.0) fader.track_margin + 2.0)
var bottom: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5, var bottom: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5,
fader.size.y - fader.track_margin - 2.0) fader.size.y - fader.track_margin - 2.0)
_push_click(bottom, true) _click(bottom, true, scene)
if not fader.is_active(): if not fader.is_active():
return _fail("a click on the fader did not take hold of it") return _fail("a click on the fader did not take hold of it")
if mouse_combat.firing:
return _fail("the click that grabbed the fader also fired the fleet's guns")
var at_bottom: float = game_state.fleet_speed_multiplier var at_bottom: float = game_state.fleet_speed_multiplier
_push_move(top) _move(top, scene)
var at_top: float = game_state.fleet_speed_multiplier var at_top: float = game_state.fleet_speed_multiplier
if at_top <= at_bottom: if at_top <= at_bottom:
return _fail("dragging the fader from bottom to top left the boost at %.2f" % at_top) return _fail("dragging the fader from bottom to top left the boost at %.2f" % at_top)
_push_click(top, false) _click(top, false, scene)
if fader.is_active(): if fader.is_active():
return _fail("the fader is still held after the button came up") return _fail("the fader is still held after the button came up")
_push_move(bottom) _move(bottom, scene)
if not is_equal_approx(game_state.fleet_speed_multiplier, at_top): if not is_equal_approx(game_state.fleet_speed_multiplier, at_top):
return _fail("the fader kept following the pointer after release") return _fail("the fader kept following the pointer after release")
print("fader: %.2fx at the bottom, %.2fx at the top, released cleanly" \ print("fader: %.2fx at the bottom, %.2fx at the top, released cleanly" \
@ -170,13 +135,10 @@ func _process(delta: float) -> bool:
# --- B. it claims the press ------------------------------ # --- B. it claims the press ------------------------------
var zoom_before: float = scene.get_node("Camera2D").zoom.x var zoom_before: float = scene.get_node("Camera2D").zoom.x
_push_click(bottom, true) _click(bottom, true, scene)
_push_click(bottom, false) _click(bottom, false, scene)
if not is_equal_approx(scene.get_node("Camera2D").zoom.x, zoom_before): if not is_equal_approx(scene.get_node("Camera2D").zoom.x, zoom_before):
return _fail("pressing the fader also moved the camera behind it") return _fail("pressing the fader also moved the camera behind it")
if mouse_combat.firing:
return _fail("the fader's press reached the guns behind it")
mouse_combat.pointer_mode = mouse_combat.PointerMode.AUTO
print("the press stops at the fader; the world behind it is untouched") print("the press stops at the fader; the world behind it is untouched")
# --- C/D/E. the formation editor ------------------------- # --- C/D/E. the formation editor -------------------------

View file

@ -21,8 +21,7 @@ extends SceneTree
# B. each supplied office adds exactly one berth, and the cap bites # B. each supplied office adds exactly one berth, and the cap bites
# C. an office that cannot pay loses its vessel, and says so # C. an office that cannot pay loses its vessel, and says so
# D. a restocked office goes back to work without being rebuilt # D. a restocked office goes back to work without being rebuilt
# E. the wedge has a place for every slot, however far out it is # E. capacity can never outrun the formation's defined positions
# F. and nothing caps the fleet at the twenty positions the wedge authors
var game_state: Node var game_state: Node
var scene: Node var scene: Node
@ -108,35 +107,10 @@ func _process(delta: float) -> bool:
GameState.BASE_FLEET_SIZE + i + 1]) GameState.BASE_FLEET_SIZE + i + 1])
print("3 supplied offices -> %d berths" % game_state.max_fleet_size()) print("3 supplied offices -> %d berths" % game_state.max_fleet_size())
# --- E. the wedge has a place for every slot ------------------ # --- E. and never more than the formation can seat ------------
# The formation used to be the fleet's real ceiling: it defined if game_state.max_fleet_size() > GameState.DEFAULT_FORMATION.size():
# twenty positions and capacity clamped itself to them. It now return _fail("capacity %d exceeds the %d positions the formation defines" \
# continues by rule instead, so what is pinned here is that the % [game_state.max_fleet_size(), GameState.DEFAULT_FORMATION.size()])
# rule keeps the authored shape - the far slots are distinct,
# and each pair still steps 30 out and 40 back from the last.
var authored: int = GameState.DEFAULT_FORMATION.size()
var probe: int = authored * 3
var seen: Dictionary = {}
for i in range(probe):
var slot: Vector2 = game_state.formation_slot(i)
var key: String = "%.1f,%.1f" % [slot.x, slot.y]
if seen.has(key):
return _fail("slot %d sits on top of slot %d at %s" % [i, int(seen[key]), key])
seen[key] = i
for i in range(authored):
if not game_state.formation_slot(i).is_equal_approx(GameState.DEFAULT_FORMATION[i]):
return _fail("growing the wedge moved authored slot %d to %s" \
% [i, game_state.formation_slot(i)])
for i in range(authored, probe - 2):
var here: Vector2 = game_state.formation_slot(i)
var behind: Vector2 = game_state.formation_slot(i + 2)
var step: Vector2 = Vector2(GameState.FORMATION_PAIR_STEP.x * signf(here.x),
GameState.FORMATION_PAIR_STEP.y)
if not (behind - here).is_equal_approx(step):
return _fail("slot %d steps %s from slot %d, expected %s" \
% [i + 2, behind - here, i, step])
print("the wedge continues past its %d authored slots, %d checked and all distinct" \
% [authored, probe])
scene = load("res://world/Main.tscn").instantiate() scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene) root.add_child(scene)
@ -213,32 +187,6 @@ func _process(delta: float) -> bool:
return _fail("a supplied office lost its vessel anyway") return _fail("a supplied office lost its vessel anyway")
print("restocked and re-crewed: the office pays and the vessel stays") print("restocked and re-crewed: the office pays and the vessel stays")
# --- F. and nothing caps the fleet at twenty ------------------
# Offices past the wedge's authored positions buy real berths,
# and the fleet really flies them: every hull gets a place of
# its own rather than piling onto the last defined slot.
var target: int = GameState.DEFAULT_FORMATION.size() + 4
while game_state.max_fleet_size() < target:
var extra: int = game_state.add_building(GameState.BUILDING_FLEET_OFFICE,
Vector2(600.0 * float(game_state.max_fleet_size()), 900.0))
_stock(extra, 500)
if game_state.max_fleet_size() != target:
return _fail("%d supplied offices gave %d berths, expected %d" \
% [game_state.fleet_offices().size(), game_state.max_fleet_size(), target])
while fleet.ships.size() < target:
if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
return _fail("berth %d could not be crewed" % (fleet.ships.size() + 1))
var places: Dictionary = {}
for i in range(fleet.ships.size()):
var offset: Vector2 = fleet.formation_offsets[i]
var place: String = "%.1f,%.1f" % [offset.x, offset.y]
if places.has(place):
return _fail("vessel %d flies the same place as vessel %d" \
% [i, int(places[place])])
places[place] = i
print("%d berths, %d vessels, %d distinct places - no ceiling at %d" \
% [target, fleet.ships.size(), places.size(), GameState.DEFAULT_FORMATION.size()])
print("TEST_FLEET_GROWTH: PASS") print("TEST_FLEET_GROWTH: PASS")
quit(0) quit(0)
return true return true

View file

@ -1 +0,0 @@
uid://bvb4aa4k1702

View file

@ -1 +0,0 @@
uid://ctxpryt5rqddw

View file

@ -1 +0,0 @@
uid://ntytohsnghw3

View file

@ -37,8 +37,8 @@ func _on_fleet_add_requested(vessel_type: String) -> void:
if fleet.add_ship(vessel_type): if fleet.add_ship(vessel_type):
menu.set_status("Added %s" % GameState.vessel_name(vessel_type)) menu.set_status("Added %s" % GameState.vessel_name(vessel_type))
else: else:
menu.set_status("Fleet is full at %d — every berth past the first is a supplied Fleet Office" \ menu.set_status("Fleet is full — unlock all skills for up to %d slots" \
% GameState.max_fleet_size()) % GameState.DEFAULT_FORMATION.size())
func _on_fleet_remove_requested(vessel_type: String) -> void: func _on_fleet_remove_requested(vessel_type: String) -> void:

View file

@ -38,7 +38,7 @@ func _centre() -> Vector2:
func _slot_screen_pos(i: int) -> Vector2: func _slot_screen_pos(i: int) -> Vector2:
return _centre() + GameState.formation_slot(i) * scale_factor return _centre() + GameState.formation[i] * scale_factor
func _screen_to_formation(p: Vector2) -> Vector2: func _screen_to_formation(p: Vector2) -> Vector2:

View file

@ -101,28 +101,6 @@ func handle_mouse_motion(event: InputEventMouseMotion) -> bool:
return true return true
# ...and listening for those events HERE rather than being handed them by
# GameController, which is what #118 did.
#
# Unhandled input reaches the last nodes in the tree first, and Main - where
# that dispatch lives - is the root, so it is served last of all.
# MouseCombatController is a child of it and claims every left press to fire
# the fleet's guns, which meant a click on the fader shot at the fader
# instead of taking hold of it: the knob never moved on a desktop. The UI
# layer sits after the combat controllers, so asking for the event here puts
# the control the player is actually pointing at ahead of the guns behind it.
#
# Touch still comes through GameController: nothing competes for a finger,
# and the joystick/fire stick ordering it arbitrates is real.
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if handle_mouse_button(event):
get_viewport().set_input_as_handled()
elif event is InputEventMouseMotion:
if handle_mouse_motion(event):
get_viewport().set_input_as_handled()
func handle_touch(event: InputEventScreenTouch) -> bool: func handle_touch(event: InputEventScreenTouch) -> bool:
return _press(event.pressed, event.position, event.index) return _press(event.pressed, event.position, event.index)

View file

@ -96,7 +96,7 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO,
spawn_types.append(GameState.VESSEL_INTERCEPTOR) spawn_types.append(GameState.VESSEL_INTERCEPTOR)
var n: int = clamp(spawn_types.size(), 1, GameState.max_fleet_size()) var n: int = clamp(spawn_types.size(), 1, GameState.max_fleet_size())
for i in range(n): for i in range(n):
var offset: Vector2 = GameState.formation_slot(i) var offset: Vector2 = GameState.formation[i]
var ship: Area2D = ship_scene.instantiate() var ship: Area2D = ship_scene.instantiate()
ship.vessel_type = spawn_types[i] ship.vessel_type = spawn_types[i]
ship.position = center + offset ship.position = center + offset
@ -178,7 +178,7 @@ func add_ship(vessel_type: String = GameState.VESSEL_INTERCEPTOR) -> bool:
if not GameState.vessel_is_fleet_eligible(vessel_type) \ if not GameState.vessel_is_fleet_eligible(vessel_type) \
or ships.size() >= GameState.max_fleet_size(): or ships.size() >= GameState.max_fleet_size():
return false return false
var offset: Vector2 = GameState.formation_slot(ships.size()) var offset: Vector2 = GameState.formation[ships.size()]
var ship: Area2D = ship_scene.instantiate() var ship: Area2D = ship_scene.instantiate()
# Before add_child, so Ship._ready() builds the right vessel - the shield # Before add_child, so Ship._ready() builds the right vessel - the shield
# ring is created there and only for a shield vessel. # ring is created there and only for a shield vessel.
@ -222,7 +222,9 @@ func apply_formation() -> void:
# Without this, editing the layout at a command centre (or pivoting it) # Without this, editing the layout at a command centre (or pivoting it)
# only affected ships built later, which read as the edit doing nothing. # only affected ships built later, which read as the edit doing nothing.
for i in range(ships.size()): for i in range(ships.size()):
formation_offsets[i] = GameState.formation_slot(i) if i >= GameState.max_fleet_size():
break
formation_offsets[i] = GameState.formation[i]
_assign_slot_targets() _assign_slot_targets()