1336 lines
45 KiB
HTML
1336 lines
45 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
|
<title>World Chunk Manual</title>
|
||
|
|
<style>
|
||
|
|
:root {
|
||
|
|
color-scheme: dark;
|
||
|
|
--bg: #08111f;
|
||
|
|
--bg-alt: #0d1a30;
|
||
|
|
--panel: #10203b;
|
||
|
|
--panel-2: #13294c;
|
||
|
|
--panel-3: #17345f;
|
||
|
|
--text: #deebff;
|
||
|
|
--muted: #a8bfdc;
|
||
|
|
--border: #33507c;
|
||
|
|
--accent: #78c1ff;
|
||
|
|
--accent-2: #abdfff;
|
||
|
|
--warn: #ffd47a;
|
||
|
|
--good: #7fe0af;
|
||
|
|
--shadow: rgba(0, 8, 20, 0.45);
|
||
|
|
}
|
||
|
|
|
||
|
|
* {
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
html, body {
|
||
|
|
margin: 0;
|
||
|
|
background:
|
||
|
|
radial-gradient(circle at top right, rgba(120, 193, 255, 0.12), transparent 28%),
|
||
|
|
linear-gradient(180deg, #08111f 0%, #060d18 100%);
|
||
|
|
color: var(--text);
|
||
|
|
font-family: "Segoe UI", Arial, sans-serif;
|
||
|
|
}
|
||
|
|
|
||
|
|
a {
|
||
|
|
color: var(--accent-2);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
a:hover {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
|
||
|
|
code, pre {
|
||
|
|
font-family: Consolas, "Courier New", monospace;
|
||
|
|
}
|
||
|
|
|
||
|
|
.shell {
|
||
|
|
min-height: 100vh;
|
||
|
|
display: grid;
|
||
|
|
grid-template-rows: 52px 1fr;
|
||
|
|
}
|
||
|
|
|
||
|
|
.topbar {
|
||
|
|
position: sticky;
|
||
|
|
top: 0;
|
||
|
|
z-index: 20;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 10px;
|
||
|
|
padding: 8px 12px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
background: linear-gradient(180deg, #163057 0%, #10203c 100%);
|
||
|
|
box-shadow: 0 8px 20px var(--shadow);
|
||
|
|
}
|
||
|
|
|
||
|
|
.topbar-title {
|
||
|
|
margin-left: auto;
|
||
|
|
color: var(--accent-2);
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 0.08em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
height: 34px;
|
||
|
|
padding: 0 12px;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 9px;
|
||
|
|
background: var(--panel-2);
|
||
|
|
color: var(--text);
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 700;
|
||
|
|
cursor: pointer;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn:hover {
|
||
|
|
background: var(--panel-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.layout {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 290px minmax(0, 1fr);
|
||
|
|
min-height: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sidebar {
|
||
|
|
position: sticky;
|
||
|
|
top: 52px;
|
||
|
|
align-self: start;
|
||
|
|
height: calc(100vh - 52px);
|
||
|
|
overflow: auto;
|
||
|
|
padding: 14px;
|
||
|
|
border-right: 1px solid var(--border);
|
||
|
|
background: rgba(10, 24, 46, 0.82);
|
||
|
|
backdrop-filter: blur(14px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sidebar-card,
|
||
|
|
.panel,
|
||
|
|
.mini-card,
|
||
|
|
.callout {
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 12px;
|
||
|
|
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||
|
|
box-shadow: 0 10px 24px var(--shadow);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sidebar-card {
|
||
|
|
padding: 14px;
|
||
|
|
display: grid;
|
||
|
|
gap: 10px;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.eyebrow {
|
||
|
|
margin: 0;
|
||
|
|
color: var(--accent);
|
||
|
|
font-size: 11px;
|
||
|
|
font-weight: 800;
|
||
|
|
letter-spacing: 0.08em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sidebar h1 {
|
||
|
|
margin: 0;
|
||
|
|
font-size: 22px;
|
||
|
|
line-height: 1.1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.muted {
|
||
|
|
margin: 0;
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 12px;
|
||
|
|
line-height: 1.55;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip-row {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip {
|
||
|
|
padding: 5px 8px;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: rgba(255, 255, 255, 0.03);
|
||
|
|
color: var(--accent-2);
|
||
|
|
font-size: 11px;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav {
|
||
|
|
display: grid;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav a {
|
||
|
|
display: block;
|
||
|
|
padding: 8px 10px;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
border-radius: 9px;
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 12px;
|
||
|
|
line-height: 1.35;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav a:hover {
|
||
|
|
border-color: var(--border);
|
||
|
|
background: rgba(255, 255, 255, 0.03);
|
||
|
|
color: var(--text);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content {
|
||
|
|
padding: 18px;
|
||
|
|
display: grid;
|
||
|
|
gap: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero {
|
||
|
|
padding: 18px;
|
||
|
|
display: grid;
|
||
|
|
gap: 12px;
|
||
|
|
background:
|
||
|
|
linear-gradient(140deg, rgba(120, 193, 255, 0.12), transparent 50%),
|
||
|
|
linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero h2,
|
||
|
|
.panel h2,
|
||
|
|
.panel h3 {
|
||
|
|
margin: 0;
|
||
|
|
line-height: 1.2;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero h2 {
|
||
|
|
font-size: 30px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero p,
|
||
|
|
.panel p,
|
||
|
|
.panel li,
|
||
|
|
.panel td,
|
||
|
|
.panel th {
|
||
|
|
font-size: 14px;
|
||
|
|
line-height: 1.62;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel {
|
||
|
|
padding: 16px;
|
||
|
|
display: grid;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-head {
|
||
|
|
display: flex;
|
||
|
|
align-items: baseline;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: 12px;
|
||
|
|
padding-bottom: 8px;
|
||
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-tag {
|
||
|
|
color: var(--accent-2);
|
||
|
|
font-size: 12px;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid {
|
||
|
|
display: grid;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid.two {
|
||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid.three {
|
||
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
|
||
|
|
.mini-card {
|
||
|
|
padding: 12px;
|
||
|
|
display: grid;
|
||
|
|
gap: 8px;
|
||
|
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
|
||
|
|
}
|
||
|
|
|
||
|
|
.mini-card h3 {
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mini-card p,
|
||
|
|
.mini-card ul,
|
||
|
|
.mini-card ol {
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.callout {
|
||
|
|
padding: 12px 13px;
|
||
|
|
background: rgba(255, 255, 255, 0.035);
|
||
|
|
}
|
||
|
|
|
||
|
|
.callout.warn {
|
||
|
|
border-color: var(--warn);
|
||
|
|
background: rgba(255, 212, 122, 0.08);
|
||
|
|
}
|
||
|
|
|
||
|
|
.callout.good {
|
||
|
|
border-color: var(--good);
|
||
|
|
background: rgba(127, 224, 175, 0.08);
|
||
|
|
}
|
||
|
|
|
||
|
|
.table {
|
||
|
|
width: 100%;
|
||
|
|
border-collapse: collapse;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
background: rgba(0, 0, 0, 0.12);
|
||
|
|
}
|
||
|
|
|
||
|
|
.table th,
|
||
|
|
.table td {
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
padding: 9px 10px;
|
||
|
|
text-align: left;
|
||
|
|
vertical-align: top;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table th {
|
||
|
|
color: var(--accent-2);
|
||
|
|
background: rgba(255, 255, 255, 0.03);
|
||
|
|
font-weight: 700;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code {
|
||
|
|
margin: 0;
|
||
|
|
padding: 12px;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 10px;
|
||
|
|
background: #0b162a;
|
||
|
|
color: var(--text);
|
||
|
|
overflow: auto;
|
||
|
|
font-size: 12px;
|
||
|
|
line-height: 1.58;
|
||
|
|
white-space: pre;
|
||
|
|
}
|
||
|
|
|
||
|
|
.footer-note {
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 1080px) {
|
||
|
|
.layout {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sidebar {
|
||
|
|
position: static;
|
||
|
|
height: auto;
|
||
|
|
border-right: none;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid.two,
|
||
|
|
.grid.three {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 760px) {
|
||
|
|
.topbar-title {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero h2 {
|
||
|
|
font-size: 24px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="shell">
|
||
|
|
<div class="topbar">
|
||
|
|
<button class="btn" type="button" onclick="window.location.href='/'">Main Editor</button>
|
||
|
|
<button class="btn" type="button" onclick="window.location.href='/wiki'">Wiki</button>
|
||
|
|
<button class="btn" type="button" onclick="window.location.href='/wiki-assets/dialogue-builder.html'">Dialogue Builder</button>
|
||
|
|
<div class="topbar-title">World Chunk Manual</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="layout">
|
||
|
|
<aside class="sidebar">
|
||
|
|
<div class="sidebar-card">
|
||
|
|
<p class="eyebrow">Manual</p>
|
||
|
|
<h1>Chunk, Layer, and Height System</h1>
|
||
|
|
<p class="muted">
|
||
|
|
This page documents the open-world storage and editor runtime as it exists in this repo today,
|
||
|
|
with the goal of making the system portable to another language without guessing at the contracts.
|
||
|
|
</p>
|
||
|
|
<div class="chip-row">
|
||
|
|
<span class="chip">Chunked world</span>
|
||
|
|
<span class="chip">Sparse height patches</span>
|
||
|
|
<span class="chip">Batch save</span>
|
||
|
|
<span class="chip">Porting reference</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="sidebar-card">
|
||
|
|
<p class="eyebrow">Sections</p>
|
||
|
|
<nav class="nav">
|
||
|
|
<a href="#core-model">1. Core Model</a>
|
||
|
|
<a href="#file-layout">2. File Layout</a>
|
||
|
|
<a href="#world-schema">3. World Metadata</a>
|
||
|
|
<a href="#chunk-schema">4. Chunk Schema</a>
|
||
|
|
<a href="#coordinates">5. Coordinates</a>
|
||
|
|
<a href="#row-encoding">6. Row Encoding</a>
|
||
|
|
<a href="#height-system">7. Height System</a>
|
||
|
|
<a href="#instances">8. Instances</a>
|
||
|
|
<a href="#loading">9. Loading Pipeline</a>
|
||
|
|
<a href="#editing">10. Editing Pipeline</a>
|
||
|
|
<a href="#saving">11. Save Pipeline</a>
|
||
|
|
<a href="#normalization">12. Normalization Rules</a>
|
||
|
|
<a href="#porting">13. Porting Checklist</a>
|
||
|
|
<a href="#pseudocode">14. Pseudocode</a>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="sidebar-card">
|
||
|
|
<p class="eyebrow">Source Of Truth</p>
|
||
|
|
<p class="muted">
|
||
|
|
The behavior described here comes from these implementation files:
|
||
|
|
<code>src/worldChunking.ts</code>,
|
||
|
|
<code>server.js</code>,
|
||
|
|
<code>src/mapEditorPopup/bootstrap.ts</code>,
|
||
|
|
<code>src/mapEditorPopup/runtime.ts</code>,
|
||
|
|
<code>src/mapEditorPopup/interactionController.ts</code>,
|
||
|
|
and <code>src/mapEditorPopup/persistenceController.ts</code>.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<main class="content">
|
||
|
|
<section class="hero panel">
|
||
|
|
<p class="eyebrow">Purpose</p>
|
||
|
|
<h2>One world, fixed-size chunks, sparse height patches, and chunk-local saves.</h2>
|
||
|
|
<p>
|
||
|
|
The editor does not treat the world as one giant saved grid. It treats the world as a fixed-size chunk
|
||
|
|
lattice, composes a visible neighborhood into a temporary working document, and writes changes back out as
|
||
|
|
chunk-local JSON files.
|
||
|
|
</p>
|
||
|
|
<div class="grid three">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>World space is global</h3>
|
||
|
|
<p>Every meaningful tile position ultimately lives in world coordinates.</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Chunk space is storage</h3>
|
||
|
|
<p>Each chunk owns its local rows, local height patches, and local instance origins.</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Height is sparse</h3>
|
||
|
|
<p>Higher-Z content is not a full extra map. It is a set of trimmed patch rectangles.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="callout good">
|
||
|
|
If you port this system, preserve the semantics first and the implementation shape second. The critical
|
||
|
|
contracts are coordinate conversion, row encoding, patch trimming, chunk normalization, and dirty-chunk save behavior.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="core-model">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>1. Core Model</h2>
|
||
|
|
<span class="panel-tag">Conceptual contract</span>
|
||
|
|
</div>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>World</h3>
|
||
|
|
<p>
|
||
|
|
A world is the top-level container. It owns chunk dimensions, tile size, default background tile,
|
||
|
|
bookmarks, spawn point, background color, editor UI state, and every chunk file beneath its directory.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Chunk</h3>
|
||
|
|
<p>
|
||
|
|
A chunk is a fixed-size rectangular storage cell addressed by <code>(chunkX, chunkY)</code>. It stores
|
||
|
|
room layers, height patches, and instances that belong to that cell.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Room layer</h3>
|
||
|
|
<p>
|
||
|
|
A room layer is a dense character grid exactly <code>width x height</code> for the chunk. Each character
|
||
|
|
is a tile symbol or a fill symbol.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Height layer</h3>
|
||
|
|
<p>
|
||
|
|
A height layer is a sparse patch. It is stored as a trimmed rectangle with <code>x</code>, <code>y</code>,
|
||
|
|
<code>rows</code>, and <code>z</code>. Empty borders are removed.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Instance</h3>
|
||
|
|
<p>
|
||
|
|
An instance is chunk-owned and stores its local tile origin. Its full <code>record.position</code> world
|
||
|
|
coordinate is reconstructed during normalization.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Neighborhood</h3>
|
||
|
|
<p>
|
||
|
|
The editor loads a chunk neighborhood, composes it into one temporary document, lets the user edit that
|
||
|
|
document, then syncs changes back into the cached per-chunk payloads.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="file-layout">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>2. File Layout</h2>
|
||
|
|
<span class="panel-tag">On-disk structure</span>
|
||
|
|
</div>
|
||
|
|
<pre class="code">content/
|
||
|
|
worlds.json
|
||
|
|
worlds/
|
||
|
|
overworld/
|
||
|
|
world.json
|
||
|
|
bookmarks.json
|
||
|
|
chunks/
|
||
|
|
0_0.json
|
||
|
|
0_1.json
|
||
|
|
1_0.json
|
||
|
|
-1_0.json</pre>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>File</th>
|
||
|
|
<th>Role</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td><code>content/worlds.json</code></td>
|
||
|
|
<td>Index of worlds. Each entry provides <code>id</code>, <code>name</code>, and <code>worldDir</code>.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>world.json</code></td>
|
||
|
|
<td>World metadata and editor metadata. No chunk payload lives here.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>bookmarks.json</code></td>
|
||
|
|
<td>Saved navigation targets for the world editor.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>chunks/<chunkX>_<chunkY>.json</code></td>
|
||
|
|
<td>Chunk-local room layers, sparse height patches, and instances.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<div class="callout warn">
|
||
|
|
Chunk filenames are part of the contract. The server and client both use
|
||
|
|
<code>{floor(chunkX)}_{floor(chunkY)}.json</code> and chunk cache keys of
|
||
|
|
<code>{floor(chunkX)}:{floor(chunkY)}</code>.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="world-schema">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>3. World Metadata</h2>
|
||
|
|
<span class="panel-tag">Index, world.json, bookmarks</span>
|
||
|
|
</div>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3><code>worlds.json</code></h3>
|
||
|
|
<pre class="code">{
|
||
|
|
"schemaVersion": 1,
|
||
|
|
"worlds": [
|
||
|
|
{
|
||
|
|
"id": "overworld",
|
||
|
|
"name": "Overworld Mock",
|
||
|
|
"worldDir": "worlds/overworld"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}</pre>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3><code>world.json</code></h3>
|
||
|
|
<pre class="code">{
|
||
|
|
"schemaVersion": 1,
|
||
|
|
"id": "overworld",
|
||
|
|
"name": "Overworld Mock",
|
||
|
|
"chunkWidth": 32,
|
||
|
|
"chunkHeight": 32,
|
||
|
|
"tileSize": 32,
|
||
|
|
"backgroundColor": "#060A14",
|
||
|
|
"defaultBackgroundTileId": "tile_5b6206b849",
|
||
|
|
"heightBlurStep": 0.1,
|
||
|
|
"editorUi": { "...": "editor panel layout state" },
|
||
|
|
"spawn": { "x": 80, "y": 80 },
|
||
|
|
"editor": {
|
||
|
|
"defaultZoom": 1,
|
||
|
|
"gridVisible": true
|
||
|
|
}
|
||
|
|
}</pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3><code>bookmarks.json</code></h3>
|
||
|
|
<pre class="code">{
|
||
|
|
"schemaVersion": 1,
|
||
|
|
"worldId": "overworld",
|
||
|
|
"bookmarks": [
|
||
|
|
{ "id": "poi_1", "label": "Town Center", "x": 120, "y": 84 }
|
||
|
|
]
|
||
|
|
}</pre>
|
||
|
|
</div>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Field</th>
|
||
|
|
<th>Meaning</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td><code>chunkWidth</code>, <code>chunkHeight</code></td>
|
||
|
|
<td>Tile dimensions of every chunk in the world. The current repo uses <code>32x32</code>.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>tileSize</code></td>
|
||
|
|
<td>Pixel size for editor/runtime rendering.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>defaultBackgroundTileId</code></td>
|
||
|
|
<td>World-level background tile inherited by chunk base cells that store <code>.</code>.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>backgroundColor</code></td>
|
||
|
|
<td>Backdrop color for the editor and viewers.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>heightBlurStep</code></td>
|
||
|
|
<td>Viewer/editor visual parameter for height blur strength. It is metadata, not map geometry.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>editorUi</code></td>
|
||
|
|
<td>Saved panel/folder organization for the editor UI.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>spawn</code></td>
|
||
|
|
<td>World tile coordinate used as a starting navigation target when no bookmark is chosen.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="chunk-schema">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>4. Chunk Schema</h2>
|
||
|
|
<span class="panel-tag">Persistent unit of map content</span>
|
||
|
|
</div>
|
||
|
|
<pre class="code">{
|
||
|
|
"schemaVersion": 1,
|
||
|
|
"worldId": "overworld",
|
||
|
|
"chunkX": 3,
|
||
|
|
"chunkY": 2,
|
||
|
|
"width": 32,
|
||
|
|
"height": 32,
|
||
|
|
"backgroundTileId": "",
|
||
|
|
"roomLayers": [
|
||
|
|
{
|
||
|
|
"layer": 0,
|
||
|
|
"rows": ["................................", "..."],
|
||
|
|
"instanceIds": []
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"layer": 1,
|
||
|
|
"rows": [" ", "..."],
|
||
|
|
"instanceIds": []
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"heightLayers": [],
|
||
|
|
"instances": []
|
||
|
|
}</pre>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Field</th>
|
||
|
|
<th>Meaning</th>
|
||
|
|
<th>Important rule</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td><code>chunkX</code>, <code>chunkY</code></td>
|
||
|
|
<td>Chunk address in world chunk space.</td>
|
||
|
|
<td>File name and payload must agree after normalization.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>width</code>, <code>height</code></td>
|
||
|
|
<td>Chunk dimensions in tiles.</td>
|
||
|
|
<td>Rows are normalized to exactly these dimensions.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>backgroundTileId</code></td>
|
||
|
|
<td>Chunk-level override of the world background tile.</td>
|
||
|
|
<td>Empty string means inherit the world background tile.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>roomLayers</code></td>
|
||
|
|
<td>Dense per-layer character rows.</td>
|
||
|
|
<td>Layer <code>0</code> always exists. At least one editable layer above it also exists.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>heightLayers</code></td>
|
||
|
|
<td>Sparse chunk-local height patches.</td>
|
||
|
|
<td>Each patch is trimmed and local to the chunk, not world-global on disk.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>instances</code></td>
|
||
|
|
<td>Chunk-owned entity records.</td>
|
||
|
|
<td><code>x</code> and <code>y</code> are local to the chunk payload.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="coordinates">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>5. Coordinate System</h2>
|
||
|
|
<span class="panel-tag">Global to local math</span>
|
||
|
|
</div>
|
||
|
|
<p>
|
||
|
|
The conversion rules are the foundation of the entire system. Everything else depends on them being stable and identical in every language port.
|
||
|
|
</p>
|
||
|
|
<pre class="code">chunkX = floor(worldX / chunkWidth)
|
||
|
|
chunkY = floor(worldY / chunkHeight)
|
||
|
|
|
||
|
|
localX = worldX - (chunkX * chunkWidth)
|
||
|
|
localY = worldY - (chunkY * chunkHeight)
|
||
|
|
|
||
|
|
worldX = (chunkX * chunkWidth) + localX
|
||
|
|
worldY = (chunkY * chunkHeight) + localY</pre>
|
||
|
|
<div class="grid three">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Chunk key</h3>
|
||
|
|
<p><code>chunkKey = "{chunkX}:{chunkY}"</code></p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Chunk filename</h3>
|
||
|
|
<p><code>fileName = "{chunkX}_{chunkY}.json"</code></p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Address helper</h3>
|
||
|
|
<p>One helper returns <code>chunkX</code>, <code>chunkY</code>, <code>localX</code>, <code>localY</code>, <code>chunkKey</code>, and <code>fileName</code>.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>World tile</th>
|
||
|
|
<th>Chunk size</th>
|
||
|
|
<th>Chunk coord</th>
|
||
|
|
<th>Local coord</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td><code>(73, 18)</code></td>
|
||
|
|
<td><code>32x32</code></td>
|
||
|
|
<td><code>(2, 0)</code></td>
|
||
|
|
<td><code>(9, 18)</code></td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>(-1, -1)</code></td>
|
||
|
|
<td><code>32x32</code></td>
|
||
|
|
<td><code>(-1, -1)</code></td>
|
||
|
|
<td><code>(31, 31)</code></td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>(32, 32)</code></td>
|
||
|
|
<td><code>32x32</code></td>
|
||
|
|
<td><code>(1, 1)</code></td>
|
||
|
|
<td><code>(0, 0)</code></td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<div class="callout warn">
|
||
|
|
Use floor division semantics for negative coordinates. Truncation toward zero is wrong here and will break
|
||
|
|
chunk ownership for tiles left or above the origin.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="row-encoding">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>6. Row Encoding Rules</h2>
|
||
|
|
<span class="panel-tag">How characters are interpreted</span>
|
||
|
|
</div>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Context</th>
|
||
|
|
<th>Stored char</th>
|
||
|
|
<th>Meaning</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td>Room layer 0</td>
|
||
|
|
<td><code>.</code></td>
|
||
|
|
<td>Inherit the chunk background tile if set, otherwise inherit the world default background tile.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Room layer 0</td>
|
||
|
|
<td><code> </code> space</td>
|
||
|
|
<td>Explicit hole / transparent empty cell on the base layer.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Room layer 0</td>
|
||
|
|
<td>Any other single char</td>
|
||
|
|
<td>Explicit tile symbol for that cell.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Room layer > 0</td>
|
||
|
|
<td><code> </code> space</td>
|
||
|
|
<td>Empty cell.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Room layer > 0</td>
|
||
|
|
<td>Any other single char</td>
|
||
|
|
<td>Explicit tile symbol for that overlay layer.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Height patch rows</td>
|
||
|
|
<td><code> </code> or <code>.</code></td>
|
||
|
|
<td>Empty cell. Dots are normalized away to spaces.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Height patch rows</td>
|
||
|
|
<td>Any other single char</td>
|
||
|
|
<td>Tile symbol to draw at that patch cell.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Dense room rows</h3>
|
||
|
|
<p>
|
||
|
|
Room rows are always normalized to the full chunk width and full chunk height. Short rows are padded.
|
||
|
|
Long rows are truncated.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Sparse height rows</h3>
|
||
|
|
<p>
|
||
|
|
Height rows are stored as tightly trimmed rectangles. Empty outer rows and columns are removed. Trailing whitespace is stripped.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="callout">
|
||
|
|
Tile identity in chunk rows is based on a single tile symbol, not a tile id. The symbol-to-tile mapping comes from the tile catalog.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="height-system">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>7. Height System</h2>
|
||
|
|
<span class="panel-tag">Sparse, order-derived Z stack</span>
|
||
|
|
</div>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Authoring model</h3>
|
||
|
|
<p>
|
||
|
|
The editor exposes a list of height layers. The list order is authoritative. The first entry is Z1,
|
||
|
|
the second is Z2, and so on. In the editor code, height layer cloning rewrites
|
||
|
|
<code>z = index + 1</code>.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Storage model</h3>
|
||
|
|
<p>
|
||
|
|
Each chunk stores only the part of a patch that overlaps that chunk. On load, those local patches are
|
||
|
|
re-expanded into neighborhood-local coordinates for the working document.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Field</th>
|
||
|
|
<th>Meaning</th>
|
||
|
|
<th>Rule</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td><code>id</code></td>
|
||
|
|
<td>Stable patch identity across saves.</td>
|
||
|
|
<td>Must be unique per chunk payload after normalization.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>name</code></td>
|
||
|
|
<td>Human-facing layer name.</td>
|
||
|
|
<td>Optional.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>z</code></td>
|
||
|
|
<td>Height level.</td>
|
||
|
|
<td>In the current editor, this is effectively derived from list order.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>x</code>, <code>y</code></td>
|
||
|
|
<td>Patch origin.</td>
|
||
|
|
<td>Chunk-local on disk, neighborhood-local in the composed working document.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>rows</code></td>
|
||
|
|
<td>Trimmed patch characters.</td>
|
||
|
|
<td>Dots are treated as empty; trailing whitespace is trimmed.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<pre class="code">Example patch on disk
|
||
|
|
|
||
|
|
{
|
||
|
|
"id": "height_2",
|
||
|
|
"name": "Tower Top",
|
||
|
|
"z": 2,
|
||
|
|
"x": 10,
|
||
|
|
"y": 6,
|
||
|
|
"rows": [
|
||
|
|
" AAA ",
|
||
|
|
"A A",
|
||
|
|
" AAA "
|
||
|
|
]
|
||
|
|
}</pre>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>How patch painting works</h3>
|
||
|
|
<p>
|
||
|
|
Painting on a height layer does not mutate chunk rows directly. It mutates the sparse patch in the working
|
||
|
|
document, expands the patch bounds if needed, trims the result, then rebuilds any overlapped chunks.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>How erasing works</h3>
|
||
|
|
<p>
|
||
|
|
Erasing writes a space, then the patch is retrimmed. If the patch becomes empty, it collapses to
|
||
|
|
<code>rows: []</code> at its trimmed origin.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="callout warn">
|
||
|
|
Do not model height layers as full-size parallel maps if you want parity with this repo. They are intentionally sparse.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="instances">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>8. Instance System</h2>
|
||
|
|
<span class="panel-tag">Chunk-owned entities</span>
|
||
|
|
</div>
|
||
|
|
<p>
|
||
|
|
Instances are stored inside the chunk that owns their origin tile. The persisted <code>x</code> and
|
||
|
|
<code>y</code> are chunk-local. During normalization, the editor/runtime populates
|
||
|
|
<code>record.position</code> with the derived world tile position.
|
||
|
|
</p>
|
||
|
|
<pre class="code">{
|
||
|
|
"id": "inst_gatekeeper_001",
|
||
|
|
"templateId": "npc_gatekeeper_bubbles",
|
||
|
|
"layer": 1,
|
||
|
|
"x": 12,
|
||
|
|
"y": 9,
|
||
|
|
"record": {
|
||
|
|
"name": "Bubbles",
|
||
|
|
"spriteId": "npc_human_style_13"
|
||
|
|
}
|
||
|
|
}</pre>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Behavior</th>
|
||
|
|
<th>Current implementation</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td>ID generation</td>
|
||
|
|
<td>Normalization guarantees an id. Duplicate mode can force new ids.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Layer ownership</td>
|
||
|
|
<td>The instance carries <code>layer</code>, and each room layer caches matching <code>instanceIds</code>.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Position clamping</td>
|
||
|
|
<td><code>x</code> and <code>y</code> are clamped into chunk bounds during normalization.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Denormalized metadata</td>
|
||
|
|
<td><code>instanceIds</code> in room layers are rebuilt from the actual instances array.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="loading">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>9. Loading Pipeline</h2>
|
||
|
|
<span class="panel-tag">From JSON to working document</span>
|
||
|
|
</div>
|
||
|
|
<ol>
|
||
|
|
<li>Load world metadata with <code>GET /api/world/:worldId</code>.</li>
|
||
|
|
<li>Read bookmarks from the same payload and choose an initial view target.</li>
|
||
|
|
<li>Convert that target into <code>centerChunkX</code> and <code>centerChunkY</code>.</li>
|
||
|
|
<li>Load a chunk neighborhood with <code>GET /api/world/:worldId/chunks?chunkX=...&chunkY=...&radius=1&createIfMissing=1</code>.</li>
|
||
|
|
<li>Compose those chunk payloads into one temporary working map.</li>
|
||
|
|
<li>Set <code>originChunkX</code>, <code>originChunkY</code>, <code>tileOffsetX</code>, and <code>tileOffsetY</code> so local document coordinates can be translated back to world coordinates.</li>
|
||
|
|
</ol>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Neighborhood radius</h3>
|
||
|
|
<p>
|
||
|
|
Initial bootstrap uses radius <code>1</code>, which means a <code>3x3</code> chunk neighborhood.
|
||
|
|
The runtime can grow this dynamically up to radius <code>4</code> depending on viewport size.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Composition result</h3>
|
||
|
|
<p>
|
||
|
|
The editor does not directly edit chunk payloads on screen. It edits the composed document made from the
|
||
|
|
visible neighborhood, then syncs back into chunk cache entries.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<pre class="code">Neighborhood composition result
|
||
|
|
|
||
|
|
originChunkX = centerChunkX - radius
|
||
|
|
originChunkY = centerChunkY - radius
|
||
|
|
composedWidth = ((radius * 2) + 1) * chunkWidth
|
||
|
|
composedHeight = ((radius * 2) + 1) * chunkHeight
|
||
|
|
tileOffsetX = originChunkX * chunkWidth
|
||
|
|
tileOffsetY = originChunkY * chunkHeight</pre>
|
||
|
|
<div class="callout">
|
||
|
|
Missing chunks are completed in memory with empty payloads so every neighborhood is a full square, even when the filesystem only has sparse chunk files.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="editing">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>10. Editing And Mutation Flow</h2>
|
||
|
|
<span class="panel-tag">How user actions change chunk data</span>
|
||
|
|
</div>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Tile paint</h3>
|
||
|
|
<ol>
|
||
|
|
<li>Paint into the composed room layer rows.</li>
|
||
|
|
<li>Translate local tile to world tile with <code>tileOffsetX/Y</code>.</li>
|
||
|
|
<li>Translate world tile to chunk and local-in-chunk coordinates.</li>
|
||
|
|
<li>Patch the cached chunk row cell directly.</li>
|
||
|
|
<li>Mark that chunk dirty.</li>
|
||
|
|
</ol>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Height paint</h3>
|
||
|
|
<ol>
|
||
|
|
<li>Mutate the sparse patch in the composed document.</li>
|
||
|
|
<li>Expand or shrink patch bounds as needed.</li>
|
||
|
|
<li>Trim empty borders.</li>
|
||
|
|
<li>Rebuild every overlapped chunk from document state.</li>
|
||
|
|
<li>Mark those chunks dirty.</li>
|
||
|
|
</ol>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Layer metadata edits</h3>
|
||
|
|
<p>
|
||
|
|
Renaming or reordering room layers does not only change the visible document. The runtime also syncs cached
|
||
|
|
chunk metadata and adjusts per-instance layer ownership.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Height layer metadata edits</h3>
|
||
|
|
<p>
|
||
|
|
Renaming, duplicating, deleting, or reordering height layers triggers metadata sync into cached chunks and usually rebuilds visible chunks from the document.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Operation</th>
|
||
|
|
<th>Implementation behavior</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td>Chunk move</td>
|
||
|
|
<td>Source chunk payload is reassigned to a new chunk address, then the source is replaced with an empty chunk.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Chunk duplicate</td>
|
||
|
|
<td>Content is copied into the destination chunk, but placed instances are intentionally not copied.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Chunk transform</td>
|
||
|
|
<td>Rows, height patches, and instance local coordinates are rotated or flipped together.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Chunk clear</td>
|
||
|
|
<td>The destination payload becomes a fresh empty chunk payload.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Chunk background fill</td>
|
||
|
|
<td>The chunk stores a chunk-local background tile override and clears explicit base cells back to dots.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="saving">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>11. Save Pipeline</h2>
|
||
|
|
<span class="panel-tag">Dirty chunks plus world metadata</span>
|
||
|
|
</div>
|
||
|
|
<ol>
|
||
|
|
<li>Ensure the composed working document is current from any dirty chunk cache edits.</li>
|
||
|
|
<li>Sync height layer metadata into cached chunks so names and Z order match the current editor stack.</li>
|
||
|
|
<li>If visible dirty chunks exist, rebuild them from the current document before persistence.</li>
|
||
|
|
<li>Collect dirty chunk payloads.</li>
|
||
|
|
<li>POST a batch payload to <code>/api/world/:worldId/chunks/batch-save</code>.</li>
|
||
|
|
<li>On success, clear the saved dirty chunk keys.</li>
|
||
|
|
</ol>
|
||
|
|
<pre class="code">POST /api/world/:worldId/chunks/batch-save
|
||
|
|
|
||
|
|
{
|
||
|
|
"world": { "...": "world.json fields" },
|
||
|
|
"bookmarks": { "...": "bookmarks.json fields" },
|
||
|
|
"chunks": [
|
||
|
|
{ "...": "normalized chunk payload" }
|
||
|
|
]
|
||
|
|
}</pre>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>What gets saved every time</h3>
|
||
|
|
<p>
|
||
|
|
World metadata and bookmarks are always included in the current batch-save payload, even if no chunk rows changed.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>What gets saved selectively</h3>
|
||
|
|
<p>
|
||
|
|
Chunk files are saved only for chunk keys in the dirty set. That is the core scaling behavior.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="normalization">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>12. Normalization Rules</h2>
|
||
|
|
<span class="panel-tag">Server and runtime repair passes</span>
|
||
|
|
</div>
|
||
|
|
<p>
|
||
|
|
The raw JSON format is not consumed directly. Both the server and editor runtime normalize aggressively.
|
||
|
|
A port needs to decide whether to keep that behavior, but if you want compatibility with saved data from this repo,
|
||
|
|
you should.
|
||
|
|
</p>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Object</th>
|
||
|
|
<th>Normalization behavior</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td>World definition</td>
|
||
|
|
<td>Chunk dimensions are clamped to at least 1, tile size to at least 8, colors are normalized, spawn is floored, and editor UI is normalized.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Room rows</td>
|
||
|
|
<td>Rows are resized to exact chunk dimensions using <code>.</code> for layer 0 and <code> </code> for other layers.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Room layers</td>
|
||
|
|
<td>Layer 0 is always guaranteed. At least one editable layer above zero is also guaranteed.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Height layers</td>
|
||
|
|
<td>Ids are deduped, negative coordinates are clipped, rows are clamped to chunk bounds, dots become spaces, and empty borders are trimmed.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Instances</td>
|
||
|
|
<td>Ids are guaranteed, coordinates are clamped into chunk bounds, and <code>record.position</code> is rewritten to world coordinates.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>instanceIds</code></td>
|
||
|
|
<td>The per-layer id lists are deduped and rebuilt from the instances array.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<div class="callout warn">
|
||
|
|
Height patches accept dots in input, but dots are not a meaningful stored value. They are normalized to empty space and then trimmed.
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="porting">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>13. Porting Checklist</h2>
|
||
|
|
<span class="panel-tag">What to preserve in another language</span>
|
||
|
|
</div>
|
||
|
|
<div class="grid two">
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Must preserve exactly</h3>
|
||
|
|
<ul>
|
||
|
|
<li>Floor-based world/chunk/local coordinate conversion.</li>
|
||
|
|
<li>Base-layer <code>.</code> inheritance semantics.</li>
|
||
|
|
<li>Sparse height patch trimming rules.</li>
|
||
|
|
<li>Chunk-local instance storage with world position reconstruction.</li>
|
||
|
|
<li>Dirty-chunk-only save behavior.</li>
|
||
|
|
<li>Neighborhood composition and decomposition logic.</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Can be implemented differently</h3>
|
||
|
|
<ul>
|
||
|
|
<li>UI technology.</li>
|
||
|
|
<li>Rendering backend.</li>
|
||
|
|
<li>In-memory cache container types.</li>
|
||
|
|
<li>HTTP framework.</li>
|
||
|
|
<li>History storage mechanism.</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="mini-card">
|
||
|
|
<h3>Recommended port modules</h3>
|
||
|
|
<ul>
|
||
|
|
<li><code>world_chunking</code>: pure coordinate and naming helpers.</li>
|
||
|
|
<li><code>world_schema</code>: normalization and validation for world/chunk/bookmark payloads.</li>
|
||
|
|
<li><code>world_compose</code>: chunk neighborhood to working-document composition.</li>
|
||
|
|
<li><code>world_decompose</code>: working-document back to chunk payload slicing.</li>
|
||
|
|
<li><code>world_persistence</code>: batch-save endpoint and file IO.</li>
|
||
|
|
<li><code>world_editor_runtime</code>: cache, dirty keys, neighborhood loads, and edit sync logic.</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="panel" id="pseudocode">
|
||
|
|
<div class="panel-head">
|
||
|
|
<h2>14. Minimal Pseudocode</h2>
|
||
|
|
<span class="panel-tag">Language-agnostic reference</span>
|
||
|
|
</div>
|
||
|
|
<h3>Resolve a world tile to a chunk cell</h3>
|
||
|
|
<pre class="code">function resolveWorldChunkAddress(worldX, worldY, chunkWidth, chunkHeight):
|
||
|
|
chunkX = floor(worldX / chunkWidth)
|
||
|
|
chunkY = floor(worldY / chunkHeight)
|
||
|
|
localX = worldX - (chunkX * chunkWidth)
|
||
|
|
localY = worldY - (chunkY * chunkHeight)
|
||
|
|
return {
|
||
|
|
chunkX,
|
||
|
|
chunkY,
|
||
|
|
localX,
|
||
|
|
localY,
|
||
|
|
chunkKey: chunkX + ":" + chunkY,
|
||
|
|
fileName: chunkX + "_" + chunkY + ".json"
|
||
|
|
}</pre>
|
||
|
|
|
||
|
|
<h3>Compose a neighborhood into a working document</h3>
|
||
|
|
<pre class="code">function composeNeighborhood(chunks, centerChunkX, centerChunkY, radius, chunkWidth, chunkHeight):
|
||
|
|
originChunkX = centerChunkX - radius
|
||
|
|
originChunkY = centerChunkY - radius
|
||
|
|
composedWidth = ((radius * 2) + 1) * chunkWidth
|
||
|
|
composedHeight = ((radius * 2) + 1) * chunkHeight
|
||
|
|
|
||
|
|
roomLayers = denseComposeRoomLayers(chunks, originChunkX, originChunkY, composedWidth, composedHeight)
|
||
|
|
heightLayers = sparseComposeHeightLayers(chunks, originChunkX, originChunkY)
|
||
|
|
instances = projectInstancesToNeighborhood(chunks, originChunkX, originChunkY)
|
||
|
|
|
||
|
|
return {
|
||
|
|
width: composedWidth,
|
||
|
|
height: composedHeight,
|
||
|
|
originChunkX,
|
||
|
|
originChunkY,
|
||
|
|
tileOffsetX: originChunkX * chunkWidth,
|
||
|
|
tileOffsetY: originChunkY * chunkHeight,
|
||
|
|
roomLayers,
|
||
|
|
heightLayers,
|
||
|
|
instances
|
||
|
|
}</pre>
|
||
|
|
|
||
|
|
<h3>Sync a painted tile into the owning chunk</h3>
|
||
|
|
<pre class="code">function syncTileEdit(localTileX, localTileY, layerNumber, storedChar):
|
||
|
|
worldTileX = tileOffsetX + localTileX
|
||
|
|
worldTileY = tileOffsetY + localTileY
|
||
|
|
|
||
|
|
chunkX = floor(worldTileX / chunkWidth)
|
||
|
|
chunkY = floor(worldTileY / chunkHeight)
|
||
|
|
localX = worldTileX - (chunkX * chunkWidth)
|
||
|
|
localY = worldTileY - (chunkY * chunkHeight)
|
||
|
|
|
||
|
|
chunk = chunkCache[chunkKey(chunkX, chunkY)]
|
||
|
|
layer = ensureRoomLayer(chunk, layerNumber)
|
||
|
|
layer.rows[localY][localX] = storedChar
|
||
|
|
dirtyChunkKeys.add(chunkKey(chunkX, chunkY))</pre>
|
||
|
|
|
||
|
|
<h3>Rebuild chunk-local height patches from the working document</h3>
|
||
|
|
<pre class="code">function buildChunkHeightLayersFromDocument(documentHeightLayers, baseTileX, baseTileY, chunkWidth, chunkHeight):
|
||
|
|
result = []
|
||
|
|
chunkRight = baseTileX + chunkWidth
|
||
|
|
chunkBottom = baseTileY + chunkHeight
|
||
|
|
|
||
|
|
for each patch in documentHeightLayers:
|
||
|
|
overlap = intersect(patch.bounds, [baseTileX, baseTileY, chunkRight, chunkBottom])
|
||
|
|
if overlap is empty:
|
||
|
|
continue
|
||
|
|
|
||
|
|
localRows = slicePatchRowsIntoOverlap(patch, overlap)
|
||
|
|
result.push({
|
||
|
|
id: patch.id,
|
||
|
|
name: patch.name,
|
||
|
|
z: patch.z,
|
||
|
|
x: overlap.left - baseTileX,
|
||
|
|
y: overlap.top - baseTileY,
|
||
|
|
rows: trimTrailingWhitespace(localRows)
|
||
|
|
})
|
||
|
|
|
||
|
|
return result</pre>
|
||
|
|
|
||
|
|
<h3>Batch save</h3>
|
||
|
|
<pre class="code">function saveWorld():
|
||
|
|
ensureWorldDocumentCurrent()
|
||
|
|
syncCachedWorldHeightLayerMetadata()
|
||
|
|
rebuildVisibleDirtyChunksFromDocument()
|
||
|
|
|
||
|
|
payload = {
|
||
|
|
world: worldMetadata,
|
||
|
|
bookmarks: bookmarkPayload,
|
||
|
|
chunks: dirtyChunkPayloads()
|
||
|
|
}
|
||
|
|
|
||
|
|
POST /api/world/{worldId}/chunks/batch-save payload
|
||
|
|
clearDirtyChunkKeysThatWereSaved()</pre>
|
||
|
|
<p class="footer-note">
|
||
|
|
This manual matches the current repository behavior, including editor-specific conventions like height Z being derived from layer order.
|
||
|
|
</p>
|
||
|
|
</section>
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|