Worldshaper/Future - Shared Contract.html

594 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Future - Shared Contract</title>
<style>
:root {
--bg: #f7f2e8;
--panel: rgba(255, 252, 247, 0.88);
--ink: #1f1b17;
--muted: #675d52;
--line: #d9ccb9;
--accent: #8d4f2a;
--accent-2: #224b5d;
--accent-3: #5f7c3a;
--shadow: 0 24px 60px rgba(44, 29, 16, 0.12);
--code-bg: #201b18;
--code-ink: #f4eee6;
--code-dim: #cbbfb2;
--ok: #edf5e2;
--warn: #fff1dc;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Aptos", "Segoe UI", sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(141, 79, 42, 0.14), transparent 32%),
radial-gradient(circle at top right, rgba(34, 75, 93, 0.12), transparent 28%),
linear-gradient(180deg, #fbf7f0 0%, var(--bg) 100%);
line-height: 1.6;
}
.page {
width: min(1100px, calc(100% - 48px));
margin: 32px auto 80px;
}
.hero,
.section,
.callout {
background: var(--panel);
border: 1px solid rgba(217, 204, 185, 0.8);
border-radius: 22px;
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}
.hero {
padding: 40px 42px 34px;
position: relative;
overflow: hidden;
}
.hero::after {
content: "";
position: absolute;
inset: auto -80px -80px auto;
width: 280px;
height: 280px;
border-radius: 999px;
background: radial-gradient(circle, rgba(141, 79, 42, 0.12), transparent 68%);
pointer-events: none;
}
.eyebrow {
display: inline-block;
margin-bottom: 12px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(34, 75, 93, 0.1);
color: var(--accent-2);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2,
h3 {
font-family: Georgia, "Times New Roman", serif;
line-height: 1.15;
margin: 0 0 14px;
letter-spacing: -0.02em;
}
h1 {
font-size: clamp(2.3rem, 4vw, 4rem);
max-width: 12ch;
}
h2 {
font-size: clamp(1.6rem, 2.8vw, 2.2rem);
}
h3 {
font-size: 1.15rem;
margin-bottom: 10px;
}
p {
margin: 0 0 14px;
color: var(--muted);
}
.hero p {
max-width: 70ch;
font-size: 1.05rem;
}
.grid {
display: grid;
gap: 22px;
margin-top: 26px;
}
.grid-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.section {
padding: 28px 30px;
margin-top: 24px;
}
.callout {
padding: 18px 20px;
margin-top: 20px;
}
.callout.ok {
background: linear-gradient(180deg, rgba(237, 245, 226, 0.92), rgba(255, 252, 247, 0.9));
}
.callout.warn {
background: linear-gradient(180deg, rgba(255, 241, 220, 0.95), rgba(255, 252, 247, 0.9));
}
.card {
border: 1px solid var(--line);
border-radius: 18px;
padding: 18px 18px 16px;
background: rgba(255, 255, 255, 0.45);
}
.card h3 {
margin-bottom: 8px;
}
ul {
margin: 10px 0 0;
padding-left: 18px;
color: var(--ink);
}
li {
margin: 6px 0;
}
strong {
color: var(--ink);
}
code {
font-family: Consolas, "Courier New", monospace;
font-size: 0.94em;
background: rgba(32, 27, 24, 0.08);
padding: 0.12em 0.36em;
border-radius: 6px;
color: #4f2410;
}
pre {
margin: 16px 0 0;
padding: 18px 20px;
overflow-x: auto;
border-radius: 16px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent),
var(--code-bg);
color: var(--code-ink);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
pre code {
background: none;
color: inherit;
padding: 0;
border-radius: 0;
}
.code-note {
display: block;
margin-bottom: 10px;
color: var(--code-dim);
font-size: 0.9rem;
}
.rubric {
display: grid;
gap: 12px;
margin-top: 18px;
}
.rubric-item {
padding: 14px 16px;
border-left: 4px solid var(--accent);
border-radius: 12px;
background: rgba(255, 255, 255, 0.52);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 14px;
font-size: 0.97rem;
}
th,
td {
text-align: left;
vertical-align: top;
border-bottom: 1px solid var(--line);
padding: 12px 10px;
}
th {
color: var(--accent-2);
font-size: 0.84rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.footer {
margin-top: 18px;
color: var(--muted);
font-size: 0.94rem;
}
@media (max-width: 900px) {
.grid-3,
.grid-2 {
grid-template-columns: 1fr;
}
.page {
width: min(100% - 24px, 1100px);
}
.hero,
.section {
padding: 24px 20px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<div class="eyebrow">Architecture Note</div>
<h1>Future - Shared Contract</h1>
<p>
The safest long-term split is to make a common core own the <strong>portable game domain</strong>,
while <strong>Worldshaper</strong> owns authoring workflows and <strong>World-Workshop</strong>
owns runtime execution, ECS, rendering, and simulation wiring.
</p>
<div class="callout ok">
<strong>Decision rule:</strong> if a module can run unchanged in tests, browser, editor, and runtime
without depending on <code>React</code>, <code>Pixi</code>, <code>Tauri</code>, or <code>bitECS</code>,
it is a strong candidate for the shared core.
</div>
</section>
<section class="section">
<h2>Recommended Ownership Split</h2>
<div class="grid grid-3">
<article class="card">
<h3>Shared Core</h3>
<p>Owns the portable contract, validation, normalization, and pure game-domain logic.</p>
<ul>
<li>Authored content types</li>
<li>Stable IDs and references</li>
<li>Schema versions and migrations</li>
<li>Reference resolution</li>
<li>Pure gameplay formulas and rule evaluation</li>
<li>Prefab and archetype definitions</li>
</ul>
</article>
<article class="card">
<h3>Runtime</h3>
<p>Owns the executable simulation and all performance-driven or transient state.</p>
<ul>
<li><code>bitECS</code> components and entity storage</li>
<li>Spawn and assembly from content to ECS</li>
<li>Systems, ticking, AI, pathfinding integration</li>
<li>Rendering, audio, input, camera</li>
<li>Runtime caches and live state</li>
</ul>
</article>
<article class="card">
<h3>Editor</h3>
<p>Owns source editing UX, layout, tools, and authoring-specific productivity features.</p>
<ul>
<li>Inspectors, forms, palettes, map editing</li>
<li>TS source rewriting and formatting</li>
<li>Undo/redo, selection, layout preferences</li>
<li>Authoring warnings and quick fixes</li>
<li>Studio-only metadata and workflows</li>
</ul>
</article>
</div>
</section>
<section class="section">
<h2>What Belongs In The Shared Core</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Safe To Share</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>Content definitions</td>
<td><code>World</code>, <code>Chunk</code>, <code>NpcTemplate</code>, <code>PlacedEntity</code>, <code>Ability</code>, <code>Dialogue</code>, <code>Item</code></td>
<td>These are authored domain concepts, not renderer or ECS details.</td>
</tr>
<tr>
<td>Type helpers</td>
<td><code>defineNpc()</code>, <code>defineWorld()</code>, <code>definePrefab()</code></td>
<td>Both editor and runtime benefit from a single shape and single set of defaults.</td>
</tr>
<tr>
<td>IDs and refs</td>
<td><code>NpcId</code>, <code>WorldId</code>, <code>DialogueId</code>, <code>SpriteId</code>, <code>TemplateId</code></td>
<td>Stable identifiers should mean the same thing everywhere.</td>
</tr>
<tr>
<td>Validation</td>
<td>Reference checks, required fields, defaulting, normalization, semantic validation</td>
<td>Content should be judged by one source of truth, not two slightly different validators.</td>
</tr>
<tr>
<td>Migrations</td>
<td><code>schemaVersion</code> upgrades and canonical output transforms</td>
<td>This keeps old content loadable in both tools.</td>
</tr>
<tr>
<td>Pure rules</td>
<td>Dialogue condition checks, loot rolls, stat formulas, cooldown math, faction logic</td>
<td>These are deterministic rules that do not need the runtime shell.</td>
</tr>
<tr>
<td>Content registry</td>
<td>Lookup and resolution APIs over loaded definitions</td>
<td>Both projects need the same view of the authored world.</td>
</tr>
</tbody>
</table>
<pre><span class="code-note">Shared core example: portable authored contract</span><code>export interface PlacedEntity {
id: EntityInstanceId;
templateId?: EntityTemplateId;
archetype: ArchetypeId;
position: { x: number; y: number; z: number };
spriteId?: SpriteId;
dialogueId?: DialogueId;
factionId?: FactionId;
tags?: string[];
overrides?: Partial&lt;EntityTemplate&gt;;
}
export function definePlacedEntity(
value: PlacedEntity,
): PlacedEntity {
return normalizePlacedEntity(value);
}</code></pre>
</section>
<section class="section">
<h2>What Should Stay Runtime-Owned</h2>
<p>
The runtime should keep everything that is shaped primarily for execution speed, frame-to-frame state,
ECS storage layout, or engine integration.
</p>
<div class="grid grid-2">
<article class="card">
<h3>Keep These In World-Workshop</h3>
<ul>
<li><code>bitECS</code> component arrays and entity IDs</li>
<li>ECS world creation and system scheduling</li>
<li>Assembly from authored entities into spawned ECS entities</li>
<li>Transient state like routes, targets, planner handles, and sprite instances</li>
<li>Rendering, audio, input, camera, spatial index, pathfinder integration</li>
</ul>
</article>
<article class="card">
<h3>Why</h3>
<ul>
<li>These are not stable authoring concepts.</li>
<li>They will change when the engine changes.</li>
<li>They often depend on engine libraries and performance tradeoffs.</li>
<li>They should be free to evolve without forcing content migrations.</li>
</ul>
</article>
</div>
<pre><span class="code-note">Runtime example: ECS remains runtime-only</span><code>const GridPosition = {
x: new Int16Array(MAX_ENTITIES),
y: new Int16Array(MAX_ENTITIES),
z: new Int16Array(MAX_ENTITIES),
};
const Target = {
x: new Int16Array(MAX_ENTITIES),
y: new Int16Array(MAX_ENTITIES),
z: new Int16Array(MAX_ENTITIES),
active: new Uint8Array(MAX_ENTITIES),
};
function spawnFriendlyNpc(world: GameWorld, def: ResolvedEntityDef): number {
const entity = addEntity(world);
addComponent(world, entity, GridPosition);
addComponent(world, entity, Target);
addComponent(world, entity, Agent);
// Runtime decides the exact ECS shape here.
return entity;
}</code></pre>
</section>
<section class="section">
<h2>What Should Stay Editor-Owned</h2>
<p>
The editor should keep everything that exists to improve authoring speed, visibility, and safety,
especially if it is tied to UI behavior or source editing mechanics.
</p>
<ul>
<li>Inspectors, map tools, palettes, graphs, and editor windows</li>
<li>Selection state, undo/redo, dirty tracking, docking, recent files</li>
<li>TS source rewriting, formatting, and file-structure conventions</li>
<li>Inline diagnostics, authoring warnings, quick fixes, bulk-edit tools</li>
<li>Editor-only metadata and local preferences</li>
</ul>
<div class="callout warn">
<strong>Important:</strong> do not move UI state or source-file IO details into the shared core.
Those will create coupling fast and usually provide very little reuse value.
</div>
</section>
<section class="section">
<h2>The Boundary To Protect</h2>
<p>
The shared core should describe <strong>what the authored thing is</strong>. The runtime should decide
<strong>how that thing becomes executable ECS state</strong>.
</p>
<div class="grid grid-2">
<article class="card">
<h3>Good Core Concepts</h3>
<ul>
<li><code>EntityTemplate</code></li>
<li><code>PlacedEntity</code></li>
<li><code>ArchetypeId</code></li>
<li><code>BehaviorId</code></li>
<li><code>StatBlock</code></li>
<li><code>SpawnRule</code></li>
<li><code>VisualRef</code></li>
</ul>
</article>
<article class="card">
<h3>Bad Core Concepts</h3>
<ul>
<li><code>GridPosition</code> typed arrays</li>
<li><code>RenderPosition</code></li>
<li><code>Target.active</code></li>
<li>Planner handles</li>
<li>Sprite instances</li>
<li>Route caches</li>
<li>Per-frame live ECS state</li>
</ul>
</article>
</div>
<pre><span class="code-note">The bridge: authored data into runtime assembly</span><code>// Shared core
export interface EntityTemplate {
id: EntityTemplateId;
archetype: ArchetypeId;
spriteId?: SpriteId;
stats?: StatBlock;
dialogueId?: DialogueId;
}
// Runtime
export function materializeEntity(
world: GameWorld,
entity: ResolvedPlacedEntity,
): number {
switch (entity.archetype) {
case "friendly_npc":
return spawnFriendlyNpc(world, entity);
case "monster":
return spawnMonster(world, entity);
case "player_spawn":
return spawnPlayerMarker(world, entity);
}
}</code></pre>
</section>
<section class="section">
<h2>Tempting, But Usually Not Worth Sharing</h2>
<div class="rubric">
<div class="rubric-item"><strong>Raw file IO:</strong> how a TS file is discovered, parsed, rewritten, and formatted should usually stay editor-side or build-side.</div>
<div class="rubric-item"><strong>React components:</strong> UI does not become more reusable just because it sits in a shared package.</div>
<div class="rubric-item"><strong>Pixi renderer code:</strong> renderer concerns are runtime concerns.</div>
<div class="rubric-item"><strong>ECS storage:</strong> component arrays and world memory layout are engine implementation details.</div>
<div class="rubric-item"><strong>Editor state:</strong> selections, panels, and tool modes are not game-domain concepts.</div>
<div class="rubric-item"><strong>Live save-state internals:</strong> runtime persistence format may overlap with content, but live simulation state should stay runtime-owned.</div>
</div>
</section>
<section class="section">
<h2>Suggested Package Shape</h2>
<pre><span class="code-note">A practical next-step layout</span><code>packages/
world-core/
src/
ids/
content/
define/
validate/
normalize/
migrate/
resolve/
rules/
registry/
world-content-build/
src/
loadTsModules/
compileBundle/
emitArtifacts/
Worldshaper/
src/
editor-ui/
source-editing/
tooling/
World-Workshop/
src/
game/
ecs/
systems/
runtime/
rendering/
assembly/</code></pre>
</section>
<section class="section">
<h2>Low-Risk Extraction Order</h2>
<ol>
<li>Extract IDs, record types, and <code>defineX()</code> helpers.</li>
<li>Extract validation, normalization, and migrations.</li>
<li>Extract reference resolution and a shared content registry.</li>
<li>Extract pure rules used by both projects.</li>
<li>Leave ECS assembly, rendering, and editor source-editing mechanics where they are.</li>
</ol>
<div class="callout ok">
<strong>Why this order works:</strong> it gives immediate reuse and consistency without forcing an early,
risky merge of the two apps' most specialized code.
</div>
<p class="footer">
Short version: the shared core should define the <strong>portable authored world</strong>. The runtime should
define the <strong>live executable world</strong>. The editor should define the <strong>authoring experience</strong>.
</p>
</section>
</main>
</body>
</html>