diff --git a/controllers/BuildController.gd b/controllers/BuildController.gd index b125acd..42cd7e6 100644 --- a/controllers/BuildController.gd +++ b/controllers/BuildController.gd @@ -839,7 +839,7 @@ func _on_hangar_crew_type(selection: Variant) -> void: if vessel_type == "": return 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()]) return var stored_index: int = GameState.stored_vessels.find(vessel_type) diff --git a/controllers/GameController.gd b/controllers/GameController.gd index 168774c..6e2aa03 100644 --- a/controllers/GameController.gd +++ b/controllers/GameController.gd @@ -383,14 +383,14 @@ func _unhandled_input(event: InputEvent) -> void: _handle_key(event) elif event is InputEventMouseMotion: _note_keyboard() + if speed_fader != null: + speed_fader.handle_mouse_motion(event) elif event is InputEventMouseButton: _note_keyboard() - # The speed fader used to be driven from here, and could not work: - # MouseCombatController is dispatched before this node (it is a - # child, and this is the root) and marks every left press handled to - # fire the guns, so the press never arrived. It listens for its own - # mouse events now - see SpeedFader._unhandled_input - and claims - # them from the UI layer, ahead of the combat controllers. + # #118: the fader gets first refusal, so dragging the boost does not + # also pan or zoom the world behind it. + if speed_fader != null and speed_fader.handle_mouse_button(event): + return if not event.pressed: return # Wheel zoom, the desktop equivalent of the pinch. diff --git a/core/GameState.gd b/core/GameState.gd index 15be5b5..f804ecf 100644 --- a/core/GameState.gd +++ b/core/GameState.gd @@ -951,16 +951,11 @@ func _advance_fleet_office_upkeep(delta: float) -> void: 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: if intro_fleet_size > 0: - return intro_fleet_size - return BASE_FLEET_SIZE + supplied_fleet_office_count() + return mini(intro_fleet_size, DEFAULT_FORMATION.size()) + return mini(BASE_FLEET_SIZE + supplied_fleet_office_count(), + DEFAULT_FORMATION.size()) 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 # keeps a deliberate shape as it grows rather than having positions invented # at unlock time. -# Twenty slots are AUTHORED. They are not a cap: the fleet grows as far as -# the offices supply it, and formation_slot() continues the wedge past the -# end of this array by the same rule the rows here already follow - each -# further pair steps 30 further out and 40 further back, so the wedge that -# arrives at fifty is the one that left at three. +# Twenty slots, because max_fleet_size() caps itself on this array's length - +# so the wedge has to be long enough for every doctrine to be worth buying. +# Each further pair continues the same V rather than starting a new shape: +# the rows step 30 out and 40 back, which is the rule the first ten already +# followed, so the wedge that arrives at twenty is the one that left at three. const DEFAULT_FORMATION: Array = [ Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32), Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56), @@ -2819,43 +2814,9 @@ const DEFAULT_FORMATION: Array = [ 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: 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: @@ -2863,7 +2824,6 @@ func rotate_formation(radians: float) -> void: # and only changing which way it faces. for i in range(formation.size()): formation[i] = formation[i].rotated(radians) - formation_rotation = wrapf(formation_rotation + radians, -PI, PI) # --- Extractors ------------------------------------------------------------ diff --git a/core/SaveGame.gd b/core/SaveGame.gd index 7003d37..c1abc5d 100644 --- a/core/SaveGame.gd +++ b/core/SaveGame.gd @@ -47,7 +47,7 @@ const FIELDS: Array = [ "unlocked", "research_points", "vessel_levels", # The fleet itself. "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", "max_fleet_speed_multiplier", "fleet_speed_multiplier", "build_time_scale", # The world as the player has seen it. diff --git a/tests/_lab_planet_visual_preview.gd.uid b/tests/_lab_planet_visual_preview.gd.uid deleted file mode 100644 index ac21c5a..0000000 --- a/tests/_lab_planet_visual_preview.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bk3grd8nn8pj1 diff --git a/tests/test_building_separation.gd.uid b/tests/test_building_separation.gd.uid deleted file mode 100644 index a32d370..0000000 --- a/tests/test_building_separation.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ckdulc4563wt5 diff --git a/tests/test_desktop_controls.gd b/tests/test_desktop_controls.gd index 895ff43..4fad953 100644 --- a/tests/test_desktop_controls.gd +++ b/tests/test_desktop_controls.gd @@ -10,19 +10,16 @@ extends SceneTree # The boost was unreachable on a desktop and the formation could not be # rearranged at all. # -# Driven through the REAL routing in each case - the viewport itself for the -# fader, BuildController for the editor - rather than by calling the -# controls directly, because the routing is what was missing: a test that -# called handle_mouse_button() itself would have passed against both -# 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. +# Driven through the REAL routing in each case (GameController for the +# fader, BuildController for the editor) rather than by calling the controls +# directly, because the routing is what was missing - a test that called +# handle_mouse_button() itself would have passed against both reports. # # Covers: # A. #118 - a click on the fader takes hold of it, dragging moves the # boost, and releasing lets go -# B. the press stops at the fader: not the camera behind it, and not the -# guns, which claim every other left press on a desktop +# B. the fader claims the press, so dragging the boost does not also zoom +# the world behind it # C. #121 - a click grabs a formation slot, moving drags it, and the # formation really changes # D. releasing drops the slot, and moving afterwards does not keep dragging @@ -70,31 +67,6 @@ func _fail(message: String) -> bool: 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: var event: InputEventMouseButton = InputEventMouseButton.new() 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") # --- 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, fader.track_margin + 2.0) var bottom: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5, fader.size.y - fader.track_margin - 2.0) - _push_click(bottom, true) + _click(bottom, true, scene) if not fader.is_active(): 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 - _push_move(top) + _move(top, scene) var at_top: float = game_state.fleet_speed_multiplier if at_top <= at_bottom: 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(): 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): return _fail("the fader kept following the pointer after release") 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 ------------------------------ var zoom_before: float = scene.get_node("Camera2D").zoom.x - _push_click(bottom, true) - _push_click(bottom, false) + _click(bottom, true, scene) + _click(bottom, false, scene) if not is_equal_approx(scene.get_node("Camera2D").zoom.x, zoom_before): 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") # --- C/D/E. the formation editor ------------------------- diff --git a/tests/test_fleet_growth.gd b/tests/test_fleet_growth.gd index d194576..0aa3fcc 100644 --- a/tests/test_fleet_growth.gd +++ b/tests/test_fleet_growth.gd @@ -21,8 +21,7 @@ extends SceneTree # B. each supplied office adds exactly one berth, and the cap bites # C. an office that cannot pay loses its vessel, and says so # 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 -# F. and nothing caps the fleet at the twenty positions the wedge authors +# E. capacity can never outrun the formation's defined positions var game_state: Node var scene: Node @@ -108,35 +107,10 @@ func _process(delta: float) -> bool: GameState.BASE_FLEET_SIZE + i + 1]) print("3 supplied offices -> %d berths" % game_state.max_fleet_size()) - # --- E. the wedge has a place for every slot ------------------ - # The formation used to be the fleet's real ceiling: it defined - # twenty positions and capacity clamped itself to them. It now - # continues by rule instead, so what is pinned here is that the - # 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]) + # --- E. and never more than the formation can seat ------------ + if game_state.max_fleet_size() > GameState.DEFAULT_FORMATION.size(): + return _fail("capacity %d exceeds the %d positions the formation defines" \ + % [game_state.max_fleet_size(), GameState.DEFAULT_FORMATION.size()]) scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) @@ -213,32 +187,6 @@ func _process(delta: float) -> bool: return _fail("a supplied office lost its vessel anyway") 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") quit(0) return true diff --git a/tests/test_guidance_marker_redraw.gd.uid b/tests/test_guidance_marker_redraw.gd.uid deleted file mode 100644 index 9d42fd9..0000000 --- a/tests/test_guidance_marker_redraw.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bvb4aa4k1702 diff --git a/tests/test_main_menu_continue.gd.uid b/tests/test_main_menu_continue.gd.uid deleted file mode 100644 index 0bafa51..0000000 --- a/tests/test_main_menu_continue.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ctxpryt5rqddw diff --git a/tests/test_save_game.gd.uid b/tests/test_save_game.gd.uid deleted file mode 100644 index fb3efa0..0000000 --- a/tests/test_save_game.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ntytohsnghw3 diff --git a/ui/DevMenuButton.gd b/ui/DevMenuButton.gd index 0d9e28b..22dc7f8 100644 --- a/ui/DevMenuButton.gd +++ b/ui/DevMenuButton.gd @@ -37,8 +37,8 @@ func _on_fleet_add_requested(vessel_type: String) -> void: if fleet.add_ship(vessel_type): menu.set_status("Added %s" % GameState.vessel_name(vessel_type)) else: - menu.set_status("Fleet is full at %d — every berth past the first is a supplied Fleet Office" \ - % GameState.max_fleet_size()) + menu.set_status("Fleet is full — unlock all skills for up to %d slots" \ + % GameState.DEFAULT_FORMATION.size()) func _on_fleet_remove_requested(vessel_type: String) -> void: diff --git a/ui/FormationEditor.gd b/ui/FormationEditor.gd index ea01f72..79a05b8 100644 --- a/ui/FormationEditor.gd +++ b/ui/FormationEditor.gd @@ -38,7 +38,7 @@ func _centre() -> 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: diff --git a/ui/SpeedFader.gd b/ui/SpeedFader.gd index 7affc29..bebda79 100644 --- a/ui/SpeedFader.gd +++ b/ui/SpeedFader.gd @@ -101,28 +101,6 @@ func handle_mouse_motion(event: InputEventMouseMotion) -> bool: 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: return _press(event.pressed, event.position, event.index) diff --git a/vessels/Fleet.gd b/vessels/Fleet.gd index 19c5d28..25422b2 100644 --- a/vessels/Fleet.gd +++ b/vessels/Fleet.gd @@ -96,7 +96,7 @@ func spawn_ships(count: int, center: Vector2 = Vector2.ZERO, spawn_types.append(GameState.VESSEL_INTERCEPTOR) var n: int = clamp(spawn_types.size(), 1, GameState.max_fleet_size()) for i in range(n): - var offset: Vector2 = GameState.formation_slot(i) + var offset: Vector2 = GameState.formation[i] var ship: Area2D = ship_scene.instantiate() ship.vessel_type = spawn_types[i] 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) \ or ships.size() >= GameState.max_fleet_size(): return false - var offset: Vector2 = GameState.formation_slot(ships.size()) + var offset: Vector2 = GameState.formation[ships.size()] var ship: Area2D = ship_scene.instantiate() # Before add_child, so Ship._ready() builds the right vessel - the shield # 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) # only affected ships built later, which read as the edit doing nothing. 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()