Harvesting & Resource Nodes
Status: Designed — independent review applied
Author: Jeroen + agent panel
Last Updated: 2026-08-26
Independent /design-review: NEEDS REVISION 2026-08-26 — all findings applied same day (C1 supply ratio 2.19 corrected in Formulas + AC-28; C2 unharvested-% claim reworked with endpoints; C3 pairwise phase-knob ranges + Edge Case 10b monotonic-band load guard + AC-33b + hard-ceiling 2.0 now load-enforced; H4 windup-tell anticipation rule; H5 memorization honesty clause + Enemy AI live-variance dependency + OQ8; M1 soft-close deposit restated (Edge Case 5b); M2 trip-texture note; M3 no-discard ruling; M4 OQ6 escalated; M5–M7 + L-pass citations/examples. Full report:design/reviews/harvesting-system-review-2026-08-26.md)
Creative Director Review (CD-GDD-ALIGN): CONCERNS 2026-08-26 — all applied same day (C1 supply-invariant scope honesty + deferred trip-count companion; C2 modifier warn ceiling 1.2–2.0, ≤2.0 binding on Crafting; A1 carcass placement purity constraint; A2 fresh-kill silhouette test; A3 Fellowship framing on OQ6). Sinew carcass node ruled LEGAL under Pillar 3 on placement-independence grounds.
Implements Pillar: Pillar 1 — The Tide Waits for No One (harvesting is what the deadline acts on); Pillar 3 — Made by Our Hands (primary material source); serves Discovery aesthetic (#1)
Overview
Harvesting is how the tide's riches become the party's materials: walk up to a resource node on a docked land — an ore seam, a fiber stand, a timber snag — and strike it. Every node is a fast, chunky interaction (a few swings, a satisfying burst of yield straight into your bag), never a hold-to-channel; richer and rarer nodes sit deeper inland, so what you harvest is decided by how far you dare push before the land departs. Beneath the verb sits the machinery the tide demands: node placement and contents are seeded per docking by the scheduler, every node's depletion is host-authoritative shared state recorded as harvest diffs keyed to that specific docking, and harvest legality is checked against Docked at the moment each swing's yield lands — legal until the exact undock instant, gone the moment after. When a land departs, its unharvested nodes leave with it, and when it recurs, it returns regrown: the tide never keeps a tab. MVP ships 2–3 resource types feeding cloth-tier crafting; yields enter the world only through the bag on someone's back (Pillar 3 — no drops, no vendors).
Player Fantasy
Harvesting's fantasy is **the push you shouldn't have made — and made anyway**: you are always deciding whether this is the trip where you go one node too far. The anchor moment: a violet-lit vein deep inland, home out of sight behind the treeline, the bag nearly full and the land groaning at its edges. Two more swings clears the vein; three more reaches the rare seam behind it; the tide-clock has gone full blue. You swing anyway — the burst hits your bag as the ground goes translucent under your boots, and you're already running, laughing in voice chat before you reach the shore threshold. The arc of a good haul is **appetite → bet → sprint → shared relief**. Harvesting's triumph is deliberately noisier and more social than combat's quiet mastery: not "I placed that strike," but *"we got it out in time."*
Its necessary shadow is the disciplined trip — the boring, correct haul home with a half-empty bag because you did the math right and the land simply didn't offer more. Not every trip is the story; some trips are the tax you pay the tide. And the fantasy's long echo is provenance: weeks later, someone forges the group's first bronze sword and you can still name the land its tin came from — docked once, at 2 a.m., seen only by you before it left half-harvested. The veins you lost become group ghost stories exactly as readily as the ones you banked (concept: "missed lands become group lore").
The tide reads as weather, never as a game-show buzzer: indifferent, legible, negotiable by speed alone. The world states facts and never judges — a missed vein is "the land left with it," never failure copy. A caught player is deposited home with the bag intact (tide Rule 9) — the sprint for the shore is pride and texture; the cost of staying too long is the vein you didn't clear, never the bag you filled.
*(Honesty clause — independent review H5: after the first few dockings of a template, the bet narrows from discovering an unknown to executing under a known plan — you know the route; the question is whether tonight's guards, detours, and partners let you run it. That narrowing is the design — earned literacy, same as the tide clock — and what keeps the decision live across repeat visits is guard variation, a named load-bearing dependency on Enemy AI.)*
**Tone and design tests this fantasy imposes (binding on future harvesting features):**
- No compensation for lateness, in any form — no last-swing-always- lands, no grace tick, no partial credit mid-swing at undock (Pillar 1's ban list, harvesting-side restatement).
- A missed vein must read as "I knew and pushed anyway" — never "the game glitched" or "I was robbed." Departure must be legible in advance through the world (tide-clock color, the land's departure tells), never a countdown HUD.
- **No hold-to-channel, no charge-up, no stamina-gate on any future node type** — every node keeps the swings-and-burst verb. If a future node "needs" a slower interaction to feel special, the answer is rarity and depth inland, never a slower verb (Pillar 4).
- Provenance survives every system that touches materials — smelting, chains, trading may never anonymize origin into a fungible unit before use. If a future UI can't answer "where did this come from," the UI failed the fantasy.
- No safe, no-deadline resource tap may ever exist — every harvestable sits inside a docking window; **the home island has no harvest nodes at all** (ruled 2026-08-27, cross-GDD review W3 — the prior "home-adjacent nodes stay minor" clause was unrepresentable in F4's docking-keyed diff and contradicted this very test). The "always deciding" tension must never get an escape valve.
Reference anchors: Minecraft for the chunky, immediate swing feedback; Escape from Tarkov (soft version only) for "what you carry isn't banked until it's home" — opportunity cost, never loss of what you had. Divergences so calibrated players arrive braced: against Minecraft, **the world heals** — nodes regrow on recurrence and the land itself leaves; nothing you mine stays mined. Against Tarkov, extraction has no death stake at all — being caught at departure deposits you home, bag intact; the only loss is what you never swung at.
Detailed Design
Core Rules
- One verb, own constants. Harvesting reuses the **Strike input and phase shape** (windup → active → recovery) — it is not a fourth verb (combat's three-verb rule holds). Its timing constants are **independently owned by this GDD's Tuning Knobs, never inherited from Combat's** — a combat TTK rebalance must never silently retune harvesting. Commitment mirrors the shape: dash may cancel Harvest:Windup (you saw the raider and bailed to reposition), nothing cancels Active, Recovery is not skippable.
- Enemies always win disambiguation — decided at windup start, once. When the strike input fires, the client chooses the swing's referent: a live hostile inside the arc wins; otherwise the nearest non-depleted node within
harvestRangealong the aim direction; otherwise a plain combat swing (Pillar 4 — never force careful aim-away from a rock while a Rusher closes). It sends **one unified strike intent** `(swingId, aimDirection, targetKind ∈ {enemy, node}, nodeId?)` — the single message shape shared with combat Rule 7 (wire encoding ⚑ netcode ADR). Phase constants follow the referent (this GDD's for a node-intent, combat's for an enemy-intent — Rule 1's independent ownership holds). The host sanity-checks the referent choice against its own state at intent receipt (generous bound, Edge Case 15's family) and then resolves a node-intent against Rule 6's three gates only — a hostile stepping into the arc after windup never hijacks a harvest swing, and a node never absorbs an enemy-intent. *(Ruled 2026-08-27, cross-GDD review: the prior "test window" wording left open whether the referent was re-decided at resolution; it is not.)* **Deterministic anticipation (independent review H4): the windup tell must visibly commit to its referent — node or enemy — before Active begins**, so the player never commits blind to which object the swing will resolve against (silhouette-first pose language; asset details → art bible //asset-spec) — which is exactly why the referent is locked at windup start and never re-decided. - Damage never interrupts a harvest swing. Combat's Hurt overlay is non-blocking everywhere — one consistent rule across both systems. The cost of harvesting under threat is HP (the mistake budget), not stolen swings. Only resolution-time re-sampling (Rules 6, 9) can void a swing's outcome.
- A node is one integer. Nodes are host-authoritative shared state whose single stored fact is
swingsRemaining; **Depleted ⟺swingsRemaining == 0is computed, never an independent flag** (no state/counter drift class of bug). Node placement, type, and count are seeded per docking by Land Dock/Undock from the tide'svariationSeed— content is a pure function of(seed, socket, cycleIndex). - Per-swing chunky yield, fully deterministic. Each successful swing immediately banks
yieldPerSwingunits of the node's material;swingsTotalswings deplete the node. Same node type, same numbers, every time, everywhere — no RNG anywhere in the yield path (house zero-variance rule; Discovery is served by placement and depth, never by rolls). - **Resolution: intent → host, three gates re-sampled at the completion tick.** Client sends the unified strike intent with `targetKind = node
and the chosennodeId` at windup start (Rule 2); the host resolves at the swing's completion tick, re-sampling in order: (a)Docked— fresh, never cached (tide H1); (b)swingsRemaining > 0; (c)CanAdd(itemId, yieldPerSwing). All pass → decrement, atomicAddto the swinger's bag, diff written synchronously (tide Edge Case 10 discipline). Any gate fails → honest no-op: nothing mutates, no error theatrics. - Full bag is all-or-nothing.
CanAddis evaluated against the full burst — no partial grant, no destroyed overflow (full-bag semantics delegated to Harvesting by Inventory Rule 3; same all-or-nothing transaction family as Inventory Rule 6). The swing lands visually (blade connects, node reacts) but depletion and yield don't advance — it reads instantly as "no room," which is the bank-or-push pressure point, not a penalty. No voluntary discard exists at MVP (ruling 2026-08-26, independent review M3): no world items, no destroy verb — a full bag of fiber at a rich sinew vein means bank it or leave the vein, and the forced return trip is intended. Any future discard verb is Inventory's to design, not this GDD's. - Contested nodes: first host-arrival wins. The gates do the work — the second swing's own re-sample sees the state the first produced; the loser gets the same landed-but-fruitless no-op as Rule 7 (honest: the node ran out). Exact same-tick tie → **lowest
OwnerClientIdresolves first** (combat F5's tiebreak, reused — a determinism convenience, not a fairness mechanism; never surfaced to players as anything but "someone got there first"). Never split, never partial-credit. - Swinger state re-sampled too. If the swinger is Dead/Downed at the resolution instant, the swing is discarded — state re-sampled at resolution, never cached (combat Edge Case 15's pattern).
- MVP resource set (3): Fiber (shore flora — reed stands, wild flax), Timber (mid-band snags and groves), Sinew (deep-band game-trail/carcass nodes). Sinew nodes are **environmental harvestables struck with the same verb — never enemy kill drops** (Pillar 3). Handshake constraint on Enemy AI: fauna-type enemies must never double as loot sources. **Binding placement constraint (CD-GDD-ALIGN A1, 2026-08-26): carcass-node placement must remain a pure function of
(seed, socket, cycleIndex)and may never derive from a fauna death event — a node that spawns from a kill is a kill-drop and fails Pillar 3; any such proposal must return to the CD gate.** - Stepped depth bands, world-telegraphed. Shore / Mid / Deep bands with discrete richness tiers and type skew, marked by terrain reads (treeline, undergrowth density) — never a HUD overlay. Rare/high-yield nodes are weighted interior **specifically so distance-from-shore and the land's tide-color state read as one glance** — push-vs-bail is a single visual judgment, not two tracked stats. No minimap node-value omniscience, ever: node existence reads by silhouette at sightline; richness reads from silhouette size/shape, never a number. **The home island has no harvest nodes** — no deadline-free resource tap exists (Player Fantasy binding test; ruled 2026-08-27, cross-GDD W3: F4's diff key is docking-scoped and cannot represent a home node, so the ban is literal — a fresh group's first fiber comes from the docked land's shore band).
- Fixed content, no party scaling. Node counts and values never scale with player count or presence — the land is the land (preserves the pure-function world). A coordinated 5-player full-clear is a Fellowship achievement, not a bug. Revisit at Tier 1 → Open Questions.
States and Transitions
Node (stored fact: swingsRemaining only; state labels are derived):
| From | Event | To | Notes |
|---|---|---|---|
| Intact | successful swing resolves | PartiallyDepleted / Depleted | swingsRemaining -= 1; Depleted when it reaches 0 |
| PartiallyDepleted | successful swing resolves | PartiallyDepleted / Depleted | same decrement |
| Depleted | — | (no transitions) | illegal target — same family as a dead enemy in combat F3 |
| any | soft-close at φ+W | teardown | diff final (tide Rule 9); the node ceases to exist |
| — | recurrence (new cycleIndex) | Intact (fresh instance) | regrown; the old diff can never match the new key |
Swinger: reuses the Strike phase shape (Harvest:Windup → Active → Recovery) with harvesting-owned durations; dash cancels Windup only; the Hurt overlay never blocks (Rule 3). **A harvest intent is accepted only from Idle/Move** (host-validated, Edge Case 15) — intents sent while Dashing or mid-swing are rejected as silent no-ops; this single rule closes both the out-of-order and the intent-while-dashing cases.
Feedback authority split (port of combat's binding table):
| Effect | Timing |
|---|---|
| Swing animation, swing sound, impact sparks/chips on the node surface | Instant — client-predicted (outcome-independent) |
Node crack-stage change, yield burst VFX, yield-into-bag flash, swingsRemaining mutation, diff write | Host-confirmed broadcast only |
swingsRemaining replicates continuously to nearby clients — crack stages inform the approach decision before anyone commits a swing. Replication mechanism: ADR-0010 §4 (2026-09-11) — swingsRemaining is a row of LandNet.Nodes (NetworkList<NodeNetRow>), written before the land's spawn and on every Decrement; crack stages derive from it on every client.
Interactions with Other Systems
| System | Harvesting consumes | Harvesting provides | Interface owner |
|---|---|---|---|
| Tide Scheduler | cycleIndex/epochIndex (asked, never recomputed); Docked gate re-sampled at completion tick (H1) | Diffs written under scheduler-reported keys | Scheduler owns cycle math; Harvesting owns diff content + write timing |
| Inventory | CanAdd/Add at resolution (atomic) | itemId + amount per landed swing; full-bag no-op behavior (delegated to Harvesting by Inventory Rule 3) | Inventory owns stacks/capacity; Harvesting owns what-and-how-much |
| Save & Persistence | Reconcile-on-load validity predicate (tide's shape) | HarvestDiff = {(epochIndex, socket, cycleIndex, nodeId), swingsRemaining} — sparse: untouched nodes write no row | Harvesting owns diff schema + reconcile semantics; Save owns storage format. Quick-spec ✅ 2026-08-26 — HarvestDiff is the ONLY droppable table (two-class rule) |
| Land Dock/Undock | Node spawn at dock (fresh swingsTotal), teardown at soft-close | — | LDU owns placement from variationSeed; Harvesting owns depletion state after spawn |
| Combat | — (peer; shares the re-sample principle and the Strike input/phase shape only) | The disambiguation rule (enemies win, Rule 2) | Peer — no shared tuning constants, by Rule 1 |
| Enemy AI | Ambient threat near valuable nodes (no direct API) | Handshake constraints: guard presence must scale with node value (joint with Land Dock/Undock); fauna enemies never drop materials | Enemy AI owns behaviors; this GDD owns the constraints |
| Crafting (downstream) | — | Material stacks via Inventory (no direct coupling); provenance mechanism ⚑ Crafting/Inventory | Mediated entirely through Inventory |
| UI/HUD | — | Crack-stage derivation (from swingsRemaining/swingsTotal, presentation-only), full-bag cue | Harvesting owns derivation; UI owns visualization |
Formulas
All harvesting math is deterministic and integer — no RNG anywhere in the yield path (house zero-variance rule; tide-style seeded content provides all variety). round() where it ever appears means round-half-away-from-zero (MidpointRounding.AwayFromZero), matching combat's pinned mode.
Baseline content values (cloth tier, MVP)
| Constant | Value | Rationale |
|---|---|---|
swingsTotal (all node types) | 3 | Locked by concept ("three swings, satisfying burst"); one node ≈ a beat, not a channel |
yieldPerSwing — Shore band | 3 | Node total 9 ≈ 45% of a stack (20) — a slot fills in ~2 nodes' walking-and-swinging |
yieldPerSwing — Mid band | 4 | Node total 12; 1.33× shore |
yieldPerSwing — Deep band | 5 | Node total 15; 1.67× shore — the bet pays visibly |
| Nodes per land template | 40 (20 shore / 14 mid / 6 deep) | 50/35/15% split; deep is scarce by design |
| Band type skew | Shore: fiber-heavy · Mid: timber-heavy · Deep: sinew + rare fiber | Each band answers "why come here" |
Supply-vs-haul check (the Pillar 1 invariant): total land supply = 20×9 + 14×12 + 6×15 = 438 units; effective haul per trip = bagSlots(10) × maxStack(20) = 200 units; **supply ratio = 438/200 = 2.19 — a perfectly efficient player needs at least 3 full trips (⌈2.19⌉)** to strip a land. A casual 2-player session (~15–25 nodes visited between them, avg 10.95 units/node) hauls ~165–275 units, leaving ~37–63% of the land unharvested at departure — and even two completely full bags (400 units) cannot strip the 438-unit supply in one trip. The tide always takes something.
**Exported cross-system constants (added 2026-08-27, Base Building design-review BL-1):** the session-haul figures derived above exist as named constants owned by this GDD — downstream CI gates bind to them by name and must never re-derive or hardcode them:
| Constant | Value | Derivation |
|---|---|---|
lowEndPartyHaul | 165 units | Low end of the ~165–275 casual 2-player session haul (15–25 nodes × 10.95 avg units/node, above) |
mixedHaulTimberPerSessionLow | 58 | Party-scoped (the same 2-player casual session as lowEndPartyHaul). Timber share of a casual mixed haul: mid band is 38% of land supply (168/438), routing skew widens to ~35–50%; low end = low haul × low share = 165 × 0.35 = 57.75 → 58. (Re-derived 2026-08-27, BB re-review CS-2: the prior 70/120 did not follow from this arithmetic — it cleared BB's perimeter gate with zero margin and read as reverse-engineered from it; BB shrank its perimeter anchor instead.) |
mixedHaulTimberPerSessionHigh | 138 | Same derivation, upper end: 275 × 0.50 = 137.5 → 138 |
**Re-derive all three whenever yieldPerSwing, node counts, bagSlots, or maxStack change** (same trigger list as the supply-vs-haul re-run rule). Known consumer: **Base Building's bootstrapRatio and perimeterSessions CI gates (its AC-45/46)** read these constants live — a stale value here silently mis-tunes three systems' shared balance surface.
Trip-texture note (independent review M2): at these numbers the binding per-trip constraint is bag capacity, not the clock — the fantasy's time-capped sprint trip occurs mainly near departure windows; most disciplined trips end "bag full," not "tide turning." Whether that mix feels right is Open Question 4's playtest; LDU's informational stripTimeEstimate readout (its Rule 7) is what makes strip times visible at authoring — no gate measures the clock (cross-GDD review C3, 2026-08-27). The fantasy text is aspirational on this axis until then. **Re-run this check after any change to yields, node counts, bagSlots, or maxStack** — if total supply / (bagSlots × maxStack) < 1.5, a single trip can strip a land and Pillar 1's tension dies.
**Scope honesty (CD-GDD-ALIGN C1, 2026-08-26; restated 2026-08-27, cross-GDD review C3): this ratio is per-harvester, calibrated to MVP's 2-player scale**. The party-adjusted ratio (÷ concurrent harvesters) is 1.10 at 2 players and 0.44 at 5. **At party scale nothing protects the supply — not this check and not the clock.** The earlier claim that "Pillar 1 is protected by the clock (travel time inside W)" was false: W is 44 hours, and even inside a single two-hour session five harvesters strip a 438-unit land in roughly ten minutes (the LDU party-clock gate built on that claim could never pass and is retired — LDU Rule 7). The honest statement is Rule 12's: a coordinated full-clear is a Fellowship achievement, not a bug, and **Pillar 1's tension is per-session opportunity cost** — the nodes the group chose not to reach tonight, the land that leaves before the next session — never supply scarcity. This check therefore guards exactly one thing: that a single efficient harvester cannot strip a land in one trip. A future retune must not read a passing ratio as party-scale coverage. LDU keeps traversalProfile and an informational stripTimeEstimate readout (no assertion) so strip times are visible at authoring. Tier 1 revisit owned by Open Question 1.
F1. harvestYield
harvestYield = round(yieldPerSwing(nodeType, band) × harvestModifier)
Variables:
| Variable | Symbol | Type | Range | Description |
|---|---|---|---|---|
| Per-swing yield | yieldPerSwing | int | 3–5 (band table) | Data-driven per node type and band |
| Harvest modifier | harvestModifier | float | 1.0 (MVP, fixed) — Tier 1 tool values TBD by Crafting | The tool hook, sibling of combat's DamageModifier: Harvesting owns the signature, Crafting owns the values. Tier 1 tools are efficiency modifiers, never hard gates (Pillar 5 — any weapon harvests any node, always). Binding ceiling on Crafting (CD C2): the top-tier-to-bare-hands ratio must stay ≤ 2.0 — beyond that, efficiency becomes de facto gatekeeping and reintroduces the power divergence the flat-stats decision cut (game-concept, Core Mechanics 5) |
| Result | harvestYield | int | ≥ 0 | Units granted per landed swing, gated by CanAdd (Rules 6–7) |
Output Range: 3–5 at MVP (modifier fixed at 1.0); bounded by the tool ladder at Tier 1. Example: deep-band sinew node, no tool bonus: round(5 × 1.0) = 5 per swing, 15 per node. **Midpoint example (the pinned rounding mode is load-bearing here):** at a Tier 1 modifier of 1.5: shore round(3 × 1.5) = round(4.5) = 5; deep `round(5 × 1.5) = round(7.5) = 8` — half-away-from-zero decides both, deterministically.
F2. nodeTotalYield (derived)
nodeTotalYield = swingsTotal × harvestYield — never stored, always computed; the diff stores only swingsRemaining (Rule 4). Worked check: shore fiber = 3 × 3 = 9 ✓ matches the baseline table.
F3. Trip-fill pacing check (design-time)
nodesPerStack = ceil(maxStack / nodeTotalYield) → shore: ⌈20/9⌉ = 3 nodes per stack; a focused 15–30 min trip visiting 12–18 nodes fills 4–6 slots — meaningful without demanding the full bag every trip. Scope: this is the all-shore case — mid (⌈20/12⌉ = 2) and deep (⌈20/15⌉ = 2) nodes fill stacks faster, so a mixed-band push fills the bag in fewer nodes. Re-run alongside the supply-vs-haul check when retuning.
F4. HarvestDiff — data contract, not a formula
`HarvestDiff = { key: (epochIndex, socket, cycleIndex, nodeId), swingsRemaining: int } with 0 ≤ swingsRemaining ≤ swingsTotal(nodeId). swingsTotal` is static content data, never persisted per-instance (a rebalance can't corrupt old diffs); untouched nodes write no row (mirroring the tide diff-key design, which likewise never persists rows for untouched state); Depleted is representable as swingsRemaining: 0 — no boolean. Yield-taken history is deliberately not recorded — the bag owns what was taken; the diff owns only what remains. Example row: {key: (0, 1, 2, "shore_fiber_014"), swingsRemaining: 1} — one swing left, two taken. (Self-declared non-formula — the documentation convention follows combat F6; ranges live here and in Tuning Knobs.)
Edge Cases
Tide soft-close legality and same-tick contest rules originate in Core Rules 6–8; entries below restate them in edge-case form where a QA tester would look for them, and add the genuinely new rulings.
Disconnects
- If a client disconnects with a harvest intent in flight: the host still resolves it normally at the completion tick — a disconnect never retroactively un-happens a committed swing; the yield
Addtargets the offline player's persisted CARRY like any other resolution (combat Edge Case 1's pattern, bag instead of HP). - If the host quits mid-harvest: the session ends (host migration is banned) — no harvesting-specific rule needed; stated so it doesn't read as an omission (combat Edge Case 3).
- "Yield granted but bag not credited" cannot exist: decrement,
Add, and diff write execute as one host-side critical section (Rule 6 + Inventory Rule 6); clients only ever see the post-state broadcast. Closed as structurally impossible — a closure note, not a scenario.
Tide boundaries
- If a swing's completion tick lands at
t ≥ φ+W: it fails outright; att = φ+W − 1it succeeds in full — no grace tick in either direction (restates tide AC-22/22b so this doc stands alone). - If a land soft-closes with a node never touched: no diff row exists and none is written — correct by sparse design; an untouched node is indistinguishable from a never-spawned one at reconcile (consistent with the tide diff-key design, which likewise never persists rows for untouched state).
5b. If a player is still on the land at φ+W (restatement): they are deposited at the home shoreline with carried inventory untouched (tide Rule 9 / AC-23; also restated by combat and the Inventory quick-spec). Being caught costs position and the unharvested land — never bag contents. Stated here so this doc stands alone (independent review M1).
- **If a legal last-tick resolution is still executing when teardown fires**: teardown is sequenced strictly after any resolution already in progress on that tick — a
t < φ+Wharvest's diff write must complete, never be raced. **Deliberately inverted from combat Rule 13**: combat discards pending resolutions at the boundary; harvesting's last-legal-tick case must complete — the intended-behavior last-tick harvest (tide Edge Case 2) depends on it.
Config validation (load-time, fail loudly)
yieldPerSwingmissing or ≤ 0 → loud reject (a silent zero-yield node reads as a bug; combat Edge Case 12 family).swingsTotal≤ 0 → loud reject — nodes must spawn Intact; enforce≥ 1.harvestModifier≤ 0 → loud reject (breaks deterministic-yield legibility).harvestModifier> 2.0 (the F1 hard ceiling) → loud reject — the ceiling is enforced, not advisory (independent review C3). Values in(harvestModifierWarnCeiling, 2.0]→ **loud warning, not rejection** (ruling 2026-08-26: structurally-broken hard-rejects; deliberately-surrendered invariants warn — tide's AC-42b precedent).- A node's
itemIdnot registered as an item → loud reject (cross-system reference validation; new to this GDD's family).
10b. **Non-monotonic band yields (`yieldPerSwing[deep] ≤ yieldPerSwing[mid] or yieldPerSwing[mid] ≤ yieldPerSwing[shore]`)** → loud reject — the push-inland premise is a load-enforced invariant, not a table footnote (independent review C3).
Bag interplay
- If
CanAddis true at intent but false at resolution: already resolved — gate (c) is the re-sample and Rule 7 states the outcome; closure note so no reader wonders if it was missed. - If the bag can absorb part of a burst (worked example: existing fiber stack has room for 2, burst is 3): all-or-nothing — no partial fill, no destroyed overflow, honest fruitless swing. **Pre-emption note**: this is the rule a future contributor is most likely to "helpfully" soften; doing so destroys the full-bag "instant, legible no" the Player Fantasy depends on.
- If CARRY mutates mid-swing (e.g., a deposit frees space between windup and completion): no hazard — gate (c) reads current state at exactly one host-serialized instant; whatever the bag holds at the completion tick is what counts ("fresh, never cached" working as intended).
Co-op
- If two players' final swings on one node land the same tick: first host-arrival wins; exact tie → lowest
OwnerClientId(Rule 8, promoted here for QA visibility). - If intents from one client arrive out of order, or spammed: structurally impossible in legitimate play — one swing in flight, Recovery unskippable. The host validates the swinger's state is Idle/Move before accepting an intent; violations are rejected as silent no-ops (no error theatrics). The host also sanity-checks node proximity against the swinger's host-side position (generous bound — exact slack ⚑ netcode ADR; friends-only posture, cheat-proofing is not a goal). Constraint owned here; validation mechanism ⚑ netcode ADR (combat's precedent). **Ruling: the no-op flavors (full bag, depleted/contested, illegal intent) are one mechanism, deliberately indistinguishable at the rules layer** — any UI differentiation is presentation-only (Open Question 6), and telemetry must never leak what the fantasy withholds.
- **If a mid-docking joiner swings at a node whose replicated state hasn't arrived**: staleness is cosmetic only (crack stages are host-confirmed broadcast); gate (b)'s re-sample turns it into an honest no-op (combat Edge Case 17's pattern). Closed by ADR-0010 §4 (2026-09-11):
swingsRemainingis aNetworkListrow delivered with the land's spawn message (source-confirmed), so the staleness window is ADR-0008's one tick.
Death / Downed
- If the swinger dies or is Downed between intent and resolution: the swing is discarded — swinger state re-sampled at resolution, never cached (Rule 9, promoted; combat Edge Case 15's pattern).
- If a Downed player sends harvest input: rejected — **Downed forecloses Strike:Windup entirely, no harvest carve-out** (ruling 2026-08-26): crawl state has no verbs, matching combat's "no-combat" reading of the shared phase machine literally.
Recurrence / persistence
- If a save from a previous epoch is loaded: any diff whose
(epochIndex, socket, cycleIndex)doesn't match scheduler-current values is stale → dropped → the node reads fully regrown (tide Edge Case 14 / AC-30/31 contract). Harvesting never derives or caches epoch/cycle values — it asks (same discipline asDocked). - **If a live diff references a
nodeIdabsent from the current template** (node-layout content patch): the orphaned diff is dropped and logged — never a crash, never a resurrect. Distinct axis from tide Edge Case 13 (template selection changes vs. node layout within a template); new Harvesting-owned ruling.
Scope closures
- No line-of-sight check at MVP (ruling 2026-08-26):
harvestRangeis melee-short and MVP terrain has no occluding geometry — matching combat's arc test, which likewise has no occlusion term. Revisit only if a future node type or terrain feature introduces genuine LOS-breaking geometry.
Dependencies
Upstream (Harvesting depends on):
| System | Hard/Soft | Interface consumed | Status |
|---|---|---|---|
| Tide Scheduler | Hard | cycleIndex/epochIndex asked, never recomputed; Docked re-sampled at completion tick (H1); teardown-after-resolution ordering (Edge Case 6) | GDD Reviewed — tide's Interactions table already carries the Harvesting row (bidirectional ✓) |
| Inventory & Hauling | Hard | CanAdd/Add atomic at resolution; full-bag semantics delegated to Harvesting by Inventory Rule 3 | Quick-spec written 2026-08-26 ✓ |
| Land Dock/Undock | Hard | Node spawn from variationSeed (placement/type/count per docking); teardown at soft-close | Quick-spec ✅ 2026-08-27 — Rule 2 owns placement from variationSeed, Rule 8 socket layout; contract confirmed |
| Save & Persistence | Hard | Stores/restores sparse HarvestDiff; drops stale/orphaned diffs per Edge Cases 19–20 | Quick-spec ✅ 2026-08-26 (design/quick-specs/save-persistence-2026-08-26.md — droppable-reconcile stage invokes tide's predicate + these edge cases) |
| Art bible | Constraint source | Silhouette-first node identity and richness readability; band terrain reads; no numeric overlays | Approved. ⚑ Art-bible fix-pass item: node rarity needs its own silhouette/size grammar that never competes with the tide's color channel |
Downstream (systems that depend on Harvesting):
| Dependent | Hard/Soft | Interface provided |
|---|---|---|
| Crafting & Gear (GDD Reviewed) | Hard | The material set (Fiber/Timber/Sinew) via Inventory; the HarvestModifier hook signature — Crafting owns values, tools are efficiency modifiers, never hard gates (Pillar 5); tool-swing durability events (Tier 1 — cross-GDD W2, 2026-08-27, crafting OQ5's harvesting half): host-side HarvestSwingLanded(swinger, toolInstance) per successful resolution, consumed by crafting F3 once tools exist — no MVP emitter |
| Base Building | Hard (named constants) | lowEndPartyHaul, mixedHaulTimberPerSessionLow/High — the exported Formulas constants BB's CI gates AC-45/46 bind to by name (row added 2026-08-27, cross-GDD W5) |
| Enemy AI (quick-spec ✅ 2026-08-27) | Constraint recipient | All three constraints adopted: fauna-no-drops (its Rule 10), guard-presence-scales-with-value with numbers (its Rule 9, guardDensityByBand monotonic + load-checked), and per-docking guard variation delivered as its Rule 8 — a pure function of variationSeed over composition/patrols/facing; playtest verifies it actually keeps the push decision live (OQ8) |
| UI/HUD (quick-spec ✅ 2026-08-27) | Soft | Crack-stage derivation, full-bag cue |
Bidirectional-consistency obligations: tide↔harvesting rows verified consistent both ways (2026-08-26); Inventory's Rule 3 explicitly delegates full-bag behavior here ✓; Land Dock/Undock, Save, Enemy AI, and UI/HUD quick-specs, when authored, must list Harvesting as a dependent and consume these interfaces exactly. Combat is a peer, not a dependency — shared Strike input/phase shape and the re-sample principle only, each stated independently (matches combat's own "Harvesting is a parallel peer" note).
Forward references owned elsewhere: intent RPC shape, resolution-tick scheduling, swingsRemaining replication, intent state-validation mechanism → netcode ADR. harvestModifier values, item definitions, provenance mechanism → Crafting GDD. Node placement per template → Land Dock/Undock quick-spec. Diff storage format → **Save quick-spec**.
Tuning Knobs
Not tuning knobs (rules, locked by this GDD): deterministic yields (no RNG), all-or-nothing CanAdd, sparse diff schema, enemies-win disambiguation, damage-never-interrupts, no partial credit at undock, fixed content (no party scaling), tools-never-gates. Changing any of these is a design change requiring GDD revision, not a config edit.
| Knob | Default | Safe range | Too low | Too high | Interacts with |
|---|---|---|---|---|---|
harvestWindup | 0.10 s | 0.08–0.15 (pairwise: 0.08 legal only if harvestActive ≥ 0.10) | Latency stops hiding in the swing | Nodes feel mushy | Guard: harvestWindup + harvestActive ≥ 0.18 s — resolution must sit ≥ 1 RTT after intent (bounds the stale-state window; same netcode invariant shape as combat's swing guard, independently owned) |
harvestActive | 0.10 s | 0.08–0.15 (pairwise: 0.08 legal only if harvestWindup ≥ 0.10) | same guard | Swings feel floaty | same guard |
harvestRecovery | 0.05 s | 0.03–0.10 | No rhythm, feels like a channel | Whiffs on depleted nodes over-punished | dash-cancel exists only in Windup |
harvestRange | 1.6 u | 1.4–2.0 | Face-hugging nodes | Harvesting across visual gaps reads wrong | No arc/range tolerance widening — the target is static; forgiveness is a latency device, not needed here |
yieldPerSwing (shore/mid/deep) | 3 / 4 / 5 | 2–8 per band | Trips feel unproductive | Strip-mining; Pillar 1 dies | Re-run the supply-vs-haul check after any change; bands must stay strictly monotonic (deep > mid > shore) or the push-inland premise dies |
swingsTotal | 3 | 2–4 | Nodes pop, no beat | Drifts toward channel feel | Concept anchor is 3 ("three swings"); crack-stage count follows it |
nodesPerLand (shore/mid/deep) | 20 / 14 / 6 | 30–60 total | Land feels empty | Supply check fails; visual clutter | supply-vs-haul check; land template authoring (LDU); retune preserving the ~50/35/15 band split — total alone doesn't protect deep scarcity |
harvestModifierWarnCeiling | 1.5 | 1.2–1.8 (must stay < 2.0 or the warn band is empty) | Tuning experiments nagged | De facto gatekeeping — power divergence returns (CD C2; hard ceiling 2.0 = the F1 binding constraint on Crafting, load-enforced per Edge Case 9) | Edge Case 9's warn/reject thresholds |
Knob-interaction warnings: yieldPerSwing, nodesPerLand, bagSlots, and maxStack form one balance system — retune together and re-run the Formulas supply-vs-haul and trip-fill checks. The harvest phase guard is a netcode invariant wearing a feel-knob costume (combat's lesson) — enforce at config load. All values live in assets/data/harvesting.json, validated per Edge Cases 7–10.
Visual/Audio Requirements
Harvesting's visual contract is owned by the art bible; this section states what harvesting imposes and cites rather than restates.
Node read contract (binding): a node must communicate three facts in silhouette at solid black, 64–96 px (§5.4), while the world moves (§1): material identity (fiber / timber / sinew — distinct shape families), richness (band/rarity read from **size and shape only — never color**: the color-temperature channel belongs exclusively to the tide clock, and nodes, being land content, are tide-tinted unlike combat sprites — every harvesting read must survive amber, neutral, and violet), and depletion state (crack stages). ⚑ This is the art-bible fix-pass item from Dependencies: node rarity needs its own silhouette/size grammar.
Crack stages: swingsTotal visual stages (3 at MVP), expressed as hard-edged shape subtraction — visible chunks missing, no gradients, no damage decals (§6.2). A Depleted node leaves a husk silhouette, not an empty space — the land visibly remembers the trip's work for the rest of the docking.
Yield burst: chunky, flat-colored, palette-locked particle burst at low count (Nuclear Throne restraint, §9); burst scale may grow with yieldPerSwing so deep-band nodes feel richer; no particle may obscure a silhouette edge (combat's rejected-VFX list applies).
Feedback timing: swing animation/sound client-predicted; crack-stage change, yield burst, and bag flash host-confirmed only (authority table in Detailed Design). Per-material impact audio (ore clink / fiber rustle / sinew tear) desirable, not MVP-blocking; yield audio lands the same frame as the burst.
Silhouette-first test for node assets (pass/fail): at solid black, 64–96 px: material identity legible; richness tier legible by size/shape; crack stage legible; a node must never read as a threat silhouette (enemy proportions are reserved, §5.2); and a carcass node must **never read as a fresh kill left by combat** (CD A2 — guard-scaled deep-band sinew must not look like loot even though it isn't).
📌 *Asset Spec note: after this GDD is approved, run /asset-spec system:harvesting-system to derive per-asset descriptions, dimensions, and generation prompts from this section.*
UI Requirements
Harvesting contributes three things to the UI layer: **world-space crack-stage feedback** (on the node itself — never a HUD health bar for nodes), a glanceable full-bag cue (a non-modal bag pulse on the fruitless-swing no-op — never an error dialog or toast, per the honest-no-op tone rule), and host-confirmed yield flyouts (the harvesting sibling of combat's damage numbers — the only numeric harvest feedback). Binding constraints: bag fill level must be glanceable without opening a menu; **no minimap or overlay may ever show node values or locations beyond sightline discovery** (Core Rule 11 restated as a UI ban); copy follows the world-facts tone rule ("the land left with it").
📌 UX Flag — Harvesting: this system has UI requirements. In Pre-Production, run /ux-design for the bag/HUD elements before writing epics; harvesting stories referencing UI must cite design/ux/hud.md, not this GDD.
Acceptance Criteria
[UNIT] = EditMode NUnit over pure logic (yield math, resolution gates, node state derivation, diff schema, config validation) — the harvest pipeline (Rule 6, F1–F4) must be designed pure/DI-testable, no live NGO session required. [INTEGRATION] = NGO runtime, Multiplayer Play Mode + Network Simulator. [MANUAL] = human feel judgment, never automated (coding standards) — but scoped to a concrete, checkable claim. All criteria use locked baseline values (cloth tier, MVP) unless the vector specifies otherwise.
Verb Shape & Commitment (Rule 1)
- AC-1 [UNIT] GIVEN Harvest:Windup, WHEN dash input fires, THEN the swing intent is revoked and state → Dashing (bail-out mirrors combat's Rule 3 pattern).
- AC-2 [UNIT] GIVEN Harvest:Active, WHEN any cancel input fires, THEN state does not change — Active is never cancellable.
- AC-3 [UNIT] GIVEN Harvest:Recovery, WHEN elapsed, THEN state → Idle/Move — Recovery is never skippable regardless of input.
- AC-4 [UNIT] GIVEN
harvestWindup/harvestActive/harvestRecoveryconfig values, WHEN inspected, THEN they are read from this GDD's own Tuning Knobs table and never fall back to or inherit Combat'sswingWindup/swingActive/swingRecoveryconstants (independent-ownership guarantee).
Disambiguation (Rule 2)
- AC-5 [UNIT] GIVEN both a live hostile and a node inside the arc at the instant the strike input fires, WHEN the referent is chosen, THEN the intent carries
targetKind = enemyand the node is not struck; GIVEN a node-intent already sent and a hostile entering the arc before the completion tick, WHEN the host resolves, THEN only the three harvest gates run — the hostile is untouched and the node is struck (referent locked at windup start, Rule 2; cross-GDD review 2026-08-27). - AC-6 [UNIT] GIVEN only nodes inside the arc at the instant the strike input fires, WHEN the referent is chosen (Rule 2, windup start), THEN the intent names the nearest non-depleted node within
harvestRangealong the aim direction, and that node is the one struck at resolution.
Damage Non-Interruption (Rule 3)
- AC-7 [UNIT] GIVEN a harvest swing in any phase, WHEN the swinger takes damage, THEN the Hurt overlay applies without changing the harvest state machine's phase or timers (non-blocking, matches combat's Hurt overlay rule).
Node Identity & Depletion (Rule 4)
- AC-8 [UNIT] GIVEN a node with
swingsRemaining = 0, WHEN its Depleted status is read, THEN it is computed asswingsRemaining == 0and no independentisDepletedflag exists anywhere in the node's stored state. - AC-9 [UNIT] GIVEN a fixed
(seed, socket, cycleIndex)tuple, WHEN node placement/type/count generation runs twice, THEN both runs produce bit-identical results (pure-function content).
Deterministic Yield (Rule 5, F1)
- AC-10 [UNIT] GIVEN
yieldPerSwing=5, harvestModifier=1.0(deep-band sinew), WHEN F1 evaluates, THENharvestYield=5per swing (worked example, pinned). - AC-11 [UNIT] GIVEN a deep-band sinew node with
swingsTotal=3, WHEN all 3 swings land successfully, THEN cumulative yield banked = 15 units (F1 × F2, pinned worked example from Formulas). - AC-12 [UNIT] GIVEN identical F1 inputs, WHEN evaluated 1,000×, THEN output is bit-identical every call — no RNG anywhere in the yield path (zero-variance rule, mirrors combat AC-9).
Resolution Pipeline (Rule 6)
- AC-13 [UNIT] GIVEN a node-intent (the unified strike intent with
targetKind = nodeand anodeId, Rule 2), WHEN the host resolves at the completion tick, THEN gates (a)Docked, (b)swingsRemaining > 0, (c)CanAddare evaluated in that order, each freshly sampled — never cached from intent time. - AC-14 [UNIT] GIVEN all three gates pass, WHEN resolution completes, THEN
swingsRemainingdecrements by 1, the item is atomicallyAdded to the swinger's bag, and the diff write happens synchronously in the same operation. - AC-15 [UNIT] GIVEN any one gate fails, WHEN resolution completes, THEN no mutation occurs anywhere (node, bag, or diff) — an honest no-op with no error signaled.
- AC-16 [UNIT] GIVEN
Dockedwas true at intent send but false at the resolution tick, WHEN gate (a) is evaluated, THEN the swing fails outright regardless of the intent-time value (fresh re-sample, tide H1).
Full-Bag All-or-Nothing (Rule 7, Edge Case 12)
- AC-17 [UNIT] GIVEN an existing stack with room for 2 units and a burst of
yieldPerSwing=3, WHEN gate (c)CanAddevaluates against the full burst, THEN the swing produces zero yield and zero decrement — no partial fill, no destroyed overflow (worked example, pinned). - AC-18 [UNIT] GIVEN
CanAddis true at intent send but false at the resolution tick, WHEN gate (c) evaluates, THEN the swing lands visually-inert (Rule 7's no-op) with no distinct code path from AC-15 — the same mechanism, not a special case.
Contested Nodes (Rule 8, Edge Case 14)
- AC-19 [UNIT] GIVEN two players' final swings on one node resolving on different ticks, WHEN the second swing's resolution runs, THEN gate (b) sees
swingsRemainingalready at 0 and the second swing is an honest no-op. - AC-19b [UNIT] GIVEN two players' final swings on one node with resolution instants on the exact same tick, WHEN resolved, THEN the swing from the lowest
OwnerClientIdapplies and the other is discarded — deterministic and repeatable across runs (tiebreak reused from combat F5).
Swinger State Re-Sample (Rule 9, Edge Cases 17–18)
- AC-20 [UNIT] GIVEN a swinger who reaches Dead or Downed after intent send but before the resolution tick, WHEN resolution runs, THEN the swing is discarded — swinger state re-sampled at resolution, never cached.
- AC-21 [UNIT] GIVEN a Downed player, WHEN a harvest input is submitted, THEN it is rejected outright — Downed forecloses Harvest:Windup entirely, no harvest-specific carve-out (ruling 2026-08-26).
MVP Resource Set (Rule 10)
- AC-22 [UNIT] GIVEN the MVP resource config, WHEN loaded, THEN exactly 3 resource types exist (Fiber, Timber, Sinew) and none are flagged as an enemy-kill drop source.
- AC-23 [UNIT] GIVEN a fauna-type enemy definition, WHEN cross-validated against the item registry, THEN it has zero associated harvestable-item drops (Pillar 3 handshake constraint with Enemy AI).
Depth Bands & Content Skew (Rule 11)
- AC-24 [UNIT] GIVEN the baseline band table, WHEN
yieldPerSwingvalues are compared, THEN Deep > Mid > Shore strictly holds (monotonic band premise). - AC-25 [UNIT] GIVEN the home-island content data, WHEN validated at load, THEN it contains zero harvest-node definitions — any node authored on the home island is a loud load reject (no deadline-free resource tap; ruled 2026-08-27, cross-GDD W3 — replaces the prior "minor/shore-tier only" allowance, which F4's docking-keyed diff could not persist).
Fixed Content, No Party Scaling (Rule 12)
- AC-26 [UNIT] GIVEN identical
(seed, socket, cycleIndex)content generation invoked with party sizes of 1 and 5, WHEN node counts and per-node values are compared, THEN they are identical — no party-count term anywhere in content generation.
Formulas — Worked Examples & Design-Time Checks (F1–F4)
- AC-27 [UNIT] GIVEN
yieldPerSwing=5 (deep sinew), harvestModifier=1.0, WHEN F1/F2 evaluate, THENharvestYield=5per swing andnodeTotalYield = 3 × 5 = 15— pins the GDD's worked example. - AC-28 [UNIT] GIVEN the baseline content table (20 shore/9 + 14 mid/12 + 6 deep/15,
bagSlots=10,maxStack=20), WHEN the supply-vs-haul check runs, THEN total supply = 438, effective haul = 200, ratio = 2.19 (⌈ratio⌉ = 3 full trips to strip) — run in CI wheneveryieldPerSwing, node counts,bagSlots, ormaxStackchange; fail the build if the ratio drops below 1.5 (F3 invariant, design-time gate). - AC-29 [UNIT] GIVEN a
HarvestDiffrecord, WHEN its schema is validated, THEN it contains exactly{key: (epochIndex, socket, cycleIndex, nodeId), swingsRemaining: int}with0 ≤ swingsRemaining ≤ swingsTotal(nodeId), andswingsTotalitself is never present in the persisted row (F4 contract).
Tide Boundary & Teardown Ordering (Edge Cases 4, 6)
- AC-30 [INTEGRATION] GIVEN a swing's completion tick at exactly
t = φ+W − 1, WHEN resolved, THEN it succeeds in full; GIVENt ≥ φ+W, THEN it fails outright — no grace tick in either direction (Edge Case 4). - AC-31 [INTEGRATION] GIVEN a legal last-tick resolution (
t < φ+W) already in progress when teardown fires on the same host tick, WHEN both are due, THEN teardown is sequenced strictly after that resolution completes — the diff write finishes, never raced (Edge Case 6; deliberately inverted from combat Rule 13's discard).
Config Validation (load-time, fail loudly — Edge Cases 7–10b)
- AC-32 [UNIT]
yieldPerSwingmissing or≤ 0→ load rejected. - AC-33 [UNIT]
swingsTotal ≤ 0→ load rejected (must be≥ 1). - AC-33b [UNIT]
yieldPerSwing[deep] ≤ yieldPerSwing[mid], oryieldPerSwing[mid] ≤ yieldPerSwing[shore]→ load rejected (Edge Case 10b — monotonic bands are load-enforced, not only baseline-tested by AC-24). - AC-34 [UNIT]
harvestModifier ≤ 0→ load rejected;harvestModifier > 2.0→ load rejected (the F1 hard ceiling, enforced); values in(harvestModifierWarnCeiling, 2.0](ceiling default 1.5) → load succeeds with a loud warning (Edge Case 9's reject/warn split). - AC-35 [UNIT] A node's
itemIdnot present in the registered item table → load rejected. - AC-36 [UNIT]
harvestWindup + harvestActive < 0.18 s→ load rejected (netcode-invariant phase guard).
Intent Validation & Live Contention (Edge Cases 14–15)
- AC-37 [INTEGRATION] GIVEN two players' harvest intents on the same node with same-tick resolution instants, WHEN resolved under live NGO timing, THEN outcomes match AC-19/AC-19b — not just the pure-logic simulation.
- AC-38 [UNIT] GIVEN a swinger not in Idle/Move (mid-swing or Dashing), WHEN a harvest intent arrives from that client, THEN the host rejects it as a silent no-op — no error theatrics, no double-resolution.
- AC-38b [UNIT] GIVEN a harvest intent naming a node beyond the host-side proximity bound of the swinger's position, WHEN validated, THEN it is rejected as a silent no-op (Edge Case 15's sanity check; exact slack ⚑ netcode ADR).
Cross-Epoch Reconcile & Orphaned Diffs (Edge Cases 19–20)
- AC-39 [UNIT] GIVEN a saved
HarvestDiffwhose(epochIndex, socket, cycleIndex)does not match the scheduler's current values, WHEN reconcile-on-load runs, THEN the diff is dropped and the node loads fully regrown (Intact). - AC-40 [UNIT] GIVEN a saved
HarvestDiffreferencing anodeIdabsent from the current land template, WHEN reconcile-on-load runs, THEN the diff is dropped and logged — never crashes, never resurrects a node.
Minimum smoke set (run first)
- AC-12 — zero-variance determinism: if yield isn't bit-identical every run, no downstream value (bag contents, diffs) can be trusted.
- AC-30 — last-tick legality: the boundary between "banked" and "the tide took it" is the entire Player Fantasy contract.
- AC-31 — teardown-after-resolution ordering: highest-blast-radius cross-system rule here, and deliberately inverted from combat — the easiest rule for a future contributor to "fix" back by mistake.
- AC-17 — full-bag all-or-nothing: explicitly flagged as the rule most likely to be "helpfully" softened; a regression looks like a UX improvement, not a bug.
- AC-39 — cross-epoch diff reconcile: the single highest-consequence persistence bug this system can produce (stale-diff resurrection or duplicated yield).
Three are [UNIT]; AC-30/AC-31 are [INTEGRATION] but run headless in Multiplayer Play Mode — together they run in under a minute as a pre-QA-handoff gate, matching combat's smoke-set shape.
Deliberately not criteria (and why)
- "The chunky burst feels satisfying" / "swings feel weighty" — feel qualities, routed to
[MANUAL]playtest evidence with lead sign-off once art/VFX exists; no numeric proxy substitutes for a body swinging it. - Exact intent RPC shape,
swingsRemainingreplication mechanism, intent state-validation mechanism — deferred to the netcode ADR (⚑ flags in Edge Cases); testing a wire shape that isn't locked yet tests the wrong thing. harvestModifiertool-tier values above 1.0 — owned by the Crafting GDD; this GDD owns only the hook signature, covered at MVP's fixed 1.0 by AC-27/AC-34.- Node visual legibility (silhouette identity, crack-stage readability, richness-by-size) — an art-bible / asset-review concern, not a pass/fail logic contract; flagged as an art-bible fix-pass item in Dependencies.
Coverage note: Core Rules 1–12 and Formulas F1–F4 each have ≥1 criterion. Config-validation Edge Cases 7–10 each have a dedicated load-time criterion (AC-32–36, with AC-36 covering the Tuning Knobs phase guard). Edge Case 3 is a closure note (structurally impossible state) — covered by AC-14's atomicity claim, not independently testable. Edge Case 16 (joiner staleness) is replication behavior deferred to the netcode ADR; AC-13/AC-16 cover only the resolution-tick re-sample, which is orthogonal.
Open Questions
| # | Question | Owner | Target |
|---|---|---|---|
| 1 | Fixed node content at 5 players — does depth stay a real bet, or does Tier 1 need a different posture? (locked fixed at MVP; pure-function world preserved) | Tier 1 design pass + playtest | Tier 1 |
| 2 | HarvestModifier tool values and tool item design (efficiency-only, never gates) | Crafting GDD | Systems Design phase |
| 3 | swingsRemaining replication strategy, intent RPC shape, host state-validation mechanism (Edge Cases 15–16) | Netcode ADR | After friend-group playtest |
| 4 | Per-swing yield (locked) vs final-swing burst — does the chunky beat land in practice, tested over a full 12–18-node trip (fatigue), not a single node? Also: does the bag-capped trip texture (see Trip-texture note) leave the clock-pressure beat frequent enough? | MVP friend-group playtest | Post-MVP tuning |
| 5 | ~~Sinew carcass-node fiction and the fauna/enemy boundary (fauna never drop materials — how do carcass nodes spawn?)~~ RESOLVED 2026-08-27 — LDU Rule 2 places carcass nodes as pure variationSeed content; Enemy AI Rule 10 binds fauna to never drop materials or spawn a carcass (cross-GDD W6) | ~~Enemy AI quick-spec + Land Dock/Undock~~ Resolved | Done |
| 6 | Do contested-node and full-bag no-ops need subtly distinguishable cues, or stay identical? (mechanically identical by design) Fellowship #2 exposure (CD A3): "my bag is full" and "Alex got there first" carry different co-op coordination meanings — presentation-only differentiation may be required to preserve the coordination signal | /ux-design + playtest | Before the first friend-group playtest (escalated by independent review — a distinct presentation cue is cheap and rules-legal) |
| 7 | ~~Reconcile the provisional Save & Persistence diff row when its quick-spec is authored~~ RESOLVED 2026-08-26 — save-persistence quick-spec Rule 5 (droppable-reconcile stage) + Rule 6 (HarvestDiff is the only droppable table) | ~~Save quick-spec~~ Resolved | Done |
| 8 | Repeat-docking Discovery: with deterministic content and 2–3 MVP templates, routes are solvable by ~session 4. This GDD deliberately does not provide per-visit variance — it is owned by guard variation (Enemy AI) and the concept's "recurring lands return possibly changed" retention hook. LDU half ANSWERED 2026-08-27 (LDU quick-spec Rule 6): at MVP "changed" = per-docking node/spawn placement variation via variationSeed — template content stays fixed; authored revisions are Tier 1. Enemy AI half: mechanism NAMED 2026-08-27 (enemy-ai quick-spec Rule 8 — guard composition/patrols/facing as a pure function of variationSeed); whether it actually keeps repeat visits live is the remaining playtest question. | ~~Both quick-specs~~ Playtest verification only | Playtest |