1070 lines
32 KiB
HTML
1070 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Worldshaper UI System Manual</title>
|
|
<style>
|
|
:root {
|
|
--bg: #12161d;
|
|
--bg-soft: #1a212b;
|
|
--panel: rgba(17, 23, 31, 0.88);
|
|
--panel-strong: rgba(25, 32, 42, 0.96);
|
|
--line: rgba(143, 179, 215, 0.2);
|
|
--text: #e8edf4;
|
|
--muted: #9fb0c3;
|
|
--accent: #d7a04b;
|
|
--accent-soft: rgba(215, 160, 75, 0.16);
|
|
--accent-cool: #7dc0c9;
|
|
--good: #8ad0a2;
|
|
--shadow: 0 22px 60px rgba(0, 0, 0, 0.35);
|
|
--radius: 20px;
|
|
--content-width: 1240px;
|
|
--nav-width: 280px;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
|
|
sans-serif;
|
|
color: var(--text);
|
|
background:
|
|
radial-gradient(circle at top left, rgba(125, 192, 201, 0.17), transparent 30%),
|
|
radial-gradient(circle at top right, rgba(215, 160, 75, 0.12), transparent 24%),
|
|
linear-gradient(180deg, #0b0f14 0%, #12161d 42%, #10151d 100%);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
body::before {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
background-image:
|
|
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
|
|
background-size: 28px 28px;
|
|
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.65), transparent 92%);
|
|
}
|
|
|
|
a {
|
|
color: inherit;
|
|
}
|
|
|
|
code,
|
|
pre {
|
|
font-family: Consolas, "Liberation Mono", Menlo, monospace;
|
|
}
|
|
|
|
.shell {
|
|
width: min(100%, var(--content-width));
|
|
margin: 0 auto;
|
|
padding: 24px;
|
|
}
|
|
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 30;
|
|
backdrop-filter: blur(16px);
|
|
background: rgba(11, 15, 20, 0.78);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.topbar-inner {
|
|
width: min(100%, var(--content-width));
|
|
margin: 0 auto;
|
|
padding: 14px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.brand-badge {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 14px;
|
|
background:
|
|
linear-gradient(135deg, rgba(215, 160, 75, 0.22), rgba(125, 192, 201, 0.3)),
|
|
rgba(255, 255, 255, 0.02);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
display: grid;
|
|
place-items: center;
|
|
box-shadow: var(--shadow);
|
|
font-size: 0.92rem;
|
|
letter-spacing: 0.12em;
|
|
}
|
|
|
|
.brand-copy h1 {
|
|
margin: 0;
|
|
font-family: Georgia, "Times New Roman", serif;
|
|
font-size: 1.2rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.brand-copy p {
|
|
margin: 2px 0 0;
|
|
color: var(--muted);
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
.menu-button {
|
|
display: none;
|
|
border: 1px solid var(--line);
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border-radius: 12px;
|
|
padding: 10px 14px;
|
|
font: inherit;
|
|
}
|
|
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: var(--nav-width) minmax(0, 1fr);
|
|
gap: 24px;
|
|
padding-top: 22px;
|
|
}
|
|
|
|
.sidebar {
|
|
position: sticky;
|
|
top: 88px;
|
|
align-self: start;
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 18px 18px 14px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
background: linear-gradient(180deg, rgba(215, 160, 75, 0.08), transparent);
|
|
}
|
|
|
|
.sidebar-header h2 {
|
|
margin: 0;
|
|
font-size: 0.96rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.sidebar-header p {
|
|
margin: 8px 0 0;
|
|
color: var(--muted);
|
|
line-height: 1.45;
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
.nav-group {
|
|
padding: 14px 14px 8px;
|
|
}
|
|
|
|
.nav-group + .nav-group {
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.nav-label {
|
|
margin: 0 0 10px;
|
|
color: var(--muted);
|
|
font-size: 0.76rem;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.nav-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.nav-list a {
|
|
display: block;
|
|
text-decoration: none;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
color: var(--muted);
|
|
border: 1px solid transparent;
|
|
transition: 160ms ease;
|
|
}
|
|
|
|
.nav-list a:hover,
|
|
.nav-list a:focus-visible {
|
|
color: var(--text);
|
|
border-color: rgba(255, 255, 255, 0.08);
|
|
background: rgba(255, 255, 255, 0.04);
|
|
outline: none;
|
|
}
|
|
|
|
.nav-list a.active {
|
|
color: var(--text);
|
|
border-color: rgba(215, 160, 75, 0.26);
|
|
background: linear-gradient(135deg, var(--accent-soft), rgba(125, 192, 201, 0.08));
|
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
}
|
|
|
|
.content {
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
.hero,
|
|
.section-card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.hero {
|
|
position: relative;
|
|
}
|
|
|
|
.hero::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: auto -10% -40% auto;
|
|
width: 360px;
|
|
height: 360px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(125, 192, 201, 0.18), transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero-inner {
|
|
padding: 34px;
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
.eyebrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: fit-content;
|
|
padding: 8px 12px;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
color: var(--accent-cool);
|
|
font-size: 0.82rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.hero h2 {
|
|
margin: 0;
|
|
max-width: 12ch;
|
|
font-family: Georgia, "Times New Roman", serif;
|
|
font-size: clamp(2.2rem, 4vw, 4rem);
|
|
line-height: 0.98;
|
|
}
|
|
|
|
.hero p {
|
|
margin: 0;
|
|
max-width: 68ch;
|
|
color: var(--muted);
|
|
font-size: 1.05rem;
|
|
line-height: 1.75;
|
|
}
|
|
|
|
.hero-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.mini-panel {
|
|
padding: 16px;
|
|
border-radius: 16px;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.mini-panel strong {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: var(--text);
|
|
}
|
|
|
|
.mini-panel span {
|
|
color: var(--muted);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.section-card {
|
|
padding: 28px;
|
|
}
|
|
|
|
.section-card h2 {
|
|
margin: 0 0 6px;
|
|
font-family: Georgia, "Times New Roman", serif;
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.section-intro {
|
|
margin: 0 0 22px;
|
|
color: var(--muted);
|
|
line-height: 1.7;
|
|
max-width: 70ch;
|
|
}
|
|
|
|
.callout {
|
|
margin: 0 0 24px;
|
|
padding: 18px 20px;
|
|
border-radius: 16px;
|
|
background: linear-gradient(135deg, rgba(125, 192, 201, 0.12), rgba(215, 160, 75, 0.08));
|
|
border: 1px solid rgba(125, 192, 201, 0.2);
|
|
}
|
|
|
|
.callout strong {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
color: var(--text);
|
|
}
|
|
|
|
.callout p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.grid-2 {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.panel {
|
|
padding: 18px;
|
|
border-radius: 16px;
|
|
background: var(--panel-strong);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.panel h3 {
|
|
margin: 0 0 10px;
|
|
font-size: 1rem;
|
|
color: var(--accent);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.panel p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.panel ul,
|
|
.section-card ul,
|
|
.section-card ol {
|
|
margin: 14px 0 0;
|
|
padding-left: 20px;
|
|
color: var(--text);
|
|
}
|
|
|
|
.section-card li + li,
|
|
.panel li + li {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.section-card li::marker,
|
|
.panel li::marker {
|
|
color: var(--accent-cool);
|
|
}
|
|
|
|
.subsection {
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.subsection + .subsection {
|
|
margin-top: 22px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
padding-top: 22px;
|
|
}
|
|
|
|
.subsection h3 {
|
|
margin: 0 0 8px;
|
|
font-size: 1.15rem;
|
|
}
|
|
|
|
.subsection p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
line-height: 1.7;
|
|
}
|
|
|
|
pre {
|
|
margin: 16px 0 0;
|
|
padding: 18px;
|
|
border-radius: 16px;
|
|
overflow-x: auto;
|
|
background: #0d1218;
|
|
color: #d6e2ef;
|
|
border: 1px solid rgba(125, 192, 201, 0.14);
|
|
}
|
|
|
|
.footer-note {
|
|
color: var(--muted);
|
|
text-align: center;
|
|
padding: 8px 0 28px;
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
@media (max-width: 980px) {
|
|
.layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.sidebar {
|
|
position: static;
|
|
display: none;
|
|
}
|
|
|
|
.sidebar.open {
|
|
display: block;
|
|
}
|
|
|
|
.menu-button {
|
|
display: inline-flex;
|
|
}
|
|
|
|
.hero-grid,
|
|
.grid-2 {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.shell,
|
|
.topbar-inner {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
}
|
|
|
|
.hero-inner,
|
|
.section-card {
|
|
padding: 22px;
|
|
}
|
|
|
|
.hero h2 {
|
|
max-width: none;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="topbar-inner">
|
|
<div class="brand">
|
|
<div class="brand-badge">UI</div>
|
|
<div class="brand-copy">
|
|
<h1>Worldshaper UI System Manual</h1>
|
|
<p>Shared architecture for the engine, runtime, and editor</p>
|
|
</div>
|
|
</div>
|
|
<button class="menu-button" type="button" aria-expanded="false" aria-controls="manual-nav">
|
|
Browse Sections
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="shell">
|
|
<div class="layout">
|
|
<aside class="sidebar" id="manual-nav">
|
|
<div class="sidebar-header">
|
|
<h2>Manual Index</h2>
|
|
<p>Navigate by responsibility, then drill down into systems, workflow, and the shared asset format.</p>
|
|
</div>
|
|
|
|
<div class="nav-group">
|
|
<p class="nav-label">Foundation</p>
|
|
<ul class="nav-list">
|
|
<li><a href="#overview">Overview</a></li>
|
|
<li><a href="#shared-model">Shared Asset Model</a></li>
|
|
<li><a href="#package-layout">Package Layout</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nav-group">
|
|
<p class="nav-label">Systems</p>
|
|
<ul class="nav-list">
|
|
<li><a href="#engine">Engine</a></li>
|
|
<li><a href="#engine-api">Runtime API</a></li>
|
|
<li><a href="#editor">Editor</a></li>
|
|
<li><a href="#game-runtime">Game Runtime</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nav-group">
|
|
<p class="nav-label">Data</p>
|
|
<ul class="nav-list">
|
|
<li><a href="#ui-assets">UI Assets</a></li>
|
|
<li><a href="#asset-example">Asset Example</a></li>
|
|
<li><a href="#versioning">Versioning</a></li>
|
|
</ul>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="content">
|
|
<section class="hero">
|
|
<div class="hero-inner">
|
|
<div class="eyebrow">Design Manual</div>
|
|
<h2>UI as a shared language between tools and play.</h2>
|
|
<p>
|
|
The editor should author UI definitions, the engine should render and manage them,
|
|
and the game should supply live data plus behavior bindings. That keeps creation,
|
|
execution, and meaning cleanly separated while still letting the same assets move
|
|
directly from tooling into runtime.
|
|
</p>
|
|
|
|
<div class="hero-grid">
|
|
<div class="mini-panel">
|
|
<strong>Engine</strong>
|
|
<span>Owns runtime behavior: rendering, layout, focus, input, and theme systems.</span>
|
|
</div>
|
|
<div class="mini-panel">
|
|
<strong>Editor</strong>
|
|
<span>Owns authoring: visual layout, hierarchy editing, inspectors, and previews.</span>
|
|
</div>
|
|
<div class="mini-panel">
|
|
<strong>Game</strong>
|
|
<span>Owns meaning: opening windows, injecting data, and resolving action ids.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="overview">
|
|
<h2>Overview</h2>
|
|
<p class="section-intro">
|
|
The system should support a workflow where the editor creates and modifies UI
|
|
visually, saves UI definitions as assets, the game loads those assets at runtime,
|
|
and the engine provides the shared UI runtime both sides depend on.
|
|
</p>
|
|
|
|
<div class="callout">
|
|
<strong>Core Rule</strong>
|
|
<p>
|
|
UI should be stored as data assets, not embedded directly in the editor and not
|
|
hardcoded only in game code. The editor authors structure, the runtime executes it,
|
|
and the game binds it to real gameplay state.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>Recommended Split</h3>
|
|
<ul>
|
|
<li>Engine owns how UI works.</li>
|
|
<li>Editor owns how UI is authored.</li>
|
|
<li>Game runtime owns what the UI means.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Design Direction</h3>
|
|
<ul>
|
|
<li>Use a retained-mode UI system.</li>
|
|
<li>Store screens and windows as declarative assets.</li>
|
|
<li>Render them through shared HTML, CSS, and JavaScript runtime code.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="shared-model">
|
|
<h2>Shared Asset Model</h2>
|
|
<p class="section-intro">
|
|
The editor should write structured UI data, and the game should read that same data
|
|
through the engine runtime. This avoids arbitrary code generation and keeps assets
|
|
portable, testable, and safe to evolve.
|
|
</p>
|
|
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>The Editor Should Save</h3>
|
|
<ul>
|
|
<li>layout</li>
|
|
<li>widget hierarchy</li>
|
|
<li>theme and style references</li>
|
|
<li>data-binding keys</li>
|
|
<li>symbolic action identifiers</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>The Editor Should Not Save</h3>
|
|
<ul>
|
|
<li>raw executable game logic</li>
|
|
<li>hardcoded runtime object instances</li>
|
|
<li>engine-internal transient state</li>
|
|
<li>function bodies embedded in assets</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="package-layout">
|
|
<h2>Package Layout</h2>
|
|
<p class="section-intro">
|
|
A package split like this keeps schema, runtime, editor, and game-facing code
|
|
independent while still sharing a common vocabulary.
|
|
</p>
|
|
<pre><code>/packages
|
|
/ui-schema
|
|
/ui-runtime
|
|
/ui-editor
|
|
/game-runtime</code></pre>
|
|
|
|
<div class="grid-2" style="margin-top: 18px;">
|
|
<div class="panel">
|
|
<h3><code>ui-schema</code></h3>
|
|
<ul>
|
|
<li>UI node definitions</li>
|
|
<li>validation</li>
|
|
<li>schema versioning</li>
|
|
<li>migrations</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3><code>ui-runtime</code></h3>
|
|
<ul>
|
|
<li>renderer</li>
|
|
<li>layout engine</li>
|
|
<li>window manager</li>
|
|
<li>widget registry</li>
|
|
<li>theme system</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3><code>ui-editor</code></h3>
|
|
<ul>
|
|
<li>scene or workspace</li>
|
|
<li>selection tools</li>
|
|
<li>property inspector</li>
|
|
<li>asset browser</li>
|
|
<li>save and load flow</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3><code>game-runtime</code></h3>
|
|
<ul>
|
|
<li>game-specific data providers</li>
|
|
<li>action handlers</li>
|
|
<li>UI asset loading</li>
|
|
<li>window orchestration</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="engine">
|
|
<h2>Engine</h2>
|
|
<p class="section-intro">
|
|
The engine should be the single owner of the UI runtime so the editor and game do
|
|
not drift apart in behavior over time.
|
|
</p>
|
|
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>Responsibilities</h3>
|
|
<ul>
|
|
<li>window lifecycle</li>
|
|
<li>widget rendering</li>
|
|
<li>layout calculation</li>
|
|
<li>input routing</li>
|
|
<li>focus management</li>
|
|
<li>dragging and resizing behavior</li>
|
|
<li>modal behavior</li>
|
|
<li>z-order and activation</li>
|
|
<li>theming and skinning</li>
|
|
<li>serialization and asset loading hooks</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Why It Belongs Here</h3>
|
|
<ul>
|
|
<li>accurate editor preview</li>
|
|
<li>fewer runtime and editor mismatches</li>
|
|
<li>one place to fix layout and interaction bugs</li>
|
|
<li>reusable widgets and themes</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="subsection">
|
|
<h3>Core Runtime Systems</h3>
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>UI Manager</h3>
|
|
<ul>
|
|
<li>open windows</li>
|
|
<li>focus order</li>
|
|
<li>active modal</li>
|
|
<li>hovered and pressed widgets</li>
|
|
<li>drag and resize operations</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Window Manager</h3>
|
|
<ul>
|
|
<li>create</li>
|
|
<li>close</li>
|
|
<li>minimize</li>
|
|
<li>maximize</li>
|
|
<li>bring to front</li>
|
|
<li>move</li>
|
|
<li>resize</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Widget Registry</h3>
|
|
<ul>
|
|
<li><code>window</code></li>
|
|
<li><code>panel</code></li>
|
|
<li><code>label</code></li>
|
|
<li><code>button</code></li>
|
|
<li><code>input</code></li>
|
|
<li><code>list</code></li>
|
|
<li><code>tabs</code></li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Theme System</h3>
|
|
<ul>
|
|
<li>fonts</li>
|
|
<li>spacing</li>
|
|
<li>borders</li>
|
|
<li>colors</li>
|
|
<li>states</li>
|
|
<li>animation references</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="engine-api">
|
|
<h2>Runtime API</h2>
|
|
<p class="section-intro">
|
|
The engine should expose stable, high-level APIs that both the editor and the game
|
|
can consume without caring about rendering internals.
|
|
</p>
|
|
<pre><code>ui.loadAsset("quest-log");
|
|
ui.createWindow(windowDefinition, bindings);
|
|
ui.closeWindow("quest-log");
|
|
ui.updateBindings("quest-log", bindings);</code></pre>
|
|
<pre><code>ui.createWindow({
|
|
definition: windowDefinition,
|
|
bindings: {
|
|
data: gameViewModel,
|
|
actions: actionRegistry
|
|
}
|
|
});</code></pre>
|
|
</section>
|
|
|
|
<section class="section-card" id="editor">
|
|
<h2>Editor</h2>
|
|
<p class="section-intro">
|
|
The editor should be an authoring tool for UI assets, not a place where gameplay code
|
|
gets embedded.
|
|
</p>
|
|
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>Responsibilities</h3>
|
|
<ul>
|
|
<li>create windows and widgets visually</li>
|
|
<li>arrange hierarchy</li>
|
|
<li>move and resize elements</li>
|
|
<li>edit properties</li>
|
|
<li>assign themes or style references</li>
|
|
<li>set data-binding keys</li>
|
|
<li>assign symbolic action identifiers</li>
|
|
<li>preview final layout using the shared runtime</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Recommended Early Features</h3>
|
|
<ul>
|
|
<li>hierarchy tree</li>
|
|
<li>canvas or workspace</li>
|
|
<li>property inspector</li>
|
|
<li>theme picker</li>
|
|
<li>save and load</li>
|
|
<li>live preview</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="subsection">
|
|
<h3>Editor Workflow</h3>
|
|
<ol>
|
|
<li>Create or open a UI asset.</li>
|
|
<li>Add a root node such as a <code>window</code> or <code>screen</code>.</li>
|
|
<li>Add child widgets like <code>panel</code>, <code>label</code>, <code>list</code>, or <code>button</code>.</li>
|
|
<li>Configure properties in an inspector.</li>
|
|
<li>Save the asset as JSON.</li>
|
|
<li>Preview the result using the same UI runtime the game uses.</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div class="subsection">
|
|
<h3>Symbolic Actions</h3>
|
|
<p>
|
|
Instead of embedding code, a button should store a stable action id and let the
|
|
game resolve it later.
|
|
</p>
|
|
<pre><code>{
|
|
"action": "inventory.useSelectedItem"
|
|
}</code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="game-runtime">
|
|
<h2>Game Runtime</h2>
|
|
<p class="section-intro">
|
|
The game runtime consumes UI assets and supplies them with live data and behavior.
|
|
It decides when a screen opens, what it displays, and how action ids map into actual
|
|
game systems.
|
|
</p>
|
|
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>Responsibilities</h3>
|
|
<ul>
|
|
<li>deciding when UI opens or closes</li>
|
|
<li>selecting which asset to load</li>
|
|
<li>supplying live data</li>
|
|
<li>resolving symbolic actions into real functions</li>
|
|
<li>updating bindings as game state changes</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Runtime Mutability</h3>
|
|
<ul>
|
|
<li>visibility</li>
|
|
<li>enabled state</li>
|
|
<li>bound text</li>
|
|
<li>bound lists</li>
|
|
<li>dynamic styling flags</li>
|
|
<li>open and closed state of windows</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="subsection">
|
|
<h3>Consumption Flow</h3>
|
|
<ol>
|
|
<li>Load a UI asset.</li>
|
|
<li>Validate it through the shared schema layer.</li>
|
|
<li>Create a runtime instance through the engine UI manager.</li>
|
|
<li>Inject data bindings and action handlers.</li>
|
|
<li>Let the engine handle rendering and interaction.</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div class="subsection">
|
|
<h3>Example Usage</h3>
|
|
<pre><code>const definition = await ui.loadAsset("quest-log");
|
|
|
|
ui.createWindow({
|
|
definition,
|
|
bindings: {
|
|
data: gameViewModel,
|
|
actions: {
|
|
"ui.closeWindow": ({ windowId }) => ui.closeWindow(windowId),
|
|
"inventory.useSelectedItem": () => inventory.useSelectedItem()
|
|
}
|
|
}
|
|
});</code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="ui-assets">
|
|
<h2>UI Assets</h2>
|
|
<p class="section-intro">
|
|
UI assets should start as serializable JSON files. The format should be stable,
|
|
versioned, and expressive enough to define hierarchy, layout, binding, and symbolic
|
|
actions without containing gameplay logic.
|
|
</p>
|
|
|
|
<div class="grid-2">
|
|
<div class="panel">
|
|
<h3>Schema Goals</h3>
|
|
<ul>
|
|
<li>schema versioning</li>
|
|
<li>stable ids</li>
|
|
<li>nested hierarchy</li>
|
|
<li>typed widgets</li>
|
|
<li>layout properties</li>
|
|
<li>style references</li>
|
|
<li>data bindings</li>
|
|
<li>symbolic actions</li>
|
|
</ul>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Recommended Fields</h3>
|
|
<ul>
|
|
<li><code>schemaVersion</code></li>
|
|
<li><code>id</code></li>
|
|
<li><code>type</code></li>
|
|
<li><code>children</code></li>
|
|
<li><code>title</code>, <code>x</code>, <code>y</code>, <code>width</code>, <code>height</code></li>
|
|
<li><code>bind</code>, <code>visibleWhen</code>, <code>enabledWhen</code>, <code>action</code></li>
|
|
<li><code>theme</code>, <code>variant</code>, <code>className</code></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section-card" id="asset-example">
|
|
<h2>Asset Example</h2>
|
|
<p class="section-intro">
|
|
A typical authored window should look like structured data the engine can interpret
|
|
consistently in both the editor and the game.
|
|
</p>
|
|
<pre><code>{
|
|
"schemaVersion": 1,
|
|
"id": "quest-log",
|
|
"type": "window",
|
|
"title": "Quest Log",
|
|
"x": 120,
|
|
"y": 80,
|
|
"width": 420,
|
|
"height": 300,
|
|
"flags": {
|
|
"draggable": true,
|
|
"resizable": true,
|
|
"closable": true
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "quest-list",
|
|
"type": "list",
|
|
"bind": "quests.active"
|
|
},
|
|
{
|
|
"id": "close-button",
|
|
"type": "button",
|
|
"text": "Close",
|
|
"action": "ui.closeWindow"
|
|
}
|
|
]
|
|
}</code></pre>
|
|
</section>
|
|
|
|
<section class="section-card" id="versioning">
|
|
<h2>Versioning</h2>
|
|
<p class="section-intro">
|
|
Every asset should declare a schema version so the format can evolve without breaking
|
|
older editor output.
|
|
</p>
|
|
|
|
<div class="callout">
|
|
<strong>Migration Policy</strong>
|
|
<p>
|
|
When the asset format changes, add migration functions in the shared schema layer.
|
|
The editor can write the newest version, while the game and engine can continue to
|
|
load older assets by upgrading them during validation.
|
|
</p>
|
|
</div>
|
|
|
|
<pre><code>{
|
|
"schemaVersion": 1
|
|
}</code></pre>
|
|
|
|
<div class="subsection">
|
|
<h3>Long-Term Extensions</h3>
|
|
<ul>
|
|
<li>prefabs and templates</li>
|
|
<li>inherited style variants</li>
|
|
<li>animation descriptors</li>
|
|
<li>localization keys</li>
|
|
<li>responsive layout rules</li>
|
|
<li>asset references to icons and images</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<p class="footer-note">
|
|
Manual generated from the UI system design notes for Worldshaper.
|
|
</p>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const menuButton = document.querySelector(".menu-button");
|
|
const sidebar = document.querySelector(".sidebar");
|
|
const navLinks = [...document.querySelectorAll(".nav-list a")];
|
|
const sections = navLinks
|
|
.map((link) => document.querySelector(link.getAttribute("href")))
|
|
.filter(Boolean);
|
|
|
|
if (menuButton && sidebar) {
|
|
menuButton.addEventListener("click", () => {
|
|
const open = sidebar.classList.toggle("open");
|
|
menuButton.setAttribute("aria-expanded", String(open));
|
|
});
|
|
}
|
|
|
|
const activateLink = (id) => {
|
|
navLinks.forEach((link) => {
|
|
const active = link.getAttribute("href") === `#${id}`;
|
|
link.classList.toggle("active", active);
|
|
});
|
|
};
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const visible = entries
|
|
.filter((entry) => entry.isIntersecting)
|
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
|
|
|
if (visible) {
|
|
activateLink(visible.target.id);
|
|
}
|
|
},
|
|
{
|
|
rootMargin: "-20% 0px -60% 0px",
|
|
threshold: [0.15, 0.35, 0.6]
|
|
}
|
|
);
|
|
|
|
sections.forEach((section) => observer.observe(section));
|
|
|
|
if (sections[0]) {
|
|
activateLink(sections[0].id);
|
|
}
|
|
|
|
navLinks.forEach((link) => {
|
|
link.addEventListener("click", () => {
|
|
if (window.innerWidth <= 980 && sidebar) {
|
|
sidebar.classList.remove("open");
|
|
menuButton?.setAttribute("aria-expanded", "false");
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|