Agent Skillsjasonkneen/tiny-world-builder › tinyworld-runtime-state

tinyworld-runtime-state

GitHub

管理TinyWorld应用的用户持久化状态,涵盖localStorage读写、云同步及默认值快照。指导渲染设置恢复、数据迁移、API交互及JSON导入导出逻辑,确保状态一致性与正确性。

.codex/skills/tinyworld-runtime-state/SKILL.md jasonkneen/tiny-world-builder

Trigger Scenarios

修改用户设置或持久化状态 实现云保存或共享功能 处理本地存储与云端的数据同步

Install

npx skills add jasonkneen/tiny-world-builder --skill tinyworld-runtime-state -g -y
More Options

Non-standard path

npx skills add https://github.com/jasonkneen/tiny-world-builder/tree/main/.codex/skills/tinyworld-runtime-state -g -y

Use without installing

npx skills use jasonkneen/tiny-world-builder@tinyworld-runtime-state

指定 Agent (Claude Code)

npx skills add jasonkneen/tiny-world-builder --skill tinyworld-runtime-state -a claude-code -g -y

安装 repo 全部 skill

npx skills add jasonkneen/tiny-world-builder --all -g -y

预览 repo 内 skill

npx skills add jasonkneen/tiny-world-builder --list

SKILL.md

Frontmatter
{
    "name": "tinyworld-runtime-state",
    "description": "Use when adding or changing persisted user state — settings defaults, audio, camera\/orbit, panel positions, feature flags, and the in-app \"Save Defaults\" pipeline that snapshots localStorage into tinyworld-defaults.json. Also covers the inline-script regex gotcha that has burned us twice."
}

Tiny World Runtime State

Most browser-local persisted user state lives in localStorage under the tinyworld:* prefix. Read/write convention: stringified primitives or JSON.stringify for objects. Never store credentials, local world saves, cloud world saves, or per-viewport pixel positions in the shipped defaults file — see exclusion list below.

Persisted render/material settings that affect shared Three materials must be re-applied during late boot, not only from control input handlers. In particular, material wear (tinyworld:render:materialWear) needs the applyPersistedMaterialSettingsOnBoot() pass so saved wear is visible on first render without toggling the slider.

Builder directional sun defaults to 10.0 (1000%) under tinyworld:render:directionalSun. Keep the one-time migration narrow: upgrade missing/old untouched 1.0 builder values to 10.0, but preserve clearly user-edited non-default values. Island Viewer has its own tinyworld:island-viewer:* graphics keys and migrates its old 1.1 default separately.

Cloud saves are separate from defaults/localStorage:

  • The account modal posts full TinyWorld JSON to Netlify Functions (/api/builds) backed by Netlify Database.
  • On authenticated boot, local named worlds from tinyworld:worlds.v1 are uploaded to /api/builds; the active unslotted tinyworld:v1 state gets a local slot first so it can be bound to a cloud row. Top-menu "My worlds" and account-modal "My Worlds" must read from the same cloud-aware list.
  • The world menu's share action posts the same full state to /api/share; public share URLs load by resolving ?share=<id> to same-origin /api/share?id=<id>.
  • Local custom assets are also synced once authenticated. /api/assets stores custom voxel-build stamps and saved asset templates, then merges the remote library into localStorage before pushing the merged local copy back up.
  • Keep snapshotCurrentState() in sync with saveState() so account saves and share URLs include grid size, islands, moorings, custom voxel stamps, camera, landscape settings, and cells outside the home board that the user edited.
  • Top-bar JSON import should accept the app's own portability shapes: a bare world state (cells at the root), cloud/account envelopes (data or state containing a world), named-world/localStorage lists, and exported asset bundles. Imported worlds should be inserted into tinyworld:worlds.v1 so the account DB sync can pick them up after login.
  • The visible top-bar JSON import affordance should be a native <label for="import-file"> trigger with an off-screen file input, not only a button that programmatically clicks a hidden input. Some browsers silently drop hidden-input file picker calls even when the click handler ran.
  • Queued account syncs must not be dropped while a previous /api/builds request is in flight. Keep a pending retry flag around twCloudWorldSyncing so imports and saves made during bootstrap still reach the database.
  • Live multiplayer rooms are ephemeral runtime state. Keep PartyKit presence (cursor, selected cells, active tool) out of saved world JSON and send durable edits as full cell.set snapshots, then apply them through setCell() so rendering and later account saves stay on the normal persistence path.

Defaults pipeline (dev → all users)

There is a "Save Defaults" button in Settings → Workspace (visible only on localhost / 127.0.0.1 / file:). When clicked:

  1. The browser snapshots every tinyworld:* localStorage key (minus the exclusion list).
  2. POSTs { settings: { key: value, ... } } to /api/save-defaults.
  3. tools/dev-server.js writes the result to tinyworld-defaults.json at the repo root.
  4. publish.sh copies that file into dist/ so it ships with the site.
  5. On every page load, the first inline <script id="tinyworld-defaults-bootstrap"> does a synchronous XMLHttpRequest for tinyworld-defaults.json. For each key the user does NOT already have in localStorage, it seeds the default. Existing user prefs win — defaults never overwrite.

The bootstrap script MUST have an attribute (e.g. id="tinyworld-defaults-bootstrap") so the tools/check.js regex doesn't grab it. See the inline-script gotcha below.

Exclusion list (must stay in sync, two copies)

Mirror these regexes in both tools/dev-server.js (server filter) and the inline setupDevSaveDefaults() IIFE (client filter):

  • /^tinyworld:v\d+$/ — serialised home world
  • /^tinyworld:worlds\.v\d+/ — multi-world saves
  • /^tinyworld:ai:key:/ — API credentials (SECURITY)
  • /^tinyworld:auth:/ — account/session credentials (SECURITY)
  • /^tinyworld:ai:prompt$/ — user prompt text
  • /^tinyworld:vehicle-demo:/ — session demo state
  • /^tinyworld:audio:music-track$/ — per-user manual music choice
  • /^tinyworld:audio:music-mode$/ — random vs manual music mode
  • /^tinyworld:welcome:dismissedId$/ — per-user welcome dismissal
  • /:backup$/ — any explicit backup
  • /\.pos$/, /-pos$/, /:pos$/ — panel/widget positions (viewport-specific)

If you persist a new value that should NOT ship as a default, add a matching pattern to both lists in the same change.

Panel/widget positions — RELATIVE, not pixels

Draggable panels (minimap, crowd panel, agent panel, future panels) MUST save their position as percentage of viewport, not absolute pixels. Absolute pixels saved on a wide monitor land off-screen for users on smaller displays.

Format:

localStorage.setItem(KEY, JSON.stringify({
  topPct: +(r.top / window.innerHeight).toFixed(4),
  leftPct: +(r.left / window.innerWidth).toFixed(4),
}));

Read with backward compatibility for legacy absolute values:

let top, left;
if (Number.isFinite(p.topPct) && Number.isFinite(p.leftPct)) {
  top = p.topPct * window.innerHeight;
  left = p.leftPct * window.innerWidth;
} else if (Number.isFinite(p.top) && Number.isFinite(p.left)) {
  top = p.top; left = p.left;
}

Always re-apply on window.addEventListener('resize') and clamp to [8, innerWidth - w - 8] / [8, innerHeight - h - 8].

The existing minimap implementation (clampMinimapPosition / setMinimapPosition / applyStoredMinimapPos / endMinimapDrag) is the reference pattern. Minimap collapse must shrink in place; do not use a translateX(...) trick that pushes the map outside the viewport.

The AI chat panel is a fixed right-side rail, not a draggable bottom prompt. Persist only width/collapse state under tinyworld:agent:panel-pos (the -pos suffix keeps it out of shipped defaults). Do not restore absolute left/top coordinates for the AI chat; it should stay anchored to the right edge, with a left-edge resize grip and a compact collapsed rail.

Audio system

Two layers:

  1. HTMLAudioElement for music (looped) and one-shot SFX (cloned per play).
  2. Web Audio (PannerNode/StereoPannerNode) for positional sources (engines, water) — distance attenuation + L/R pan based on (sourceWorldPos - camera.position) projected onto camera-right.

State keys (AUDIO_LS):

  • tinyworld:audio:music / music-muted / music-track / music-mode
  • tinyworld:audio:sfx / sfx-muted
  • tinyworld:audio:ambient / ambient-muted
  • tinyworld:audio:engines / engines-muted

Music tracks: MUSIC_TRACKS array (currently 6 horizon + 1 rising). Random playback must use only MUSIC_RANDOM_TRACKS / the music-horizon-* files; music-rising-1.mp3 stays selectable manually but should not ship as a default or be picked by automatic random playback. Avoid prop engine files (large-prop-engine-*, foley-propellers-*) — the planes have jet engines, use foley-rocket-engines-1..4. Water variants: foley-water-1..4. Loop seams are hidden by overlaying two variants at different start offsets and per-source gains.

UI: single #sound-icon button lives inside the toolbar (appended in buildToolbar() near the audio panel reference). Click toggles the floating #sound-panel with track list + 4 volume rows (Music, Effects, Ambient, Engines). currentMusicTrack() resolves the persisted choice or random.

Camera / view persistence

Single key tinyworld:view.camera holds:

{ "mode": "perspective", "azimuth": 1.2, "polar": 0.9, "viewSize": 8.2,
  "target": { "x": 0, "y": 0, "z": 0 } }

updateCamera() schedules a throttled save (250ms debounce) every frame the camera changes. On boot, the let declarations read this key and apply with clamping (clampViewSize, MIN_ORBIT_POLAR/MAX_ORBIT_POLAR). Ships in defaults — sets the welcome shot for new users.

Feature flags

  • tinyworld:worlds.activeTinyverse.v1 is per-device navigation state for refreshing back into the last active Tinyverse world. It is not a world save, not account-synced, and must stay excluded from shipped defaults in both tools/dev-server.js and the client Save Defaults filter.
  • Tinyverse room teardown and user-facing exit are separate concerns: 47-worlds-room.js internal leaveRoom() should only tear down sockets/HUDs/ avatars/minimaps. User-facing island exits should call WS.exitToWorldPicker(), which clears tinyworld:worlds.activeTinyverse.v1, restores the pre-room builder state through WS.restoreFreeform(), and opens the picker overlay. Do not hide picker navigation inside minimap or teardown helpers; that has previously exposed legacy multi-gate selector boards.
  • Tinyverse room play mode is temporary. Use __tinyworldMode.setPlayTemporary() when forcing multiplayer play chrome, do not persist that to tinyworld:build-play-mode.v1, and make room exit restore Build so the toolbar is not trapped hidden after refresh/exit.
  • In-app Home controls should reopen the reusable welcome/launch modal through window.__tinyworldShowWelcomeLaunch() instead of navigating to / or logging the user out. That launch modal is the canonical route back to Tinyverse / Battleworlds / Build.
  • World-selection stargates are real in-island travel points again, but only as one center gate per island. The /api/worlds normalization and the client universe overlay should strip legacy multi-gate cells, replace any center object with a single { kind: 'stargate', dest: '__world-picker' }, and keep the picker itself as UI chrome rather than a Nexus board.
  • Tinyverse world entry must use the real /api/worlds detail response and its signed join token, including deploy-preview/test hosts. Do not create tokenless client-only preview worlds for entry; a PartyKit world room with a join secret will correctly downgrade those joins to observer.
  • PartyKit world-room walkability must be server-authoritative and solid by default: only empty tiles, water, bridges, stargates, plants/animals, and low ground cover are standable. Buildings, trees, rocks, fences, model stamps, voxel builds, and unknown future object kinds must block movement.
  • tinyworld:features:cluso — legacy Cluso flag; no app runtime path reads this key. The Cluso embed is now injected local-dev-only by tools/dev-server.js (see tinyworld-single-file SKILL), not gated by this key.
  • tinyworld:features:ai — AI panel. AI surfaces ([data-ai-interface]) are hidden on prod via html.ai-disabled, enabled by local host / ?ai=1 / this flag. Additionally, signed-in accounts whose email is in AI_ACCOUNT_ALLOWLIST (in 30-ui-boot-wiring.js) unlock AI live on login (applyAccountAiEntitlement) and revert on logout — tied to the account, not persisted to this key.
  • tinyworld:features:model-stamp-api — stamp-defaults dev endpoint.

Inline <script> gotcha (read this!)

tools/check.js uses this regex to extract the main app script:

html.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/);

It matches the first plain <script> through to the last </script></body>. If you add an extra inline <script> block (e.g. a bootstrap loader), it MUST have an attribute so the regex skips it:

<script id="my-bootstrap">...</script>   <!-- ✓ regex ignores -->
<script>...</script>                     <!-- ✗ would be conflated -->

Symptom when wrong: npm test fails with inline app script syntax error: Unexpected token '<' because the regex grabbed your bootstrap + the </script><script> separator + the main app.

Validation

After any persistence change:

  1. node tools/check.js — inline JS syntax + schema parity.
  2. node tools/smoke-static.js — no-browser smoke.
  3. Browser at http://localhost:3000/tiny-world-builder with clean localStorage in a fresh tab — confirm defaults seed correctly and the app doesn't error.
  4. Then with existing localStorage — confirm user prefs are NOT overwritten.

Common pitfalls

  • Saving panel positions as absolute pixels (do RELATIVE %).
  • Persisting an API key, prompt text, or world save into defaults (add to exclusion list in both server + client).
  • Adding a new inline <script> without an attribute (breaks npm test).
  • Forgetting to restart npm run dev after editing tools/dev-server.js — the running process won't have the new route, returns 405.
  • Removing a temporary <input type="file"> while the native file picker is still open. Dynamic JSON pickers should clean up after change/cancel, not via a short timeout.
  • Letting a hard-coded camera default drift from DEFAULT_AZIMUTH/ DEFAULT_POLAR/DEFAULT_TARGET — keep restored state clamped to those ranges.

Export ↔ saveState parity (full portability)

The JSON file export (#export handler in 20-input-place-erase.js) must serialize the same payload as saveState() (29-persistence-api.js) so an imported world is fully self-contained. Both include: islands (serializeEditableIslands), moorings (serializeMooringCables, carries each cable's style), cells, voxelBuildStamps (referencedVoxelBuildStamps(cells) — inlines custom block voxels/customParts/footprint), camera, landscape, and planetLandscape. applyState() restores voxelBuildStamps on import. Model stamps are bundled manifest assets referenced by appearance.modelStampId (no binary to embed). When adding any new persisted world concept, add it to both saveState and the export object, and handle it in applyState.

Version History

  • 2ffaf91 Current 2026-07-06 00:20

Same Skill Collection

.agents/skills/lightweight-3d-effects/SKILL.md
.agents/skills/threejs-primitive-reconstructor/SKILL.md
.agents/skills/tinyworld-i18n/SKILL.md
.claude/skills/tinyworld-i18n/SKILL.md
.codex/skills/threejs-primitive-reconstructor/SKILL.md
.codex/skills/tinyworld-25d-template-sprites/SKILL.md
.codex/skills/tinyworld-asset-editing/SKILL.md
.codex/skills/tinyworld-auto-batching/SKILL.md
.codex/skills/tinyworld-block-button-style/SKILL.md
.codex/skills/tinyworld-cctv-truman/SKILL.md
.codex/skills/tinyworld-ghost-world-gen/SKILL.md
.codex/skills/tinyworld-integrations/SKILL.md
.codex/skills/tinyworld-island-and-planes/SKILL.md
.codex/skills/tinyworld-island-viewer/SKILL.md
.codex/skills/tinyworld-lowpoly-stylized-3d/SKILL.md
.codex/skills/tinyworld-lowpoly-world-prompt/SKILL.md
.codex/skills/tinyworld-mesh-terrain/SKILL.md
.codex/skills/tinyworld-opacity-torch/SKILL.md
.codex/skills/tinyworld-render-performance/SKILL.md
.codex/skills/tinyworld-settings/SKILL.md
.codex/skills/tinyworld-shader-fx/SKILL.md
.codex/skills/tinyworld-single-file/SKILL.md
.codex/skills/tinyworld-tile-variation/SKILL.md
.codex/skills/tinyworld-tinyverse-collectibles/SKILL.md
.codex/skills/tinyworld-tinyverse-race-track/SKILL.md
.codex/skills/tinyworld-tool-icons-and-modes/SKILL.md
.codex/skills/tinyworld-visual-qa/SKILL.md
.codex/skills/tinyworld-webxr/SKILL.md
.agents/skills/3d-modeling/SKILL.md
.agents/skills/fractal/SKILL.md
.agents/skills/poly-pizza-api/SKILL.md

Metadata

Files
0
Version
8afdf00
Hash
b6806654
Indexed
2026-07-06 00:20

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-04 07:04
浙ICP备14020137号-1 $Гость$