diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 95e1598f..3922630b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -22,6 +22,23 @@ ClawDnD's MCP servers depend on these packages: - **httpx** (BSD-3-Clause) — HTTP client for the dnd5eapi.co fallback. - **kokoro** (Apache-2.0) — local text-to-speech (added with the voice backend). +## Bundled browser runtime assets + +The local OpenWorlds viewer surface vendors these files under +`viewer/openworlds/vendor/` so the packaged macOS app can render without live CDN +dependencies: + +- **React 18.3.1** (`react-18.3.1.development.js`, MIT) +- **ReactDOM 18.3.1** (`react-dom-18.3.1.development.js`, MIT) +- **Babel Standalone 7.29.0** (`babel-standalone-7.29.0.min.js`, MIT) +- **Google Fonts families** (`Cinzel`, `Cormorant Garamond`, + `IM Fell English`, `JetBrains Mono`, open font licenses) + +See `viewer/openworlds/vendor/THIRD_PARTY_NOTICES.md` for source URLs and file +hashes. These development-mode runtime files are a fidelity bridge for the first +local packaged surface; release hardening should replace Babel-in-browser with a +compiled bundle while keeping all assets local. + ## Structural references — learn-from only (NO code copied) These projects informed ClawDnD's design. After verifying their licensing diff --git a/viewer/openworlds/SOURCE.md b/viewer/openworlds/SOURCE.md new file mode 100644 index 00000000..ad69fc4b --- /dev/null +++ b/viewer/openworlds/SOURCE.md @@ -0,0 +1,48 @@ +# OpenWorlds Surface Source Contract + +This directory hosts the first exact-fidelity OpenWorlds browser surface for the +local viewer. It is intentionally a viewer asset bundle, not a new engine, +rules, voice, or campaign-state authority. + +## Source Artifact + +- Source artifact: `OpenWorlds.zip` +- Source SHA256: `8e9e2b885764fd3492b74b2d02eda5db9827eb087121054e8ca52e9ace10fd0a` +- Extraction label used during review: `openworlds-design-2026-05-25/openworlds` +- Provenance: internal design handoff for ClawDnD/OpenWorlds implementation, not + a blanket license for third-party screenshots, uploads, or generated images. + +## Included Source Files + +The production viewer route maps source files into this directory as follows: + +| Source artifact entry | Repo path | +| --- | --- | +| `openworlds/Open Worlds.html` | `viewer/openworlds/index.html` | +| `openworlds/styles.css` | `viewer/openworlds/styles.css` | +| `openworlds/data.js` | `viewer/openworlds/data.js` | +| `openworlds/app.jsx` | `viewer/openworlds/app.jsx` | +| `openworlds/chrome.jsx` | `viewer/openworlds/chrome.jsx` | +| `openworlds/tooltip.jsx` | `viewer/openworlds/tooltip.jsx` | +| `openworlds/toast.jsx` | `viewer/openworlds/toast.jsx` | +| `openworlds/camp-sidebar.jsx` | `viewer/openworlds/camp-sidebar.jsx` | +| `openworlds/screen-*.jsx` | `viewer/openworlds/screen-*.jsx` | + +## Intentional Exclusions + +- `openworlds/tweaks-panel.jsx` is excluded from the production route. The app + still keeps the prototype tweak defaults in `app.jsx`, but no Open Design + sandbox panel is loaded. +- `openworlds/screenshots/*`, uploaded images, and reference captures are not + included. They remain reference-only unless a later PR clears provenance. +- Live CDN runtime imports are not used. React, ReactDOM, Babel standalone, and + the Google font files required by the prototype are vendored under + `viewer/openworlds/vendor/`. + +## State Boundary + +For this PR, `data.js` is non-canonical demo data used only to render the exact +surface. Future PRs replace those rows with read-only viewer APIs. The browser +may read viewer endpoints and, once backed actions exist, post player intent to +`POST /move`; it must not write `snapshot.json`, `play-state`, `qa/state`, +inventory, quests, XP, world clocks, or companion state directly. diff --git a/viewer/openworlds/app.jsx b/viewer/openworlds/app.jsx new file mode 100644 index 00000000..e8499bed --- /dev/null +++ b/viewer/openworlds/app.jsx @@ -0,0 +1,179 @@ +/* App router + tweaks */ + +const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ + "palette": "warm", + "ornaments": true, + "candle": true +}/*EDITMODE-END*/; + +function App() { + const [state, setState] = React.useState(window.INITIAL_STATE || {}); + const [screen, setScreen] = React.useState("launcher"); + const [campMode, setCampMode] = React.useState(false); + const [t, setTweak] = (window.useTweaks + ? window.useTweaks(TWEAK_DEFAULTS) + : [TWEAK_DEFAULTS, () => {}]); + + React.useEffect(() => { + document.documentElement.setAttribute("data-palette", t.palette || "warm"); + }, [t.palette]); + + // Keyboard shortcuts + React.useEffect(() => { + const onKey = (e) => { + const target = e.target; + if (e.metaKey || e.ctrlKey || e.altKey) return; + if ( + target instanceof Element && + (target.closest("input, textarea, select, [contenteditable='true']") || + target.isContentEditable) + ) { + return; + } + const map = { + "t": "table", + "x": "combat", + "p": "dialogue", + "m": "map", + "c": "character", + "i": "inventory", + "f": "forge", + "r": "relations", + "j": "journal", + "b": "bestiary", + "a": "acts", + "$": "merchant", + "w": "launcher", + "n": "create", + "s": "seed", + ",": "settings", + "?": "settings", + }; + const id = map[e.key.toLowerCase()]; + if (id) { + e.preventDefault(); + setScreen(id); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + const navigate = (id, opts) => { + if (opts?.openCamp) setCampMode(true); + setScreen(id); + }; + + const campaigns = Array.isArray(state?.campaigns) ? state.campaigns : []; + const current = + campaigns.find((c) => c.id === state?.activeCampaign) || + campaigns[0] || + { title: "Open Worlds", day: "" }; + + return ( +
+ The chronicle holds the page in reserve. What it will say depends on the road taken. +
+{act.synopsis}
+ ++ The chronicle has heard rumour of this, but has not yet seen it with its own eyes. Investigate, encounter, or be told to fill this page. +
++ {entry.body} +
+ + {entry.tactics && ( + <> +{entry.tactics}
+ > + )} + + {entry.loot && ( + <> ++ {hero.lineage} +
+ ++ {hero.name} finds a quiet hour. The chronicle pauses with them. Choose what kind of rest to claim from the road. +
+ ++ {hero.name} reads by the dying fire. Choose what will be at hand when the day breaks. Unchosen spells remain bound to the page. +
+ + {availableSpells.map((group) => { + const cur = prepared[group.level] || []; + const max = slots[group.level] || 0; + if (max === 0) return null; + return ( ++ Lineage decides not your worth but your starting reach. Most heroes find themselves regardless. +
++ Your calling chooses what you wake up doing every morning. It does not choose what wakes you. +
++ Twenty points, six gifts. Higher scores cost more. Pathfinder rules. +
++ When you press Bind, the chronicle records this hero. The choices below this line will be remembered by every road taken hereafter. There is no penalty for hesitation; the parchment is patient. +
+{selected.desc}
+ +
+ Read a tome. Apprentice a master. Find a scroll.
+ The chronicle will record the moment.
+
+ {item.desc} +
+ ++ {quest.entry} +
+ + {quest.entries?.map((e, i) => ( +{e.text}
++ {c.recap} +
++ {selected.description} +
+ ++ {merchant.greeting} +
+ +| + | Item | +Kind | +Weight | +Price | ++ |
|---|---|---|---|---|---|
|
+ |
+
+ {it.name}
+ {it.qty > 1 && ×{it.qty}}
+ |
+
+ |
+ + {it.weight || "—"} + | ++ + {shownPrice} + + gp + | ++ + | +
{f.body}
+ +{n.body}
+ ++ This chronicle is sown for a heroic register in the post-tabletop tradition of Brevoy and the Stolen Marches. The reading voice is communal — we, when we walked; we, when we found. Decisions are remembered. Failure is rarely permanent but always written down. Salt and silence have meanings the rules will not state. +
++ Open Worlds is a chronicle engine for tabletop games — a parchment laid across a Mac window, kept by an attentive but unintrusive scribe. Built by candlelight, intended for long roads and patient evenings. +
+