Initial import
This commit is contained in:
commit
ab891a315c
773 changed files with 257255 additions and 0 deletions
153
Release/docs/DIALOGUE_SYSTEM_FLOWCHART.svg
Normal file
153
Release/docs/DIALOGUE_SYSTEM_FLOWCHART.svg
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="2100" viewBox="0 0 1600 2100">
|
||||
<defs>
|
||||
<style>
|
||||
.title { font: 700 34px 'Segoe UI', Tahoma, sans-serif; fill: #0f172a; }
|
||||
.label { font: 600 18px 'Segoe UI', Tahoma, sans-serif; fill: #0f172a; }
|
||||
.small { font: 500 16px 'Segoe UI', Tahoma, sans-serif; fill: #1f2937; }
|
||||
.edge { font: 600 15px 'Segoe UI', Tahoma, sans-serif; fill: #334155; }
|
||||
.node { fill: #eef4ff; stroke: #355caa; stroke-width: 2.2; rx: 12; }
|
||||
.decision { fill: #fff6db; stroke: #ad7c00; stroke-width: 2.2; }
|
||||
.terminal { fill: #ffe5e5; stroke: #a23434; stroke-width: 2.2; rx: 20; }
|
||||
.group { fill: #f8fafc; stroke: #94a3b8; stroke-width: 1.5; rx: 14; stroke-dasharray: 8 6; }
|
||||
.arrow { stroke: #334155; stroke-width: 2.2; fill: none; marker-end: url(#arrowHead); }
|
||||
</style>
|
||||
<marker id="arrowHead" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#334155" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="20" y="20" width="1560" height="2060" fill="#ffffff" stroke="#cbd5e1" stroke-width="2" rx="18" />
|
||||
<text x="800" y="72" text-anchor="middle" class="title">New RPG Dialogue Runtime Flowchart</text>
|
||||
<text x="800" y="103" text-anchor="middle" class="small">From interaction start to dialogue close, including choice/condition routing and reactions</text>
|
||||
|
||||
<rect x="90" y="140" width="1420" height="420" class="group" />
|
||||
<text x="110" y="168" class="label">A) Open Dialogue and Build Runtime Graph</text>
|
||||
|
||||
<rect x="620" y="190" width="360" height="64" class="node" />
|
||||
<text x="800" y="228" text-anchor="middle" class="label">Player presses E near NPC</text>
|
||||
|
||||
<rect x="620" y="286" width="360" height="64" class="node" />
|
||||
<text x="800" y="324" text-anchor="middle" class="label">openNpcDialogue()</text>
|
||||
|
||||
<rect x="620" y="382" width="360" height="64" class="node" />
|
||||
<text x="800" y="420" text-anchor="middle" class="label">updateQuestProgression()</text>
|
||||
|
||||
<rect x="620" y="478" width="360" height="64" class="node" />
|
||||
<text x="800" y="516" text-anchor="middle" class="label">Build runtime graph from content</text>
|
||||
|
||||
<path d="M800 254 L800 286" class="arrow" />
|
||||
<path d="M800 350 L800 382" class="arrow" />
|
||||
<path d="M800 446 L800 478" class="arrow" />
|
||||
|
||||
<rect x="90" y="590" width="1420" height="430" class="group" />
|
||||
<text x="110" y="618" class="label">B) Start Node Selection</text>
|
||||
|
||||
<rect x="150" y="655" width="380" height="76" class="node" />
|
||||
<text x="340" y="688" text-anchor="middle" class="label">defaultDialogueNodeId exists</text>
|
||||
<text x="340" y="712" text-anchor="middle" class="small">and resolves to a node?</text>
|
||||
|
||||
<polygon points="800,650 940,708 800,766 660,708" class="decision" />
|
||||
<text x="800" y="703" text-anchor="middle" class="label">Yes</text>
|
||||
<text x="800" y="724" text-anchor="middle" class="small">use default node</text>
|
||||
|
||||
<rect x="1070" y="655" width="360" height="76" class="node" />
|
||||
<text x="1250" y="688" text-anchor="middle" class="label">Sort nodes by order, then id</text>
|
||||
<text x="1250" y="712" text-anchor="middle" class="small">find candidate roots</text>
|
||||
|
||||
<rect x="1070" y="760" width="360" height="76" class="node" />
|
||||
<text x="1250" y="793" text-anchor="middle" class="label">Filter by dialogueNodeMatchesContext</text>
|
||||
<text x="1250" y="817" text-anchor="middle" class="small">pick highest priority</text>
|
||||
|
||||
<rect x="620" y="892" width="360" height="76" class="node" />
|
||||
<text x="800" y="925" text-anchor="middle" class="label">DialogueSession.openGraph()</text>
|
||||
<text x="800" y="949" text-anchor="middle" class="small">set currentNodeId</text>
|
||||
|
||||
<path d="M980 510 L980 708 L940 708" class="arrow" />
|
||||
<path d="M530 693 L660 708" class="arrow" />
|
||||
<path d="M940 708 L1070 708" class="arrow" />
|
||||
<path d="M1250 731 L1250 760" class="arrow" />
|
||||
<path d="M1250 836 L980 930" class="arrow" />
|
||||
|
||||
<polygon points="800,1015 930,1068 800,1121 670,1068" class="decision" />
|
||||
<text x="800" y="1062" text-anchor="middle" class="label">Start node</text>
|
||||
<text x="800" y="1083" text-anchor="middle" class="small">exists?</text>
|
||||
|
||||
<rect x="1090" y="1035" width="300" height="64" class="terminal" />
|
||||
<text x="1240" y="1072" text-anchor="middle" class="label">Close dialogue</text>
|
||||
|
||||
<rect x="620" y="1155" width="360" height="64" class="node" />
|
||||
<text x="800" y="1192" text-anchor="middle" class="label">applyNodeReactions(startNode)</text>
|
||||
|
||||
<path d="M800 968 L800 1015" class="arrow" />
|
||||
<path d="M930 1068 L1090 1068" class="arrow" />
|
||||
<path d="M800 1121 L800 1155" class="arrow" />
|
||||
|
||||
<rect x="90" y="1250" width="1420" height="780" class="group" />
|
||||
<text x="110" y="1278" class="label">C) Main Dialogue Loop (Input + Routing)</text>
|
||||
|
||||
<polygon points="800,1310 940,1368 800,1426 660,1368" class="decision" />
|
||||
<text x="800" y="1362" text-anchor="middle" class="label">Current node</text>
|
||||
<text x="800" y="1383" text-anchor="middle" class="small">has choices?</text>
|
||||
|
||||
<polygon points="430,1460 570,1518 430,1576 290,1518" class="decision" />
|
||||
<text x="430" y="1512" text-anchor="middle" class="label">Continue key</text>
|
||||
<text x="430" y="1533" text-anchor="middle" class="small">pressed?</text>
|
||||
|
||||
<rect x="230" y="1610" width="400" height="76" class="node" />
|
||||
<text x="430" y="1643" text-anchor="middle" class="label">resolveNodeConditionalNext()</text>
|
||||
<text x="430" y="1667" text-anchor="middle" class="small">first passing condition wins</text>
|
||||
|
||||
<polygon points="430,1722 560,1775 430,1828 300,1775" class="decision" />
|
||||
<text x="430" y="1769" text-anchor="middle" class="label">Next id exists?</text>
|
||||
|
||||
<rect x="230" y="1860" width="400" height="76" class="node" />
|
||||
<text x="430" y="1893" text-anchor="middle" class="label">advanceTo(next)</text>
|
||||
<text x="430" y="1917" text-anchor="middle" class="small">apply entered node reactions</text>
|
||||
|
||||
<polygon points="1170,1460 1310,1518 1170,1576 1030,1518" class="decision" />
|
||||
<text x="1170" y="1512" text-anchor="middle" class="label">Any visible</text>
|
||||
<text x="1170" y="1533" text-anchor="middle" class="small">choices?</text>
|
||||
|
||||
<polygon points="980,1610 1120,1668 980,1726 840,1668" class="decision" />
|
||||
<text x="980" y="1662" text-anchor="middle" class="label">Continue key</text>
|
||||
<text x="980" y="1683" text-anchor="middle" class="small">pressed?</text>
|
||||
|
||||
<polygon points="1300,1610 1440,1668 1300,1726 1160,1668" class="decision" />
|
||||
<text x="1300" y="1662" text-anchor="middle" class="label">Number key</text>
|
||||
<text x="1300" y="1683" text-anchor="middle" class="small">1..9 pressed?</text>
|
||||
|
||||
<rect x="1110" y="1760" width="380" height="76" class="node" />
|
||||
<text x="1300" y="1793" text-anchor="middle" class="label">Map visible index to actual choice</text>
|
||||
<text x="1300" y="1817" text-anchor="middle" class="small">apply choice reaction</text>
|
||||
|
||||
<rect x="1110" y="1868" width="380" height="76" class="node" />
|
||||
<text x="1300" y="1901" text-anchor="middle" class="label">resolveChoiceTarget()</text>
|
||||
<text x="1300" y="1925" text-anchor="middle" class="small">met: nextId, else unmetNextId</text>
|
||||
|
||||
<path d="M800 1219 L800 1310" class="arrow" />
|
||||
<path d="M660 1368 L430 1518" class="arrow" />
|
||||
<path d="M940 1368 L1170 1518" class="arrow" />
|
||||
|
||||
<path d="M430 1576 L430 1610" class="arrow" />
|
||||
<path d="M430 1686 L430 1722" class="arrow" />
|
||||
<path d="M430 1828 L430 1860" class="arrow" />
|
||||
|
||||
<path d="M1170 1576 L980 1668" class="arrow" />
|
||||
<path d="M1170 1576 L1300 1668" class="arrow" />
|
||||
|
||||
<path d="M980 1726 L430 1722" class="arrow" />
|
||||
<path d="M1300 1726 L1300 1760" class="arrow" />
|
||||
<path d="M1300 1836 L1300 1868" class="arrow" />
|
||||
<path d="M1110 1906 L630 1898" class="arrow" />
|
||||
|
||||
<path d="M430 1936 L430 1984 L800 1984 L800 1426" class="arrow" />
|
||||
<path d="M1300 1944 L1300 1984 L800 1984" class="arrow" />
|
||||
|
||||
<rect x="690" y="2010" width="220" height="52" class="terminal" />
|
||||
<text x="800" y="2042" text-anchor="middle" class="label">Dialogue closed</text>
|
||||
|
||||
<path d="M560 1775 L690 2036" class="arrow" />
|
||||
<path d="M1240 1068 L1240 2036 L910 2036" class="arrow" />
|
||||
|
||||
<text x="96" y="2068" class="small">Note: Text rendering uses resolveNodeConditionalText() and displays the first passing condition text. Continue hint uses condition-resolved next id.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
233
Release/docs/DIALOGUE_SYSTEM_RUNTIME.md
Normal file
233
Release/docs/DIALOGUE_SYSTEM_RUNTIME.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# Dialogue System Runtime Spec
|
||||
|
||||
This document is a code-aligned reference for how dialogue is parsed, evaluated, and executed in-game.
|
||||
|
||||
## Quick Visual
|
||||
|
||||

|
||||
|
||||
## 1) Data Model (Current Canonical Shape)
|
||||
|
||||
Dialogue nodes are authored primarily with arrays:
|
||||
|
||||
- `conditions[]`
|
||||
- `text`
|
||||
- `conditionType`
|
||||
- `conditionValue` (for `item`, use `itemId:quantity`)
|
||||
- `conditionStepId`
|
||||
- `conditionNot`
|
||||
- `nextId`
|
||||
- `reactions[]`
|
||||
- `reactionType`
|
||||
- `reactionValue`
|
||||
- `choices[]`
|
||||
- `text`
|
||||
- `nextId`
|
||||
- `conditionType`
|
||||
- `conditionValue`
|
||||
- `conditionStepId`
|
||||
- `conditionNot`
|
||||
- `reactionType`
|
||||
- `reactionValue`
|
||||
|
||||
Compatibility fields still exist in runtime structures (`text`, `conditionType`, `conditionValue`, `conditionStepId`, `conditionNot`, `nextId`, `reactionType`, `reactionValue`) and are used as fallback when needed.
|
||||
|
||||
## 2) Parsing Order (JSON -> Runtime Content)
|
||||
|
||||
Parsing entrypoint:
|
||||
|
||||
- `ContentManager::loadAll()`
|
||||
- `readDialogueNodes()` for each NPC
|
||||
|
||||
Per dialogue node, parsing order is:
|
||||
|
||||
1. Read legacy/base fields first (`id`, `text`, legacy condition/reaction/next/order fields).
|
||||
2. Read `conditions[]`.
|
||||
- Each condition uses `condition.text`, defaulting to base `node.text` if missing.
|
||||
3. If `conditions[]` is empty, synthesize exactly one condition from legacy/base fields.
|
||||
4. Read `reactions[]`.
|
||||
5. If `reactions[]` is empty, only synthesize a fallback reaction if legacy reaction fields were present in JSON.
|
||||
- This allows intentionally empty reaction arrays.
|
||||
6. Read `choices[]`.
|
||||
- Supports migration path for very old choice payloads missing `conditionType`.
|
||||
|
||||
## 3) Runtime Graph Build Order
|
||||
|
||||
At interaction start (`E` near NPC):
|
||||
|
||||
1. `openNpcDialogue()` calls `updateQuestProgression()`.
|
||||
2. NPC node defs are converted to runtime `game::DialogueNode` objects.
|
||||
3. Conditions, reactions, and choices are copied into runtime node vectors.
|
||||
4. Start node is selected with `selectDialogueStartNodeId()`.
|
||||
5. `DialogueSession::openGraph()` is called.
|
||||
6. If a start node exists, node reactions are applied immediately (`applyNodeReactions()`).
|
||||
|
||||
## 4) Start Node Selection Logic
|
||||
|
||||
`selectDialogueStartNodeId()` order:
|
||||
|
||||
1. If `defaultDialogueNodeId` exists and is valid, use it.
|
||||
2. Else, sort nodes by `order`, then `id`.
|
||||
3. Build inbound edge set from:
|
||||
- node `nextId`
|
||||
- choice `nextId`
|
||||
4. Candidate roots are nodes with no inbound references.
|
||||
- If none, all nodes become candidates.
|
||||
5. Filter candidates by `dialogueNodeMatchesContext()`.
|
||||
6. Pick highest `dialogueNodePriority()`.
|
||||
- Priority: `quest_step_completed` > `quest_started/quest_completed` > `item/flag` > default.
|
||||
7. Tie-break by smaller `order`.
|
||||
8. If no contextual match, fall back to first sorted node.
|
||||
|
||||
## 5) Condition Evaluation Order
|
||||
|
||||
Core function: `doesConditionPass(type, value, stepId, conditionNot)`.
|
||||
|
||||
Evaluation sequence:
|
||||
|
||||
1. Evaluate base condition type.
|
||||
2. Supported types include:
|
||||
- `always`
|
||||
- `flag`
|
||||
- `item` (supports `itemId:quantity`; quantity defaults to `1` if omitted)
|
||||
- `level`
|
||||
- `currency` (supports `key:amount` format)
|
||||
- `skill`
|
||||
- `quest_started`, `quest_completed`
|
||||
- `quest_step_completed`
|
||||
3. Apply NOT inversion if `conditionNot == true`.
|
||||
|
||||
Node-level match order:
|
||||
|
||||
- `resolveMatchedNodeCondition(node)` scans `node.conditions` from index 0 upward.
|
||||
- First passing condition wins.
|
||||
|
||||
That winning condition is then used for:
|
||||
|
||||
- Display text via `resolveNodeConditionalText()` (`condition.text`, else legacy `node.text`).
|
||||
- Continue target via `resolveNodeConditionalNext()` (`condition.nextId`, else legacy `node.nextNodeId`).
|
||||
|
||||
## 6) Choice Visibility and Selection Order
|
||||
|
||||
Choice visibility:
|
||||
|
||||
- `buildVisibleChoiceIndices(node)` iterates choices in list order.
|
||||
- Includes only choices where `doesChoiceMeetConditions(choice)` returns true.
|
||||
|
||||
When user presses numeric choice key:
|
||||
|
||||
1. Map key `1..9` to visible index.
|
||||
2. Resolve real choice index from `visibleChoiceIndices`.
|
||||
3. Apply choice reaction first (`applyDialogueReaction`).
|
||||
4. Resolve target with `resolveChoiceTarget(choice)`:
|
||||
- If condition passes -> `nextId`
|
||||
- Else close
|
||||
5. Advance to target with `advanceTo()`.
|
||||
6. Apply entered-node reactions.
|
||||
7. `updateQuestProgression()`.
|
||||
|
||||
## 7) Reactions Execution Order
|
||||
|
||||
`applyNodeReactions(node)`:
|
||||
|
||||
1. If `node.reactions` is non-empty, execute each in array order.
|
||||
2. Else fallback to legacy single reaction fields.
|
||||
|
||||
`applyDialogueReaction(type, value)` supports:
|
||||
|
||||
- `grant_flag` / `grant_quest_flag`
|
||||
- `grant_item` using `itemId:quantity` values such as `copper_ore:1`
|
||||
- `start_quest`
|
||||
- `complete_quest`
|
||||
|
||||
## 8) Input Execution Paths
|
||||
|
||||
### A) Dialogue open + no choices
|
||||
|
||||
- Continue key (`E` / Enter / Space):
|
||||
1. Resolve condition-based next node
|
||||
2. Advance or close
|
||||
3. Apply entered-node reactions (if advanced)
|
||||
|
||||
### B) Dialogue open + choices exist
|
||||
|
||||
- If visible choices exist:
|
||||
- Number keys choose branch
|
||||
- If no visible choices:
|
||||
- Continue key uses node condition-based next/close path
|
||||
|
||||
### C) Escape
|
||||
|
||||
- `Esc` closes dialogue immediately.
|
||||
|
||||
## 9) Rendering Order (Dialogue Box)
|
||||
|
||||
`renderDialogueBox()` draws:
|
||||
|
||||
1. Speaker bar
|
||||
2. Body text from `resolveNodeConditionalText()`
|
||||
3. Visible choices list (if any)
|
||||
4. Footer hint:
|
||||
- `[E] Continue` if condition-resolved next exists
|
||||
- `[E] Close` otherwise
|
||||
|
||||
## 10) Flowchart
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Player presses E near NPC] --> B[openNpcDialogue]
|
||||
B --> C[updateQuestProgression]
|
||||
C --> D[Build runtime graph from content nodes]
|
||||
D --> E[selectDialogueStartNodeId]
|
||||
E --> F[DialogueSession.openGraph]
|
||||
F --> G{Start node exists?}
|
||||
G -- No --> Z[Close]
|
||||
G -- Yes --> H[applyNodeReactions on start node]
|
||||
|
||||
H --> I[Dialogue loop]
|
||||
I --> J{Current node has choices?}
|
||||
|
||||
J -- No --> K{Continue key pressed?}
|
||||
K -- No --> I
|
||||
K -- Yes --> L[resolveNodeConditionalNext via first passing condition]
|
||||
L --> M{next exists?}
|
||||
M -- No --> Z
|
||||
M -- Yes --> N[advanceTo next]
|
||||
N --> O[applyNodeReactions on entered node]
|
||||
O --> I
|
||||
|
||||
J -- Yes --> P[buildVisibleChoiceIndices]
|
||||
P --> Q{Any visible choices?}
|
||||
|
||||
Q -- No --> R{Continue key pressed?}
|
||||
R -- No --> I
|
||||
R -- Yes --> L
|
||||
|
||||
Q -- Yes --> S{Number key 1..9?}
|
||||
S -- No --> I
|
||||
S -- Yes --> T[Map visible index -> actual choice]
|
||||
T --> U[apply choice reaction]
|
||||
U --> V[resolveChoiceTarget]
|
||||
V --> W{target exists?}
|
||||
W -- No --> Z
|
||||
W -- Yes --> X[advanceTo target]
|
||||
X --> Y[applyNodeReactions on entered node]
|
||||
Y --> I
|
||||
```
|
||||
|
||||
## 11) Practical Authoring Implications
|
||||
|
||||
- Condition order is behavior-critical. Put the most specific conditions first.
|
||||
- Item conditions should be authored as `itemId:quantity` so runtime quantity checks are explicit.
|
||||
- In the editor, `conditionType=item` surfaces item picker + quantity input and writes `conditionValue` in that format.
|
||||
- Dialogue line text should be authored on condition entries (`conditions[].text`).
|
||||
- Empty `reactions[]` is valid and now preserved.
|
||||
- Choice order affects both visual order and numeric selection mapping.
|
||||
|
||||
## 12) Primary Source Files
|
||||
|
||||
- `src/content/ContentManager.cpp`
|
||||
- `src/game/Game.cpp`
|
||||
- `src/game/Dialogue.cpp`
|
||||
- `src/content/ContentTypes.hpp`
|
||||
- `src/game/Dialogue.hpp`
|
||||
134
Release/docs/MAP_EDITOR_FLOWCHART.svg
Normal file
134
Release/docs/MAP_EDITOR_FLOWCHART.svg
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1320" viewBox="0 0 1600 1320">
|
||||
<defs>
|
||||
<style>
|
||||
.title { font: 700 34px 'Segoe UI', Tahoma, sans-serif; fill: #0f172a; }
|
||||
.subtitle { font: 500 16px 'Segoe UI', Tahoma, sans-serif; fill: #334155; }
|
||||
.section { fill: #f8fbff; stroke: #94a3b8; stroke-width: 1.6; rx: 16; stroke-dasharray: 8 6; }
|
||||
.node { fill: #eef4ff; stroke: #355caa; stroke-width: 2.1; rx: 14; }
|
||||
.node2 { fill: #edfdf5; stroke: #2f855a; stroke-width: 2.1; rx: 14; }
|
||||
.node3 { fill: #fff7ed; stroke: #c2410c; stroke-width: 2.1; rx: 14; }
|
||||
.terminal { fill: #ffe8e8; stroke: #b42318; stroke-width: 2.1; rx: 20; }
|
||||
.label { font: 700 18px 'Segoe UI', Tahoma, sans-serif; fill: #0f172a; }
|
||||
.small { font: 500 15px 'Segoe UI', Tahoma, sans-serif; fill: #1f2937; }
|
||||
.tiny { font: 500 13px 'Segoe UI', Tahoma, sans-serif; fill: #475569; }
|
||||
.edge { stroke: #334155; stroke-width: 2.4; fill: none; marker-end: url(#arrowHead); }
|
||||
.edge2 { stroke: #2f855a; stroke-width: 2.4; fill: none; marker-end: url(#arrowHeadGreen); }
|
||||
.edge3 { stroke: #c2410c; stroke-width: 2.4; fill: none; marker-end: url(#arrowHeadOrange); }
|
||||
.hint { fill: #475569; font: 600 13px 'Segoe UI', Tahoma, sans-serif; }
|
||||
.pill { fill: #dbeafe; stroke: #60a5fa; stroke-width: 1.2; rx: 999; }
|
||||
.pillText { font: 700 12px 'Segoe UI', Tahoma, sans-serif; fill: #1d4ed8; }
|
||||
</style>
|
||||
<marker id="arrowHead" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#334155" />
|
||||
</marker>
|
||||
<marker id="arrowHeadGreen" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#2f855a" />
|
||||
</marker>
|
||||
<marker id="arrowHeadOrange" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#c2410c" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="18" y="18" width="1564" height="1284" fill="#ffffff" stroke="#cbd5e1" stroke-width="2" rx="20" />
|
||||
<text x="800" y="64" text-anchor="middle" class="title">New RPG Map Editor Request Flow</text>
|
||||
<text x="800" y="94" text-anchor="middle" class="subtitle">From clicking a map record, to editing in the popup, to saving, refreshing, and closing</text>
|
||||
|
||||
<rect x="70" y="132" width="1460" height="250" class="section" />
|
||||
<text x="92" y="162" class="label">A) Open the popup editor</text>
|
||||
|
||||
<rect x="100" y="190" width="220" height="64" class="node" />
|
||||
<text x="210" y="226" text-anchor="middle" class="label">Click Maps record</text>
|
||||
|
||||
<rect x="390" y="190" width="240" height="64" class="node" />
|
||||
<text x="510" y="226" text-anchor="middle" class="label">Select Editor tab</text>
|
||||
|
||||
<rect x="710" y="180" width="360" height="84" class="node" />
|
||||
<text x="890" y="214" text-anchor="middle" class="label">MapEditorPanels opens popup</text>
|
||||
<text x="890" y="238" text-anchor="middle" class="small">buildRoomEditorHtml() injects serialized map + NPC + sprite data</text>
|
||||
|
||||
<rect x="1140" y="180" width="340" height="84" class="node" />
|
||||
<text x="1310" y="214" text-anchor="middle" class="label">Popup boots ready</text>
|
||||
<text x="1310" y="238" text-anchor="middle" class="small">layers, palette, history, NPC overlays</text>
|
||||
|
||||
<path d="M320 222 L390 222" class="edge" />
|
||||
<path d="M630 222 L710 222" class="edge" />
|
||||
<path d="M1070 222 L1140 222" class="edge" />
|
||||
|
||||
<rect x="70" y="414" width="1460" height="410" class="section" />
|
||||
<text x="92" y="444" class="label">B) Work inside the popup</text>
|
||||
|
||||
<rect x="100" y="482" width="280" height="74" class="node2" />
|
||||
<text x="240" y="515" text-anchor="middle" class="label">Paint tiles</text>
|
||||
<text x="240" y="539" text-anchor="middle" class="small">brush palette + grouped history</text>
|
||||
|
||||
<rect x="430" y="482" width="280" height="74" class="node2" />
|
||||
<text x="570" y="515" text-anchor="middle" class="label">Move / add NPCs</text>
|
||||
<text x="570" y="539" text-anchor="middle" class="small">drag, snap, assign templates</text>
|
||||
|
||||
<rect x="760" y="482" width="280" height="74" class="node2" />
|
||||
<text x="900" y="515" text-anchor="middle" class="label">Edit map info</text>
|
||||
<text x="900" y="539" text-anchor="middle" class="small">name, width, height</text>
|
||||
|
||||
<rect x="1090" y="482" width="340" height="74" class="node2" />
|
||||
<text x="1260" y="515" text-anchor="middle" class="label">Switch layers or maps</text>
|
||||
<text x="1260" y="539" text-anchor="middle" class="small">posts map-open request to host</text>
|
||||
|
||||
<path d="M380 519 L430 519" class="edge2" />
|
||||
<path d="M710 519 L760 519" class="edge2" />
|
||||
<path d="M1040 519 L1090 519" class="edge2" />
|
||||
|
||||
<rect x="240" y="610" width="1120" height="140" class="node3" />
|
||||
<text x="800" y="646" text-anchor="middle" class="label">Popup mutates local state first</text>
|
||||
<text x="800" y="672" text-anchor="middle" class="small">selected layer, tile rows, NPC record, history, and selection state stay in memory until save or close</text>
|
||||
<text x="800" y="698" text-anchor="middle" class="tiny">The host editor does not change until the popup posts a message or the API save completes.</text>
|
||||
|
||||
<path d="M800 556 L800 610" class="edge3" />
|
||||
|
||||
<rect x="70" y="852" width="1460" height="250" class="section" />
|
||||
<text x="92" y="882" class="label">C) Save and synchronize</text>
|
||||
|
||||
<rect x="120" y="920" width="250" height="74" class="terminal" />
|
||||
<text x="245" y="952" text-anchor="middle" class="label">Click Save</text>
|
||||
<text x="245" y="976" text-anchor="middle" class="small">popup collects current state</text>
|
||||
|
||||
<rect x="430" y="910" width="330" height="94" class="node3" />
|
||||
<text x="595" y="944" text-anchor="middle" class="label">POST /api/content/maps</text>
|
||||
<text x="595" y="968" text-anchor="middle" class="small">writes map index + per-map files</text>
|
||||
|
||||
<rect x="810" y="910" width="330" height="94" class="node3" />
|
||||
<text x="975" y="944" text-anchor="middle" class="label">POST /api/content/npcs</text>
|
||||
<text x="975" y="968" text-anchor="middle" class="small">writes map-local NPC instances</text>
|
||||
|
||||
<rect x="1190" y="920" width="250" height="74" class="node3" />
|
||||
<text x="1315" y="952" text-anchor="middle" class="label">Notify host</text>
|
||||
<text x="1315" y="976" text-anchor="middle" class="small">map-editor-saved</text>
|
||||
|
||||
<path d="M370 957 L430 957" class="edge3" />
|
||||
<path d="M760 957 L810 957" class="edge3" />
|
||||
<path d="M1140 957 L1190 957" class="edge3" />
|
||||
|
||||
<rect x="70" y="1124" width="1460" height="160" class="section" />
|
||||
<text x="92" y="1154" class="label">D) Close or change map</text>
|
||||
|
||||
<rect x="110" y="1188" width="320" height="66" class="terminal" />
|
||||
<text x="270" y="1220" text-anchor="middle" class="label">Leave Editor tab / close popup</text>
|
||||
|
||||
<rect x="490" y="1188" width="360" height="66" class="node" />
|
||||
<text x="670" y="1220" text-anchor="middle" class="label">Host closes popup and revokes blob URL</text>
|
||||
|
||||
<rect x="910" y="1188" width="300" height="66" class="terminal" />
|
||||
<text x="1060" y="1216" text-anchor="middle" class="label">Switch map in popup</text>
|
||||
<text x="1060" y="1237" text-anchor="middle" class="tiny">map-editor-open-map</text>
|
||||
|
||||
<rect x="1250" y="1188" width="240" height="66" class="node" />
|
||||
<text x="1370" y="1220" text-anchor="middle" class="label">Host reloads data</text>
|
||||
|
||||
<path d="M1220 1188 C1100 1140, 920 1138, 670 1188" class="edge" />
|
||||
<path d="M430 1221 L490 1221" class="edge" />
|
||||
<path d="M1210 1221 L1250 1221" class="edge" />
|
||||
|
||||
<rect x="120" y="104" width="250" height="20" class="pill" />
|
||||
<text x="245" y="118" text-anchor="middle" class="pillText">Popup-owned workflow</text>
|
||||
<rect x="1200" y="104" width="220" height="20" class="pill" />
|
||||
<text x="1310" y="118" text-anchor="middle" class="pillText">Host sync + save callbacks</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
550
Release/docs/OPEN_WORLD_CHUNK_V1.md
Normal file
550
Release/docs/OPEN_WORLD_CHUNK_V1.md
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
# Open World Chunking V1
|
||||
|
||||
This document proposes a concrete v1 data model and editor/runtime architecture for replacing the current finite `map` model with a streamed open world.
|
||||
|
||||
The goal is not "fake endless maps glued together." The goal is:
|
||||
|
||||
- one continuous world coordinate space
|
||||
- chunked storage and loading
|
||||
- seamless editing across chunk boundaries
|
||||
- sparse height overrides
|
||||
- chunk-local save/write operations
|
||||
- renderer and editor surfaces that only load and draw what is nearby
|
||||
|
||||
## Plain-English Model
|
||||
|
||||
Think of the world as graph paper that goes on forever.
|
||||
|
||||
- The player and editor work in one global coordinate space: `(worldX, worldY)`.
|
||||
- That graph paper is cut into fixed-size square sheets called `chunks`.
|
||||
- Each chunk stores only the tiles, height patches, and instances that belong to that square.
|
||||
- The game and editor only load the nearby chunks.
|
||||
|
||||
The user should never feel like they are "switching maps." They should feel like they are scrolling around one world.
|
||||
|
||||
## V1 Recommendation
|
||||
|
||||
V1 should introduce a new top-level concept: `world`.
|
||||
|
||||
Current:
|
||||
|
||||
- `maps.json`
|
||||
- one map entry owns rows, layers, height layers, instances
|
||||
|
||||
Proposed:
|
||||
|
||||
- `worlds.json`
|
||||
- one world entry defines chunk rules and metadata
|
||||
- chunk files hold actual terrain/layer/instance data
|
||||
|
||||
Keep support for multiple worlds. Even in an "open world" game, multiple worlds are still useful for:
|
||||
|
||||
- overworld
|
||||
- interiors or special dimensions
|
||||
- test worlds
|
||||
- developer sandboxes
|
||||
|
||||
## Goals
|
||||
|
||||
- Seamless editing across arbitrary world coordinates
|
||||
- Automatic chunk creation when painting or placing far away
|
||||
- No giant monolithic world file for tile data
|
||||
- Chunk-local save/load for performance
|
||||
- Reuse current room-layer and height-patch ideas where possible
|
||||
- Replace minimap with world navigation, coordinates, and bookmarks
|
||||
|
||||
## Non-Goals For V1
|
||||
|
||||
- Infinite procedural terrain generation
|
||||
- Networked world streaming
|
||||
- Region compression or advanced binary storage
|
||||
- Fully general LOD terrain synthesis
|
||||
- Rewriting the whole engine to 3D or voxel storage
|
||||
|
||||
## Chosen Chunk Size
|
||||
|
||||
The chosen v1 chunk size is `32x32`.
|
||||
|
||||
Reason:
|
||||
|
||||
- simpler mental math in tooling and code
|
||||
- slightly better fit for common viewport batching
|
||||
- cleaner chunk index math
|
||||
- easier future optimization
|
||||
|
||||
`25x25` would also work, but v1 should standardize on `32x32` and avoid making chunk size configurable until the new architecture is stable.
|
||||
|
||||
## Coordinate Model
|
||||
|
||||
The world uses two coordinate spaces:
|
||||
|
||||
1. World coordinates
|
||||
- Absolute tile coordinates in the world
|
||||
- Example: `(73, -12)`
|
||||
|
||||
2. Chunk-local coordinates
|
||||
- Tile coordinates within a chunk
|
||||
- Example: chunk `(2, -1)` local tile `(9, 20)`
|
||||
|
||||
Conversion:
|
||||
|
||||
```txt
|
||||
chunkX = floor(worldX / chunkWidth)
|
||||
chunkY = floor(worldY / chunkHeight)
|
||||
|
||||
localX = worldX - (chunkX * chunkWidth)
|
||||
localY = worldY - (chunkY * chunkHeight)
|
||||
```
|
||||
|
||||
This is the core of the whole system.
|
||||
|
||||
## Proposed File Structure
|
||||
|
||||
```txt
|
||||
content/
|
||||
worlds.json
|
||||
worlds/
|
||||
overworld/
|
||||
world.json
|
||||
bookmarks.json
|
||||
chunks/
|
||||
0_0.json
|
||||
1_0.json
|
||||
0_1.json
|
||||
-1_0.json
|
||||
```
|
||||
|
||||
### `content/worlds.json`
|
||||
|
||||
Index of available worlds.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"worlds": [
|
||||
{
|
||||
"id": "overworld",
|
||||
"name": "Overworld",
|
||||
"worldDir": "worlds/overworld"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `content/worlds/overworld/world.json`
|
||||
|
||||
World-level metadata only.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "overworld",
|
||||
"name": "Overworld",
|
||||
"chunkWidth": 32,
|
||||
"chunkHeight": 32,
|
||||
"tileSize": 32,
|
||||
"defaultBackgroundTileId": "tile_grass_01",
|
||||
"spawn": { "x": 0, "y": 0 },
|
||||
"editor": {
|
||||
"defaultZoom": 1,
|
||||
"gridVisible": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `content/worlds/overworld/bookmarks.json`
|
||||
|
||||
Replaces the current minimap's "jump around the finite map" role with saved navigation targets.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"worldId": "overworld",
|
||||
"bookmarks": [
|
||||
{ "id": "town_center", "label": "Town Center", "x": 120, "y": 84 },
|
||||
{ "id": "north_tower", "label": "North Tower", "x": 145, "y": 32 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Chunk file
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"worldId": "overworld",
|
||||
"chunkX": 2,
|
||||
"chunkY": -1,
|
||||
"width": 32,
|
||||
"height": 32,
|
||||
"backgroundTileId": "tile_grass_01",
|
||||
"roomLayers": [
|
||||
{
|
||||
"layer": 0,
|
||||
"name": "Ground",
|
||||
"rows": ["................................", "..."],
|
||||
"instanceIds": []
|
||||
},
|
||||
{
|
||||
"layer": 1,
|
||||
"name": "Walls",
|
||||
"rows": [" ", "..."],
|
||||
"instanceIds": ["inst_gatekeeper_001"]
|
||||
}
|
||||
],
|
||||
"heightLayers": [
|
||||
{
|
||||
"id": "height_001",
|
||||
"name": "Tower Level 1",
|
||||
"z": 1,
|
||||
"x": 10,
|
||||
"y": 8,
|
||||
"rows": [
|
||||
" 4444 ",
|
||||
" 4 4 ",
|
||||
" 4444 "
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "inst_gatekeeper_001",
|
||||
"templateId": "npc_gatekeeper_bubbles",
|
||||
"layer": 1,
|
||||
"x": 12,
|
||||
"y": 9,
|
||||
"record": {
|
||||
"name": "Bubbles",
|
||||
"spriteId": "npc_human_style_13",
|
||||
"faction": "dangerous_gatekeeper"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Why This Structure
|
||||
|
||||
This splits data by ownership:
|
||||
|
||||
- `world.json`: rules and metadata
|
||||
- `bookmarks.json`: editor navigation helpers
|
||||
- chunk files: actual editable world content
|
||||
|
||||
That keeps save operations local and avoids turning one file into a bottleneck.
|
||||
|
||||
## Editor Data Model
|
||||
|
||||
The editor runtime should stop treating one "map" as the active document.
|
||||
|
||||
Instead it should have:
|
||||
|
||||
```ts
|
||||
type OpenWorldSession = {
|
||||
worldId: string;
|
||||
chunkWidth: number;
|
||||
chunkHeight: number;
|
||||
tileSize: number;
|
||||
cameraWorldX: number;
|
||||
cameraWorldY: number;
|
||||
loadedChunks: Record<string, WorldChunk>;
|
||||
dirtyChunks: Record<string, boolean>;
|
||||
activeLayer: number;
|
||||
activeHeightLayerId: string;
|
||||
editingTargetKind: "room" | "height";
|
||||
bookmarks: WorldBookmark[];
|
||||
};
|
||||
```
|
||||
|
||||
The main change is that the editor should think in `world coordinates` first and `chunk ownership` second.
|
||||
|
||||
## Editing Rules
|
||||
|
||||
### Painting tiles
|
||||
|
||||
When the user paints:
|
||||
|
||||
1. Convert pointer position to `(worldX, worldY)`
|
||||
2. Resolve chunk and local coordinate
|
||||
3. Create chunk if it does not exist
|
||||
4. Edit the right chunk layer row
|
||||
5. Mark that chunk dirty
|
||||
6. Patch only the visible surfaces touched by the stroke
|
||||
|
||||
If a rectangle, circle, or fill crosses chunk boundaries, the brush simply touches multiple chunks in one action.
|
||||
|
||||
### Placing instances
|
||||
|
||||
When the user drops an instance:
|
||||
|
||||
1. Resolve world tile
|
||||
2. Resolve chunk
|
||||
3. Create chunk if missing
|
||||
4. Add instance to that chunk
|
||||
5. Mark chunk dirty
|
||||
|
||||
### Painting far away
|
||||
|
||||
Yes, the editor should create all needed empty chunks automatically if the user paints or places content in a region that has never been created before.
|
||||
|
||||
It should not create a giant range of intermediate chunks just because the camera moved there. Chunks should be created lazily:
|
||||
|
||||
- create on first edit
|
||||
- optionally create on explicit "stamp empty region" tooling
|
||||
|
||||
## Runtime Load Model
|
||||
|
||||
The game should not load the whole world.
|
||||
|
||||
Instead:
|
||||
|
||||
1. Determine the player's current chunk
|
||||
2. Load a chunk radius around the player
|
||||
3. Keep nearby chunks hot
|
||||
4. Unload distant chunks
|
||||
|
||||
Example:
|
||||
|
||||
- player is in chunk `(10, 4)`
|
||||
- active chunk radius is `1`
|
||||
- load chunks from `(9..11, 3..5)`
|
||||
|
||||
That gives a `3x3` neighborhood.
|
||||
|
||||
This is the same core idea the editor should use for panning and visible drawing.
|
||||
|
||||
## Renderer Model
|
||||
|
||||
The renderer should operate in visible chunk slices, not full-world arrays.
|
||||
|
||||
Suggested v1 pipeline:
|
||||
|
||||
1. Determine visible world tile bounds from camera
|
||||
2. Determine which chunks intersect that area
|
||||
3. Ensure those chunks are loaded
|
||||
4. Draw visible base layers from those chunks
|
||||
5. Apply active height patches for current Z
|
||||
6. Draw instances
|
||||
7. Draw overlays and cursors
|
||||
|
||||
## Surface Caching
|
||||
|
||||
The current tile-surface work already points in the right direction.
|
||||
|
||||
V1 should move to:
|
||||
|
||||
- one cached surface per visible chunk layer
|
||||
- one cached patch surface per height patch
|
||||
- redraw only invalidated chunks or patches
|
||||
|
||||
That is much better than treating the whole world as one giant canvas.
|
||||
|
||||
## Height Layer Model In Open World
|
||||
|
||||
The current direction of sparse patch-based height overrides is still the right one.
|
||||
|
||||
V1 should keep height layers separate from ordinary draw layers.
|
||||
|
||||
Normal chunk data:
|
||||
|
||||
- base room layers define the world at Z0
|
||||
|
||||
Height patch data:
|
||||
|
||||
- sparse overrides define what appears at higher Z values
|
||||
|
||||
When the player goes up a ladder:
|
||||
|
||||
- current Z changes
|
||||
- renderer continues drawing visible Z0 world
|
||||
- renderer applies any visible patch data matching that Z
|
||||
|
||||
This is exactly the kind of thing chunking helps with, because only nearby patches need to be considered.
|
||||
|
||||
## Instance Model
|
||||
|
||||
Instances should remain data-driven and chunk-owned.
|
||||
|
||||
Recommended v1 storage:
|
||||
|
||||
- store instances inside the chunk that owns their tile origin
|
||||
- keep instance `x` and `y` local to the chunk file
|
||||
- derive world position in memory
|
||||
|
||||
Alternative:
|
||||
|
||||
- store world `x` and `y` directly in the chunk file
|
||||
|
||||
Either can work. For consistency with chunk-local tile rows, chunk-local instance positions are cleaner on disk.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "inst_blacksmith_001",
|
||||
"templateId": "npc_blacksmith",
|
||||
"layer": 1,
|
||||
"x": 18,
|
||||
"y": 7,
|
||||
"record": {
|
||||
"name": "Mera"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## World Navigation UI
|
||||
|
||||
The current minimap should be replaced.
|
||||
|
||||
Recommended v1 navigation tools:
|
||||
|
||||
- current world coordinate display
|
||||
- jump to X/Y
|
||||
- bookmarks / waypoints
|
||||
- "center on player"
|
||||
- chunk grid overlay toggle
|
||||
- optional chunk overview panel showing loaded and dirty chunks
|
||||
|
||||
This is much more useful for an open world than a single minimap squeezed into a corner.
|
||||
|
||||
## Save Model
|
||||
|
||||
The editor should save only dirty chunks and world metadata.
|
||||
|
||||
Recommended split:
|
||||
|
||||
- save `world.json` only when world-level settings change
|
||||
- save `bookmarks.json` only when bookmarks change
|
||||
- save only touched chunk files when content changes
|
||||
|
||||
This is the core performance and safety advantage.
|
||||
|
||||
## Undo / History Model
|
||||
|
||||
History can no longer assume one finite map snapshot as the main unit.
|
||||
|
||||
Recommended v1 history operation format:
|
||||
|
||||
```ts
|
||||
type ChunkEditOperation = {
|
||||
type: "chunk_edit";
|
||||
chunkKey: string;
|
||||
before: Partial<WorldChunk>;
|
||||
after: Partial<WorldChunk>;
|
||||
};
|
||||
```
|
||||
|
||||
For multi-chunk brushes:
|
||||
|
||||
```ts
|
||||
type MultiChunkEditOperation = {
|
||||
type: "multi_chunk_edit";
|
||||
chunks: ChunkEditOperation[];
|
||||
};
|
||||
```
|
||||
|
||||
This is important because world editing will regularly cross chunk boundaries.
|
||||
|
||||
## API Shape
|
||||
|
||||
Suggested API direction:
|
||||
|
||||
- `GET /api/worlds`
|
||||
- `GET /api/world/:worldId`
|
||||
- `GET /api/world/:worldId/bookmarks`
|
||||
- `GET /api/world/:worldId/chunks?x=10&y=4&radius=1`
|
||||
- `POST /api/world/:worldId/chunk/:chunkX/:chunkY`
|
||||
- `POST /api/world/:worldId/chunks/batch-save`
|
||||
- `POST /api/world/:worldId/bookmarks`
|
||||
|
||||
The important change is that APIs become chunk-aware instead of map-aware.
|
||||
|
||||
## Migration From Current Map Model
|
||||
|
||||
The easiest migration path is not "rewrite everything at once."
|
||||
|
||||
Recommended migration steps:
|
||||
|
||||
1. Introduce `world` and `chunk` data structures alongside current maps
|
||||
2. Build an importer that converts one existing map into one or more chunks
|
||||
3. Keep existing tile/layer/height patch formats as similar as possible
|
||||
4. Build a separate open-world editor mode first
|
||||
5. Retire the old map model only after the new one is stable
|
||||
|
||||
For migration:
|
||||
|
||||
- one `100x100` map can become `4x4` chunks at `25x25`
|
||||
- or `4x4` chunks at `32x32` with padded edges
|
||||
|
||||
## What Must Be Rewritten
|
||||
|
||||
This is a major architecture change. The following systems would need meaningful refactors:
|
||||
|
||||
- popup bootstrap and document loading
|
||||
- editor runtime state
|
||||
- renderer surface caching
|
||||
- minimap/navigation UI
|
||||
- history model
|
||||
- persistence API
|
||||
- map switcher UX
|
||||
|
||||
The following ideas can be reused:
|
||||
|
||||
- tile symbol rendering
|
||||
- tile/instance palettes
|
||||
- sparse height patch logic
|
||||
- layer editing concepts
|
||||
- chunk/surface invalidation patterns
|
||||
|
||||
## V1 Risks
|
||||
|
||||
- Trying to preserve too much of the finite-map abstraction
|
||||
- Storing too much redundant empty chunk data
|
||||
- Keeping full-world snapshots in history
|
||||
- Letting chunk creation happen implicitly on camera movement
|
||||
- Rebuilding all visible chunk surfaces too often
|
||||
|
||||
## Recommended V1 Boundaries
|
||||
|
||||
To keep this realistic, v1 should do only this:
|
||||
|
||||
- one open world
|
||||
- fixed chunk size
|
||||
- chunk-local room layers
|
||||
- sparse chunk-local height patches
|
||||
- chunk-owned instances
|
||||
- bookmarks instead of minimap
|
||||
- batch save of dirty chunks
|
||||
- visible-chunk-only rendering
|
||||
|
||||
That is already a large and meaningful overhaul.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
The safest order is:
|
||||
|
||||
1. Add new world/chunk schemas and API routes without removing current map routes
|
||||
2. Build a converter that turns one current map into one chunked world
|
||||
3. Add a chunk-aware loader in parallel with the current popup bootstrap
|
||||
4. Replace the popup minimap with coordinates, bookmarks, and jump controls
|
||||
5. Convert rendering from one-map surfaces to per-chunk surfaces
|
||||
6. Convert tile painting, height painting, and instance placement to world-coordinate edits
|
||||
7. Convert save/history to batch dirty chunks
|
||||
8. Only then remove the old finite-map assumptions
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
This direction makes sense.
|
||||
|
||||
The strongest version of it is:
|
||||
|
||||
- stop thinking in "many maps stitched together"
|
||||
- start thinking in "one world partitioned into chunks"
|
||||
|
||||
That keeps the user experience seamless while keeping storage and rendering manageable.
|
||||
|
||||
If we choose to proceed, the next best step is:
|
||||
|
||||
1. finalize the exact chunk JSON schema
|
||||
2. build the map-to-world importer
|
||||
3. add a parallel open-world editor path before deleting the current map editor
|
||||
2341
Release/docs/dialogue-builder.html
Normal file
2341
Release/docs/dialogue-builder.html
Normal file
File diff suppressed because it is too large
Load diff
1808
Release/docs/index.html
Normal file
1808
Release/docs/index.html
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue