Skip to content

Commit 3350dc3

Browse files
CodeManEthanclaude
andcommitted
Genesis: living details — crops ripen, logs lie, saplings come up
Props stop being one sprite for the whole day. CROPS RIPEN. A crop plot is three sprites now: sprouts on bare drills before ~10:00, the rows that have always been there until ~16:00, and tall gold after. The stage is a pure function of the clock and a ±40 world-minute jitter off the plot's own seed, so a valley's fields do not all turn on the same stroke. `buildCropRow` is untouched and is still the middle stage; the two new sprites are built from the same pool seeds, so a field keeps its colour and its earth as it ripens. FELLED LOGS. A tree that goes over leaves its trunk beside the stump for ninety world-minutes, then the yards haul it. The honest clock for that needed the snapshot to say WHEN a tree became a stump, not just that it had — scene-side it could only ever have been "the frame I noticed the slot flip", which is a different world after a scrub. So WorldSnapshot grows one field, `felled: Map<treeId, t>`, written by the `chop-done` event that already existed: no new event type, TYPE_RANK untouched, applyEvent/emptySnapshot updated in timeline.ts and fixture.ts alike, and serialize() in the harness taught the field so check (j) still compares all of the snapshot. A new check (j2) holds `felled` to the events and to `trees` at midnight and at midday. SAPLINGS. Three to five come up after ~18:00 on ground the day's axes cleared — anchored to road clears standing clear of every town, so regrowth lands on the lane through the wood and not under somebody's new roof, and each one still gated on its anchor actually being a stump. VEG CACHE. The layer's two fingerprints say "nothing changed unless the snapshot grew", and all three of these change on the clock instead, so each carries its own sweep: crops and saplings are recomputed outright every sync (a dozen plots, five saplings), and a felled log turns ON through the existing tree paths but OFF through `veg.logOn`, the short list of trunks currently on the ground. None of the three caches a state of its own — every one recomputes what it wants from (snapshot, t) and lets `want` compare it against `veg.on` — so a backward scrub still needs nothing beyond `invalidateVeg`. Log slots and sapling anchors are allocated only for trees the map has already promised to a plot or a road, which is the complete set that can ever become a stump; the other two thousand trees are unchanged two-slot trees. Gates: genesis:check 61/61, sweep 200 seeds 1x/4x, perf worst avg 2.6ms / max 4.7ms against 4:8, astro build 7 pages, console clean. Scrubbing to the same t twice gives identical pixels, and the veg layer at 08:00, 13:00 and 19:30 is identical whether reached by loading or by scrubbing back from 21:00. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B7t8Qgo2z11gAegjSKAr7b
1 parent 80d3628 commit 3350dc3

7 files changed

Lines changed: 624 additions & 8 deletions

File tree

scripts/genesis-timeline-stats.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,9 @@ function serialize(snap) {
312312
return JSON.stringify({
313313
t: snap.t,
314314
trees: m(snap.trees),
315+
// living details: fell times. Listed explicitly, because (j) compares
316+
// serialisations and anything left out of here is silently not checked.
317+
felled: m(snap.felled ?? new Map()),
315318
buildings: m(snap.buildings),
316319
roads: m(snap.roads),
317320
bridges: m(snap.bridges),
@@ -593,6 +596,26 @@ function run(label, map, pace = 1) {
593596
for (let t = 0.5; t <= 24.0001; t += 0.5) advance(hop3, tl, Math.min(t, 24));
594597
if (serialize(hop3) !== serialize(end)) bad(`(j) 48-hop advance != snapshotAt(24)`);
595598

599+
/* (j2) living details: fell times agree with the chop-done events, and with
600+
* `trees` — the felled log's whole clock hangs off this map, so a stump with
601+
* no fell time (or a fell time with no stump) is a log that never lies down
602+
* or never gets hauled. Checked at mid-day too, where `felled` is partial. */
603+
for (const [id, t] of end.felled ?? []) {
604+
if (chopDone.get(id) !== t) bad(`(j2) felled[${id}] = ${t}, chop-done at ${chopDone.get(id)}`);
605+
if (end.trees.get(id) !== 'stump') bad(`(j2) felled[${id}] but tree is ${end.trees.get(id)}`);
606+
}
607+
for (const [id, st] of end.trees) {
608+
if (st === 'stump' && !(end.felled ?? new Map()).has(id))
609+
bad(`(j2) tree ${id} is a stump at t=24 with no fell time`);
610+
}
611+
{
612+
const mid = snapshotAt(map, tl, 12);
613+
for (const [id, t] of mid.felled ?? []) {
614+
if (!(t <= 12 + 1e-9)) bad(`(j2) felled[${id}] = ${t} present in the t=12 snapshot`);
615+
if (mid.trees.get(id) !== 'stump') bad(`(j2) t=12: felled[${id}] but tree is not a stump`);
616+
}
617+
}
618+
596619
/* (l) road felling: nothing is left standing on finished road surface */
597620
for (const r of map.roads) {
598621
const list = roadEv.get(r.id) ?? [];

src/components/designs/genesis/Catalog.tsx

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -730,14 +730,19 @@ const COVERAGE: { head: string; body: string }[] = [
730730
'seasonCanvas runs over the six DECIDUOUS pools and the ground tint. crop, haystack, reeds and flowers are summer art in every month, so a winter valley still has a green cornfield in it.',
731731
},
732732
{
733-
head: 'Trees have two states and a nudge',
733+
head: 'A tree still falls in one nudge',
734734
body:
735-
'standing / stump, with felling drawn as the standing sprite shoved ±2px. No half-fallen trunk, no felled log on the ground, no sapling.',
735+
'The ladder now runs standing → felling → stump → stump with its log → stump, and three to five saplings come up on cleared ground in the evening. The fall itself is still the standing sprite shoved ±2px: there is no half-fallen trunk, no lean, and nothing between “upright” and “gone”.',
736736
},
737737
{
738-
head: 'Props are one sprite each',
738+
head: 'Crops ripen; nothing else does',
739739
body:
740-
'crop never ripens, haystack never shrinks, crates never stack higher as a town grows. Progress is expressed by buildings alone.',
740+
'crop is three sprites now and moves through them on the clock. haystack, crates, barrels and cart are still one sprite for the whole day — a haystack never grows as it is built up, and a store’s crates never stack higher as the town around them does. Everything except the fields still expresses progress through buildings alone.',
741+
},
742+
{
743+
head: 'The log is hauled by nobody',
744+
body:
745+
'A felled trunk lies for 90 world-minutes and then stops existing. No carrier walks out to it, no cart takes it, and the lumber pile in the yard does not grow by one when it goes — the two halves of the valley’s timber economy are drawn, and never joined up.',
741746
},
742747
];
743748

@@ -907,6 +912,40 @@ export default function Catalog() {
907912
const props = useMemo(() => poolItems(PROP_POOLS, 'pp'), [pools]);
908913
const treasure = useMemo(() => poolItems(TREASURE_POOLS, 'tz'), [pools]);
909914

915+
/* ---- living details (additive) ---------------------------------------- */
916+
917+
/** One field at three ages. Pool index is shared, so a plot keeps its
918+
* colour and its earth as it ripens; two of the four seeds shown. */
919+
const cropStages = useMemo(() => {
920+
const out: Item[] = [];
921+
const stages: [string, string][] = [
922+
['crop-sprout', 'sprout · before 10:00'],
923+
['crop', 'rows · 10:00–16:00'],
924+
['crop-tall', 'tall · after 16:00'],
925+
];
926+
for (let v = 0; v < 2; v++) {
927+
for (const [kind, sub] of stages) {
928+
const p = pools[kind] ?? [];
929+
if (p[v]) out.push(item(`cs-${v}-${kind}`, kind, spriteCanvas(p[v]), { sub }));
930+
}
931+
}
932+
return out;
933+
}, [pools]);
934+
935+
/** What a felling leaves on the ground, and what comes up after it. */
936+
const treeAfter = useMemo(() => {
937+
const out: Item[] = [];
938+
(pools.log ?? []).forEach((sp, i) =>
939+
out.push(item(`lg-${i}`, 'log', spriteCanvas(sp), { sub: TREE_KINDS[i] ?? `kind ${i}` }))
940+
);
941+
(pools.sapling ?? []).forEach((sp, i) =>
942+
out.push(item(`sa-${i}`, 'sapling', spriteCanvas(sp), { sub: `seed ${i + 1}` }))
943+
);
944+
return out;
945+
}, [pools]);
946+
947+
/* ---- end living details (additive) ------------------------------------ */
948+
910949
const extras = useMemo(() => {
911950
const out: Item[] = [];
912951
out.push(item('ex-nameboard', 'nameboard', spriteCanvas(buildNameBoard(ACCENT))));
@@ -1244,8 +1283,8 @@ export default function Catalog() {
12441283

12451284
const counts = {
12461285
structures: structures.length + materials.length + stages.length + roofs.length,
1247-
trees: trees.length + treeStates.length,
1248-
props: props.length + treasure.length + extras.length,
1286+
trees: trees.length + treeStates.length + treeAfter.length,
1287+
props: props.length + treasure.length + extras.length + cropStages.length,
12491288
inhabitants: anims.length,
12501289
wildlife: wildlife.length,
12511290
infra: bridges.length + roads.length + grounds.length,
@@ -1307,23 +1346,37 @@ export default function Catalog() {
13071346
id="trees"
13081347
title="Trees"
13091348
count={counts.trees}
1310-
blurb="buildTree(kind, seed) — seven kinds, four seeds each, plus the two states a tree can be caught in."
1349+
blurb="buildTree(kind, seed) — seven kinds, four seeds each, the states a tree can be caught in, and what its felling leaves behind."
13111350
>
13121351
<Row title={`Kinds (${TREE_KINDS.length})`} items={trees} />
13131352
<Row
13141353
title="States"
13151354
note="A tree under the axe is the standing sprite offset ±2px; once felled it is replaced by a stump."
13161355
items={treeStates}
13171356
/>
1357+
{/* ---- living details (additive) ---- */}
1358+
<Row
1359+
title={`After the axe (${treeAfter.length})`}
1360+
note="The trunk lies beside its stump for 90 world-minutes and is then hauled off to the yards — one log per tree kind, because the bark is all that tells them apart at this size. Three to five saplings come up on the day's cleared ground after 18:00, each beside a stump the day actually made."
1361+
items={treeAfter}
1362+
/>
1363+
{/* ---- end living details (additive) ---- */}
13181364
</Section>
13191365

13201366
<Section
13211367
id="props"
13221368
title="Props & scatter"
13231369
count={counts.props}
1324-
blurb="Every pooled kind from makePools(), all pool variants, plus the four built on demand by propSprite(). “unused” means gen.ts never emits it."
1370+
blurb="Every pooled kind from makePools(), all pool variants, plus the four built on demand by propSprite(). “unused” means gen.ts never emits it. The log and sapling pools are shelved with the trees, whose felling is the only thing that asks for them."
13251371
>
13261372
<Row title={`Pools (${PROP_POOLS.length} kinds)`} items={props} />
1373+
{/* ---- living details (additive) ---- */}
1374+
<Row
1375+
title="Crop stages (3)"
1376+
note="The one prop that is not a single sprite. A plot is sprouts before ~10:00, the rows that have always been here until ~16:00, and tall gold after — the stage is a pure function of the clock and a ±40-minute jitter off the plot's own seed, so a valley's fields do not all turn on the same stroke. All three stages share a pool index, so a field keeps its colour and its earth as it ripens."
1377+
items={cropStages}
1378+
/>
1379+
{/* ---- end living details (additive) ---- */}
13271380
<Row
13281381
title={`Treasure (${TREASURE_POOLS.length} states)`}
13291382
note="Pooled, but never a PropSpec: chests are their own list on the map. The timeline walks a found chest along all three — mound, prised open, emptied."

src/components/designs/genesis/fixture.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,9 @@ export function fixtureEmptySnapshot(map: GenesisMap): WorldSnapshot {
610610
return {
611611
t: 0,
612612
trees: new Map(),
613+
/* ---- living details (additive) ---- */
614+
felled: new Map(),
615+
/* ---- end living details (additive) ---- */
613616
buildings,
614617
roads: new Map(),
615618
bridges: new Map(),
@@ -657,6 +660,9 @@ function applyEvent(snap: WorldSnapshot, ev: GenesisEvent): void {
657660
break;
658661
case 'chop-done':
659662
snap.trees.set(ev.treeId, 'stump');
663+
/* ---- living details (additive) ---- */
664+
snap.felled.set(ev.treeId, ev.t);
665+
/* ---- end living details (additive) ---- */
660666
break;
661667
case 'survey':
662668
snap.buildings.set(ev.buildingId, { status: 'surveyed', progress: 0.05 });

0 commit comments

Comments
 (0)