Game speed controls: pause, 1x and fast-forward #103

Closed
opened 2026-09-17 07:47:28 +01:00 by liamjd · 1 comment
Owner

The colony runs at two real minutes per in-game day and roughly two hours per six-month contract (see docs/colony-game-time-and-calendar.md). That pacing is deliberate, but it means long stretches where nothing needs a decision, and short stretches during a threat window where the player wants to watch closely. Both directions need a control.

This is a player-facing feature, not only a debug convenience — but the debug need is what surfaced it, and is the more urgent half.

The clock already supports this

GameClock needs no changes. speed: float and paused: bool are plain properties, delta_for() exists for anything simulating, and #65 already pinned the awkward parts with tests:

  • changing speed keeps the banked fraction of a tick, so scrubbing speed does not drift the clock
  • speed applies from the moment it changes, not retroactively
  • speed = 0.0 behaves as a pause

So this issue is input actions, a HUD widget, and two decisions — not clock work.

Use the right knob

speed, never ticks_per_second. They look interchangeable and are not:

what it is who moves it
GameClock.speed how fast game time runs the player, at any moment
GameClock.ticks_per_second simulation fidelity a developer, tuning pawn smoothness

ticks_per_second is an engineering constant that content will be calibrated against, is rejected once the clock has started, and is pinned by test_ticks_per_second_defaults_to_ten. During #87 it was edited in source to watch the contract bar move, which broke that test and would have shipped a 200x game had it not been caught. A runtime speed control removes the temptation.

Two things that need deciding

1. paused currently has exactly one writer, and that is load-bearing

GameClock._on_main_menu_changed() sets paused = enabled unconditionally. Add a player-facing pause and there are two writers, so closing the main menu un-pauses a game the player had deliberately paused. The bug is subtle and will be blamed on the menu.

Options: a pause-reason set (_pause_reasons: Dictionary, paused if non-empty), or a separate player_paused flag OR-ed with the menu one. The reason-set generalises better — a threat alert or a modal dialog will each want to pause without fighting the others — but it is more machinery than one extra bool. Worth picking deliberately rather than by accident.

2. reset() does not touch speed

GameClock.reset() zeroes _now and _accumulator only, so a second contract inherits the speed the last one ended on. Landing a new colony at 3x is probably wrong; landing it paused is definitely wrong. Decide whether reset() should restore 1x, and say so in a test either way — #87 added tests for exactly this class of "the autoload outlives the run" bug.

Proposed speeds

Pause, 1x, 2x, 3x. Three forward speeds is what the pacing table supports — a day is 2 min / 1 min / 40 s — and more than that outruns the player's ability to react to a threat. Deliberately not offering an arbitrary slider.

Bind to speed_pause, speed_normal, speed_fast, speed_faster as new input actions (the project already declares its actions in project.godot; see #78). Spacebar for pause and 1/2/3 are the genre convention.

AC

  • Input actions for pause and each speed, declared in project.godot
  • A HUD control showing the current speed, with the active one visibly selected — the player must be able to tell 1x from 3x without pressing anything
  • Player pause and main-menu pause do not clobber each other; closing the menu restores whatever the player had set
  • Decided and tested: what reset() does to speed on a new run
  • Unit tests for the pause-reason logic, whichever shape it takes
  • A debug-only over-speed for development (e.g. 50x behind the debug flag) so that watching a month pass never requires editing ticks_per_second again

Not in scope: slow motion below 1x, and per-system time scaling (animations continuing while the sim is paused).

Blocks nothing, but anything needing visual verification over game-months — the threat timeline especially — is painful without it.

The colony runs at two real minutes per in-game day and roughly two hours per six-month contract (see `docs/colony-game-time-and-calendar.md`). That pacing is deliberate, but it means long stretches where nothing needs a decision, and short stretches during a threat window where the player wants to watch closely. Both directions need a control. This is a **player-facing feature**, not only a debug convenience — but the debug need is what surfaced it, and is the more urgent half. ## The clock already supports this `GameClock` needs no changes. `speed: float` and `paused: bool` are plain properties, `delta_for()` exists for anything simulating, and #65 already pinned the awkward parts with tests: - changing speed keeps the banked fraction of a tick, so scrubbing speed does not drift the clock - speed applies from the moment it changes, not retroactively - `speed = 0.0` behaves as a pause So this issue is input actions, a HUD widget, and two decisions — not clock work. ## Use the right knob **`speed`, never `ticks_per_second`.** They look interchangeable and are not: | | what it is | who moves it | |---|---|---| | `GameClock.speed` | how fast game time runs | the player, at any moment | | `GameClock.ticks_per_second` | simulation fidelity | a developer, tuning pawn smoothness | `ticks_per_second` is an engineering constant that content will be calibrated against, is rejected once the clock has started, and is pinned by `test_ticks_per_second_defaults_to_ten`. During #87 it was edited in source to watch the contract bar move, which broke that test and would have shipped a 200x game had it not been caught. A runtime speed control removes the temptation. ## Two things that need deciding ### 1. `paused` currently has exactly one writer, and that is load-bearing `GameClock._on_main_menu_changed()` sets `paused = enabled` unconditionally. Add a player-facing pause and there are two writers, so closing the main menu un-pauses a game the player had deliberately paused. The bug is subtle and will be blamed on the menu. Options: a pause-reason set (`_pause_reasons: Dictionary`, paused if non-empty), or a separate `player_paused` flag OR-ed with the menu one. The reason-set generalises better — a threat alert or a modal dialog will each want to pause without fighting the others — but it is more machinery than one extra bool. Worth picking deliberately rather than by accident. ### 2. `reset()` does not touch `speed` `GameClock.reset()` zeroes `_now` and `_accumulator` only, so a second contract inherits the speed the last one ended on. Landing a new colony at 3x is probably wrong; landing it paused is definitely wrong. Decide whether `reset()` should restore 1x, and say so in a test either way — #87 added tests for exactly this class of "the autoload outlives the run" bug. ## Proposed speeds Pause, 1x, 2x, 3x. Three forward speeds is what the pacing table supports — a day is 2 min / 1 min / 40 s — and more than that outruns the player's ability to react to a threat. Deliberately not offering an arbitrary slider. Bind to `speed_pause`, `speed_normal`, `speed_fast`, `speed_faster` as new input actions (the project already declares its actions in `project.godot`; see #78). Spacebar for pause and `1`/`2`/`3` are the genre convention. ## AC - [x] Input actions for pause and each speed, declared in `project.godot` - [x] A HUD control showing the current speed, with the active one visibly selected — the player must be able to tell 1x from 3x without pressing anything - [x] Player pause and main-menu pause do not clobber each other; closing the menu restores whatever the player had set - [x] Decided and tested: what `reset()` does to `speed` on a new run - [x] Unit tests for the pause-reason logic, whichever shape it takes - [x] A debug-only over-speed for development (e.g. 50x behind the debug flag) so that watching a month pass never requires editing `ticks_per_second` again Not in scope: slow motion below 1x, and per-system time scaling (animations continuing while the sim is paused). Blocks nothing, but anything needing visual verification over game-months — the threat timeline especially — is painful without it.
Author
Owner

Closed in PR #104

Closed in PR #104
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder#103
No description provided.