Game speed control: pause with two owners, and speed presets #114
No reviewers
Labels
No labels
assets
bug
duplicate
enhancement
help wanted
invalid
question
testing
ui
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
liamjd/UntitledColonyBuilder!114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "113-game-speed-control"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #113.
GameClockgains a player-facing control surface: pause with two independent owners, four speed presets, and signals a panel can render from. Keyboard and on-screen controls both drive the same API.Pause has two owners and one answer
Before this,
_on_main_menu_changed()assignedpauseddirectly. A player who paused deliberately and then opened and closed the menu was silently resumed — the menu'sfalseoverwrote the player'strue.pausedis now derived and not assignable:player_paused or menu_paused. The fix is not "assign more carefully"; it is that pause has two inputs and one answer, so a third owner later (a threat alert, a modal) costs one more input rather than another ordering bug.paused_changedreports the derived value, not the input that moved. Two owners holding the simulation is still one stop, so opening the menu over an already-paused game emits nothing, and the player releasing their pause while the menu is still open emits nothing either. The first draft emitted from each setter and got both of those backwards — the second case announcedfalsewhile the game was still frozen.Consequently the nine
clock.paused = ...writes intest_game_clock.gdmove toclock.player_paused, and that file's API note now points at the new spec for how the two combine.Speed
SPEED_PRESETS = [1.0, 2.0, 5.0, 10.0], withset_speed_preset(index)as the single placespeed_index,speedandspeed_changedmove together.faster()andslower()are steps through it, so the clamp and the did-it-actually-change guard cannot drift apart.Presets clamp rather than wrap: dropping a player from maximum speed to 1x on one extra press reads as the game stalling. 1x is the floor — pause is a separate control, not 0x.
speedremains a free float off the preset list, so 0.5x slow motion and any future modifier do not have to become presets.The number row selects by position, not by value:
game_speed_1is the first preset rather than 1x, so a balance pass that changes the values does not leave a key naming a speed that no longer exists. Those four actions were previously registered inproject.godotand handled nowhere.Speed never changes what a tick is. Ten seconds at 1x and one second at 10x produce the same tick sequence. That is the property the deterministic threat timeline rests on, and the reason
ticks_per_secondis frozen mid-run whilespeedis not.Decision: ticks-per-frame is left uncapped
advance()is unchanged and still drains every tick it owes inside one frame. Capping would smooth high speeds but let the clock fall behind wall time under load, and the honest version is easier to reason about while there is nothing expensive on the tick. Worth revisiting once #75's work loop runs per tick — that is the path that will hitch first at 10x.Verification
513 passing, 0 failing, 1 pendingacross 514 tests. The pending is the pre-existing #85 marker intest_task_tree.gd.test/unit/test_game_clock_controls.gdadds 32 tests: pause ownership and precedence,toggle_pause()touching only the player's half, preset stepping and clamping,set_speed_preset()direct selection, signal firing on the derived value only, and whatreset()does to both.test_game_clock.gdcontinues to own the accumulator, tick counting and re-entrancy.Manually verified headed: keyboard and button controls, the label, and the pause glyph.
Two bugs found in the headed pass that no unit test could see
Space was ambiguous after clicking a speed button.
game_pauseis bound to Space, Godot's defaultui_acceptalso includes Space, and the buttons defaulted toFOCUS_ALL. So clicking+gave it focus, and the next Space both resumed the game and re-pressed the focused button — one extra speed step, or two if the key repeated, sinceBaseButton'sui_accepthandling does not filter echo the wayis_action_pressed()does. Fixed withfocus_mode = 0on the three HUD buttons: they are pointer targets, and the keyboard equivalents already exist as real actions.The speed label was written from half the state. Two handlers each wrote it from their own signal, so pressing speed-up while paused replaced "Paused" with "3.0x" on a frozen simulation — the same two-inputs-one-answer problem as
pauseditself, one layer up in the view. Now one_render_speed()reads both values, connected to both signals viaunbind(1)and called once in_ready()for the startup paint, which was otherwise showing the scene's hardcoded "1.0x" over a game that starts paused behind the menu.Not done here
.tscnwiring are verified headed, per CLAUDE.md.test_game_clock_controls.gd'safter_eachrestoresGameState.main_menu = falserather than the captured value.GameStateandGameClockare both real autoloads, so flipping the menu here pauses the global clock, andpawn_view.gdreadsGameClock.delta_for()every frame — restoring the capturedtrueleft ten pawn-movement tests failing with "the pawn didn't move". The hazard predates this branch and is worth its own issue: the pawn tests depend on global clock state and never say so, so the symptom lands nowhere near the cause.🤖 Generated with Claude Code