aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/app.d.ts7
-rw-r--r--engine/career/career.ts2
-rw-r--r--engine/economics/affordability.ts3
-rw-r--r--engine/economics/economic.ts12
-rw-r--r--engine/entities/base.ts2
-rw-r--r--engine/entities/event-log-entry.ts2
-rw-r--r--engine/entities/flow-entry.ts2
-rw-r--r--engine/entities/institution.ts4
-rw-r--r--engine/entities/memoir.ts9
-rw-r--r--engine/entities/person-name.ts6
-rw-r--r--engine/entities/person.ts67
-rw-r--r--engine/entities/place.ts4
-rw-r--r--engine/entities/relationship.ts18
-rw-r--r--engine/entities/residence.ts2
-rw-r--r--engine/entities/routine.ts4
-rw-r--r--engine/entities/scheduled-event.ts4
-rw-r--r--engine/entities/world-event.ts6
-rw-r--r--engine/entities/world.ts18
-rw-r--r--engine/health/health.ts7
-rw-r--r--engine/health/mental-health.ts2
-rw-r--r--engine/state/dependency.ts3
-rw-r--r--engine/state/mood.ts1
-rw-r--r--engine/state/satisfaction.ts5
-rw-r--r--engine/traits/attachment.ts2
-rw-r--r--engine/traits/big-five.ts4
-rw-r--r--engine/traits/core-beliefs.ts4
-rw-r--r--engine/traits/dark-triad.ts6
-rw-r--r--engine/traits/orientation.ts2
-rw-r--r--engine/traits/values.ts4
-rw-r--r--hooks/client.ts4
-rw-r--r--persistence/db.ts8
-rw-r--r--persistence/schema.ts19
-rw-r--r--rng/seeded.ts9
-rw-r--r--routes/+layout.svelte1
-rw-r--r--routes/+layout.ts1
-rw-r--r--routes/+page.svelte1
-rw-r--r--tests/determinism/rng.test.ts8
-rw-r--r--tests/unit/time/calendar.test.ts8
-rw-r--r--tests/unit/time/gameTime.test.ts4
-rw-r--r--tests/unit/utils/result.test.ts1
-rw-r--r--time/calendar.ts67
-rw-r--r--time/gameTime.ts21
-rw-r--r--time/granularity.ts20
-rw-r--r--time/speed.ts12
-rw-r--r--utils/result/constructors.ts2
-rw-r--r--utils/result/map.ts4
-rw-r--r--utils/result/predicates.ts2
-rw-r--r--utils/result/types.ts4
-rw-r--r--utils/result/unwrap.ts2
49 files changed, 190 insertions, 220 deletions
diff --git a/app/app.d.ts b/app/app.d.ts
index a59afa2..242ec56 100644
--- a/app/app.d.ts
+++ b/app/app.d.ts
@@ -1,13 +1,6 @@
-// See https://svelte.dev/docs/kit/types#app.d.ts for details on these types.
-// Populate App interfaces as systems come online.
declare global {
namespace App {
- // interface Error {}
- // interface Locals {}
- // interface PageData {}
- // interface PageState {}
- // interface Platform {}
}
}
diff --git a/engine/career/career.ts b/engine/career/career.ts
index aa19584..e8a5af2 100644
--- a/engine/career/career.ts
+++ b/engine/career/career.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { InstitutionId } from '../entities/base'
+import type { InstitutionId } from '@hollowdark/engine/entities/base'
export type CareerTrajectory = 'rising' | 'steady' | 'declining' | 'stalled'
diff --git a/engine/economics/affordability.ts b/engine/economics/affordability.ts
index ef57d3f..827ccd8 100644
--- a/engine/economics/affordability.ts
+++ b/engine/economics/affordability.ts
@@ -1,8 +1,7 @@
/**
* The "What you could afford" surface on the Money screen. Hidden numbers
* resolve into qualitative feasibility labels so the player answers
- * "can I afford this?" without ever seeing a number (docs/09-economy.md
- * §"The 'What you could afford' design").
+ * "can I afford this?" without ever seeing a number.
*/
export type Feasibility =
| 'comfortable'
diff --git a/engine/economics/economic.ts b/engine/economics/economic.ts
index 1197cde..2d37ced 100644
--- a/engine/economics/economic.ts
+++ b/engine/economics/economic.ts
@@ -1,9 +1,8 @@
/**
- * Economic state. All numbers in "marks" (the world's universal currency).
- * The player never sees any of these — they surface as prose and qualitative
- * affordability context (docs/09-economy.md, ARCHITECTURE.md §14).
+ * A coarse wealth tier derived from the hidden economic state. Never
+ * displayed numerically; informs surface prose ("struggling", "comfortable",
+ * "old money").
*/
-
export type EconomicClass =
| 'destitute'
| 'struggling'
@@ -44,6 +43,11 @@ export interface Debt {
readonly apr: number
}
+/**
+ * Hidden economic state for a character. All numbers are in "marks" (the
+ * world's universal currency). Never surfaced to the player as digits —
+ * the simulation drives prose and qualitative affordability context.
+ */
export interface EconomicState {
readonly cashOnHand: number
readonly accounts: readonly Account[]
diff --git a/engine/entities/base.ts b/engine/entities/base.ts
index 84b9acf..c1cf6d8 100644
--- a/engine/entities/base.ts
+++ b/engine/entities/base.ts
@@ -4,7 +4,6 @@ import type { Brand } from '@hollowdark/utils/types/brand'
/**
* Branded IDs — string at runtime, distinct at compile time so a PersonId
* can't silently flow into a slot that expects a RelationshipId.
- * (utils/types/brand.ts)
*/
export type PersonId = Brand<string, 'PersonId'>
export type RelationshipId = Brand<string, 'RelationshipId'>
@@ -26,7 +25,6 @@ export type EntityKind = 'person' | 'relationship' | 'institution' | 'place' | '
* their specific id/kind pair — `Person extends BaseEntity<PersonId, 'person'>`.
* `deterministicSeed` powers lazy Tier 3 regeneration: given the seed, the
* entity's trajectory is fully reproducible without persisted state.
- * (ARCHITECTURE.md §26)
*/
export interface BaseEntity<Id extends string = string, K extends EntityKind = EntityKind> {
readonly id: Id
diff --git a/engine/entities/event-log-entry.ts b/engine/entities/event-log-entry.ts
index f31536d..1e107f4 100644
--- a/engine/entities/event-log-entry.ts
+++ b/engine/entities/event-log-entry.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { EventLogEntryId, PersonId } from './base'
+import type { EventLogEntryId, PersonId } from '@hollowdark/engine/entities/base'
/**
* Consequences logged against a specific event resolution. The shape is
diff --git a/engine/entities/flow-entry.ts b/engine/entities/flow-entry.ts
index bd781d5..ba2b11a 100644
--- a/engine/entities/flow-entry.ts
+++ b/engine/entities/flow-entry.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { FlowEntryId, PersonId, PlaceId, WorldEventId } from './base'
+import type { FlowEntryId, PersonId, PlaceId, WorldEventId } from '@hollowdark/engine/entities/base'
/**
* A compact snapshot of the context that produced a flow passage. Kept
diff --git a/engine/entities/institution.ts b/engine/entities/institution.ts
index 4b3c050..d89aac2 100644
--- a/engine/entities/institution.ts
+++ b/engine/entities/institution.ts
@@ -1,6 +1,6 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { BaseEntity, InstitutionId, PersonId, PlaceId } from './base'
-import type { CultureDescriptor } from './place'
+import type { BaseEntity, InstitutionId, PersonId, PlaceId } from '@hollowdark/engine/entities/base'
+import type { CultureDescriptor } from '@hollowdark/engine/entities/place'
export type InstitutionType =
| 'company'
diff --git a/engine/entities/memoir.ts b/engine/entities/memoir.ts
index c0f44f3..1f3f37f 100644
--- a/engine/entities/memoir.ts
+++ b/engine/entities/memoir.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { EventLogEntryId, MemoirId, PersonId } from './base'
+import type { EventLogEntryId, MemoirId, PersonId } from '@hollowdark/engine/entities/base'
export interface MemoirChapter {
readonly order: number
@@ -12,9 +12,10 @@ export interface MemoirChapter {
}
/**
- * Generated at character death. 15,000–30,000 words, 8–15 chapters. Persists
- * forever in the world's archive; descendants may find it on the Memoirs
- * shelf or referenced in flow (docs/14-memoirs.md, ARCHITECTURE.md §17).
+ * A character's generated life story. Produced at death, 15,000–30,000
+ * words across 8–15 chapters. Persists forever in the world's archive;
+ * descendants may find it on the Memoirs shelf or see it referenced in
+ * their own flow.
*/
export interface Memoir {
readonly id: MemoirId
diff --git a/engine/entities/person-name.ts b/engine/entities/person-name.ts
index 5f84b41..8c234dc 100644
--- a/engine/entities/person-name.ts
+++ b/engine/entities/person-name.ts
@@ -1,8 +1,8 @@
/**
* A character's name as tracked by the simulation. Surnames follow
- * inheritance rules per region/era (docs/19-names.md) — including keep-
- * maiden, hyphenate, matrilineal, and ad-hoc changes — so the structure
- * preserves both the current surname and the maiden form when relevant.
+ * inheritance rules per region and era — including keep-maiden, hyphenate,
+ * matrilineal, and ad-hoc changes — so the structure preserves both the
+ * current surname and the maiden form when relevant.
*/
export interface PersonName {
readonly given: string
diff --git a/engine/entities/person.ts b/engine/entities/person.ts
index beabede..1dc28a1 100644
--- a/engine/entities/person.ts
+++ b/engine/entities/person.ts
@@ -1,26 +1,26 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { CareerState } from '../career/career'
-import type { EconomicState } from '../economics/economic'
-import type { Condition, HealthState } from '../health/health'
-import type { MentalHealthState } from '../health/mental-health'
-import type { Dependency } from '../state/dependency'
-import type { MoodState } from '../state/mood'
-import type { SatisfactionProfile } from '../state/satisfaction'
-import type { Scar } from '../state/scar'
-import type { AttachmentDistribution } from '../traits/attachment'
-import type { BigFiveProfile } from '../traits/big-five'
-import type { CoreBeliefs } from '../traits/core-beliefs'
-import type { DarkTriadProfile } from '../traits/dark-triad'
-import type { SexualOrientation } from '../traits/orientation'
-import type { ValuesOrientation } from '../traits/values'
-import type { BaseEntity, PersonId, PlaceId, RelationshipId } from './base'
-import type { EventLogEntry } from './event-log-entry'
-import type { PersonName } from './person-name'
-import type { ReputationProfile } from './reputation'
-import type { ResidenceEntry } from './residence'
-import type { StatusDescriptor } from './status'
-
-/** Modes of death per docs/20-death-textures.md. */
+import type { CareerState } from '@hollowdark/engine/career/career'
+import type { EconomicState } from '@hollowdark/engine/economics/economic'
+import type { Condition, HealthState } from '@hollowdark/engine/health/health'
+import type { MentalHealthState } from '@hollowdark/engine/health/mental-health'
+import type { Dependency } from '@hollowdark/engine/state/dependency'
+import type { MoodState } from '@hollowdark/engine/state/mood'
+import type { SatisfactionProfile } from '@hollowdark/engine/state/satisfaction'
+import type { Scar } from '@hollowdark/engine/state/scar'
+import type { AttachmentDistribution } from '@hollowdark/engine/traits/attachment'
+import type { BigFiveProfile } from '@hollowdark/engine/traits/big-five'
+import type { CoreBeliefs } from '@hollowdark/engine/traits/core-beliefs'
+import type { DarkTriadProfile } from '@hollowdark/engine/traits/dark-triad'
+import type { SexualOrientation } from '@hollowdark/engine/traits/orientation'
+import type { ValuesOrientation } from '@hollowdark/engine/traits/values'
+import type { BaseEntity, PersonId, PlaceId, RelationshipId } from '@hollowdark/engine/entities/base'
+import type { EventLogEntry } from '@hollowdark/engine/entities/event-log-entry'
+import type { PersonName } from '@hollowdark/engine/entities/person-name'
+import type { ReputationProfile } from '@hollowdark/engine/entities/reputation'
+import type { ResidenceEntry } from '@hollowdark/engine/entities/residence'
+import type { StatusDescriptor } from '@hollowdark/engine/entities/status'
+
+/** The ten modes of death the simulation can resolve. */
export type DeathMode =
| 'expected_old_age'
| 'sudden_accident'
@@ -35,7 +35,7 @@ export type DeathMode =
/**
* The defining demographic facts of a birth, captured in prose-ready form.
* The familyContext string is the one-paragraph summary the opening scene
- * draws on (docs/17-first-hour.md §"Birth moment").
+ * draws on.
*/
export interface BirthRecord {
readonly date: GameTime
@@ -51,15 +51,15 @@ export interface DeathRecord {
}
/**
- * NPC simulation fidelity tier. 1 = full weekly simulation (~10-30 entities),
- * 2 = quarterly compressed, 3 = generated on demand from seed.
- * See docs/06-autonomy.md §"Tiered simulation fidelity".
+ * NPC simulation fidelity tier. 1 = full weekly simulation (~10–30
+ * close-orbit entities), 2 = quarterly compressed (dormant relatives,
+ * drifted friends), 3 = generated on demand from seed (everyone else).
*/
export type SimulationTier = 1 | 2 | 3
/**
* Person — the main simulated entity. Players and NPCs share this shape.
- * Fields are grouped by trait layer (docs/04-traits.md):
+ * Fields are grouped by trait layer:
*
* Layer 1 temperament (Big Five + Dark Triad) — mostly stable
* Layer 2 developmental (attachment, core beliefs, values, orientation)
@@ -75,65 +75,52 @@ export interface Person extends BaseEntity<PersonId, 'person'> {
readonly birth: BirthRecord
readonly death: DeathRecord | null
- // Layer 1 — temperament
readonly bigFive: BigFiveProfile
readonly darkTriad: DarkTriadProfile
- // Layer 2 — developmental
readonly attachment: AttachmentDistribution
readonly coreBeliefs: CoreBeliefs
readonly conscienceCapacity: number
readonly values: ValuesOrientation
readonly orientation: SexualOrientation
- // Layer 3 — fluctuating state
readonly mood: MoodState
readonly stress: number
readonly energy: number
readonly traumaLoad: number
readonly satisfaction: SatisfactionProfile
- // Layer 4 — acquired
readonly skills: ReadonlyMap<string, number>
readonly knowledge: ReadonlyMap<string, number>
readonly habits: readonly string[]
readonly dependencies: readonly Dependency[]
readonly scars: readonly Scar[]
- // Layer 5 — social
readonly reputation: ReputationProfile
readonly status: StatusDescriptor
readonly networkCapital: number
- // Health
readonly health: HealthState
readonly chronicConditions: readonly Condition[]
readonly mentalHealthState: MentalHealthState
- // Economic / career
readonly economic: EconomicState
readonly career: CareerState
- // Relationships + location
readonly relationshipIds: readonly RelationshipId[]
readonly currentPlaceId: PlaceId
readonly residenceHistory: readonly ResidenceEntry[]
- // Simulation metadata
readonly tier: SimulationTier
readonly lastSimulatedAt: GameTime
- // Event history
readonly eventLog: readonly EventLogEntry[]
readonly memorableMoments: readonly string[]
- // Player-character flags
readonly isPlayerCharacter: boolean
readonly playerCharacterStartedAt: GameTime | null
readonly playerCharacterEndedAt: GameTime | null
- // Family (denormalised for fast access; the authoritative source is the
- // Relationship entities keyed family_parent / family_child / family_spouse)
readonly parentIds: readonly [PersonId | null, PersonId | null]
readonly childIds: readonly PersonId[]
readonly spouseIds: readonly PersonId[]
diff --git a/engine/entities/place.ts b/engine/entities/place.ts
index 2a38c41..8ec1f9b 100644
--- a/engine/entities/place.ts
+++ b/engine/entities/place.ts
@@ -1,4 +1,4 @@
-import type { BaseEntity, PersonId, PlaceId } from './base'
+import type { BaseEntity, PersonId, PlaceId } from '@hollowdark/engine/entities/base'
export type PlaceType = 'region' | 'city' | 'neighborhood' | 'specific_location'
@@ -40,7 +40,6 @@ export interface Place extends BaseEntity<PlaceId, 'place'> {
readonly type: PlaceType
readonly parentPlaceId: PlaceId | null
- // Present for regions and cities; null for specific locations.
readonly culture: CultureDescriptor | null
readonly climate: ClimateDescriptor | null
readonly economy: EconomicCharacter | null
@@ -48,7 +47,6 @@ export interface Place extends BaseEntity<PlaceId, 'place'> {
readonly population: number
- // Present for specific locations (houses, workplaces); null for containers.
readonly ownerId: PersonId | null
readonly currentResidents: readonly PersonId[]
readonly propertyValue: number | null
diff --git a/engine/entities/relationship.ts b/engine/entities/relationship.ts
index ec00007..2a7710b 100644
--- a/engine/entities/relationship.ts
+++ b/engine/entities/relationship.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { BaseEntity, PersonId, RelationshipId } from './base'
+import type { BaseEntity, PersonId, RelationshipId } from '@hollowdark/engine/entities/base'
export type RelationType =
| 'family_parent'
@@ -30,10 +30,9 @@ export type RelationType =
export type RelationshipState = 'warm' | 'neutral' | 'strained' | 'ruptured' | 'dormant'
/**
- * The four axes of intimacy (docs/07-relationships.md §"Relationship state
- * vector"). Each 0–1. Tracked separately because a marriage can be high
- * on practical and low on emotional — that's a specific life texture, not
- * a compatibility score.
+ * The four axes of intimacy. Each 0–1. Tracked separately because a
+ * marriage can be high on practical and low on emotional — that's a
+ * specific life texture, not a compatibility score.
*/
export interface IntimacyAxes {
readonly emotional: number
@@ -88,9 +87,9 @@ export interface LoveLanguageMatrix {
}
/**
- * Asymmetric perception: A and B each have their own mental model of the
- * relationship. Large asymmetries fire scenes (confession, rebuff, shocked
- * realisation). See docs/07-relationships.md §"Asymmetric perception".
+ * Asymmetric perception: A and B each carry their own mental model of the
+ * relationship. Large asymmetries fire scenes — confession, rebuff,
+ * shocked realisation.
*/
export interface RelationshipPerception {
readonly perceivedType: RelationType
@@ -162,15 +161,12 @@ export interface Relationship extends BaseEntity<RelationshipId, 'relationship'>
readonly lastInteractionAt: GameTime
readonly interactionFrequency: number
- // Romantic-only details; null when the relationship isn't romantic.
readonly romanticPhase: RomanticPhase | null
readonly sexualActivity: SexualActivityState | null
readonly infidelityHistory: readonly InfidelityEvent[]
readonly commitmentLevel: number
- // Family-only.
readonly familyRelationType: FamilyRelation | null
- // Professional-only.
readonly workRelationType: WorkRelation | null
}
diff --git a/engine/entities/residence.ts b/engine/entities/residence.ts
index a052c42..83ddce8 100644
--- a/engine/entities/residence.ts
+++ b/engine/entities/residence.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { PlaceId } from './base'
+import type { PlaceId } from '@hollowdark/engine/entities/base'
/** A span of time a character lived at a specific place. Open-ended if the
* character still lives there. */
diff --git a/engine/entities/routine.ts b/engine/entities/routine.ts
index 251cbde..a7068c1 100644
--- a/engine/entities/routine.ts
+++ b/engine/entities/routine.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { PersonId, RoutineId } from './base'
+import type { PersonId, RoutineId } from '@hollowdark/engine/entities/base'
export type RoutineCategory = 'work' | 'relationships' | 'self' | 'home' | 'play' | 'service'
@@ -27,7 +27,7 @@ export interface RoutineItem {
/**
* Persistent weekly commitments for a character. Routines run silently in
* the flow stream — the player sets them once, they keep running until
- * changed (docs/05-time-system.md §"Routines and flow").
+ * changed.
*/
export interface Routine {
readonly id: RoutineId
diff --git a/engine/entities/scheduled-event.ts b/engine/entities/scheduled-event.ts
index 08be670..254e979 100644
--- a/engine/entities/scheduled-event.ts
+++ b/engine/entities/scheduled-event.ts
@@ -1,10 +1,10 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { PersonId, ScheduledEventId } from './base'
+import type { PersonId, ScheduledEventId } from '@hollowdark/engine/entities/base'
/**
* A future event trigger, either at a fixed time or when conditions hold.
* Taking a bribe at 30 might schedule a `bribery_audit` at +15 years with
- * elevated weight (ARCHITECTURE.md §6 §"Scheduled events").
+ * elevated weight.
*/
export interface ConditionalTrigger {
readonly kind: 'conditional'
diff --git a/engine/entities/world-event.ts b/engine/entities/world-event.ts
index 8b4e2a6..ca158cc 100644
--- a/engine/entities/world-event.ts
+++ b/engine/entities/world-event.ts
@@ -1,5 +1,5 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { BaseEntity, PersonId, PlaceId, WorldEventId } from './base'
+import type { BaseEntity, PersonId, PlaceId, WorldEventId } from '@hollowdark/engine/entities/base'
export type EventCategory =
| 'pandemic'
@@ -17,8 +17,8 @@ export type EventCategory =
export type SeverityLevel = 'mild' | 'moderate' | 'severe' | 'catastrophic'
/**
- * A named macro event decorating the timeline. See docs/02-world-events.md.
- * The character-level impact of each event is applied per-person via
+ * A named macro event decorating the timeline — a pandemic, war, cultural
+ * shift, economic crash. Character-level impact is applied per-person via
* templates referenced by contentRef; this entity is the world-scale record.
*/
export interface WorldEvent extends BaseEntity<WorldEventId, 'world_event'> {
diff --git a/engine/entities/world.ts b/engine/entities/world.ts
index 920fe05..8d0b328 100644
--- a/engine/entities/world.ts
+++ b/engine/entities/world.ts
@@ -1,11 +1,11 @@
import type { GameTime } from '@hollowdark/time/gameTime'
-import type { PersonId, PlaceId, WorldEventId, WorldId } from './base'
-import type { ScheduledEvent } from './scheduled-event'
+import type { PersonId, PlaceId, WorldEventId, WorldId } from '@hollowdark/engine/entities/base'
+import type { ScheduledEvent } from '@hollowdark/engine/entities/scheduled-event'
/**
- * Macro economic state tracked at the world scale. Individual characters'
- * economics are derived against this background (docs/09-economy.md
- * §"Macro economy").
+ * Macro economic state tracked at the world scale — inflation, employment,
+ * market index, recession depth. Individual characters' economics derive
+ * against this background.
*/
export interface MacroEconomicState {
readonly inflationAnnual: number
@@ -33,14 +33,14 @@ export interface CrisisState {
}
/**
- * The world container. One continuous world per player (ARCHITECTURE.md §16,
- * docs/16-world-continuity.md). Time in this world never resets once
- * created; successive player characters advance it forward.
+ * The world container. One continuous world per player. Time in this world
+ * never resets once created; successive player characters advance it forward.
+ * `createdAt` is a real-world ISO timestamp, not a `GameTime`.
*/
export interface World {
readonly id: WorldId
readonly seed: string
- readonly createdAt: string // ISO timestamp, real-world clock — not a GameTime
+ readonly createdAt: string
readonly currentGameTime: GameTime
diff --git a/engine/health/health.ts b/engine/health/health.ts
index e08433d..4c69d34 100644
--- a/engine/health/health.ts
+++ b/engine/health/health.ts
@@ -41,10 +41,9 @@ export interface Condition {
}
/**
- * A condition the simulation tracks but the character doesn't yet know about.
- * Surface paths include worsening symptoms, doctor visits, routine screens,
- * or incidental discovery (docs/08-mental-health.md §"Suicide risk" for the
- * comparable mental-health pattern; this covers physical equivalents).
+ * A condition the simulation tracks but the character doesn't yet know
+ * about. Surface paths include worsening symptoms, doctor visits, routine
+ * screens, or incidental discovery.
*/
export interface UndiagnosedCondition {
readonly id: string
diff --git a/engine/health/mental-health.ts b/engine/health/mental-health.ts
index 7130177..2e54e71 100644
--- a/engine/health/mental-health.ts
+++ b/engine/health/mental-health.ts
@@ -48,7 +48,7 @@ export interface Medication {
/**
* Hidden even from the character until crisis. The simulation knows; the
- * prose surfaces behaviour, not numbers (docs/08-mental-health.md §"Suicide").
+ * prose surfaces behaviour, never numbers.
*/
export interface SuicidalRisk {
readonly current: number
diff --git a/engine/state/dependency.ts b/engine/state/dependency.ts
index 47787da..e3aca72 100644
--- a/engine/state/dependency.ts
+++ b/engine/state/dependency.ts
@@ -1,8 +1,7 @@
import type { GameTime } from '@hollowdark/time/gameTime'
/**
- * A substance or behavioural dependency at a given stage of progression.
- * Stages mirror docs/08-mental-health.md §"Addiction modeled honestly":
+ * A substance or behavioural dependency at a given stage of progression:
* experimentation → regular use → problem use → dependence → crisis →
* recovery | chronic | death.
*/
diff --git a/engine/state/mood.ts b/engine/state/mood.ts
index 501a699..205b5ec 100644
--- a/engine/state/mood.ts
+++ b/engine/state/mood.ts
@@ -4,7 +4,6 @@ import type { GameTime } from '@hollowdark/time/gameTime'
* Current-week emotional state on the valence × arousal plane.
* valence -1 (negative) to +1 (positive)
* arousal -1 (calm) to +1 (activated)
- * See docs/08-mental-health.md §"Separate variables".
*/
export interface MoodState {
readonly valence: number
diff --git a/engine/state/satisfaction.ts b/engine/state/satisfaction.ts
index a10421b..eae65ec 100644
--- a/engine/state/satisfaction.ts
+++ b/engine/state/satisfaction.ts
@@ -1,8 +1,7 @@
/**
* Life satisfaction split into hedonic (day-to-day pleasure) and eudaimonic
- * (sense of meaning / purpose). They can diverge, and eudaimonic matters
- * more for end-of-life peace (docs/13-spirituality.md §"Life satisfaction
- * is distinct from happiness"). Each 0–1.
+ * (sense of meaning / purpose). They can diverge; eudaimonic matters more
+ * for end-of-life peace. Each 0–1.
*/
export interface SatisfactionProfile {
readonly hedonic: number
diff --git a/engine/traits/attachment.ts b/engine/traits/attachment.ts
index 7af4c1b..eae77fa 100644
--- a/engine/traits/attachment.ts
+++ b/engine/traits/attachment.ts
@@ -1,7 +1,7 @@
/**
* Attachment is tracked as a distribution, not a single tag. Fractions sum
* to 1.0; a character is rarely purely one style. Set by age 3 based on
- * caregiver behaviour, mostly fixed thereafter (docs/04-traits.md §"Layer 2").
+ * caregiver behaviour, mostly fixed thereafter.
*/
export interface AttachmentDistribution {
readonly secure: number
diff --git a/engine/traits/big-five.ts b/engine/traits/big-five.ts
index 425c91c..dbf5bd7 100644
--- a/engine/traits/big-five.ts
+++ b/engine/traits/big-five.ts
@@ -1,6 +1,6 @@
/**
- * Big Five temperament profile, each 0–100. Mostly stable across a life
- * with slow drift under sustained conditions. See docs/04-traits.md §"Layer 1".
+ * Big Five temperament profile. Each axis is 0–100, mostly stable across
+ * a life with slow drift under sustained conditions.
*/
export interface BigFiveProfile {
readonly openness: number
diff --git a/engine/traits/core-beliefs.ts b/engine/traits/core-beliefs.ts
index 4e3b85a..f35daa0 100644
--- a/engine/traits/core-beliefs.ts
+++ b/engine/traits/core-beliefs.ts
@@ -1,6 +1,6 @@
/**
- * Core beliefs shape how adult events are interpreted (docs/04-traits.md
- * §"Layer 2"). Each 0–100. Formed in early childhood, harder to shift after.
+ * Core beliefs shape how adult events are interpreted. Each axis is 0–100,
+ * formed in early childhood and harder to shift after.
*/
export interface CoreBeliefs {
readonly selfWorth: number
diff --git a/engine/traits/dark-triad.ts b/engine/traits/dark-triad.ts
index e8e1450..fff49c5 100644
--- a/engine/traits/dark-triad.ts
+++ b/engine/traits/dark-triad.ts
@@ -1,7 +1,7 @@
/**
- * Dark Triad profile, each 0–100. Distribution is skewed low — most
- * characters sit in the 20–40 range; a small minority above 60 drives
- * specific life shapes (docs/04-traits.md §"Dark Triad characters in play").
+ * Dark Triad profile. Each axis is 0–100. Distribution in the simulation
+ * skews low — most characters sit in the 20–40 range; a small minority
+ * above 60 drives specific life shapes.
*/
export interface DarkTriadProfile {
readonly narcissism: number
diff --git a/engine/traits/orientation.ts b/engine/traits/orientation.ts
index 60decd9..e72a9cf 100644
--- a/engine/traits/orientation.ts
+++ b/engine/traits/orientation.ts
@@ -1,6 +1,6 @@
/**
* Gender identity and sexual orientation. Continuous axes plus a discrete
- * gender tag. See docs/04-traits.md §"Layer 2" and docs/10-sexuality.md.
+ * gender tag.
*
* sexualAttraction 0 = hetero-exclusive, 100 = homo-exclusive
* romanticAttraction independent continuous axis
diff --git a/engine/traits/values.ts b/engine/traits/values.ts
index 0d67d16..83ae529 100644
--- a/engine/traits/values.ts
+++ b/engine/traits/values.ts
@@ -1,7 +1,7 @@
/**
* Values orientation. Each axis is 0–100 where 0 anchors the left-named
- * pole and 100 the right (e.g., tradition_vs_novelty: 0 = deeply traditional,
- * 100 = novelty-seeking). See docs/04-traits.md §"Layer 2".
+ * pole and 100 the right (e.g., `tradition_vs_novelty`: 0 = deeply
+ * traditional, 100 = novelty-seeking).
*/
export interface ValuesOrientation {
readonly tradition_vs_novelty: number
diff --git a/hooks/client.ts b/hooks/client.ts
index b65ecdc..844ac2c 100644
--- a/hooks/client.ts
+++ b/hooks/client.ts
@@ -1,9 +1,5 @@
-// Client-side setup runs once when the app mounts.
-// Later: service worker registration, audio unlock on first gesture,
-// persistent-storage request, IndexedDB hydration. Empty for now.
import type { ClientInit } from '@sveltejs/kit'
export const init: ClientInit = async () => {
- // Intentionally empty. Systems attach here as they come online.
}
diff --git a/persistence/db.ts b/persistence/db.ts
index 213e19b..d8647b0 100644
--- a/persistence/db.ts
+++ b/persistence/db.ts
@@ -20,7 +20,7 @@ import {
CONTENT_CACHE_SCHEMA_V1,
USER_DATA_DB_NAME,
USER_DATA_SCHEMA_V1
-} from './schema'
+} from '@hollowdark/persistence/schema'
/**
* A single key/value pair on the Settings table. Keys are short slugs
@@ -62,8 +62,8 @@ export interface CachedAudioTrack {
/**
* User save data — the player's world(s). Persists forever on device,
- * untouched by content updates (technical/04-persistence.md §"Core
- * decisions"). Changes to this schema require a migration.
+ * untouched by content updates. Changes to this schema require a
+ * migration in `persistence/migrations` (added later).
*/
export class HollowdarkUserData extends Dexie {
worlds!: Table<World, string>
@@ -88,7 +88,7 @@ export class HollowdarkUserData extends Dexie {
/**
* Content cache — the compiled JSON chunks the client fetched from the
* CDN. Independently versioned from user data; can be cleared without
- * touching saves (ARCHITECTURE.md §3).
+ * touching saves.
*/
export class HollowdarkContentCache extends Dexie {
content!: Table<CachedContentChunk, string>
diff --git a/persistence/schema.ts b/persistence/schema.ts
index 4d11edd..8d381d0 100644
--- a/persistence/schema.ts
+++ b/persistence/schema.ts
@@ -1,23 +1,17 @@
/**
- * IndexedDB schema strings for each of the three Dexie databases.
+ * IndexedDB schema for the user-data database. The string values follow
+ * Dexie's index notation:
*
- * The schema is versioned deliberately — any change here requires a
- * migration in persistence/migrations.ts (added later). Snapshot tests
- * lock these strings so accidental drift shows up as a failing test
- * rather than a silent mismatch between code and on-device data.
- *
- * Index syntax recap:
* &field primary key (unique)
* ++id auto-incrementing primary key
* field plain index on a scalar
* [a+b] compound index
* *field multi-entry index on an array field
*
- * See ARCHITECTURE.md §24 for the user-data schema and §7 for the content
- * cache; technical/04-persistence.md for the two-database separation
- * (user data persists forever, content is replaceable per update).
+ * Frozen at module load so a runtime typo on the wrong key throws rather
+ * than silently mutating the schema object. Snapshot tests lock these
+ * strings; any change here requires a migration.
*/
-
export const USER_DATA_SCHEMA_V1 = Object.freeze({
worlds: '&id, seed, currentPlayerCharacterId',
people:
@@ -35,16 +29,19 @@ export const USER_DATA_SCHEMA_V1 = Object.freeze({
settings: '&key'
})
+/** IndexedDB schema for the content cache — replaceable per content update. */
export const CONTENT_CACHE_SCHEMA_V1 = Object.freeze({
content: '&chunkId, version, fetchedAt',
manifest: '&id'
})
+/** IndexedDB schema for the audio cache — blobs + a manifest row. */
export const AUDIO_CACHE_SCHEMA_V1 = Object.freeze({
audio: '&trackId, version, fetchedAt',
manifest: '&id'
})
+/** Current persistence schema version. Bump when any schema above changes. */
export const SCHEMA_VERSION = 1 as const
export const USER_DATA_DB_NAME = 'HollowdarkUserData' as const
diff --git a/rng/seeded.ts b/rng/seeded.ts
index 52c4719..6d9f991 100644
--- a/rng/seeded.ts
+++ b/rng/seeded.ts
@@ -1,13 +1,12 @@
-import { deriveSeed, hashString } from './derive'
+import { deriveSeed, hashString } from '@hollowdark/rng/derive'
/**
* Seeded PRNG. All gameplay randomness routes through this interface —
- * Math.random is forbidden in gameplay code (see ARCHITECTURE.md §26
- * and eslint.config.js).
+ * Math.random is forbidden in gameplay code (see the ESLint rule).
*
* Guarantees:
* - Same seed produces the same infinite sequence, bit-for-bit.
- * - sub(label) produces a deterministic child stream, independent of
+ * - `sub(label)` produces a deterministic child stream, independent of
* how the parent stream is consumed.
* - Byte-level reproducibility across runs and machines.
*/
@@ -101,8 +100,6 @@ class SeededRNGImpl implements SeededRNG {
cumulative += weight
if (roll < cumulative) return value
}
- // Numerical safety: if we fall off the end due to floating-point drift,
- // return the last item rather than throw.
return items[items.length - 1]![0]
}
diff --git a/routes/+layout.svelte b/routes/+layout.svelte
index d62f17d..35fb2d7 100644
--- a/routes/+layout.svelte
+++ b/routes/+layout.svelte
@@ -1,5 +1,4 @@
<script lang="ts">
- // Global CSS is loaded from static/css/app.css via <link> in app/app.html.
let { children } = $props()
</script>
diff --git a/routes/+layout.ts b/routes/+layout.ts
index 0b9e7c5..a032602 100644
--- a/routes/+layout.ts
+++ b/routes/+layout.ts
@@ -1,3 +1,2 @@
-// Pre-render everything. Static output matches adapter-static.
export const prerender = true
export const ssr = false
diff --git a/routes/+page.svelte b/routes/+page.svelte
index 00e0ed0..eeff5b2 100644
--- a/routes/+page.svelte
+++ b/routes/+page.svelte
@@ -1,5 +1,4 @@
<script lang="ts">
- // Main play screen. Placeholder until the flow stream is wired up in step 4+.
</script>
<main class="placeholder">
diff --git a/tests/determinism/rng.test.ts b/tests/determinism/rng.test.ts
index 57a1e10..5d003b0 100644
--- a/tests/determinism/rng.test.ts
+++ b/tests/determinism/rng.test.ts
@@ -157,8 +157,6 @@ describe('createRNG — output shape', () => {
])
if (pick === 'rare') lowCount++
}
- // Expected ~5, allow plenty of slack for variance; just confirm the
- // distribution isn't inverted.
expect(lowCount).toBeLessThan(50)
})
@@ -212,15 +210,9 @@ describe('SeededRNG.sub — sub-RNG derivation', () => {
})
describe('byte-level determinism snapshot', () => {
- // Locks the PRNG implementation. Saved worlds depend on exact byte
- // reproduction — if this test breaks, it means the PRNG changed, and
- // every existing save relying on this seed will diverge. Treat failure
- // as a migration concern, not a "just update the snapshot" fix.
test('createRNG("hollowdark").next() × 5 matches canonical sequence', () => {
const rng = createRNG('hollowdark')
const first5 = [rng.next(), rng.next(), rng.next(), rng.next(), rng.next()]
- // Values computed from the current mulberry32 + xmur3 implementation.
- // Reproducible locally via: createRNG('hollowdark').next() × 5.
expect(first5).toMatchInlineSnapshot(`
[
0.14088608953170478,
diff --git a/tests/unit/time/calendar.test.ts b/tests/unit/time/calendar.test.ts
index ee8d789..efbec97 100644
--- a/tests/unit/time/calendar.test.ts
+++ b/tests/unit/time/calendar.test.ts
@@ -72,10 +72,10 @@ describe('monthSeason', () => {
})
test('specific month → season mappings', () => {
- expect(monthSeason(1)).toBe('spring') // Thawing
- expect(monthSeason(4)).toBe('summer') // Highsun
- expect(monthSeason(7)).toBe('autumn') // Firstfall
- expect(monthSeason(10)).toBe('winter') // Rainfall
+ expect(monthSeason(1)).toBe('spring')
+ expect(monthSeason(4)).toBe('summer')
+ expect(monthSeason(7)).toBe('autumn')
+ expect(monthSeason(10)).toBe('winter')
expect(monthSeason(13)).toBe('festival')
})
})
diff --git a/tests/unit/time/gameTime.test.ts b/tests/unit/time/gameTime.test.ts
index ca6f4b1..9503e60 100644
--- a/tests/unit/time/gameTime.test.ts
+++ b/tests/unit/time/gameTime.test.ts
@@ -137,7 +137,6 @@ describe('addWeeks', () => {
})
test('52 weeks lands in the festival, not at next year', () => {
- // 52 × 7 = 364 days; Thawing 1 + 364 = Festival 5 (day 365 - 1)
expect(addWeeks(makeGameTime(1111, 1, 1), 52)).toMatchObject({
year: 1111,
month: 13,
@@ -146,8 +145,6 @@ describe('addWeeks', () => {
})
test('negative weeks moves backward', () => {
- // Greening 1 (doy 31) minus 7 days = Thawing 24 (doy 24).
- // Months are 30 days in this calendar, not 31.
expect(addWeeks(makeGameTime(1111, 2, 1), -1)).toMatchObject({
year: 1111,
month: 1,
@@ -174,7 +171,6 @@ describe('addMonths', () => {
})
test('overflow into next year wraps through the 13-month cycle', () => {
- // Rimefrost 15 + 2 months → Festival → Thawing next year, day preserved (15)
expect(addMonths(makeGameTime(1111, 12, 15), 2)).toMatchObject({
year: 1112,
month: 1,
diff --git a/tests/unit/utils/result.test.ts b/tests/unit/utils/result.test.ts
index bdd4923..49cf86f 100644
--- a/tests/unit/utils/result.test.ts
+++ b/tests/unit/utils/result.test.ts
@@ -67,7 +67,6 @@ describe('type-narrowing', () => {
test('isOk narrows to Ok<T>', () => {
const r: Result<number, string> = ok(10)
if (isOk(r)) {
- // Type-level check — if isOk doesn't narrow, this line fails to compile.
const n: number = r.value
expect(n).toBe(10)
} else {
diff --git a/time/calendar.ts b/time/calendar.ts
index ce61de4..e2f229f 100644
--- a/time/calendar.ts
+++ b/time/calendar.ts
@@ -1,8 +1,7 @@
/**
- * The Hollowdark calendar: 12 months × 30 days + a 5-day year-end festival,
- * 365 days per year, 7-day weeks.
+ * Number of days in a regular month.
*
- * Month order (per docs/01-world.md and style-bible/00-style-bible.md):
+ * Month order:
*
* spring 1 Thawing 2 Greening 3 Blossomtide
* summer 4 Highsun 5 Amberhaze 6 Harvestmark
@@ -10,20 +9,30 @@
* winter 10 Rainfall 11 Hollowdark 12 Rimefrost
* festival 13 Year's End Festival (5 days)
*
- * The year begins with Thawing (spring) and closes with the festival,
- * which sits between Rimefrost and the next year's Thawing. This matches
- * the style bible's cold-half / warm-half grouping and the "year-end
- * festival" phrasing in world lore.
+ * The year begins with Thawing and closes with the festival, which sits
+ * between Rimefrost and the next year's Thawing.
*/
-
export const DAYS_PER_MONTH = 30
+
+/** Twelve named months — all except the festival. */
export const REGULAR_MONTH_COUNT = 12
+
+/** Days in the Year's End Festival — the thirteenth "month". */
export const FESTIVAL_DAYS = 5
+
+/** Month index for the festival. */
export const FESTIVAL_MONTH = 13
-export const MONTHS_PER_YEAR = REGULAR_MONTH_COUNT + 1 // 12 regular + festival
-export const DAYS_PER_YEAR = REGULAR_MONTH_COUNT * DAYS_PER_MONTH + FESTIVAL_DAYS // 365
+
+/** Total month slots in the calendar cycle (12 regular + festival). */
+export const MONTHS_PER_YEAR = REGULAR_MONTH_COUNT + 1
+
+/** Days in a year: 12 × 30 + 5 = 365. */
+export const DAYS_PER_YEAR = REGULAR_MONTH_COUNT * DAYS_PER_MONTH + FESTIVAL_DAYS
+
+/** Seven-day weeks. */
export const DAYS_PER_WEEK = 7
+/** Month names in calendar order. Index 0 is Thawing; index 12 is the festival. */
export const MONTH_NAMES = [
'Thawing',
'Greening',
@@ -40,23 +49,25 @@ export const MONTH_NAMES = [
"Year's End Festival"
] as const
+/** The named-month type derived from `MONTH_NAMES`. */
export type MonthName = (typeof MONTH_NAMES)[number]
+/** Four regular seasons plus a distinct `festival` tag for the year-end. */
export type Season = 'spring' | 'summer' | 'autumn' | 'winter' | 'festival'
const SEASON_BY_MONTH: readonly Season[] = [
- 'spring', // Thawing
- 'spring', // Greening
- 'spring', // Blossomtide
- 'summer', // Highsun
- 'summer', // Amberhaze
- 'summer', // Harvestmark
- 'autumn', // Firstfall
- 'autumn', // Stormturn
- 'autumn', // Ashfall
- 'winter', // Rainfall
- 'winter', // Hollowdark
- 'winter', // Rimefrost
+ 'spring',
+ 'spring',
+ 'spring',
+ 'summer',
+ 'summer',
+ 'summer',
+ 'autumn',
+ 'autumn',
+ 'autumn',
+ 'winter',
+ 'winter',
+ 'winter',
'festival'
]
@@ -66,21 +77,33 @@ function validateMonth(monthIndex: number): void {
}
}
+/**
+ * Return the named month for a 1-based month index. Throws on invalid input.
+ * @param monthIndex 1 (Thawing) through 13 (Year's End Festival).
+ */
export function monthName(monthIndex: number): MonthName {
validateMonth(monthIndex)
return MONTH_NAMES[monthIndex - 1] as MonthName
}
+/**
+ * Return the season label for a 1-based month index. The festival has its
+ * own tag ('festival') rather than reusing one of the four seasons.
+ */
export function monthSeason(monthIndex: number): Season {
validateMonth(monthIndex)
return SEASON_BY_MONTH[monthIndex - 1] as Season
}
+/**
+ * Days in the given month. 30 for regular months, 5 for the festival.
+ */
export function daysInMonth(monthIndex: number): number {
validateMonth(monthIndex)
return monthIndex === FESTIVAL_MONTH ? FESTIVAL_DAYS : DAYS_PER_MONTH
}
+/** True when the supplied month index is the festival slot. */
export function isFestival(monthIndex: number): boolean {
return monthIndex === FESTIVAL_MONTH
}
diff --git a/time/gameTime.ts b/time/gameTime.ts
index 2f2fed7..adebc4f 100644
--- a/time/gameTime.ts
+++ b/time/gameTime.ts
@@ -8,16 +8,15 @@ import {
REGULAR_MONTH_COUNT,
daysInMonth,
monthName
-} from './calendar'
+} from '@hollowdark/time/calendar'
/**
* A position in game time. Immutable — all arithmetic returns a new value.
*
- * year integer (negative allowed for pre-1111 historical events)
+ * year integer (negative allowed for pre-epoch historical events)
* month 1..12 for regular months, 13 for year-end festival
* day 1..30 for regular months, 1..5 for festival
* tickOfDay 0 outside crisis mode; crisis mode subdivides the day
- * (docs/22-crisis-mode.md)
*/
export interface GameTime {
readonly year: number
@@ -61,23 +60,21 @@ function absoluteDays(time: GameTime): number {
}
function fromAbsoluteDays(abs: number, tickOfDay: number): GameTime {
- // Guard against non-integer arithmetic drift — time is whole days only,
- // tickOfDay handles sub-day resolution in crisis mode.
if (!Number.isFinite(abs)) {
throw new Error(`Non-finite absolute day count: ${abs}`)
}
const year = Math.floor(abs / DAYS_PER_YEAR)
- const rem = abs - year * DAYS_PER_YEAR // 0..364
- const doy = rem + 1 // 1..365
- const festivalStart = REGULAR_MONTH_COUNT * DAYS_PER_MONTH + 1 // 361
+ const rem = abs - year * DAYS_PER_YEAR
+ const doy = rem + 1
+ const festivalStart = REGULAR_MONTH_COUNT * DAYS_PER_MONTH + 1
let month: number
let day: number
if (doy >= festivalStart) {
month = FESTIVAL_MONTH
- day = doy - festivalStart + 1 // 1..5
+ day = doy - festivalStart + 1
} else {
- month = Math.floor((doy - 1) / DAYS_PER_MONTH) + 1 // 1..12
- day = ((doy - 1) % DAYS_PER_MONTH) + 1 // 1..30
+ month = Math.floor((doy - 1) / DAYS_PER_MONTH) + 1
+ day = ((doy - 1) % DAYS_PER_MONTH) + 1
}
return { year, month, day, tickOfDay }
}
@@ -105,7 +102,6 @@ export function addMonths(time: GameTime, months: number): GameTime {
if (!Number.isInteger(months)) {
throw new Error(`addMonths requires an integer (got ${months})`)
}
- // Zero-based cycle arithmetic: months are 1..13, so we work in 0..12.
const totalCycles = (time.month - 1) + months
const yearDelta = Math.floor(totalCycles / MONTHS_PER_YEAR)
const monthIndex = ((totalCycles % MONTHS_PER_YEAR) + MONTHS_PER_YEAR) % MONTHS_PER_YEAR
@@ -171,7 +167,6 @@ export function toAbsoluteDays(time: GameTime): number {
return absoluteDays(time)
}
-// Re-exports for convenience at the 'time' import.
export {
DAYS_PER_MONTH,
DAYS_PER_WEEK,
diff --git a/time/granularity.ts b/time/granularity.ts
index 0643c47..1acb122 100644
--- a/time/granularity.ts
+++ b/time/granularity.ts
@@ -1,12 +1,7 @@
/**
- * Tick granularity by life stage.
- *
- * One tick represents a different span of time depending on the character's
- * age — infancy advances in years because there isn't weekly texture worth
- * resolving, adulthood in weeks because that's the rhythm the design lives
- * at. See docs/05-time-system.md and ARCHITECTURE.md §5.
+ * The eight life stages a character moves through. Used to pick how much
+ * wall-clock time one simulation tick represents at a given age.
*/
-
export type LifeStage =
| 'infancy'
| 'early_childhood'
@@ -17,8 +12,15 @@ export type LifeStage =
| 'late_adult'
| 'elderly'
+/** How much game time one tick advances. */
export type TickUnit = 'year' | 'season' | 'month' | 'week'
+/**
+ * Tick granularity per life stage. Infancy advances in years because
+ * there isn't weekly texture worth resolving; adulthood in weeks because
+ * that's the rhythm the design lives at; elderly in months as the pace
+ * slows again.
+ */
export const TICK_UNIT_BY_LIFE_STAGE: Readonly<Record<LifeStage, TickUnit>> = {
infancy: 'year',
early_childhood: 'season',
@@ -30,6 +32,9 @@ export const TICK_UNIT_BY_LIFE_STAGE: Readonly<Record<LifeStage, TickUnit>> = {
elderly: 'month'
}
+/**
+ * Bucket an age in whole years into its life stage. Throws on negative age.
+ */
export function lifeStageForAge(ageYears: number): LifeStage {
if (ageYears < 0) throw new Error(`Invalid age: ${ageYears}`)
if (ageYears < 3) return 'infancy'
@@ -42,6 +47,7 @@ export function lifeStageForAge(ageYears: number): LifeStage {
return 'elderly'
}
+/** Shortcut: map an age directly to its tick unit. */
export function tickUnitForAge(ageYears: number): TickUnit {
return TICK_UNIT_BY_LIFE_STAGE[lifeStageForAge(ageYears)]
}
diff --git a/time/speed.ts b/time/speed.ts
index 9a9bb11..d5cba2e 100644
--- a/time/speed.ts
+++ b/time/speed.ts
@@ -1,11 +1,11 @@
/**
- * Player-controlled simulation speed. Only three states ever exist
- * (docs/05-time-system.md): time is stopped, running at reading pace,
- * or running fast with compressed flow.
- *
- * Scenes auto-set the effective speed to 'paused'; the intended speed is
- * preserved so the simulation returns to it when the scene resolves.
+ * Player-controlled simulation speed. Only three states ever exist: time
+ * is stopped, running at reading pace, or running fast with compressed
+ * flow. Scenes auto-set the effective speed to `paused`; the intended
+ * speed is preserved so the simulation returns to it when the scene
+ * resolves.
*/
export type Speed = 'paused' | 'play' | 'fast'
+/** The three speeds, in canonical order. */
export const SPEEDS: readonly Speed[] = ['paused', 'play', 'fast']
diff --git a/utils/result/constructors.ts b/utils/result/constructors.ts
index b0ed958..6fec0f4 100644
--- a/utils/result/constructors.ts
+++ b/utils/result/constructors.ts
@@ -1,4 +1,4 @@
-import type { Err, Ok } from './types'
+import type { Err, Ok } from '@hollowdark/utils/result/types'
export function ok<T>(value: T): Ok<T> {
return { ok: true, value }
diff --git a/utils/result/map.ts b/utils/result/map.ts
index 9cf44a1..6d8697e 100644
--- a/utils/result/map.ts
+++ b/utils/result/map.ts
@@ -1,5 +1,5 @@
-import { err, ok } from './constructors'
-import type { Result } from './types'
+import { err, ok } from '@hollowdark/utils/result/constructors'
+import type { Result } from '@hollowdark/utils/result/types'
/** Map the value of an ok result; pass errors through unchanged. */
export function mapResult<T, U, E>(r: Result<T, E>, f: (value: T) => U): Result<U, E> {
diff --git a/utils/result/predicates.ts b/utils/result/predicates.ts
index 315eb0d..16e37af 100644
--- a/utils/result/predicates.ts
+++ b/utils/result/predicates.ts
@@ -1,4 +1,4 @@
-import type { Err, Ok, Result } from './types'
+import type { Err, Ok, Result } from '@hollowdark/utils/result/types'
export function isOk<T, E>(r: Result<T, E>): r is Ok<T> {
return r.ok
diff --git a/utils/result/types.ts b/utils/result/types.ts
index ee95c08..e22856f 100644
--- a/utils/result/types.ts
+++ b/utils/result/types.ts
@@ -8,7 +8,7 @@ export type Err<E> = { readonly ok: false; readonly error: E }
* Result<T, E> — a recoverable-error return type for functions that can
* fail without throwing. Used at system boundaries (save/load, content
* validation, manifest fetch) where the caller needs to decide whether a
- * failure is fatal. Internal pure logic uses plain returns and throws
- * Error for programmer errors (rules/01-code-style.md).
+ * failure is fatal. Internal pure logic still throws `Error` for
+ * programmer errors.
*/
export type Result<T, E = Error> = Ok<T> | Err<E>
diff --git a/utils/result/unwrap.ts b/utils/result/unwrap.ts
index 84e658e..8f980dd 100644
--- a/utils/result/unwrap.ts
+++ b/utils/result/unwrap.ts
@@ -1,4 +1,4 @@
-import type { Result } from './types'
+import type { Result } from '@hollowdark/utils/result/types'
/**
* Extract the value from an ok result; throw on error. Reserve for tests