Calendar: convert ticks to an in-game date, and fix the units of time #87
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
#65 gives us a monotonic tick counter. Everything the design needs from time beyond that — day/night, eclipses, tidal peaks, seasons, and the forecast the player buys sensor tiers to see — is a pure function of
GameClock.now. This issue adds that function and the constants it reads.Nothing here needs new state, and
advance()does not change. It is deliberately filed ahead of the threat system because the threat timeline cannot be designed until the units exist.Proposed constants
ticks_per_second= 10TICKS_PER_DAY= 1200TICKS_PER_DAY / (tps * speed)These two must be independent constants. Defining
TICKS_PER_DAY = 120 * ticks_per_secondmeans that tuning tps later for pawn-movement smoothness silently rewrites every authored duration in the game.Day divided into 24 hours of 50 ticks. 1200 divides by 2, 3, 4, 6, 8, 12 and 24, which matters when placing eclipse windows on fractions of a day.
What that buys:
Seasons cannot fit inside a run
Per
docs/colony-builder-ideas.md, the long cycle is the gas giant's eccentric orbit around the star — years, not days. Against a ~60-day contract the player experiences a slice of a season and never a whole one.But for gameplay reasons, I am willing to compress this - suspension of disbelief and all that.
So the calendar must be queried at an absolute moon-date the run is seeded into ("you land in late winter"), not counted from tick 0 of the run. A run therefore has both a run-relative day number (for the contract deadline and the UI) and an absolute date (for the threat timeline). Getting this wrong is cheap now and expensive after the forecast UI exists.
AC
GameDate(or equivalent) that convertsnow-> day / hour / phase, and back:ticks_until(day)for the forecast queryticks_per_secondbecomes aconst(or is settable only before the first tick) — a mid-run change invalidates every computed forecast and desyncs a replayGameClock.reset()zeroes_nowand_accumulator, satisfying #65's "starts at 0 on a new run": the autoload outlives a run, so landing a second contract currently starts on day 400ticks_untilagreeing with the forward conversiondocs/, since they are pacing decisions and not implementation detailsNot in scope: rendering a day/night cycle, the threat timeline itself, and sensor-tier lead time. This is the substrate all three need.
Depends on #65. Blocks the
build_timeunit issue, which needsTICKS_PER_DAY.Decisions from the design conversation
Recorded here rather than rewritten into the body above, so the original reasoning stays legible. Everything below refines the AC; none of it removes one.
The calendar is the orbital mechanics, and a contract is six orbits
A contract is ~6 in-game months and ~2 hours of real play, with 4-6 threat events inside it. That only reconciles with the 2-hour figure already in this issue if a month is 10 days — which is the physically correct answer rather than a fudge, because on a moon the "month" is the orbital period around the gas giant.
A 60-day contract is then exactly 72,000 ticks = 2 hours at 1x, and spans two seasons, so "you land in late winter" is an arc the player actually experiences rather than a label. One significant event per orbit gives 6 per contract, the top of the 4-6 range, with no random director needed.
4 seasons x 3 months x 10 days divides evenly in both directions, which is what keeps
ticks_untilexact.The calendar is a projection, never an accumulator
All arithmetic happens in ticks.
GameDateis produced bydate_at(tick)and is read-only. There is deliberately no "advance this date by a day" function, because a second code path for time is what makes round-trips drift. This is what makes the round-trip AC provable rather than approximately true.One crossing point between run-relative and absolute
GDScript cannot give absolute ticks and run-relative ticks distinct types - both are
int, and the compiler will happily let them be mixed. The substitute: the additionrun_epoch_tick + GameClock.nowappears exactly once, in a named accessor onGameState, and no other file writes that+. Calendar functions take an absolute tick and say so in the parameter name.run_epoch_ticklives onGameStatebesiderun_seed(it is run configuration, seeded deterministically), soGameClocknever learns the calendar exists and stays a pure tick source.Constants live with the calendar, not the clock
TICKS_PER_DAYmoves offGameClocktoGameCalendar(common/game_calendar.gd). DerivingTICKS_PER_DAYfromTICKS_PER_HOURis fine and is not what this issue warned against - both are design constants. The prohibition is only on reaching across toticks_per_second, which is engineering.Two pacing knobs, two jobs: in-game durations are retuned via the calendar constants, real-world pace via
ticks_per_second/speed. Neither disturbs the other.The HUD shows contract progress, not a date
Top-right shows "Month 3 of 6" plus a day/night indicator, framing time as "how much of my contract is left" - the decision the player is actually making. Minutes are not tracked or displayed at all; days and hours exist in the model but not on screen. The absolute moon-date stays available through the API for the threat timeline, satisfying "both available and clearly distinguished" structurally rather than visually.
Two gaps this opens, neither previously in scope here:
GameState.contract_days = 60as a named placeholder pointing at the future contract resource.Already satisfied by #65
reset()zeroes_nowand_accumulator-globals/GameClock.gd:49ticks_per_secondis settable only before the first tick -globals/GameClock.gd:8One wrinkle worth knowing: that guard tests
now > 0, andreset()putsnowback to 0, so tps becomes mutable again between runs. That is probably the desired behaviour (a new run is a new forecast) but it is currently accidental rather than stated.Knock-on for #88
TICKS_PER_DAYmoves toGameCalendar, so #88's conversion readsGameCalendar.TICKS_PER_DAYrather thanGameClock.TICKS_PER_DAY. #88's own reasoning - effort denominated in pawn-ticks, authored asbuild_days: float- stands unchanged and is the right call; see the comment there for what the 10-day orbit does to those numbers.