Base Building
Status: Second re-review 2026-08-27 (fresh session, 4 specialists + CD, scoped to CS-1..3; verdict NEEDS REVISION, narrow — CS-1 closed clean; CS-4 (
perimeterSegmentsrow + AC-20b sweep) and CS-5 (placerHasExitstaleness/edge/candidate semantics + Edge Case 21 pins) applied same day plus the recommended pass; seedesign/reviews/base-building-rereview-2-2026-08-27.md); third re-review 2026-08-27 (fresh session, 4 specialists + CD, scoped to CS-4/CS-5; verdict NEEDS REVISION, narrow — CS-6 (knob-row bounds 25–57 → 35–58, gate subject named) and CS-7 (AC-14b constructible, AC-14c BFS pinned, host-exactness mechanism, maze boundary) applied same day plus the recommended pass; seedesign/reviews/base-building-rereview-3-2026-08-27.md); fourth re-review 2026-08-27 (fresh session, 4 specialists + CD, scoped to CS-6/CS-7; verdict NEEDS REVISION, narrow — blocking CS-8 (wallCost3–6 → 3–4), CS-10 (Edge Case 17 bound rebound toplacementReach < 2 × gridCellSize, AC-64b), CS-11 (AC-14b companion pinned, AC-14c coordinates), CS-12 (Edge Case 21 pin (iii) fresh search); recommended CS-9/13–16; nice CS-17 — all applied same day; seedesign/reviews/base-building-rereview-4-2026-08-27.md); fifth re-review 2026-08-27 (fresh session, 4 specialists + CD, scoped to CS-8–CS-16 with the closure step — every numeric cell passed; verdict NEEDS REVISION, narrow — one blocking surface: CS-11's pinned AC-14c hop-2 GIVEN was unreachable under Edge Case 21's own clear-cells-only rule, and under that rule no search depth beyond one hop is reachable at all, so CS-18 retired the BFS hop bound (Edge Case 17 ratio row, AC-64b, pin (i)'s⌊placementReach / gridCellSize⌋) in favour of a four-neighbour ring check with a binding reference block, rewrote AC-14c, added AC-14e, plus the recommended/nice tail (Teleport()claim → ADR input, CS-13 two-phase split, AC-14b fixture constraints, knob-row wording); user decisions: ring check over BFS, pseudocode block adopted; seedesign/reviews/base-building-rereview-5-2026-08-27.md; no sixth panel — CD gates the revision inline). Prior: re-review 2026-08-27 (design/reviews/base-building-rereview-2026-08-27.md, CS-1..3 applied); independent /design-review 2026-08-27 (design/reviews/base-building-review-2026-08-27.md), all 5 change-sets applied and held.
Author: Jeroen + agent panel
Last Updated: 2026-08-27
Creative Director Review (CD-GDD-ALIGN): CONCERNS (resolved) 2026-08-26 — C1 orbit test added to Rule 3 + schema-negative AC-7b; C2 staleness falsification criterion added (OQ8); C3 "no move verb" reclassified from locked rule to MVP scope decision with Tier 1 revisit trigger. Advisories A1–A5 routed (A1 producer sizing flag on OQ4, A2/A3 fantasy copy + AC-48b, A4 propagated to the phase brief, A5 art-bible fix-pass). Crafting OQ9 negative ruling APPROVED on merits: naming the MVP home-facing dead zone is pillar-compliant; all three available "fixes" violate an anti-pillar.
Implements Pillar: Pillar 2 — Home Is the Fixed Point (primary); serves Expression (#3) and Fellowship (#2) aesthetics
Overview
Base Building is how the group's permanent home island grows from camp to keep: a build mode in which any player places walls, crafting stations (workbench now, forge to come), and storage containers on the home island, paying material costs drawn from CARRY plus nearby storage at the instant of placement. It is the physical expression of Pillar 2 — the base is the thing that lasts while everything else drifts, and the concept names it the group's core motivation: progression on something that is your own and endures. Beneath the act sits a small data layer other systems read as fact: Crafting asks whether a station of the required tier is in range (stationPresent is this system's answer), Inventory's SharedStorage containers exist where Base Building placed them, and Save persists the structure list. The base is a host-authoritative structure registry plus placement rules — nothing simulates while nobody plays. Structures are never inventory items (Crafting Rule 10, peer contract): build mode draws materials directly at placement in one atomic host commit per structure. MVP ships the basic set — walls, workbench, storage — enough to stand the base up and prove the loop's final beat; fortification depth, station tiers beyond the workbench, and defense pressure belong to Tier 1+.
Player Fantasy
Base building's fantasy is the fixed point — "home doesn't move." The payoff isn't the structure; it's the contrast. The anchor moment: limping back from a bad run — the land nearly took you, half a bag, the coast already cooling violet behind you — and crossing the shore threshold onto home ground. Nothing is chasing you. The walls are exactly where you left them. Where the whole rest of the game is motion — lands arrive, lands leave, everything out there is on a clock — the base is the one thing that only ever changes because one of you changed it. That is Pillar 2 made physical, and it lands hardest at the worst moments.
Its secondary register is collective authorship: home doesn't move, and everyone's hands are on it. At MVP this reads as a **group artifact**: the layout is nobody's individual plan — walls, bench, and chests landed where different hands put them, and a new friend should read "this is theirs, collectively" from the shape — never from a plaque. (Shifting a badly-placed bench is a demolish-and-re-place round trip at MVP — free in materials by Rule 9, but a deliberate multi-step act, not a casual nudge; the low-friction fix-each-other's-placements beat matures with Rule 9's Tier 1 revisit trigger.)
Its necessary shadow is staleness: if "fixed" ever reads as "frozen" — wallpaper instead of pride — the fantasy dies. The fixed point must feel earned-static: still because you made it and it held, not still because nothing can happen there.
Deferred honestly (Tier 1+): the walls-as-memory fantasy — "that wall is from our first week", the base legibly encoding *who built what, when* ("all of us built this, at different times" read from the shapes alone) — needs structure variety, rotation, and visible age the MVP set (three uniform snap-aligned structures) cannot provide. Named here as the direction, not claimed as an MVP experience; the whole-shape test's clean silhouette deliberately wins that trade at MVP (design-review 2026-08-27, Axis 3 ruling). **This is one of two unverified-at-MVP admissions about the same "keep, not shelter" claim** — the other is perimeter length: Formulas' perimeter check states plainly that the 50-segment anchor is session-length-derived and whether it reads as a keep on the real island is unverified until the spatial budget is authored (third re-review 2026-08-27). Both route to OQ5/OQ8; read together, not as two small independent risks.
MVP honesty clause (rules crafting OQ9 in the negative): MVP base building is a finite stand-it-up project — walls, a workbench, storage, placed in the first sessions and then done. It is not a session-to-session creative outlet, and this GDD does not claim otherwise: after base-up, the home-facing dead zone crafting's review flagged is real and accepted at MVP. The ongoing-outlet role belongs to Tier 1+ building scope. (Crafting OQ9 updated to cite this ruling.)
**Tone and design tests this fantasy imposes (binding on future base-building features):**
- The safer-before-bigger test. Every future feature is evaluated against "does this make home feel more solid/held" before "does this make home bigger." Growth serves shelter, never sprawl.
- Placement is an event. Placing a structure must read as a deliberate, weighty act at the moment of placing — a thunk and a shape, never a menu confirmation.
- The no-ledger test. No feature may track, display, or rank individual contribution — no "walls built by" stat, no per-player build counts, ever (the same lever crafting's no-craft-count ban closes). Authorship stays a social fact read from the base itself.
- The whole-shape test. Placement feedback must keep the cumulative base silhouette legible while placing — players add to a base, never drop objects on a field (art bible silhouette-first contract).
Reference anchor: Valheim's first raised hall as the moment a camp becomes a home — minus the raids: at MVP nothing attacks the base; walls are shelter psychology and boundary-drawing, not (yet) fortification, and the doc says so plainly. **The MVP anchor is therefore the *quieter half* of the Valheim moment (CD A2)** — Valheim's hall lands partly because raids test the walls; that half arrives with Tier 1 defense. If the raised-hall feeling underdelivers at MVP playtest, read it as this deliberate scope decision before reading it as an execution failure.
Detailed Design
Core Rules
- Structures are data. `StructureDef = {structureId, category: wall|station|storage, footprint (w×h grid cells), cost[(itemId, count)], stationTier? (iff station), storageSlots? (iff storage)}` — all values data-driven, validated at load. A **parallel shape, not
RecipeDefreuse** (crafting Rule 10 grants the choice; RecipeDef's station/output/craftTime fields don't map, and footprint/tier/slots have no analog — a small honest struct beats nullable fields). - Anyone builds, always (Pillar 5). Build mode is available to every player from session one — no unlocks, no builder role, no permission tiers, ever.
- Building is legal only on the home island. Docked lands are never buildable (Pillar 2 — no second bases). Each dock socket carries a hard no-build exclusion radius (
socketNoBuildRadius): walls can never block a socket — tide Rule 9 deposits caught players at the socket's authored shorelinedepositPoint(LDU Rule 8), so the shore must always be usable. This is a systemic requirement, not a tuning choice (validated against the actual point at load — AC-63b). The orbit test (CD C1, binding): crafting stations and shared storage exist only at home. No structure, item, or mechanic may relocatestationPresentorSharedStorageavailability onto a docked land — a portable bench or field cache would sever the haul-home terminus and make home optional. Pillar 2's "all content orbits it" is enforced here, at the provider, not at each consumer: a future proposal that passes every placement rule but sites station-or-storage function off-island fails this test. - Grid placement, snap-with-nudge (ruling 2026-08-26). Structures snap to the world grid and auto-align to adjacent structures' snap edges when the cursor is near — walls meet at clean corners by construction (whole-shape test, silhouette-first contract), and the nudge gives placement its deliberate-act weight.
- Build mode is a client-local mode. Toggled entry; ghost preview follows cursor (M+KB) or aim stick (gamepad — no hover-only interactions); movement stays live; the mode **persists across placements** (multi-wall sessions are the common case). The ghost is pure client-side prediction — zero replicated state until commit. Its minimal data contract is `ghostSnapResult = {snappedPosition, isValid}` — snap resolution is a pure function of input position (never framerate or input device), so grid quantization and gamepad/mouse parity are unit-testable (QA review 2026-08-26).
- Placement commit is instant and atomic (ruling 2026-08-26 — no placement timer; weight comes from the thunk and the shape, never duration, matching crafting's own no-added-duration principle). On confirm: one host critical section re-samples every F1 gate and then debits cost + writes the instance, all-or-nothing. Cost draws from **CARRY plus any home STORAGE within
stationRangeof the placement spot** (ruling 2026-08-26 — the shared knob, placement-spot-centric: materials near the wall pay for the wall), as one Inventory-Rule-6-shape transaction. Any gate failing at commit = honest no-op — ghost stays up, nothing debited. Two ghosts racing one footprint or one material pool: first host-arrival wins; exact-tick tie → lowestOwnerClientId(house tiebreak). - Structures are anonymous — no builder field exists, by design. The deliberate inverse of crafting's maker's mark: gear carries
crafterNamebecause the signature fantasy needs it; structures carry nothing because the no-ledger test forbids it (a field that exists invites a leaderboard — the same logic as crafting's CD C2 aggregation ban, applied one layer earlier). Do not "fix" this by copying the mark pattern. Norotation, nocreatedAtCycleat MVP (YAGNI — walls-as-memory is Tier 1's to build if it comes). - Structure lifecycle: Present. That's the whole machine. No HP, no damage state, no decay, no under-construction state (nothing attacks the base at MVP; a construction state would need interrupt/griefing rules with zero fantasy payoff). A wall's entire MVP function is collision — it blocks movement. The art bible's patched-wall variant is Tier 1 base-defense scope; its mention there must not smuggle a damage state in here.
- Demolish exists; refund is total. Any player demolishes any structure (no ownership — Rule 7). Refund = 100% of
cost, credited to the demolisher's CARRY in one atomic transaction, gated onCanAdd— refund can't fit → honest no-op with a world-fact reason. Partial refund is banned: an unexplained material sink that taxes fixing each other's placements (collective authorship). **Sole exception (re-review 2026-08-27, CS-1): inert relics** — structures whoseStructureDefno longer exists after a patch — have no current cost to refund and demolish for exactly zero (Edge Case 13). That is not a partial refund of a costed structure; it is the only case wherecostis undefined, and it is the only exception this rule has. **Storage containers must be empty first** — non-emptySharedStoragedemolish is a no-op ("empty the chest first"): auto-transfer is a silent-loss path the duplication/loss invariant exists to prevent. **No move verb at MVP** (reclassified per CD C3: a *scope decision with a revisit trigger*, not a permanent rule) — demolish + re-place is the only relocation path; the finite stand-it-up project doesn't justify move tooling. Revisit trigger: when Tier 1 building scope opens, re-evaluate — the "available but skipped" beat depends on fixing being genuinely low-friction, and demolish-and-re-place is low-friction only while the base is small. Adding move later is additive and breaks no invariant (refund = current cost = rebuild cost, AC-36 — move is already expressible as an atomic demolish+place with zero material delta). - Demolish never blocks on use (ruling 2026-08-26). Demolishing a station mid-someone's-craft-beat is legal: the in-flight craft's own commit re-samples
stationPresentand lands as an honest no-op — nothing was debited (crafting Rule 7's existing discipline absorbs the race; zero new coupling). Stated as a cross-system closure:stationPresentis falsifiable at any instant, including mid-beat. - Split ruling (design-review 2026-08-27, Axis 2): 11a — Atomicity (locked): no queues, no batches, no blueprints-then-fill — at most one ghost and one in-flight commit per player, ever; no pending-state may exist between commits. (Crafting's Rule 8 scoped its ban to station crafting; this closes the gap for building explicitly.) 11b — Input cadence (
/ux-design-owned): repeat-placement affordances — hold-to-repeat, drag-a-row — are permitted, provided each cell resolves as its own complete atomic commit (Rule 6) before the next fires. Atomicity constrains the commit pipeline, not the confirm gesture; the 50-segment perimeter must not require 50 discrete aim-confirm-wait cycles. **Named trade-off (re-review 2026-08-27):** a drag-row deliberately spends some of the per-placement "event" weight (the thunk-and-a-shape tone test) for session-length practicality —/ux-designmust re-validate the tone test against a dragged row specifically, not only single placements, and for a non-host client a row is N sequential confirm → RTT → thunk cycles, so AC-48c's playtest must include a drag-row segment. - Placing storage instantiates the container. A storage-category commit creates one
SharedStorageunit (Inventory's construct and authority — host-auth,storageSlotscapacity; "component" in the design sense — whether it is aUnityEngine.Component, a spawned NetworkObject, or a plain replicated data record is the netcode ADR's call — **decided 2026-09-11: a registry row onBaseNet(ADR-0011) plus a spawnedStorageNetobject holding the contents (ADR-0010), bound by instance id**) at BB's chosen position. BB sites containers; Inventory owns their contents. stationPresent(tier)is this system's provided fact: true iff a station instance of the required tier (or higher, for repair — crafting's downward-inclusive rule) exists with the asker withinstationRange. Consumers re-sample it fresh at their own commit instants — it is never cacheable host-side (Rule 10's consequence); a client's replicated registry copy is a cache and is legal for display only. Two evaluation contexts (pinned 2026-08-27, netcode review): the host commit-instant evaluation is the authoritative one and is never cached or memoized; client-side advisory reads (UI greying, availability hints) evaluate against the client's replicated registry copy and are inherently a stale point-in-time snapshot — legal for display, never for gating a commit (the same provisional-vs-authoritative split F1's ghost already has). A client may only request a commit; it never pre-validates against its copy and skips the host re-sample (AC-39b).
`StructureInstance = {instanceId (host-monotonic, never reused), structureId, position}` — the whole registry.
States and Transitions
| From | Event | To | Notes |
|---|---|---|---|
| Idle/Move | build-mode toggle | BuildMode (ghost) | Client-local; no host involvement |
| BuildMode | confirm input, gates pass at host | BuildMode | Instant atomic commit; structure appears; mode persists |
| BuildMode | confirm input, any gate fails | BuildMode | Honest no-op; ghost stays; reason string shown |
| BuildMode | toggle / any non-build action | Idle/Move | Ghost vanishes; nothing committed. Non-build action, defined (re-review 2026-08-27): any input from an Input System action map other than the Build map — attack, interact (chest/station), inventory, menu. Move and camera inputs live on the shared Player map and never exit (Rule 5). The structure picker is visible only in this state |
Demolish is an instant targeted action from Idle/Move, not a state. Structures hold no state machine (Present only, Rule 8).
Feedback authority split (house pattern):
| Effect | Timing |
|---|---|
| Ghost preview, validity tint, snap/nudge alignment, grid display | Instant — client-side (outcome-independent) |
Structure appears/disappears, cost debit, refund credit, SharedStorage creation | Host-confirmed broadcast only |
Interactions with Other Systems
| System | BB consumes | BB provides | Interface owner |
|---|---|---|---|
| Inventory | Atomic CanRemove/Remove(cost) at placement; CanAdd/Add(refund) at demolish (Rule 6 shape) | Siting + instantiation of SharedStorage containers | Inventory owns stacks/atomicity/contents; BB owns placement |
| Crafting | Material item definitions (peer, crafting Rule 10) | stationPresent(tier) — re-sampled at every craft/repair commit, falsifiable mid-beat (Rule 10) | BB owns structures/costs/placement entirely; peer both ways |
| Tide Scheduler | Home-island permanence + socket positions (for exclusion zones) — static facts, no runtime coupling | — | Scheduler's jurisdiction is docked lands; home is outside it |
| Save | Reconcile predicate for StructureRegistry | StructureInstance[] list; storage contents persist via Inventory's format, never duplicated here | Quick-spec ✅ 2026-08-26 — pipeline stage 'permanent reconcile' invokes BB's predicate |
| Enemy AI | — none — | — none — | Home is hostile-free at MVP (stated so it doesn't read as an omission) |
| Land Dock/Undock | — none — | — none — | Home is permanent, outside LDU jurisdiction |
| UI/HUD | — | Ghost validity state incl. which gate fails (per-cause reason strings, crafting Rule 3's pattern); structure picker data | UI owns presentation 📌 /ux-design |
| Base-Defense Events (Tier 1) | — | — | Explicitly out of scope; no rows designed for it — named so absence reads as intent |
Formulas
Base-building math is deterministic and data-driven — no RNG anywhere (house rule). All costs and constants live in assets/data/structures.json, validated at load (Edge Cases).
Baseline structure table (MVP, ruling 2026-08-26)
| Structure | Category | Footprint | Cost | Material logic |
|---|---|---|---|---|
| Wall segment | wall | 1×1 | 4 timber | Timber-only — walls read as wood; the repeatable bulk sink stays mid-band |
| Workbench | station (cloth) | 2×1 | 6 fiber + 10 timber | Lashings + frame; shore+mid only — buildable before the first sword, which it gates |
| Storage chest | storage (40 slots) | 1×1 | 8 fiber | Cheap, placeable off a single shore trip |
No sinew in any structure (ruling 2026-08-26): sinew is the deep-band reward gating Reinforced Garb (crafting AC-55) — walls competing for it would throttle base-up on the scarcest band. Base-up requires no inland risk, ever: home stays the safe new-friend space (Pillar 5).
Bootstrap check (party-scoped, BB's first-session gate): workbench + storage + first sword + first garb = (6F+10T) + 8F + (6F+8T) + 10F = 30 fiber + 18 timber = 48 units, against harvesting's exported lowEndPartyHaul (165 — a first-class named constant in harvesting's Formulas section as of 2026-08-27) → ratio ≈ 0.29. Deliberately the biggest single material event of session one — base-up should feel like the session's project — while leaving ~71% of the haul spare for a rough first session. Bootstrap order: dock → harvest shore+mid → workbench (gates all crafting) → sword/garb → storage (ungated, whenever convenient). *(The pre-sword fight for the workbench's mid-band timber is defined: combat's unarmed strike does 14 — cross-GDD review D2, 2026-08-27 — so "no inland risk" means no deep-band risk; mid-band guards are fightable bare-handed.) (Terminology note: crafting's "base kit's materials" means exactly these session-one harvestable materials — no starting-inventory or pre-placed-structure system exists anywhere.)*
**Perimeter check (the finite project — anchor revised 2026-08-27 re-review CS-2: 50 segments, was 60):** a modest full perimeter = 50 segments × 4 timber = 200 timber. At a realistic mixed-haul wall-dedicated pace (mixedHaulTimberPerSessionLow/High = 58–138 — harvesting's exported party-scoped constants, derived there as session haul × mid-band timber share and cited here by name), that is 2–4 casual party sessions of wall-building at the exported extremes (200/138 → 2; 200/58 → ⌈3.45⌉ = 4) — the band the MVP honesty clause promises: a real multi-session project, never one trivial session, never a treadmill. A 25-segment minimal shelter (~100 timber, ~1–2 sessions) is a legitimate faster path — **the never-one-trivial-session guarantee applies to the full 50-segment perimeter only; the shelter is deliberately faster and does not carry it. The gate's subject is perimeterSegments — the full-perimeter anchor constant the CI gate consumes — not every perimeter a player might build (third re-review 2026-08-27, CS-6).** The 25-segment shelter is a player choice that sits below the knob's gate-safe floor by construction (gate-safe: a value at which the perimeterSessions CI gate passes at both exported haul extremes — defined at first use, fourth re-review 2026-08-27, CS-17) (25 × 4 = 100 timber → ⌈100/138⌉ = 1 session at the high haul extreme, which the gate rejects for the anchor and which is exactly the shelter's point); it is not a knob setting and the knob row does not govern it. **The 50-segment anchor is provisional** until home-island content authoring confirms the island fits it (Dependencies → home-island content authoring); the prior 60-segment anchor was dropped because harvesting's honest low end (58) put it at 5 sessions. **Stated plainly (second re-review 2026-08-27): 50 is a session-length-derived number, not a shape-derived one** — it was chosen to keep the 2–4 session band once harvesting's constants were corrected, and whether 50 segments reads as a keep rather than a shelter on the actual island is unverified until the spatial budget is authored. If it reads as a shelter, the CI gate's 2–4 sessions is an accurate number describing a fantasy failure, and the anchor moves (within the knob's 35–58 gate-safe range) or the island does. **Owner and trigger (third re-review 2026-08-27):** home-island content authoring (the Dependencies row that owes the spatial budget) makes this call during the spatial-budget authoring pass, with a procedural check, not a visual impression: does the authored placeable region admit ≥ perimeterSegments wall cells plus the MVP station, chest and walk-space footprints? (**"Walk-space" is not yet a defined quantity — fourth re-review 2026-08-27, CS-16:** until home-island authoring pins a walk-corridor width in cells and the perimeter's assumed shape, this check is a stated intent, not a runnable procedure; those two inputs are the fit check's own open items, owed by the same authoring pass.) If yes, the anchor stands and only OQ5's playtest can still fault it; if no, the anchor moves first (it is the cheaper edit) and the island only if 35 segments still does not fit. (If the anchor is revised after players have built, Edge Case 14's cost-drift precedent applies: no retroactive structure changes; only the anchor and the CI gate move — CS-17.) This compounds with the Player Fantasy's deferred walls-as-memory admission — the same fantasy, two unverified legs.
F1. placementLegality
`canPlace = withinReach AND withinBounds AND noOverlap AND noPlayerOccupancy AND placerHasExit AND validTerrain AND notInSocketExclusion AND CanRemove(cost, CARRY ∪ STORAGE-within-stationRange-of-placementSpot)`
Variables:
| Symbol | Type | Range | Description |
|---|---|---|---|
withinReach | bool | — | Placement spot within placementReach (6 u) of the player (Edge Case 12 ruling) |
withinBounds | bool | — | Footprint entirely inside the home island's placeable region |
noOverlap | bool | — | Footprint intersects no existing StructureInstance's footprint |
noPlayerOccupancy | bool | — | No footprint cell contains a host-known player position — defined as the host's most recently received owner-authoritative NetworkTransform state for that client (the host's own player: read from the live Transform the host owns — under NGO 2.x owner-authoritative NetworkTransform the owner holds one value, no delayed copy, so there is no transport hop and no remoteInterpBuffer in the read; this is exact for a structural reason, not because the host has fresher-than-RTT knowledge of anyone else — third re-review 2026-08-27, CS-7; exact means free of transport and interpolation staleness — whether the read sees the host player's current-frame position additionally depends on tick ordering this GDD does not pin, two unknowns: which execution phase the commit critical section runs in and which phase owner movement writes in: ⚑ netcode ADR input, Edge Case 21's list — fourth re-review 2026-08-27, CS-13; restated CS-18) — at the commit re-sample. Best-effort for remote players by construction (one RTT stale); Edge Case 21 holds the actual guarantee (added 2026-08-27, design-review BL-3a; reframed same day, re-review CS-3) |
placerHasExit | bool | — | Evaluated against the registry with the candidate footprint already applied: the placing player's own occupied cell has ≥ 1 orthogonally adjacent cell that (a) lies inside the home island's placeable region, (b) is buildable terrain, and (c) is covered by no structure footprint — existing instances or the candidate. A neighbour outside the placeable region is not an exit (island-edge cells are out-of-bounds, not merely non-buildable — the modal shoreline case). Evaluated for the placer only, against the placer's host-known position (a local one-hop neighbourhood check, not a path search; contrast Edge Case 21's N-hop escape search). Staleness parity with noPlayerOccupancy (second re-review 2026-08-27, CS-5): exact for the host's own player (same mechanism as the noPlayerOccupancy row: the host owns its own transform, local read, no interpolation buffer); one RTT best-effort for a non-host placer, whose host-known position is the last owner-authoritative state received. The solo guarantee is exact because a solo player is always the host; a non-host who self-encloses inside the RTT window has, by definition, another player in session, and Edge Case 22's social closure covers that case. (The placer standing inside the candidate footprint is not a separate case: noPlayerOccupancy already fails it, and the host's own position is exact.) Added 2026-08-27, re-review CS-3; Edge Case 23 |
validTerrain | bool | — | All footprint cells are buildable terrain (not water/cliff) |
notInSocketExclusion | bool | — | No footprint cell within socketNoBuildRadius of any dock socket center (F4) |
cost | (itemId,count)[] | per StructureDef, non-empty | Per the Baseline structure table |
stationRange | float (u) | 3 — Crafting-owned knob (safe 2–6), consumed here (ownership pinned 2026-08-27; was mislabeled "locked") | Draw radius centered on the placement spot, not the player (ruling 2026-08-26); double duty with F3 — see Knob-interaction warnings |
CanRemove | bool | — | Inventory's atomic affordability check over CARRY ∪ STORAGE-in-range |
canPlace | bool | — | Result |
Output Range: boolean; the ghost preview evaluates it provisionally every frame for validity tint, but only the host's **commit-instant re-sample** is authoritative (Rule 6). Any conjunct false at commit = honest no-op — ghost persists, nothing debited. Example: wall footprint 1×1 on buildable grass 4 u from the player (within placementReach 6 u), 6 u from the nearest socket (socketNoBuildRadius 4 u → clear), no structure underfoot, no player standing in the cell, the player's own cell keeps a free neighbour, 4 timber reachable from CARRY + a chest 2 u from the placement spot → all eight conjuncts true → canPlace = true → commit fires.
F2. demolishLegality
`canDemolish = structureExists AND (category ≠ storage OR storageEmpty) AND CanAdd(refund, demolisher.CARRY)`
Variables:
| Symbol | Type | Range | Description | ||
|---|---|---|---|---|---|
structureExists | bool | — | Target instanceId still in the registry — re-sampled fresh, never cached from targeting time | ||
category | enum | wall\ | station\ | storage | From the instance's StructureDef |
storageEmpty | bool | — | Evaluated only for storage: SharedStorage.contentCount == 0 | ||
refund | (itemId,count)[] | = 100% of cost | No partial refund exists (Rule 9). Sole exception: inert relics refund exactly zero — their cost is undefined (Edge Case 13, revised 2026-08-27) | ||
CanAdd | bool | — | Inventory's capacity check against the demolisher's CARRY only — refunds never route to STORAGE | ||
canDemolish | bool | — | Result |
Output Range: boolean, re-sampled at the demolish action's own commit instant (F1's discipline). Examples: non-empty chest → storageEmpty = false → no-op, reason "empty the chest first". Wall demolish with a full bag → CanAdd = false → no-op, reason *"not enough room to carry the refund"* — the structure stays standing.
F3. stationPresent — two named predicates
`stationPresentExact(tier) = ∃ s ∈ StructureRegistry : s.category = station AND s.stationTier = tier AND dist(asker, s.position) ≤ stationRange`
`stationPresentAtLeast(tier) = ∃ s ∈ StructureRegistry : s.category = station AND s.stationTier ≥ tier AND dist(asker, s.position) ≤ stationRange`
Two named predicates rather than one parameterized call: the comparison is a crafting-owned fact about what crafting is asking (craft = exact tier; repair = downward-inclusive), and a mode argument would let a caller silently pick the wrong one. Each caller states its requirement by which predicate it invokes.
Variables:
| Symbol | Type | Range | Description | ||
|---|---|---|---|---|---|
tier | enum | cloth\ | bronze\ | … | The tier the caller requires |
s.stationTier | enum | — | A candidate station instance's tier | ||
dist(asker, s.position) | float (u) | ≥ 0 | Euclidean distance, asker to station | ||
stationRange | float (u) | 3 — Crafting-owned knob (safe 2–6) | Same knob as F1's draw radius — one value, two semantics; moving it moves both (Knob-interaction warnings) |
Output Range: boolean; the host commit-instant evaluation is never cached — falsifiable at any instant (Rule 10), including mid-craft-beat. Client-side advisory reads are stale-by-replication snapshots, display only (Rule 13's two-context split). Example: a bronze station 2 u from the asker: stationPresentExact(bronze) ✓; stationPresentExact(cloth) ✗ (crafting cloth still needs a cloth bench); stationPresentAtLeast(cloth) ✓ (repairing cloth gear at a bronze station — downward-inclusive).
F4. socketExclusion
`notInSocketExclusion(footprint) = ∀ cell ∈ footprint, ∀ socket ∈ Sockets : dist(cell, socket.position) > socketNoBuildRadius`
Variables:
| Symbol | Type | Range | Description |
|---|---|---|---|
cell | (float,float) | — | Each grid cell the candidate footprint occupies |
socket.position | (float,float) | fixed, scheduler-owned | Dock socket center |
socketNoBuildRadius | float (u) | > 0, tuning knob | Hard exclusion radius — walls can never enter it (Rule 3) |
Output Range: boolean; a single failing cell against a single socket fails the whole footprint. Example: a 2×1 footprint with one cell 3.5 u from a socket at socketNoBuildRadius = 4 → that cell fails → placement illegal regardless of the other cell.
CI gates (live-recomputed — designed in from the start)
Both gates recompute from live named constants at test time — never hardcoded literals (the lesson of crafting AC-54's review retrofit, applied at authoring time). The cross-system inputs are **harvesting's exported constants** (lowEndPartyHaul, mixedHaulTimberPerSessionLow/High — first-class named values in harvesting's Formulas section as of 2026-08-27, owned there, cited here by name; design-review BL-1 closed the gap where these existed only as prose) plus crafting's live recipe costs and this table's live structure costs:
- `bootstrapRatio = (workbench + storage + sword + garb costs) / lowEndPartyHaul
— assert0.10 ≤ ratio ≤ 0.35` (today ≈ 0.29). - `perimeterSessions = ⌈(perimeterSegments × wallCost) / mixedHaulTimberPerSession⌉
withperimeterSegments` = 50 (this GDD's named anchor), evaluated at both exported extremes — assert2 ≤ sessions ≤ 4at each (today: 200/138 → 2 and 200/58 → ⌈3.45⌉ = 4; both inside the band). Both harvesting constants are party-scoped (a 2-player MVP session hauling together); the gate must never be fed a per-player figure.
**Per-knob safe ranges are marginal, not joint (design-review 2026-08-27 BL-2).** Each Tuning Knobs safe range holds only for that knob moving alone; corners of the joint box FAIL these gates (all four cost knobs at +50% → bootstrapRatio 0.436). **A marginal range can also fail with nothing else moved (fourth re-review 2026-08-27, CS-9):** the prior example here — wallCost 6 at mixedHaulTimberPerSessionLow 58 → ⌈300/58⌉ = 6 sessions — was never a corner: 58 is harvesting's live exported low extreme, one of the two values AC-46 asserts at on every run, so wallCost 5 and 6 failed the gate at everyone else's defaults and the wallCost row's own 3–6 range was wrong for a session (corrected to 3–4, CS-8). Green gates prove pacing arithmetic only — never that the anchor reads as a keep on the authored island (Formulas' perimeter check; Player Fantasy's two unverified legs — CS-16). **The CI gates, not the per-knob ranges, are the joint authority** — any retune of any participating knob re-runs both gates before it lands.
Economy scope note (design-review 2026-08-27): every cost in this table is a one-time sink — BB contributes no recurring material sink, so after base-up (~session 3) the timber/fiber faucets have no BB destination while harvesting's faucet recurs unbounded. Accepted for MVP; whether the haul loop retains its bank-or-push tension after sink saturation is OQ11's playtest question (owner: economy + producer), and any lifetime-sink-vs-faucet invariant belongs to a cross-system economy pass, not this GDD.
Edge Cases
Commit races
- If two ghosts commit on the same footprint the same tick: each commit is its own atomic host transaction in arrival order; the second's
noOverlapre-sample sees the first's placed structure and fails as an honest no-op. Exact same-tick tie → **lowestOwnerClientIdresolves first** (house tiebreak — crafting Edge Case 1, combat F5, harvesting Rule 8). "Same tick" pinned (2026-08-27, netcode review): under an ordered host message queue, arrival order is itself total and a true tie cannot arise. **Arrival order is always authoritative when it is distinguishable**; the ascending-OwnerClientIdrule is a tiebreak for implementations that batch a frame's pending commit intents without preserving arrival order within the batch — it never reorders intents whose arrival order is known (re-review 2026-08-27: the prior "a batch MUST be resolved in ascending order" read as an unconditional reordering that contradicted "first host-arrival wins"). ⚑ netcode ADR input: host-arrival order as the universal cross-system tiebreak requires all host-rail commit RPCs to share a single ordered channel (or an explicit host sequence number) — an assumption, not an NGO default, once per-RPC QoS tuning begins. - If a placement commit and a craft commit race the same materials: both are Inventory-Rule-6-shape transactions and queue identically in host-arrival order. **No BB-side or crafting-side lock exists or may ever be added** (crafting Edge Case 2's rule, symmetric here).
- **If a placement commit races a STORAGE withdrawal from the same chest**: same discipline — arrival order; the loser's
CanRemove/CanAddre-sample reflects the post-state and succeeds or fails honestly. No special ordering. - If two demolishes target the same instance the same tick: the first's commit removes it; the second re-samples
structureExists = false→ honest no-op ("already gone"). Order is moot — only one can win. - If a demolish races a deposit into the chest being demolished:
storageEmptyis re-sampled fresh at the demolish commit instant (F2). Deposit lands first → demolish no-ops on a non-empty chest; demolish lands first → the deposit races a vanished container and resolves under Inventory's own Rule 6/7 ordering. - **If a placement's STORAGE-in-range draw includes a chest a concurrent demolish removes**: closed by F1's re-sample discipline —
CanRemoveevaluates at the commit instant against whatever STORAGE still exists in range; the vanished chest simply isn't in the draw.
Disconnects
- If a player disconnects with build mode open: build mode is client-local (Rule 5) — the ghost ceases to exist with the client; no host state was ever touched; nothing to resolve.
- If a player disconnects exactly at a placement commit tick: the host resolves normally — a disconnect never un-happens a fired atomic commit; the structure is created and the cost debited from the offline player's persisted CARRY/reachable STORAGE (crafting Edge Case 5's pattern; only the debit side exists here — placement delivers no item).
- If a player disconnects exactly at a demolish commit tick: the host resolves normally — the refund lands in the offline player's persisted CARRY (crafting Edge Case 5's pattern, refund side).
- If the host quits mid-anything: the session ends (host migration banned) — stated so it doesn't read as an omission (combat Edge Case 3, crafting Edge Case 6).
Ghost / preview
- If the ghost shows valid at preview but a gate fails at commit: the canonical honest no-op (F1/F2 discipline) — ghost persists, nothing debited, no error theatrics.
- Placement reach (ruling 2026-08-26): the ghost renders anywhere but **confirms only within
placementReach(6 u, safe range 5–8) of the player** — slightly paststationRangeso the ghost can lead the cursor; "you must be near what you're building" (the deliberate-act fantasy; unlimited reach invites sight-unseen misplacement). Beyond reach =canPlace = false, same reason-string family as every other F1 gate.placementReachjoins F1's gate list and the Tuning Knobs table; validated> 0at load.
Registry / persistence
- If a save references a patch-removed
structureId: kept as an inert relic — position/footprint preserved (the base silhouette holds; "home doesn't move" applies to patches too), generic fallback visual, inert (no repair, nostationPresentcontribution), still blocksnoOverlapwhile standing. Never auto-removed, never auto-refunded — but **any player may demolish a relic for exactly zero refund** (revised 2026-08-27, design-review: its cost is undefined, so zero is the honest refund; reason string "nothing left to salvage";CanAdd(0)is trivially true, so the demolish never no-ops on a full bag). Players choose whether the tombstone stays — the prior forced-permanence posture contradicted the whole-shape test and Edge Case 18's registry bound. **Footprint preservation mechanism (ADR-0011, 2026-09-11): thestructuressave table carries afootprintcolumn per row and the wire row carriesW, H, so a relic keeps its full silhouette without a footprint field onStructureInstance(AC-24 untouched). Named cost (re-review 2026-08-27):** removing a shippedstructureIdin a patch destroys every material invested in its standing instances with no compensation — a one-off destructive event, not a recurring sink, accepted for a friends-only game exactly as Edge Case 14 accepts cost drift; it must be a deliberate, changelog-worthy decision, never routine data cleanup. The relic's broken-outline silhouette (Visual/Audio Requirements) is the pre-commitment signal that nothing is left to salvage; the demolish prompt's zero preview confirms it rather than reveals it. **Player-facing consequence, stated (second re-review 2026-08-27):** the group learns of the loss only on their next session, from the broken outline and the zero-refund preview — there is no other notification path, by design (no notification system exists and none is justified for a one-off event); the patch's changelog is the only advance warning. - If a patch changes a
structureId's cost after placement: no placement-time cost is stored (StructureInstancecarries no cost snapshot) — demolish always refunds the currentStructureDefcost. Honest consequence: a cost-lowering patch shrinks refunds on already-placed structures; a friends-only game accepts this rather than building a cost-versioning system with no other use. - **If a save references a storage instance whose
SharedStoragecontent list is missing**: BB's registry entry is unaffected (StructureInstancenever stores contents — Inventory owns that table entirely); Inventory's reconcile handles the missing list as its own orphan case. The container still exists and occupies space regardless. instanceIdcollision or reuse: never — host-side monotonic counter, persisted with the save, never reused (Rule 13; crafting Edge Case 7's pattern). Mechanism/replication: ADR-0011 (2026-09-11) — the counter is persisted in thestructurestable and each row replicates asStructureNetRow.InstanceIdon the in-sceneBaseNetlist.
Config validation (load-time, fail loudly)
- One compact family, matching the sibling GDDs:
| Condition | Outcome |
|---|---|
cost references an unregistered itemId | loud reject |
footprint.width ≤ 0 or footprint.height ≤ 0 | loud reject |
Duplicate structureId | loud reject |
Unknown category enum value | loud reject |
Any cost[].count ≤ 0, or empty cost[] | loud reject (a free structure is the placement-side free-item exploit) |
category = station with no stationTier, or stationTier on a non-station | loud reject |
category = storage with storageSlots ≤ 0, or storageSlots on a non-storage | loud reject |
stationTier not in Crafting's known tier enum | loud reject (cross-system reference validation) |
socketNoBuildRadius ≤ 0 | loud reject — would let walls block the tide's shoreline deposit point (tide Rule 9) |
Any socket's authored depositPoint (LDU Rule 8; tide Rule 9 deposits caught players there) farther than socketNoBuildRadius from that socket, or not on buildable-adjacent shore | loud reject — "the shore must always be usable" is systemic (Rule 3) and is validated against the actual deposit point, not just > 0 (AC-63b; rebound 2026-08-27 re-review CS-2 — the prior "deposit landing cells" artifact never existed in tide) |
structures.json fails to load on the async Addressables boot path (missing key, parse exception) | routes to the same boot-blocking fatal-config state as every row above — never a silent fallback to defaults (owner: engine-programmer boot sequence; added 2026-08-27 re-review) |
placementReach ≤ 0 or stationRange ≤ 0 | loud reject — stationRange is the shared knob already enforced at Crafting's load; BB confirms the shared value loaded rather than re-deriving it |
Retired row (fifth re-review 2026-08-27, CS-18): the placementReach < 2 × gridCellSize reject (added CS-7, rebound CS-10, tested by AC-64b) is gone — Edge Case 21's check no longer has a hop bound, so no BB load-time relation between placementReach and gridCellSize exists. gridCellSize is OQ6's snap constant only.
Scope closures
- Registry growth is a stated non-concern: structures are demolishable and refundable, so the registry tracks only what stands; a friends-only base is low hundreds of rows at most (crafting Edge Case 21's pattern).
- If all stations of a tier are demolished mid-era: every recipe of that tier reads
stationPresentExact = false— a supply-of-stations problem, never a re-lock;KnownRecipes/era knowledge (crafting Rules 2–3) is entirely independent of station presence. - If the group demolishes the last workbench: legal — no ownership, no protection (Rules 7/9). **No material softlock is possible by construction** (claim renamed 2026-08-27 — it covers materials only; the spatial case is Edge Case 22): full refund returns exactly the rebuild cost (refund = current cost = rebuild cost, always), so the round-trip is free in materials; the only cost is the walk.
Player/structure interaction (added 2026-08-27, design-review BL-3)
- If a placement commit targets a cell a player occupies: illegal —
noPlayerOccupancy(F1's fourth conjunct) re-samples host-known player positions inside the commit critical section; reason string "someone is standing there". **This gate is best-effort for remote players by construction, not occasionally (reframed 2026-08-27, re-review CS-3):** under owner-authoritativeNetworkTransformthe host's copy of a remote player's position is the last state that client sent — one RTT stale on every commit, the modal condition rather than a rare race. The conjunct catches the host's own player exactly and remote players approximately; the actual guarantee is the correction below, not the gate. Correction on overlap: when the newly replicated structure's footprint intersects the local player's cell, that player's **own client runs a deterministic ring check** — the four orthogonal neighbours of the player's own cell, examined in the fixed order N, E, S, W; the first that lies inside the placeable region, is buildable terrain, and is covered by no structure footprint (existing or the triggering one) is the destination — and repositions its own owner-authoritative transform to it. **Fifth re-review 2026-08-27, CS-18 — this retires the FIFO breadth-first search of CS-7/CS-10/CS-11:** with one predicate for both traversal and acceptance, a clear-cells-only search returns a clear hop-1 cell before it ever expands from it, so depth beyond one hop was unreachable and the⌊placementReach / gridCellSize⌋hop bound, Edge Case 17's ratio row and AC-64b were dead machinery (CS-11's pinned hop-2 case blocked all four hop-1 neighbours and asserted a hop-2 answer no correct implementation could reach); the expand-through-blocked reading that would have made depth reachable was rejected because it pops a player through solid walls and guts Edge Case 22. The order is the pin: two implementations that differ only in which of several clear neighbours they choose return different cells, and AC-14c distinguishes them. **Reference implementation (binding — every acceptance criterion that pins a GIVEN against this check is walked against this block, never against the prose):**
``` // pure; plain-data inputs only (Acceptance Criteria purity mandate) Cell? FindCorrectionCell(Cell seed, ITerrainQuery terrain, IReadOnlyList<Footprint> registry, Region placeable) foreach d in [N, E, S, W]: // fixed order — the only tie-break c = seed + d if !placeable.Contains(c): continue // out-of-region is never a destination if !terrain.IsBuildable(c): continue if registry.AnyCovers(c): continue // includes the footprint that fired the check return c return null // enclosed → Edge Case 22 ```
The check reads no player positions — a neighbour another player stands on is a legal destination (a fresh occupancy read on the correcting client would be the same one-RTT-stale read this edge case exists to survive; landing beside someone is ordinary movement's business). This is a discrete logical reposition, never a physics-solver push (walls are non-dynamic colliders that do not push players; Box2D contact resolution would give a contact normal, not a free cell). Owner-rail corrects owner; other clients see it as an ordinary NetworkTransform update through normal interpolation — nothing is predicted or rewound, so no forbidden pattern is touched. **Observer asymmetry, proposed (third re-review 2026-08-27, CS-7; owner and trigger named fourth re-review, CS-15):** the mover snaps (it is a teleport-snap event on the owner rail, per (iv) — teleport-snap names the owner-side discrete write, not the observer treatment); observers see a one-buffer slide, unlike tide deposit and death respawn, which cut the interpolation buffer for observers too. **Mechanically the asymmetry as proposed requires a plain interpolated NetworkTransform state update rather than Teleport() (the API the other two events use is what cuts the buffer); whether a one-cell discrete write actually survives NetworkTransform's own snap thresholds as an interpolated update, and what an observer sees along that one-cell path, are ADR questions, not settled facts (fifth re-review 2026-08-27, CS-18 — the prior sentence asserted the mechanism ahead of the ADR that owns it). Settled 2026-09-11 by ADR-0009 §5:** the correction is a plain owner-side position write; NGO 2.13.2's PositionThreshold gates sending only and the receiver has no snap threshold, so observers slide the one cell over the buffer (its V2 verifies in play); SnapReason does not grow. Accepted because a one-cell nudge sliding over remoteInterpBuffer (~1 tick) reads as motion the wall's own thunk already explains, and cutting the buffer would buy nothing visible. **Player-movement owns the teleport-snap list and must ratify this asymmetry in its Rule 6 (routed; BB does not declare it unilaterally). Owner and trigger (CS-15): the netcode architecture ADR ratifies or rejects it at the first NGO integration story that touches NetworkTransform; until then it is a proposal in both documents (⚑ ADR input, below). Pinned (second re-review 2026-08-27, CS-5): (i) the check is bounded by construction** — the four orthogonal neighbours of the player's own cell and no farther (CS-18; the prior ⌊placementReach / gridCellSize⌋ BFS-hop bound and Edge Case 17's ≥ 2 hops row are retired — gridCellSize remains OQ6's to pin for snap, but this check no longer consumes it) — the player's own cell is never a destination; (ii) the correction **re-checks the overlap immediately before writing** and aborts if the player has already walked clear — it never repositions on a stale trigger. **The window this closes is local and cross-frame, not a host race (third re-review 2026-08-27):** the replicated footprint's arrival and the correction's execution are not guaranteed to run on the same client frame as the trigger, and owner movement is instant, so the player's own input can clear the cell in between — the re-check catches exactly that gap and nothing else; (iii) a player has at most one in-flight correction — a second qualifying replication event while one is pending is re-evaluated after the first completes — **"completes" = the owner's local synchronous transform write in (ii) finishing on the owner's own frame; no host acknowledgement is awaited or defined (third re-review 2026-08-27), so "in-flight" spans at most one frame; the cap is therefore a closure against a future asynchronous acknowledgement, not an operative guard today, and no AC exercises it (fourth re-review 2026-08-27, CS-12) —** and the re-evaluation **invokes the free-cell check fresh, at the player's current (post-correction) cell, against the then-live registry — never a re-check of the first correction's stashed result** (CS-12; the cheaper reading breaks the accepted-race repair below — walk it: P at cell A, wall W1 lands on A, P moves to B; a second wall W2 whose noPlayerOccupancy read P's stale position at A commits onto B; a re-check of the A-rooted result finds nothing to do and P stays under W2, while a fresh check at B moves P off it — AC-14e). Idempotent under this reading only: if the player's current cell is itself clear at invocation, the (ii) re-check finds no overlap and nothing is written; (iv) this reposition is a **third authority teleport-snap event** alongside tide deposit and death respawn, registered in the player-movement quick-spec's Rule 6 enumeration — movement owns that list, BB does not add to it unilaterally. If **none of the four neighbours** is clear, the player is enclosed and Edge Case 22 applies — "enclosed" is a fact about the ring, not a path search: a clear cell two steps away behind a blocked neighbour does not count and cannot be reached without passing through a wall (CS-18). The displaced player's experience: being nudged out from under a friend's wall is world-friction (Pillar 4), not a bug — but it must read as one, so the reposition carries a tell (Visual/Audio Requirements) rather than a silent snap. **Accepted race:** during the correction window a second concurrent placement's noPlayerOccupancy may read the displaced player's stale pre-correction position; its outcome is honest either way (a false pass is corrected by this same path under (iii) — which is why (iii) must run the search fresh, CS-12 — a false fail is a harmless no-op). ⚑ netcode ADR inputs: wall-collider replication timing vs. owner-rail movement; whether this self-correction write passes through the owner rail's normal movement bounds checks; the write-ordering slot of the reposition relative to same-frame owner movement prediction (the re-check in (ii) is the design-level guard, not the ordering mechanism); **the read-ordering slot of F1's commit critical section relative to the host's own same-frame movement update — two unknowns: which execution phase the commit critical section runs in (Update / FixedUpdate / a transport callback) and which phase owner movement writes in (fourth re-review 2026-08-27, CS-13 — the F1 rows' "exact" covers transport, not tick order; split into its two inputs CS-18)**; the stale-read race above; **whether the correction write goes through NetworkTransform.Teleport() (which cuts observer interpolation, as tide deposit and death respawn do) or a plain interpolated state write — the observer asymmetry above requires the latter (CS-13/CS-15) — and whether a one-cell discrete write survives NetworkTransform's snap thresholds as interpolated at all (CS-18). Guardrail on every resolution (CS-18):** the owner rail stays the only writer of a player's transform — the host may re-broadcast the registry or re-signal the trigger, never write a remote player's position (Forbidden Patterns: no remote-player prediction, no host-side movement authority).
- If a player is fully enclosed by walls with a full CARRY (demolish refund can't fit → F2 no-ops): **accepted and documented, resolved socially** — escape is any other player demolishing from outside (anyone demolishes anything, Rule 9), or the trapped player banking into a chest within reach first. Your friends walled you in; your friends let you out. No spill-on-ground or refund-forfeit mechanism exists (there is no world-item system) — which is exactly why Edge Case 20's guarantee is now explicitly material-only. Reaching this state is either another player's walls (this case) or Edge Case 21's correction finding no free cell. **Scope (re-review 2026-08-27, CS-3; widened second re-review CS-5):** this social closure covers enclosure by other players' walls and the one self-enclosure
placerHasExitcannot catch — a non-host placer closing a loop inside the RTT window between their true position and the host's stale copy (F1, staleness parity). That player is by definition not alone (netcode-session quick-spec: a non-host client presupposes a live host, and host quit ends the session for everyone — no non-host-only state exists), so the same social exit applies. **Remedy tier, acknowledged (third re-review 2026-08-27):** this non-host self-enclosure case gets the weakest of BB's three remedies — prevented-at-source (host self-enclosure, Edge Case 23) > active correction (wall lands on you, Edge Case 21) > social-only (this case) — accepted, not overlooked, because the case exists only when another player exists. **Sub-case split (fourth re-review 2026-08-27, CS-14):** the social-only tier applies to the placer whose true cell is adjacent to the newly sealed perimeter and covered by no footprint — there is no footprint-intersection event, so nothing fires. If instead the RTT-stale commit's footprint lands on the placer's own true cell, Edge Case 21's replication-triggered correction fires on the placer's own client exactly as for a bystander (its trigger has no placer exclusion — which holds because Rule 5's zero-prediction ghost means the placer's own client learns of the footprint by the same replication event as everyone else, CS-18) — that sub-case gets active correction, one tier up. (The tier ladder is a reviewer's taxonomy for checking remedy coverage, not a player-facing concept — CS-18.) Self-enclosure by the host — the only player who can be solo and therefore has no social exit — is prevented exactly at the source by F1'splacerHasExitconjunct (Edge Case 23); the "bank into a chest first" line above is the avoidance path, not an escape from the trapped state. Walling a friend in as a prank is legal and self-correcting under Rule 9 (anyone demolishes); it is named here so nobody reads it as an exploit. - If a placement would wall the placer into their own cell (a drag-row closing a loop around the builder — Rule 11b makes this easy to reach without noticing): illegal —
placerHasExit(F1) fails at the commit re-sample; reason string "no way out" (retitled second re-review 2026-08-27 — the prior "that would wall you in" was the set's only second-person predictive string; every sibling is a third-person world-fact — convention, not requirement: reason-string register is/ux-design's, which may retitle any string in this family; fourth re-review 2026-08-27, CS-17). A solo host has no second player to demolish from outside and every downstream mitigation (spill, forfeit, teleport-home) is banned by an anti-pillar, so this is prevented at the source rather than accepted (re-review 2026-08-27, CS-3). The trade, named (second re-review CS-5): this is the one F1 gate that polices a choice about the player's own body rather than a fact about the world — a Pillar 5 agency cost, paid by every placer in every session, accepted for the solo host's sake because a session-composition-dependent rule (enforce only when nobody else is online) would make the same placement legal or illegal depending on who is present. Ruled conjunct-over-document by the CD on 2026-08-27; recorded here, not reopened. The check is local — one free orthogonal neighbour of the placer's own cell — not a path search; a player can still build a maze around themselves with an exit, which is their business — **legal up to and excluding the one segment that would remove the placer's own last exit; that segment, and only that segment, fails with "no way out" (third re-review 2026-08-27 — the boundary stated, not implied). The concrete forgone pattern:** the sealed room — closing all four walls around yourself and a chest, then demolishing your way out — is now impossible; the last wall of any closed room can be placed only from a legal standing position outside it — **which requires an in-region, buildable cell withinplacementReachof the gap to exist (fourth re-review 2026-08-27, CS-16).** On the baseline grid this holds for ordinary rooms; a room built flush against the region edge or water can have no such cell and is then permanently unsealable by anyone — a benign failure (nobody is trapped) that surfaces, for the placer attempting the seal from outside, as awithinReach/validTerrainreject; a placer inside the room still gets "no way out" exactly as above (CS-18). Home-island content authoring must not author isolable interior spaces where that happens — same owner and trigger as Formulas' spatial-budget fit check. **Drag-row consequence, stated (CS-16):** the ghost evaluates one candidate at a time (F1 row: the candidate footprint) with no lookahead across a pending row, so a drag-row (Rule 11b) that would seal the placer commits every cell up to the sealing one and stops there with "no way out", the ghost tinting that cell only when the drag reaches it. No pre-warning exists at MVP; any lookahead affordance is/ux-design's (Rule 11b) and is not designed here. Exact for the host (host-owned transform, local read — F1 row), best-effort for a non-host placer (F1 row; the miss routes to Edge Case 22).
Dependencies
Upstream (Base Building depends on):
| System | Hard/Soft | Interface consumed | Status |
|---|---|---|---|
| Inventory & Hauling | Hard | Atomic CanRemove/Remove (placement cost, CARRY ∪ STORAGE-in-range of spot) + CanAdd/Add (refund, CARRY-only) — Rule 6 shape; SharedStorage component instantiated per storage placement | Quick-spec ✅ |
| Crafting | Peer | Material item definitions (fiber/timber). StructureDef is BB's own parallel shape — RecipeDef reuse declined (Rule 1; crafting Rule 10 granted the choice) | GDD Reviewed |
| Tide Scheduler | Soft (static facts) | Home-island permanence; socket positions for F4 exclusion zones — no runtime coupling | GDD Reviewed |
| Save & Persistence | Hard | StructureInstance[] registry persistence + reconcile predicate (Edge Cases 13–16: inert relics, current-cost refunds, orphan posture) | Quick-spec ✅ 2026-08-26 — adopts the EC 13–16 posture, lists BB as dependent; two-class rule: registry is permanent |
| Harvesting | Hard (named constants) | lowEndPartyHaul, mixedHaulTimberPerSessionLow/High — the exported Formulas constants this GDD's CI gates AC-45/46 bind to by name (row added 2026-08-27, cross-GDD W5; harvesting's downstream table carries the mirror) | GDD Reviewed |
| Player Movement | Soft | Wall collision is movement's blocker set (movement Rule 4 ↔ AC-27); Edge Case 21's owner-rail correction is registered in movement Rule 6's teleport-snap list (row added 2026-08-27, cross-GDD W5) | Quick-spec ✅ 2026-08-27 |
| Art bible | Constraint source | Home shape language (soft/rounded), structures never shape-harden, silhouette-first tests | Approved |
Downstream (systems that depend on Base Building):
| Dependent | Hard/Soft | Interface provided |
|---|---|---|
| Crafting | Hard | stationPresentExact(tier) / stationPresentAtLeast(tier) (F3) — re-sampled at every craft/repair commit, falsifiable mid-beat |
| Inventory & Hauling | Hard | Container siting: each storage placement instantiates one SharedStorage at BB's chosen position |
| UI/HUD | Soft | Ghost validity state with per-cause reason strings; structure picker data |
| Enemy AI | — | None — home is hostile-free at MVP (stated so absence reads as intent) |
| Base-Defense Events (Tier 1) | — | Named as future dependent; no rows designed for it (first-to-slip discipline) |
Bidirectional-consistency check: Crafting already names BB in both directions (Rule 10 + its dependency tables) ✓; Inventory's quick-spec forward-refs container placement to BB ✓; the Save quick-spec lists BB as a dependent and adopts the Edge Case 13–16 reconcile posture (Rule 5 permanent-reconcile stage, OQ7 resolved 2026-08-26) ✓; LDU has no runtime coupling either way (home is outside its jurisdiction) but authors the per-socket depositPoint AC-63b validates ✓.
**Forward references owned elsewhere (expanded 2026-08-27, design-review):** → netcode ADR: structure-registry replication — NetworkObject-per- structure vs. client instantiation from a replicated list, a load-bearing choice for what "instant/atomic" means operationally (BB story scoping is sequenced behind this decision); late-joiner sync (default: full-snapshot-on-join — the registry is small; snapshot ordering vs. in-flight commits must be decided); instanceId issuance/replication plus reissue-on-save-rollback; what "persisted CARRY" means for a mid-disconnect player under the owner-rail model (Edge Cases 8–9); single-ordered-channel host-arrival plus the batch-vs-arrival ruling (Edge Case 1); Edge Case 21 in full — wall-collider replication timing vs. owner-rail movement, the deterministic free-cell check, whether the self-correction write passes the owner rail's movement bounds checks, and the stale-read race for a second concurrent placement; placerHasExit's RTT-staleness exposure for non-host placers (same class as noPlayerOccupancy — the ADR either closes it or records it as the accepted residual routed to Edge Case 22); the write-ordering slot of Edge Case 21's reposition relative to same-frame owner movement prediction (its registration as a teleport-snap authority event in the player-movement quick-spec's Rule 6 is already recorded there and is not ADR-gated; only the ordering and bounds-check questions remain open — third re-review 2026-08-27); serialization of the correction when two structures land on one player inside a single correction window; a numeric or tick-based **staleness bound for the client's replicated registry copy** (Rule 13's advisory reads are currently "stale, unbounded"). → technical-director: "all host-rail commits are single non-yielding synchronous blocks on the host main thread" as a binding cross-cutting constraint (asserted throughout as "atomic"/"critical section" but stated as a requirement nowhere — it breaks silently in coroutine-heavy code); host-vs-client instant-commit feel asymmetry (AC-48c); Render-Graph-native shader cost of the dashed ghost outline and reach-following grid overlay vs. the <100 draw-call budget. → home-island content authoring: buildable-region/terrain authoring, plus the buildable-area spatial budget — nobody has yet computed whether the island minus socket exclusions and terrain actually fits a 50-segment perimeter + stations + chests + walk space; perimeterSessions guards a fiction until this is checked, and the perimeterSegments anchor stays provisional until it is. → Inventory + Save quick-spec: SharedStorage content serialization.
Tuning Knobs
Not tuning knobs (rules, locked by this GDD): full refund (never partial — inert relics are the sole exception and refund exactly zero, Rule 9 / Edge Case 13), empty-chest-first demolish, structure anonymity (no builder field), home-island-only building + the orbit test, instant commit (no placement timer), no queues/batches/pending-state (Rule 11a — repeat input cadence is 11b, /ux-design-owned and NOT locked), grid snap. Changing any is a design change requiring GDD revision + CD gate. *("No move verb" is deliberately NOT on this list — it is an MVP scope decision with a Tier 1 revisit trigger, Rule 9 / CD C3.)*
| Knob | Default | Safe range | Too low | Too high | Interacts with |
|---|---|---|---|---|---|
wallCost | 4 timber | 3–4 (re-derived 2026-08-27, fourth re-review CS-8 — the prior 3–6 was refuted by this row's own "too high" cell: at the 50-segment anchor, 5 → ⌈250/58⌉ = 5 and 6 → ⌈300/58⌉ = 6 sessions, both outside the band. Worked bounds at both haul extremes: 3 → ⌈150/58⌉ = 3, ⌈150/138⌉ = 2 ✓; 4 → ⌈200/58⌉ = 4, ⌈200/138⌉ = 2 ✓. Marginal only — perimeterSegments at its default. Gate-derived: this range is a consequence of the perimeterSessions band at the current anchor, not an independent design judgement — CS-18) | Below 3 the fast haul extreme drops to one session (⌈50 × 2 / 138⌉ = 1), breaking the band's lower bound — same structure as the perimeterSegments row. (The prior "trivial in < 1 session" was false in kind: at the slow end wallCost 2 is still ⌈100/58⌉ = 2 sessions; the floor is set by the fast end.) | Above 4 the 50-segment perimeter exceeds 4 sessions at the honest timber low end (⌈50 × 5 / 58⌉ = 5) — treadmill territory | perimeterSessions CI gate; harvesting timber yields |
perimeterSegments | 50 | 35–58 (re-derived 2026-08-27, third re-review CS-6 — the second re-review's 25–57 was wrong at both ends: 25 fails the gate at the high haul extreme (⌈100/138⌉ = 1 < 2), and 57 was refuted by this row's own "too high" cell, which computed 58 as passing. The gate is two-sided at two extremes: the lower band bound forces segments × 4 > 138 → ≥ 35; the upper forces segments × 4 ≤ 232 → ≤ 58. Worked bounds: 35 → ⌈140/58⌉ = 3, ⌈140/138⌉ = 2 ✓; 58 → 232/58 = 4 exactly, ⌈232/138⌉ = 2 ✓; the set [35, 58] has no holes. This range governs the anchor constant only — the 25-segment shelter Formulas names is a player path below the gate-safe floor by design, not a knob value; see Formulas' gate-subject clause. The band carries no design preference within it — any value in range is CI-legal; 50 is chosen for headroom at the honest low haul, not for a felt pacing quality — CS-17. Marginal only — wallCost at its default (4), CS-18) | Below 35 the high haul extreme drops to one session (⌈34 × 4 / 138⌉ = 1), breaking the band's lower bound — the floor is set by the fast end, not the slow one. (At the slow end alone, two sessions holds for 15–29 segments; a floor derived from 58 only is not a floor.) | Above 58 the perimeter exceeds 4 sessions at the honest timber low end (⌈59 × 4 / 58⌉ = 5) — treadmill territory | perimeterSessions CI gate; home-island spatial budget (provisional until authored) |
workbenchCost | 6 fiber + 10 timber | ±50% | Affordable before the first real trip — the placement event is undercut | First craft slips past session one — breaks crafting's first-session guarantee | bootstrapRatio gate; crafting recipe costs |
storageChestCost | 8 fiber | 4–12 | Trivial — acceptable; storage should be early | Delays early banking, no upside | bootstrapRatio |
storageSlots (per chest) | 40 | 24–80 | Banking blocks at MVP | No harm, no point | = Inventory's storage_slots_home — one value; Inventory owns the default |
placementReach | 6 u | 5–8 | Backpedal-placement fiddliness | Sight-unseen placement; whole-shape erosion | stationRange (the draw radius stays 3 u, independent) |
socketNoBuildRadius | 4 u | 3–8 | Walls crowd the tide's deposit shoreline (Rule 3's hazard) | Shrinks the placeable shore area | Socket layout; re-check when Tier 1 adds 3–4 sockets ⚑ |
Knob-interaction warnings: structure costs + harvesting yields + crafting recipe costs form one balance system — bootstrapRatio and perimeterSessions are the shared invariants, both live-recomputed from the interacting systems' named constants; never retune one side without running both gates. **Per-knob safe ranges are marginal-only — the CI gates are the joint authority** (corners of the joint box fail; see the CI-gates section). **stationRange is Crafting's knob (safe 2–6), consumed here in two semantics at once** — F1's cost-draw radius (placement-spot-centred) and F3's station reach (asker-centred) move together by design; retuning it for crafting reach also retunes placement material-draw, and Crafting's knob row must carry the same warning. All BB values live in assets/data/structures.json, validated per Edge Case 17.
Visual/Audio Requirements
Base Building's visual contract is owned by the art bible; this section states what BB imposes and cites rather than restates.
Structures stay in home's rounded vocabulary, permanently: wall, workbench, and storage chest render in §3.2's guardrail shape language — soft, rounded, hand-built — and never shape-harden regardless of material tier (the gear-only hardening rule does not apply to structures). At MVP all three ship Camp-stage scale and material (§6.1: small-footprint, thatch/timber, Driftwood Grey/undyed tones). Keep-stage growth has no trigger defined anywhere; ⚑ **art-bible fix-pass item: §6.1 describes Camp→Keep growth but no system states what advances a structure between stages** — flagged, not invented here (MVP never leaves Camp stage).
Ghost grammar: outline-only (ruling 2026-08-26). Valid placement = the ghost renders with a solid ink outline; invalid = a **broken/dashed outline**; zero fill tint in either state. Maximally palette-pure (reuses §1's outline language, no new UI color pattern) and colorblind-safe by construction (shape-coded, never hue-coded, §4.6). The failing gate's reason string carries the specifics (UI Requirements); the outline carries only the binary. ⚑ **art-bible fix-pass item (design-review 2026-08-27): pin a minimum dash-segment/gap ratio and run §1's strip-to-black test on both ghost outline states at the locked 2 px outline weight and gameplay zoom** — solid-vs-dashed must survive the same silhouette discipline as final assets, or the invalid state is illegible in motion.
Ghost evaluation is a hot path: the client-side provisional F1 re-evaluation runs every frame in build mode (Rule 5) and must use non-allocating queries and cached buffers — no per-frame collection allocation (implementation note, re-review 2026-08-27).
**Grid: full overlay within placementReach, build mode only (ruling 2026-08-26).** The grid renders only inside placementReach of the player and only while build mode is active — legible for the multi-wall common case (Rule 5), with the diegetic-first cost (§7.1) bounded by the small radius and the mode gate. Never a full-canvas grid; never visible outside build mode.
**The placement thunk, made concrete (ruling 2026-08-26): squash-stretch + settle, paired.** On host-confirmed commit the structure lands 1 px high and eases down with a single impact-only squash-and-stretch beat (§5.3's reserved-for-impact budget — existing tech, no new VFX) plus the completion sound below. No particle flourish, no fanfare — "placement is an event" is delivered by motion and sound within existing budgets (a dust-puff was considered and rejected: the atmospheric-overlay budget is already spent on departure mist, §2, and §6.2 bans per-object bespoke VFX).
Inert-relic fallback (Edge Case 13, ruling 2026-08-26): a flat, undetailed grey silhouette box at the structure's original footprint **with a broken-outline edge accent — the ghost's invalid grammar reused** — so it reads as deliberately dead, never as a rendering bug. ⚑ art-bible fix-pass item: the relic treatment needs a proper section alongside the Camp→Keep gap; this is the floor, not the spec.
Silhouette-first test for BB assets (pass/fail): at solid black, 64–96 px per §8.A: a workbench never reads as a threat or a resource node; a wall segment reads as boundary, not prop, at gameplay zoom; storage reads as container, distinguishable from a station at a glance; and — BB's addition to the house pattern — the **base's cumulative silhouette stays legible as one shape** while placing (the whole-shape test made into an asset check): a screenshot of the base mid-build must not read as scattered objects on a field.
**Tide-tint: home structures are exempt by construction, not by carve-out.** §4.3's tint applies only to DriftLayer-tagged sprites on docked lands; home-island sprites are never tagged, and structures are home-island-only (Rule 3) — so structures are never tide-tinted, reaching combat sprites' outcome through category rather than exemption.
Displaced-player tell (Edge Case 21, second re-review 2026-08-27): when a wall lands on a player and the correction moves them, the move is a discrete step — always exactly one cell (Edge Case 21's ring check, fifth re-review 2026-08-27, CS-18; the prior "regardless of hop count" clause described the retired multi-hop search) — one settle beat on the moved sprite (the same impact-only squash-and-stretch budget as the placement thunk, reused, no new VFX) and no sound of its own — the wall's own thunk already marks the event. Silent snapping reads as a bug; a fanfare reads as punishment; one settle beat reads as "the wall got here first."
Audio: placement thunk host-confirmed with the commit (never predicted); demolish sound distinct and equally understated, host-confirmed; build-mode enter/exit cues brief and non-musical; **no ghost-move tick** (continuous cursor audio invites noise fatigue across multi-wall sessions). All understated per house voice — no fanfare anywhere.
Feedback authority split (audio/visual, extending the States table):
| Effect | Timing |
|---|---|
| Ghost outline state, grid display, build-mode enter/exit cue | Instant — client-predicted |
| Placement thunk + squash/settle + structure appearance; demolish sound + removal | Host-confirmed broadcast only |
📌 *Asset Spec note: after this GDD is approved, run /asset-spec system:base-building.*
UI Requirements
Base Building contributes: the build-mode picker (the MVP set is three entries — a list, not a grid of cards; shows each structure's cost with a **live affordability read centred on the ghost's current position once build mode is active** — the same CARRY ∪ STORAGE-in-range-of-placement-spot predicate F1's commit gate evaluates, so preview and gate can never disagree on what is checked; they can still disagree on freshness (the picker reads the client's replicated copy, Rule 13), which resolves as Edge Case 11's honest no-op. The picker is visible only inside BuildMode (States table), so a ghost always exists while it is open and the read is always ghost-centred. *Fixed 2026-08-27 — the prior player-centred wording contradicted AC-19's placement-spot-centric draw; "never disagree by construction" narrowed to the predicate same day (re-review).* Greyed entries carry per-cause world-fact reason strings, crafting Rule 3's pattern); the ghost preview (validity outline + which gate fails as a short world-fact string — "too far away", "too close to the dock", "someone is standing there", "no way out", "not enough timber in reach", "blocked" — never a generic "invalid"). Reason-string precedence and stability (added 2026-08-27): when multiple F1 conjuncts fail at once, the displayed string is the **first failing gate in F1's stated order** (reach → bounds → overlap → occupancy → exit → terrain → socket → materials — ordered so that resolving an earlier failure is a precondition for the later check being meaningful: the materials gate's in-range STORAGE draw depends on the final spot, so it is judged last), held stable while that gate remains the first failure. That stability clause bounds flicker within one sustained failure only; flicker between gates at a cell boundary (e.g. noOverlap toggling at a wall edge) needs a **minimum display duration or snap hysteresis** — a named /ux-design deliverable, not implied by "held stable" (re-review 2026-08-27). Crafting's three reason strings need the identical contract; flagged there as a cross-system pattern gap. Also contributed: grid/snap feedback during build mode only (never outside it); the demolish prompt (target highlight + refund preview — showing what comes back is what makes full-refund legible, and AC-28b makes the preview a tested data contract; "empty the chest first" on a blocked storage demolish). **Demolish targeting is explicitly delegated to /ux-design** (a decision, not an omission — 2026-08-27): BB provides the candidate set (structures within interaction range) and requires gamepad parity, no hover-only path, and a deterministic disambiguation rule for dense clusters (suggested default: cursor-raycast on M+KB, nearest-in-reticle-cone on gamepad); the selection model itself is /ux-design's deliverable. **The gamepad stick-to-ghost transfer function** (cursor-velocity vs. cell-step, acceleration, deadzone) is likewise a named /ux-design deliverable — AC-8's device-parity claim covers snap outcome only, not input effort (OQ10). And **no build-log, no contribution stats, no per-player build history anywhere, ever** (Rule 7's no-ledger test restated as a UI ban, same shape as crafting's aggregation ban). Copy follows the world-facts tone rule throughout — a blocked placement is a fact about the world, never a scolding.
📌 UX Flag — Base Building: this system has UI requirements. In Pre-Production, run /ux-design for the build-mode/picker/demolish screens before writing epics; BB stories referencing UI must cite design/ux/, not this GDD. The ghost reason strings join crafting's three recipe reason strings and the harvesting no-op cues on the pre-playtest /ux-design list.
Acceptance Criteria
[UNIT] = EditMode NUnit over pure logic (placement/demolish legality gates F1–F2, station-presence predicates F3, socket exclusion F4, config validation, the material-softlock invariant) — the placement/demolish pipeline (Rules 6, 9–10, F1–F4) must be designed pure/DI-testable, no live NGO session required. **Purity mandate (added 2026-08-27, design-review BL-4): the pipeline consumes injected plain-data snapshots — terrain via an ITerrainQuery-style abstraction, the registry as a plain collection, player positions as a passed-in list — never live Tilemap/Physics2D/NGO queries. The production Tilemap/physics adapters are thin and are NOT unit-tested; only the pure predicates are. Without this factoring the [UNIT] label on F1–F4 is unachievable (EditMode has no reliable physics scene).** [INTEGRATION] = NGO runtime, Multiplayer Play Mode + Network Simulator — reserved for commit races under live timing, disconnect handling, and cross-system races with Crafting/Inventory. [MANUAL] = human feel/readability judgment, never automated — but scoped to a concrete, checkable claim. All criteria use locked baseline values (Baseline structure table, MVP) unless the vector specifies otherwise.
Structures as Data (Rule 1)
- AC-1 [UNIT] GIVEN a
StructureDefwith valid fields, WHEN loaded, THEN it parses into the schema (structureId,category,footprint,cost,stationTier?,storageSlots?) without error and is retrievable bystructureId. - AC-2 [UNIT] GIVEN the compiled
StructureDeftype, WHEN reflected over (an automated allow-list assertion re-run every CI pass — never a one-time manual search; wording fixed 2026-08-27), THEN its field set is exactly{structureId, category, footprint, cost, stationTier?, storageSlots?}— norecipeId,craftTime, or any otherRecipeDef-only field exists (Rule 1's declined-reuse decision).
Anyone Builds, Always (Rule 2)
- AC-3 [UNIT] GIVEN any player at session start (no prior progression state), WHEN build mode availability is queried, THEN it returns available — no unlock flag, no builder-role field, no permission tier exists anywhere in the schema.
Home-Island-Only Building (Rule 3, F4)
- AC-4 [UNIT] GIVEN a placement attempt on a docked land, WHEN F1 evaluates, THEN
withinBounds=falseandcanPlace=falseregardless of every other conjunct — docked lands are never buildable. - AC-5 [UNIT] GIVEN a footprint entirely outside
socketNoBuildRadiusof every dock socket, WHEN F4 evaluates, THENnotInSocketExclusion=true(pinned worked example: 2×1 footprint, nearest cell 6 u from a socket,socketNoBuildRadius=4→ clear). - AC-6 [UNIT] GIVEN a footprint with exactly one cell within
socketNoBuildRadiusof one socket, WHEN F4 evaluates, THEN the whole footprint fails — one failing cell against one socket fails the whole check (pinned worked example: cell 3.5 u from socket at radius 4 → illegal). - AC-7 [UNIT] GIVEN
socketNoBuildRadiusat its configured value, WHEN F4 is tested at the exact boundary distance, THEN a cell at distance> socketNoBuildRadiuspasses and a cell at distance≤ socketNoBuildRadiusfails — boundary inclusivity pinned in one direction. - AC-7b [UNIT] GIVEN every
SharedStorageinstantiation and every F3 evaluation call site, WHEN constructed or evaluated, THEN the position argument asserts as an on-home-island coordinate (bounds check against the home-island region) — the mechanically testable core of the orbit test (Rule 3 / CD C1). The unbounded half — "no future mechanism may ever site station/storage function off-island" — is an architecture invariant, not a unit test: it lives on the code-review checklist and in/architecture-review(reclassified 2026-08-27; a finite suite cannot prove an open-ended negative).
Grid Placement, Snap-with-Nudge (Rule 4)
- AC-8 [UNIT] GIVEN a raw candidate position near the world grid, WHEN the placement position is resolved via
ghostSnapResult, THEN it quantizes to the nearest valid grid cell — snap is a pure function of input position, not client framerate or input device. - AC-9 [UNIT] GIVEN two candidate positions each resolved via
ghostSnapResultto adjacent grid cells, WHEN their resolved footprint boundaries are compared, THEN the shared edge coordinates are identical to snap-tolerance epsilon — zero gap, zero overlap, by arithmetic (split 2026-08-27 from the old combined visual-judgment claim). - AC-9b [MANUAL] GIVEN the same two structures rendered together, WHEN a lead reviews a screenshot at gameplay zoom, THEN sign off that the corner reads clean — evidence logged in
production/qa/evidence/(the honest visual-judgment half; whole-shape test).
Build Mode Is Client-Local (Rule 5)
- AC-10 [UNIT] GIVEN build mode is toggled on with no confirm input issued, WHEN host state is inspected, THEN nothing has changed — the ghost is zero-replicated-state by construction.
- AC-11 [UNIT] GIVEN build mode is active, WHEN a structure is placed and committed, THEN build mode remains active afterward (does not auto-exit) — the mode persists across placements (multi-wall sessions).
- AC-11b [UNIT] (added 2026-08-27) GIVEN build mode is active, WHEN the toggle input fires again or a non-build action (any action from a map other than Build — States table) is taken, THEN build mode transitions to Idle/Move and the ghost ceases to render — the States table's exit half, previously untested (an implementation that never exits would have passed every prior AC).
- AC-12 [INTEGRATION] GIVEN a player exits or disconnects with build mode open, WHEN observed from another client, THEN no ghost or partial structure ever existed on the host — build mode leaves no host trace (Edge Case 7).
Placement Commit Is Instant and Atomic (Rule 6, F1)
- AC-13 [UNIT] GIVEN all eight F1 conjuncts true, WHEN F1 evaluates, THEN
canPlace=true(pinned worked example: wall footprint on buildable grass, 4 u from the player withinplacementReach6 u, 6 u from the nearest socket againstsocketNoBuildRadius4 u, no overlap, no player in the footprint, the placer's own cell keeping a free neighbour, 4 timber reachable from CARRY + a chest 2 u from the placement spot). - AC-14 [UNIT] GIVEN any one F1 conjunct false, WHEN F1 evaluates, THEN
canPlace=false— tested independently for each of the eight gates (withinReach,withinBounds,noOverlap,noPlayerOccupancy,placerHasExit,validTerrain,notInSocketExclusion,CanRemove);validTerraingets its own case fed by a mockedITerrainQuery(it is the conjunct most likely to leak a live-Tilemap dependency), andplacerHasExitis tested with the placer's cell surrounded on three sides plus the candidate footprint closing the fourth. - AC-14b [UNIT] (added 2026-08-27 second re-review, CS-5; rewritten third re-review CS-7 — the prior GIVEN needed one footprint to cover two perpendicular neighbours of a convex corner, an L-shape no baseline structure has, so the test could never pass) GIVEN the placer standing on the placeable region's edge where three of its four orthogonal neighbours are unavailable — at least one out-of-region, the rest covered by existing structure footprints — and the single remaining in-bounds, buildable, unoccupied neighbour is covered by the candidate footprint (a baseline 1×1 wall suffices), WHEN F1 evaluates, THEN
placerHasExit=false. Out-of-region cells are never counted as exits: an implementation that treats "no cell" as "not blocked" reports an exit here and fails. A companion case (pinned fourth re-review 2026-08-27, CS-11 — the prior "displaced by one cell" named no bearing and admitted a vacuous far-displacement reading and a reading that flippednoOverlapinstead), identical except that the candidate 1×1 footprint is placed one cell further outward along the same bearing as the previously-blocked exit — the exit cell stays clear; the candidate remainswithinReach,withinBoundsand overlaps nothing, soplacerHasExitis the only conjunct that changes — assertsplacerHasExit=true. Fixture constraints (fifth re-review 2026-08-27, CS-18): the isolation claim holds only if two cells along the bearing stay withinplacementReach(gridCellSize≤ 3 u at the 6 u default — OQ6 is unpinned, so the fixture states this rather than assuming it) and the placeable region extends ≥ 3 cells along that bearing from the placer; otherwisewithinReachorwithinBoundsflips alongsideplacerHasExitand the companion tests the wrong conjunct. - AC-14c [UNIT] (added 2026-08-27 second re-review; BFS pinned CS-7/CS-11; rewritten fifth re-review 2026-08-27, CS-18 — the CS-11 GIVEN blocked all four hop-1 neighbours and asserted the hop-2 cell (1,1), which a clear-cells-only search can never reach; no reachable hop-2 case exists, so the search is now Edge Case 21's ring check and this test is walked against its reference block) GIVEN a player's cell at the origin intersected by a newly committed footprint, a plain-data
ITerrainQuery(the same abstraction AC-14 names forvalidTerrain— one definition of "buildable" across both tests, CS-17), a plain registry, and the placeable region, WHENFindCorrectionCellruns as a pure function, THEN: (i) with N (0,1) and E (1,0) both clear and S, W blocked, it returns N (0,1) — the fixed N, E, S, W order, not a nearest-by-bearing or lowest-index choice; (ii) with all four neighbours blocked — the triggering 2×1 workbench covering the origin and E, W out-of-region, N and S under existing 1×1 walls — it returns none, and an implementation that steps through a blocked neighbour to a clear cell beyond it returns a cell here and fails; (iii) a neighbour that is clear terrain but outside the placeable region is never returned even when it is the only clear one (the out-of-region ≠ exit rule, AC-14b's mirror). The algorithm is pure per this section's purity mandate; only its replication/write mechanics are netcode-ADR territory. - AC-14e [UNIT] (added fifth re-review 2026-08-27, CS-18 — closes the pin (iii) fresh-check gap the fourth re-review left untested; distinct from the in-flight cap, which no AC exercises by design) GIVEN player P at cell A, a first wall W1 committed on A whose correction moved P to B (A's first clear neighbour under N, E, S, W), and a second wall W2 committed on B because its
noPlayerOccupancyread P's pre-correction position at A (Edge Case 21's accepted-race sequence), WHEN W2's footprint intersects P's cell and the correction fires again, THENFindCorrectionCellruns at B against a registry containing both W1 and W2 and returns B's first clear neighbour in the same order — never a re-use of the A-rooted result. A companion asserts that if P's current cell is already clear at invocation (P walked off B first), the (ii) re-check finds no overlap and no transform write occurs. - AC-14d [UNIT] (added fourth re-review 2026-08-27, CS-15 — the sequential proof of Edge Case 23's stated boundary, which AC-14/14b's single snapshots do not give) GIVEN a placer on an interior cell with all four orthogonal neighbours in-region, buildable and clear, WHEN three baseline 1×1 walls are committed in sequence, each covering one of the placer's remaining clear neighbours and each evaluated by F1 against the registry as it stands at that commit, THEN all three commit (
placerHasExit=trueat each — one exit always remains), and a fourth candidate covering the sole remaining clear neighbour evaluatesplacerHasExit=falsewith reason "no way out" — the maze is legal up to and excluding exactly the last-exit segment. (The other seven conjuncts hold trivially throughout — each candidate is adjacent to the stationary placer, overlaps no prior wall, and sits on socket-clear buildable terrain with 4 timber in reach; onlyplacerHasExitchanges across the sequence — CS-18.) - AC-15 [UNIT] GIVEN a confirm input begins, WHEN inspected before the host critical section resolves, THEN nothing is debited and no structure instance exists — no partial state between confirm and commit.
- AC-16 [UNIT] GIVEN a confirm input at commit, WHEN the host critical section runs, THEN gate re-sample + cost debit + structure-instance write execute as one atomic all-or-nothing transaction — partial application is structurally impossible.
- AC-17 [UNIT] GIVEN any F1 gate fails at the commit re-sample, WHEN commit resolves, THEN the placement is an honest no-op — ghost persists, nothing debited, reason string names the failing gate.
- AC-18 [UNIT] GIVEN cost is drawn from CARRY plus in-range STORAGE, WHEN
CanRemoveevaluates, THEN both sources are treated as one Inventory-Rule-6-shape transaction — never two independent checks that could desync. - AC-19 [UNIT] GIVEN a placement spot with STORAGE in range, WHEN
stationRangefor cost-draw is measured, THEN the draw radius is centered on the placement spot, not the player position (ruling 2026-08-26). - AC-20 [INTEGRATION] GIVEN two ghosts committing on the same footprint the same tick under live host timing, WHEN both resolve, THEN the first host-arrival wins and the second's
noOverlapre-sample fails as an honest no-op (Edge Case 1; see AC-20c for what the harness can actually assert about arrival order). - AC-20b [UNIT] GIVEN the pure tiebreak resolver invoked directly with a batch of pending commit intents whose internal arrival order is indistinguishable (the input carries no arrival sequence — the only case Edge Case 1 routes to the tiebreak; a plain function input — no host loop, no tick concept; split 2026-08-27: "exact same host tick" is not observable under an ordered host queue; GIVEN narrowed 2026-08-27 second re-review CS-4 — the prior wording asserted the unconditional reordering Edge Case 1 disavows), WHEN resolved, THEN the batch is processed in ascending
OwnerClientIdorder — deterministic across runs (Edge Case 1's tiebreak function in isolation). A companion case feeds the same resolver a batch with arrival sequence numbers and asserts the output preserves arrival order regardless ofOwnerClientId. Input shape (third re-review 2026-08-27): the intent type carries an optional arrival-sequence field, and a batch is fully-sequenced or fully-unsequenced by construction — a mixed batch (some intents sequenced, some not) is rejected at batch construction, never resolved; a third case asserts that rejection. - AC-20c [INTEGRATION] GIVEN two clients' placement confirms arriving within the same simulator-controlled tick window (Multiplayer Play Mode + Network Simulator latency control — the closest a harness gets to simultaneity, stated as such rather than promised as exactness), WHEN both resolve, THEN exactly one commits and the other honest-no-ops — the test asserts mutual exclusion only, never which client wins, since arrival order under the simulator is not itself asserted deterministic (the live half of Edge Case 1).
- AC-21 [INTEGRATION] GIVEN a placement commit and a craft commit racing the same materials under live host timing, WHEN both resolve, THEN they queue in plain host-arrival order with no BB-side or crafting-side lock (Edge Case 2).
- AC-22 [INTEGRATION] GIVEN a placement commit racing a STORAGE withdrawal from the same chest under live host timing, WHEN both resolve, THEN arrival order applies and the loser's
CanRemove/CanAddre-sample reflects the post-state honestly (Edge Case 3). - AC-23 [INTEGRATION] GIVEN a placement's STORAGE-in-range draw includes a chest a concurrent demolish removes, WHEN the placement commit resolves, THEN
CanRemoveevaluates against whatever STORAGE still exists at that instant — the vanished chest is simply absent from the draw (Edge Case 6).
Structure Anonymity (Rule 7)
- AC-24 [UNIT] GIVEN the compiled
StructureDefandStructureInstancetypes, WHEN reflected over (automated allow-list assertion:StructureInstance= exactly{instanceId, structureId, position}— re-run every CI pass, never a one-time manual search; wording fixed 2026-08-27), THEN no builder/owner/creator field of any kind exists — norotation, nocreatedAtCycle, nobuilderName(the schema-negative mirror of crafting AC-12). - AC-25 [UNIT] [DEFERRED — activates when
/ux-design's data contracts exist; N/A until then, never "failing"] GIVEN every structure-listing or structure-inspection UI data contract, WHEN its compiled contract type is reflected over (automated assertion, same discipline as AC-24), THEN no sort/group/filter/count affordance keyed on a builder identity exists — there is no field to aggregate on (Rule 7's no-ledger test).
Structure Lifecycle: Present Only (Rule 8)
- AC-26 [UNIT] GIVEN a placed
StructureInstance, WHEN its schema is inspected, THEN it contains no HP, damage, decay, or under-construction field —{instanceId, structureId, position}is the entire shape. - AC-27 [INTEGRATION] GIVEN a placed wall in a live session, WHEN a player attempts to move through its footprint, THEN movement is blocked — collision is the wall's entire MVP function (reclassified from [UNIT] 2026-08-27: collision requires a live physics scene and was never EditMode-pure).
Demolish Exists; Refund Is Total (Rule 9, F2)
- AC-28 [UNIT] GIVEN all three F2 conjuncts true, WHEN F2 evaluates, THEN
canDemolish=trueand refund equals exactly 100% of the structure's currentcost(zero for inert relics — Edge Case 13). - AC-28b [UNIT] (added 2026-08-27) GIVEN the demolish prompt's data contract for a target structure, WHEN inspected, THEN it contains a refund preview equal to the current
StructureDef.cost(zero for relics) — "showing what comes back is what makes full-refund legible" made testable at the data layer; whether players perceive the guarantee remains playtest territory. - AC-29 [UNIT] GIVEN any one F2 conjunct false, WHEN F2 evaluates, THEN
canDemolish=false— tested independently for each of the three gates (structureExists, storage-empty-if-storage,CanAdd). - AC-30 [UNIT] GIVEN a non-empty
SharedStoragecontainer, WHEN demolish is attempted, THEN it is a no-op with reason "empty the chest first" —storageEmptygates every storage demolish, no auto-transfer path exists. - AC-31 [UNIT] GIVEN a demolish whose refund does not fit in the demolisher's CARRY, WHEN F2 evaluates, THEN it is a no-op with a room-in-CARRY reason string and the structure remains standing — refund routes to CARRY only, never STORAGE.
- AC-32 [UNIT] GIVEN any refund calculation for a structure whose
StructureDefexists, WHEN compared tocost, THEN it is never partial — no refund value less than 100% of current cost is producible by any code path. Inert relics (noStructureDef, Edge Case 13 / AC-49) are the sole excepted case and refund exactly zero by definition — not partially (re-review 2026-08-27, CS-1). - AC-33 [UNIT] GIVEN Base Building's closed command set — the implementers of its
IStructureCommand-style interface (name pinned at implementation) — and the Build action map in the project's.inputactionsasset, WHEN the implementers are reflected over and the asset's action list is deserialized (two assertions, one test), THEN neither contains a move/relocate verb — demolish + re-place is the only relocation path (Rule 9). (Rewritten 2026-08-27 re-review: "the API surface" is not a reflectable target; a command type and an input asset are.) - AC-34 [INTEGRATION] GIVEN two demolishes targeting the same instance the same tick under live host timing, WHEN both resolve, THEN the first removes it and the second's
structureExistsre-sample returns false, resolving as an honest "already gone" no-op (Edge Case 4). - AC-35 [INTEGRATION] GIVEN a demolish racing a deposit into the same chest under live host timing, WHEN both resolve, THEN
storageEmptyis re-sampled fresh at the demolish commit instant and the outcome depends purely on arrival order (Edge Case 5).
No-Softlock Invariant (Edge Case 20)
- AC-36 [UNIT] GIVEN any structure's current
StructureDef.cost, WHEN refund-on-demolish and cost-to-rebuild-immediately-after are compared, THEN refund = current cost = rebuild cost, always — the full demolish/re-place round-trip is provably free in materials by construction (the material-softlock proof — the spatial case is Edge Case 22's accepted social closure; must hold even after a content-patch cost change, since no placement-time cost snapshot is stored — Edge Case 14). - AC-37 [UNIT] GIVEN the last workbench of a tier is demolished, WHEN
stationPresentExact/stationPresentAtLeastare queried immediately after, THEN they return false with no other side effect — no lock, no penalty state, consistent with AC-36's free round-trip (Edge Case 19's boundary case, restated as a positive assertion).
Demolish Never Blocks on Use (Rule 10)
- AC-38 [INTEGRATION] GIVEN a player mid-craft-beat at a station under live host timing, WHEN another player demolishes that station before the craft's commit instant, THEN the craft's own
stationPresentre-sample fails and the craft resolves as crafting's own honest no-op (Rule 10). (The companion claim — no BB-side interrupt/griefing subsystem exists — is an architecture-absence statement moved to the code-review checklist 2026-08-27; a race test cannot verify an absence.) - AC-39 [UNIT] GIVEN
stationPresentExact/stationPresentAtLeastqueried once, WHEN the registry is mutated (a station added or removed) and the predicate queried again with no cache-clear call in between, THEN the second result reflects the mutation — the behavioral proof the predicate re-evaluates live rather than memoizing (rewritten 2026-08-27: "no caching layer exists" is a code-review claim, not a test outcome; this is its testable proxy; Rule 10's consequence). - AC-39b [UNIT] (added 2026-08-27 re-review) GIVEN a client whose replicated registry copy says a commit would fail (or succeed), WHEN the confirm input fires, THEN the commit request is sent to the host regardless — the client never substitutes its cached read for the host re-sample (Rule 13's two-context split made testable: a client can only request, never pre-validate-and-skip).
No Queues, No Batches (Rule 11)
- AC-40 [UNIT] GIVEN a player mid-placement-ghost, WHEN a second placement intent is submitted before the first commits or cancels, THEN the second is rejected or replaces the first ghost — one ghost, one commit at a time per player, no queue exists.
Placing Storage Instantiates the Container (Rule 12)
- AC-41 [INTEGRATION] GIVEN a storage-category placement commit resolves on the host, WHEN Inventory's authoritative state is inspected through Inventory's own query interface, THEN exactly one
SharedStorageinstance (whatever concrete representation the netcode ADR selects — component, NetworkObject, or replicated record) is associated with BB's chosen position and reportsstorageSlotscapacity matching theStructureDef— BB sites it, Inventory owns it from that instant on. Sequenced behind the netcode ADR like the registry-replication items; the prior "component… exists at position" wording tested an unlocked shape (re-review 2026-08-27).
stationPresent — Two Named Predicates (Rule 13, F3)
- AC-42 [UNIT] GIVEN a bronze station 2 u from the asker, WHEN both predicates are evaluated for
tier=bronzeandtier=cloth, THENstationPresentExact(bronze)=true,stationPresentExact(cloth)=false,stationPresentAtLeast(cloth)=true(pinned worked example — exact vs. downward-inclusive). - AC-43 [UNIT] GIVEN a station at exactly
stationRangedistance from the asker, WHEN F3 evaluates the boundary, THEN the station counts as present —dist ≤ stationRangeis inclusive (restated from F3's formula so the expected value is never an inference exercise, AC-7's pattern) and consistent across both predicates. - AC-44 [UNIT] GIVEN the compiled Crafting-facing contract type (the
IStationPresenceProvider-style interface BB exposes to Crafting — name pinned at implementation), WHEN reflected over (automated assertion, AC-24's discipline), THEN only the two explicitly named predicates are exposed — no single parameterizedstationPresent(tier, mode)call exists (Rule 13's anti-silent-wrong-mode design).
CI Gates — Live-Recomputed (Formulas section)
- AC-45 [UNIT] GIVEN harvesting's exported
lowEndPartyHaulplus crafting's live recipe costs and this table's live structure costs, WHEN the bootstrap gate runs, THEN it recomputesbootstrapRatiofrom those named constants at test time and asserts0.10 ≤ bootstrapRatio ≤ 0.35— never a hardcoded literal of the current ≈0.29 value (the crafting AC-54/review-C1 lesson, applied at authoring time). - AC-45b [UNIT] (mutation-sensitivity — its own named test,
test_bootstrapRatio_reactsToConstantChange; split out 2026-08-27) GIVEN the baseline ratio computed once, WHEN one named input constant (e.g.lowEndPartyHaul) is perturbed by a fixed known delta and the ratio recomputed, THEN the new ratio differs from baseline by exactly the amount the formula's arithmetic predicts — not merely "differs at all" (an any-delta assertion can pass by coincidence; arithmetic consistency is what proves live recomputation). - AC-46 [UNIT] GIVEN the live
wallCost, this GDD's namedperimeterSegments(50), and harvesting's exported party-scopedmixedHaulTimberPerSessionLow/High, WHEN the perimeter gate runs, THEN it recomputesperimeterSessions = ⌈(perimeterSegments × wallCost) / mixedHaulTimberPerSession⌉at both exported extremes at test time and asserts2 ≤ perimeterSessions ≤ 4at each — never a hardcoded literal (anchor revised 60 → 50, re-review 2026-08-27 CS-2). Passing proves session-length pacing only — not that the anchor reads as a keep on the authored island (Formulas' perimeter check; fourth re-review 2026-08-27, CS-16), and this gate does not assert the spatial fit check, which has no automated form until home-island authoring pins walk-corridor width and perimeter shape (CS-18). - AC-46b [UNIT] (mutation-sensitivity,
test_perimeterSessions_reactsToConstantChange) — same arithmetic-consistency requirement as AC-45b, perturbingmixedHaulTimberPerSessionLow.
Placement Reach (Edge Case 12)
- AC-47 [UNIT] GIVEN a placement spot within
placementReach(6 u) of the player, WHENwithinReachevaluates, THEN it returns true; GIVEN a spot beyondplacementReach, THEN it returns false with a reach-specific reason string (F1's gate list). - AC-48 [MANUAL] GIVEN the ghost preview rendered beyond
placementReach, WHEN a tester views it, THEN it still renders (visible-but-unconfirmable) rather than disappearing — the ghost leads the cursor even out of confirm range (a concrete, checkable render-state claim, not a feel claim). - AC-48b [INTEGRATION] GIVEN a placement confirm input, WHEN the thunk sound and squash/settle animation fire, THEN both are triggered only by the host-confirmed broadcast and never by the local confirm input — a predicted thunk that later no-ops is worse than no thunk (CD A3; the testable core of "placement is an event").
- AC-48c [MANUAL] (added 2026-08-27, netcode review B3) GIVEN the friend-group playtest at the session's measured RTT, WHEN a non-host player places structures across one build session, THEN they report placement as responsive — AC-48b means joiners eat one RTT of dead air between confirm and thunk that the host never experiences, and this was previously asserted acceptable by analogy, never validated (movement earned the same conclusion with a prototype and a measured band). If falsified, the fallback (an anticipatory pre-beat vs. accepting the delay) is a design decision, not a discovery. ⚑ technical-director: this host-vs-client asymmetry applies to every instant-commit system, not just BB.
Registry / Persistence — Inert Relics, Current-Cost Refunds (Edge Cases 13–16)
- AC-49 [UNIT] GIVEN a save referencing a patch-removed
structureId, WHEN reconcile runs, THEN the instance is retained with position/footprint preserved, flagged inert (no repair, nostationPresentcontribution), still blocksnoOverlap, and is demolishable by any player for exactly zero refund — never auto-removed, never auto-refunded (Edge Case 13, revised 2026-08-27). - AC-50 [UNIT] GIVEN a
StructureInstancefor a structure whoseStructureDef.costchanged after placement, WHEN demolished, THEN the refund uses the currentStructureDefcost, not any placement-time value — confirmed by inspecting thatStructureInstancecarries no cost field to snapshot (Edge Case 14). - AC-51 [UNIT] GIVEN a save referencing a storage instance whose
SharedStoragecontent list is missing, WHEN BB's registry is reconciled, THEN theStructureInstanceentry is unaffected — BB's registry never stores contents, so the orphan is entirely Inventory's reconcile concern (Edge Case 15). - AC-52 [UNIT] GIVEN repeated placement commits across a save/load cycle, WHEN
instanceIds are inspected, THEN they are host-side monotonic, persisted, and never reused (Edge Case 16).
Disconnects (Edge Cases 8–9)
- AC-53 [UNIT] (relabelled from [INTEGRATION] 2026-08-27 re-review — a mocked direct invocation with no NGO runtime is [UNIT] by this section's own taxonomy) GIVEN a placement commit whose issuing player is marked disconnected before the critical section runs (a mocked disconnect state — the commit path invoked directly, no live socket teardown; rewritten 2026-08-27 because a real disconnect racing a real commit is unfixably flaky and violates the project's determinism rule), WHEN the host processes it, THEN the commit resolves normally — structure created, cost debited from the offline player's persisted CARRY/reachable STORAGE (Edge Case 8; the claim under test is the commit path's indifference to connection state, not literal tick coincidence). ⚑ netcode ADR must pin what "persisted CARRY" means for a mid-disconnect player under the owner-rail model.
- AC-54 [UNIT] (relabelled, as AC-53) GIVEN a demolish commit whose issuing player is marked disconnected before the critical section runs (same mocked-disconnect construction as AC-53), WHEN the host processes it, THEN the commit resolves normally — the refund lands in the offline player's persisted CARRY (Edge Case 9).
Config Validation (load-time, fail loudly — Edge Case 17)
- AC-55 [UNIT]
costreferencing an unregistereditemId→ load rejected. - AC-56 [UNIT]
footprint.width ≤ 0orfootprint.height ≤ 0→ load rejected. - AC-57 [UNIT] Duplicate
structureId→ load rejected. - AC-58 [UNIT] Unknown
categoryenum value → load rejected. - AC-59 [UNIT] Any
cost[].count ≤ 0, or emptycost[]→ load rejected (the free-structure placement-side exploit). - AC-60 [UNIT]
category=stationwith nostationTier, orstationTierpresent on a non-station → load rejected. - AC-61 [UNIT]
category=storagewithstorageSlots ≤ 0, orstorageSlotspresent on a non-storage → load rejected. - AC-62 [UNIT]
stationTiernot present in Crafting's known tier enum → load rejected (cross-system reference validation). - AC-63 [UNIT]
socketNoBuildRadius ≤ 0→ load rejected. - AC-63b [UNIT] (added 2026-08-27; rebound same day, re-review CS-2) GIVEN the socket list with each socket's authored
depositPoint(plain data from LDU Rule 8's layout — the point tide Rule 9 deposits caught players at), WHEN load validation runs, THENdist(socket.position, socket.depositPoint) ≤ socketNoBuildRadiusfor every socket — else loud reject. (socketNoBuildRadius > 0alone proves nothing about clearance: a 0.01 radius passes AC-63 and still strands deposited players behind walls. The prior wording bound to "deposit landing cells", an artifact tide never defined — the BL-1 failure class recurring; the deposit is now a named per-socket point.) - AC-63c [UNIT] (added 2026-08-27 second re-review) GIVEN a socket whose
depositPointis withinsocketNoBuildRadiusbut is not on buildable-adjacent shore per a mockedITerrainQuery, WHEN load validation runs, THEN load is rejected — Edge Case 17 row 10's second clause, previously validated in the table but untested. - AC-64 [UNIT]
placementReach ≤ 0orstationRange ≤ 0→ load rejected. - AC-64b — retired fifth re-review 2026-08-27, CS-18 (the
placementReach < 2 × gridCellSizerow it tested is gone with Edge Case 21's hop bound; the number is kept so the review trail resolves — no test carries it).
Scope Closures (Edge Cases 18–19)
- AC-65 [UNIT] GIVEN all stations of a given tier are demolished, WHEN every recipe of that tier is queried, THEN
stationPresentExactreturns false for all of them andKnownRecipes/era knowledge is unaffected — a supply problem, never a re-lock (Edge Case 19).
Minimum smoke set (run first)
- AC-16 — commit atomicity: if gate-resample + debit + instance-write can partially apply, every structure in the registry is untrustworthy and the base-permanence promise (Pillar 2) is unverifiable — the highest-blast-radius rule in this GDD.
- AC-36 — the material-softlock invariant (refund = current cost = rebuild cost): the one proof the "no punishment for fixing mistakes" claim rests on; a regression means the base can be permanently degraded by an honest accident, which no other rule catches.
- AC-45 / AC-46 (one smoke item) — the CI gates recomputing from live constants: the exact bug class crafting's review C1 caught (a hardcoded literal masquerading as a live invariant); a static-literal regression silently stops protecting every future balance pass across three systems at once.
- AC-24 — structure anonymity schema-negative: if a builder field ever appears, the no-ledger test (a pillar-level constraint) is broken invisibly — nothing else in the suite would catch a field that simply exists unused.
- AC-30 / AC-31 (one smoke item) — demolish-refund gating (empty-chest-first, CanAdd-gated): Base Building's loss/duplication invariant — a regression here means materials or container contents silently vanish.
All five run as pure logic, headless, together under a minute — matching the sibling smoke-set shape and blast-radius framing.
Deliberately not criteria (and why)
- "Placement reads as a deliberate, weighty act" / "the thunk and the shape" / "home doesn't move" as a felt anchor moment — feel qualities, routed to
[MANUAL]playtest/lead-sign-off evidence once art/audio exists (mirrors crafting's era-turner exclusion). - Auto-align visual cleanliness at corners beyond raw grid quantization — AC-8/AC-9 cover the testable quantization and edge arithmetic; the look of a clean corner is AC-9b's
[MANUAL]scope only. - Structure-registry replication, late-joiner sync mechanism,
instanceIdissuance/replication wire shape — deferred to the netcode ADR (⚑ flagged throughout); testing an unlocked wire shape tests the wrong thing. - Buildable-region/terrain authoring correctness (
validTerrain's source data) — owned by home-island content authoring; F1 only tests that the gate consumes the terrain flag correctly. SharedStoragecontent serialization format — Inventory + Save's territory entirely; AC-51 covers only BB's registry-side non-involvement.- Registry growth / row-count ceiling (Edge Case 18) — a stated non-concern with no pass/fail surface; not a criterion by design.
- Art bible silhouette-first pass/fail for structures at solid black — an art-bible/asset-review concern; not this GDD's criterion to own.
Coverage note (revised 2026-08-27): Core Rules 1–13 and F1–F4 each have ≥ 1 criterion (Rule 1→AC-1–2; 2→AC-3; 3/F4→AC-4–7b — the orbit test's testable core at AC-7b, its unbounded half on the code-review / /architecture-review checklist, and Rule 3's shore-usability claim now validated at AC-63b (a previously undisclosed gap: only > 0 was checked); 4→AC-8–9b; 5→AC-10–12 incl. the exit half at AC-11b; 6/F1→AC-13–19 incl. noPlayerOccupancy and placerHasExit (AC-14b edge-of-region); the race criteria AC-20–23 are listed once, under Edge Cases 1–6 below; 7→AC-24–25; 8→AC-26–27; 9/F2→AC-28–35 incl. the refund-preview contract at AC-28b; 10→AC-38–39; 11→AC-40 (Rule 11a; 11b is /ux-design's); 12→AC-41; 13/F3→AC-42–44). Every row of Edge Case 17's validation table has a dedicated criterion (AC-55–64 + AC-63b). CI gates at AC-45–46 with named mutation-sensitivity tests AC-45b/46b. The material-softlock proof (Edge Case 20) at AC-36–37; spatial enclosure (Edge Case 22) is an accepted social closure with no automatable criterion, by ruling; occupied-cell placement (Edge Case 21) via AC-13/14's fourth conjunct, its free-cell check as a pure function at AC-14c, pin (iii)'s fresh re-check at AC-14e, and only its replication/write mechanics being netcode-ADR territory; self-enclosure (Edge Case 23) via the eighth conjunct placerHasExit in AC-14/14b (single-placement snapshots) and the maze-sequence boundary at AC-14d. Every row of Edge Case 17 now has a criterion for each clause (AC-63c covers row 10's shore clause). Rule 13's client-may-only-request contract at AC-39b. Anonymity (Rule 7) as reflection-based allow-list assertions at AC-24–25. Edge Cases 1–6 split UNIT/INTEGRATION per whether live timing is load-bearing (AC-20–23 incl. the AC-20b/20c split, AC-34–35); Edge Cases 7–10 at AC-12, AC-53–54 (now mocked-disconnect constructions, relabelled [UNIT]; Edge Case 10 — host-quit — is a stated closure with no BB-specific criterion, matching crafting Edge Case 6's posture). Edge Case 11 is a closure covered by AC-17's re-sample claim; Edge Case 12 → AC-47–48c (a line previously missing from this note); Edge Cases 18–19 at AC-65 and AC-37.
Open Questions
| # | Question | Owner | Target |
|---|---|---|---|
| 1 | Camp→Keep growth trigger: art bible §6.1 describes the stages but nothing defines what advances a structure (material threshold? manual upgrade? pure Tier 1 scope?). MVP never leaves Camp — must be answered before Tier 1 building scope begins | Art-bible fix-pass + Tier 1 building pass | Tier 1 |
| 2 | socketNoBuildRadius at Tier 1 socket counts: does 4 u hold when 3–4 sockets shrink the placeable shore? Owner confirmed 2026-08-27: LDU quick-spec Rule 8 authors socket layout and names this re-check | LDU (Rule 8) + Tier 1 pass | Tier 1 |
| 3 | Inert-relic proper visual design (floor spec'd here: grey silhouette + broken-outline accent) | Art-bible fix-pass | Pre-Production |
| 4 | The accepted MVP home-facing dead zone (Player Fantasy honesty clause; crafting OQ9's negative ruling): does it actually bite with the real friend group, or does base-up + hauling absorb it? If it bites hard, Tier 1 building scope is the pre-agreed answer — never MVP filler structures. ⚑ Producer (CD A1): "Tier 1 building scope" is an unpriced schedule commitment — it needs a sizing note before the MVP playtest, so if this fires the answer is a decision, not a discovery. Sizing note (2026-08-27, Technical Setup gate — PR concern 3; provisional, owner producer): Tier 1 building scope ≈ 2–3 weeks part-time — structure variety (3–4 new StructureDefs incl. a door/gate, ~3 days), rotation (schema + snap + ghost, ~2 days), visible age (createdAtCycle + wear-stage sprites, ~3 days incl. art), move verb (atomic demolish+place, ~1 day — Rule 9's revisit), Camp→Keep trigger design (OQ1, ~2 days), plus per-structure art. First-to-slip inside it: visible age. If OQ4 fires at playtest the decision is "commit these ~2–3 weeks in Tier 1 or accept the dead zone" — priced, not discovered | MVP playtest + producer | MVP playtest |
| 5 | Perimeter pacing: does the 50-segment / 2–4 session estimate match real group behavior? (The perimeterSessions band 2–4 is the guard, not the target experience; anchor provisional pending the spatial budget) | MVP playtest | MVP playtest |
| 6 | ~~gridCellSize / snap-tolerance constants: ghostSnapResult makes snap testable, but the cell size (1 u = 32 px?) and nudge tolerance are unpinned — pin before AC-8 can run~~ RESOLVED 2026-08-28 — ADR-0003: 1 cell = 1 world unit = one 32 px terrain tile at PPU 32 (the camera's 64 px/u is a 2× display scale, not a second PPU); Cell(int X, int Y), Cell.FromWorld = floor, Cell.Center = +0.5; every range in this GDD is already in cells; AC-14b's ≤ 3 u fixture constraint holds. Nudge tolerance stays an /ux-design deliverable (Rule 11b / OQ10) | ~~Implementation + /asset-spec~~ Resolved (ADR-0003) | Done |
| 7 | ~~Save quick-spec must adopt the Edge Case 13–16 reconcile posture and list BB as dependent~~ RESOLVED 2026-08-26 — design/quick-specs/save-persistence-2026-08-26.md Rule 5 (permanent-reconcile stage) + Rule 6 (two-class rule) adopt the posture verbatim | ~~Save quick-spec~~ Resolved | Done |
| 8 | Staleness falsification (CD C2 — distinct from OQ4): does the base still read as earned-static (pride) rather than frozen (wallpaper) by the end of MVP playtesting? Falsification signals: any player describing the base as "done" or "background", navigating past it without looking, or the group ceasing to comment on it entirely. A group can have plenty to do on the tide while the base has stopped meaning anything — OQ4 would return "no bite" while the Pillar 2 fantasy quietly failed. **Pre-agreed response if falsified: visible age / structure variety is the Tier 1 lever (the deferred walls-as-memory fantasy) — never activity generation; staleness is a reading problem, not a doing problem, and treating it as a doing problem is how filler structures get in through the back door**. Compounding note (re-review 2026-08-27): OQ9's missing arrival beat and OQ1's Camp→Keep gap both work against this bar — if OQ8 falsifies, check them as contributing causes before reaching for Tier 1 content levers. | MVP playtest | MVP playtest |
| 9 | The arrival beat (design-review 2026-08-27): the fantasy's anchor moment — crossing the shore threshold onto home ground — has no implementing mechanism in any system: no threshold feedback, no arrival cue, nothing fires on the crossing. Placement got a full spec (thunk, AC-48b); arrival got none, and a [MANUAL] sign-off cannot evaluate a moment nobody built. Which system owns the beat (ambient shift? HUD suppression? light warmth?) — flagged, deliberately not invented here | BB + /ux-design + art-bible fix-pass | Pre-Production |
| 10 | Gamepad stick-to-ghost transfer function (cursor-velocity vs. cell-step, acceleration, deadzone, snap-boundary hysteresis): unspecified, and it — not AC-8's snap purity — determines whether gamepad build mode is viable (outcome parity ≠ effort parity) | /ux-design | Pre-Production |
| 11 | Post-saturation haul tension (economy review 2026-08-27 — distinct from OQ4 and OQ8): after ~session 3, timber/fiber/sinew have no remaining sinks (walls one-time, gear indestructible, repair free, chests cheap) while the harvest faucet recurs unbounded — does bank-or-push tension survive sink saturation with the real group? Also in scope (re-review): chest count is unbounded, 8 fiber each, fully refundable — the stated bet is that footprint, walk distance, and session-one fiber opportunity cost bound it in practice; if the group carpets the base in chests turn one and banks everything before every departure, that is this OQ firing, not a separate finding. A group could answer "no bite" on OQ4 and "still earned-static" on OQ8 while the haul loop quietly dies. Any lifetime-sink-vs-faucet invariant belongs to a cross-system economy pass, not this GDD | Economy + producer | MVP playtest |
| 12 | From /ux-design build-mode (2026-08-31): (a) the ghost reason-string family gained two instances — bounds "past the island's edge" (off-island: "not home ground", per AC-4 withinBounds) and terrain "not buildable here" — authored under this GDD's retitling grant; adopt into UI Requirements' list. (b) Rule 11b's repeat gesture is hold-to-repeat (pattern #31), drag-a-row rejected — Rule 11b's own prose ("AC-48c's playtest must include a drag-row segment") and the drag-row reasoning in the EC23 / self-sealing passages should be retargeted to "a held-repeat row"; AC-48c's current text carries no row segment. (c) Mode entry off-island is legal (every cell fails bounds) — a UX ruling filling a GDD silence; confirm or add a location gate to Rule 5. | game-designer | before the first BB story |