594 lines
18 KiB
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<EntityTemplate>;
|
|
}
|
|
|
|
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>
|