aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-04-22 09:53:17 +0530
committerBobby <[email protected]>2026-04-22 09:53:17 +0530
commitaaf299374d767d38f532d148bd0b5d89db4e6574 (patch)
tree550a51b58cd417a124ff513fd2dc16c9802ed33f
parent8877f532599011ca02f267863189d4ffe6755955 (diff)
downloadhollowdark-aaf299374d767d38f532d148bd0b5d89db4e6574.tar.xz
hollowdark-aaf299374d767d38f532d148bd0b5d89db4e6574.zip
Generate a fresh year-1111 world on Begin and persist it to the user-data database
-rw-r--r--routes/+page.svelte8
-rw-r--r--tests/unit/worldgen/world.test.ts48
-rw-r--r--worldgen/world.ts75
3 files changed, 130 insertions, 1 deletions
diff --git a/routes/+page.svelte b/routes/+page.svelte
index babf994..ffa8428 100644
--- a/routes/+page.svelte
+++ b/routes/+page.svelte
@@ -10,6 +10,8 @@
hasCompletedInitialLoad,
markInitialLoadComplete
} from '@hollowdark/loading/lifecycle'
+ import { saveWorld } from '@hollowdark/persistence/worlds'
+ import { generateWorld } from '@hollowdark/worldgen/world'
type View = 'loading' | 'begin'
@@ -25,7 +27,11 @@
view = 'begin'
})
- function handleBegin(): void {}
+ async function handleBegin(): Promise<void> {
+ const world = generateWorld()
+ await saveWorld(world)
+ beginState = await detectBeginState()
+ }
function handleContinue(): void {}
function handleSettings(): void {
goto(resolve('/settings'))
diff --git a/tests/unit/worldgen/world.test.ts b/tests/unit/worldgen/world.test.ts
new file mode 100644
index 0000000..7f7e0ce
--- /dev/null
+++ b/tests/unit/worldgen/world.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, test } from 'vitest'
+
+import type { WorldId } from '@hollowdark/engine/entities/base'
+import { FIRST_EVER_YEAR, generateWorld } from '@hollowdark/worldgen/world'
+
+const FIXED_ID = 'test-world-1111' as WorldId
+const FIXED_SEED = 'test-seed-1111'
+const FIXED_CREATED_AT = '2026-04-22T09:00:00.000Z'
+
+describe('generateWorld', () => {
+ test('lands at year 1111, Thawing 1', () => {
+ const world = generateWorld({ id: FIXED_ID, seed: FIXED_SEED, createdAt: FIXED_CREATED_AT })
+ expect(world.currentGameTime).toEqual({
+ year: FIRST_EVER_YEAR,
+ month: 1,
+ day: 1,
+ tickOfDay: 0
+ })
+ })
+
+ test('starts with no characters, no events, no crisis', () => {
+ const world = generateWorld({ id: FIXED_ID, seed: FIXED_SEED, createdAt: FIXED_CREATED_AT })
+ expect(world.currentPlayerCharacterId).toBeNull()
+ expect(world.playedCharacterIds).toEqual([])
+ expect(world.tierOneIds).toEqual([])
+ expect(world.tierTwoIds).toEqual([])
+ expect(world.activeEventIds).toEqual([])
+ expect(world.scheduledEvents).toEqual([])
+ expect(world.crisisState.active).toBe(false)
+ expect(world.politicsByRegion.size).toBe(0)
+ })
+
+ test('seeded generation is deterministic given fixed inputs', () => {
+ const a = generateWorld({ id: FIXED_ID, seed: FIXED_SEED, createdAt: FIXED_CREATED_AT })
+ const b = generateWorld({ id: FIXED_ID, seed: FIXED_SEED, createdAt: FIXED_CREATED_AT })
+ expect(a).toEqual(b)
+ })
+
+ test('defaults the seed to the generated id when only id is provided', () => {
+ const world = generateWorld({ id: FIXED_ID, createdAt: FIXED_CREATED_AT })
+ expect(world.seed).toBe(FIXED_ID)
+ })
+
+ test('records the current schema version in settings', () => {
+ const world = generateWorld({ id: FIXED_ID, seed: FIXED_SEED, createdAt: FIXED_CREATED_AT })
+ expect(world.settings.schemaVersion).toBe(1)
+ })
+})
diff --git a/worldgen/world.ts b/worldgen/world.ts
new file mode 100644
index 0000000..b2e9bd0
--- /dev/null
+++ b/worldgen/world.ts
@@ -0,0 +1,75 @@
+import type { WorldId } from '@hollowdark/engine/entities/base'
+import type {
+ CrisisState,
+ MacroEconomicState,
+ World,
+ WorldSettings
+} from '@hollowdark/engine/entities/world'
+import { SCHEMA_VERSION } from '@hollowdark/persistence/schema'
+import { makeGameTime } from '@hollowdark/time/gameTime'
+import { APP_VERSION_FULL } from '@hollowdark/utils/version/version'
+
+/**
+ * The year the first-ever character in any Hollowdark save is born. Per
+ * design, this number is never shown to the player again — subsequent
+ * characters are born in whatever year the world has reached.
+ */
+export const FIRST_EVER_YEAR = 1111 as const
+
+const DEFAULT_ECONOMY: MacroEconomicState = Object.freeze({
+ inflationAnnual: 0.02,
+ employmentRate: 0.94,
+ marketIndex: 1.0,
+ recessionDepth: 0
+})
+
+const DEFAULT_CRISIS: CrisisState = Object.freeze({
+ active: false,
+ crisisEventId: null,
+ sceneIndex: 0,
+ startedAt: null
+})
+
+export interface GenerateWorldInput {
+ readonly id?: WorldId
+ readonly seed?: string
+ readonly createdAt?: string
+}
+
+/**
+ * Produce a fresh `World` record sitting at the start of year 1111. No
+ * characters, no scheduled events, no political state — the shape is a
+ * clean slate that downstream generators (people, regions, institutions)
+ * fill in before the simulation begins to tick.
+ *
+ * Entropy is injected here only: the default `seed` comes from
+ * `crypto.randomUUID()`, the sole non-deterministic call in a world's
+ * lifetime. Tests and imports can supply their own seed for replay.
+ */
+export function generateWorld(input: GenerateWorldInput = {}): World {
+ const id = input.id ?? (crypto.randomUUID() as WorldId)
+ const seed = input.seed ?? id
+ const createdAt = input.createdAt ?? new Date().toISOString()
+
+ const settings: WorldSettings = {
+ contentVersionAtCreation: APP_VERSION_FULL,
+ schemaVersion: SCHEMA_VERSION
+ }
+
+ return {
+ id,
+ seed,
+ createdAt,
+ currentGameTime: makeGameTime(FIRST_EVER_YEAR, 1, 1),
+ currentPlayerCharacterId: null,
+ playedCharacterIds: [],
+ tierOneIds: [],
+ tierTwoIds: [],
+ economy: DEFAULT_ECONOMY,
+ politicsByRegion: new Map(),
+ activeEventIds: [],
+ scheduledEvents: [],
+ crisisState: DEFAULT_CRISIS,
+ settings
+ }
+}