Skip to content

Single-click select, multi-select modifiers, glued radial menu#22

Open
jasonkneen wants to merge 5 commits into
mainfrom
improve-object-selection-radial-menu
Open

Single-click select, multi-select modifiers, glued radial menu#22
jasonkneen wants to merge 5 commits into
mainfrom
improve-object-selection-radial-menu

Conversation

@jasonkneen
Copy link
Copy Markdown
Owner

@jasonkneen jasonkneen commented May 30, 2026

Summary

Improves object selection and fixes the radial popup menu jitter when the camera moves.

Selection (Select tool)

  • Plain click selects the single object under the cursor and shows the radial menu (was a deliberate no-op before).
  • Shift+click extends a contiguous range (rectangle from the last selected cell to the clicked cell, additive).
  • Cmd/Ctrl+Shift+click toggles an individual cell on/off — discontiguous "different places" selection. (Re-enables a Cmd/Ctrl modifier that was previously disabled, scoped to Cmd+Shift so it won't fire while reaching for a system shortcut.)
  • Empty click clears the selection.
  • Shift+drag marquee is unchanged and still works with all tools.
  • Wires up the previously-unused toggleCellSelection helper; reuses existing setRectangleSelection / clearSelection.

Radial menu jitter

Root cause: updateCamera() sets camera.position + lookAt but never calls camera.updateMatrixWorld(), so tickRadialMenu projected the menu against the previous frame's camera during orbit, causing it to swim. Fix:

  • Call camera.updateMatrixWorld() before projecting the gizmo position.
  • Position the menu via subpixel translate3d (GPU-composited) instead of snapped left/top.

Files

  • engine/world/20-input-place-erase.js — pointerdown/pointerup selection state machine + lastSelectionAnchor.
  • engine/world/33-radial-menu.js — matrix refresh + translate3d positioning.
  • styles/tiny-world.css.radial-menu anchoring.

Verification (real app, trusted input events, projection math)

  • Jitter: menu screen position equals ground-truth object projection every orbit frame (max offset 0px) and at rest — glued, no swim.
  • Instant circular menu: plain click on an object shows the 8-button ring the same frame (framesUntilVisible: 0).
  • Shift+click range (4-cell run), Cmd+Shift toggle (add then remove), empty-click clear — all pass.
  • Regression: Shift+drag marquee intact (24-cell rectangle); 0 console errors; npm run smoke passes.

Note

  • npm run check fails on a pre-existing mismatch between the embedded WORLD_SCHEMA and world.schema.json, unrelated to this change (neither file is touched here; the JS syntax gate passes). Worth a separate fix.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added radial menu for quick object selection and property adjustments
    • Implemented grid-based island placement system with 8-slot positions
    • Added asset library export/import for sharing custom objects across devices
    • Enabled world loading directly from URLs via query parameters
  • Bug Fixes

    • Fixed minimap drag responsiveness on touch devices
    • Improved mooring cable routing to avoid collision hazards
  • Enhancements

    • Enhanced NPC crowd navigation with improved pathfinding
    • Improved terrain rendering for custom islands

Review Change Stack

Introduce a prototype radial object menu and add first-class support for bespoke custom voxel objects. Changes include:

- Add engine/world/33-radial-menu.js: new DOM radial menu with color, size, rotate, move, duplicate and drill-down color subring; exposes tickRadialMenu() for per-frame positioning and actions.
- Wire tickRadialMenu into the animation loop (25-animation-loop-schema.js) and include the new script in tiny-world-builder.html.
- Add CSS for the radial menu in styles/tiny-world.css.
- Expand world schema to support customParts/customName/customPart so AI-authored bespoke objects can be described as arrays of low-poly primitives.
- Add materializeCustomPartCells in persistence (29-persistence-api.js) to convert cell.customParts into registered voxel build stamps and rewrite cells to reference voxelBuildId before normalization/validation; call this during applyState/applyStatePatch.
- Update AI object/enhancement code (21-object-transform-voxel-build.js, 26-ai-generation.js) to avoid defaulting to Japanese-themed seeds, broaden the enhancement trigger regex, allow creation of brand-new objects (not just enhancement), and instruct the scene/primitive assembler to propose and place hero/landmark objects that can be converted to bespoke customParts later.

These changes enable the editor to accept AI-generated custom objects, materialize them into stamps, and provide a quick radial UI for common object actions.
Add support for loading a world from the ?world URL param during boot (sanitized same-origin URL): show a placeholder scene immediately, reset camera defaults, and fetch+swap in the remote world when it arrives. Update the radial menu to recognize island selections: restrict actions to those supported by island transforms (move/rotate), apply rotation directly to the selected island (and call applyEditableIslandTransform if present), and rebuild the root ring when selection type changes. Update publish.sh to copy a local data/ directory into dist/data so same-origin world JSON can be published. Also add fork-improvements-report.md documenting fork findings and recommended lifts.
Introduce lightweight storage/toast utilities (twToast, twSafeSetItem, twDownloadJSON, twPickJSONFile) to surface localStorage errors and handle safe saves/downloads. Embed/export/import support for custom voxel-build stamps and asset templates (referencedVoxelBuildStamps, registerEmbeddedVoxelBuildStamps, exportAssetLibrary, importAssetLibrary), and persist embedded custom builds when saving worlds. Add an 8-slot island placement workflow: snapping hologram slots, nearest-slot selection, hover/placement integration, and an on-tool-select snap. Replace several direct localStorage.setItem calls with twSafeSetItem and tidy save payload handling. Also adjust ghost generation shadow flags so tile plates neither cast nor receive shadows. Minor wiring for UI menu items and cross-module island helpers exposed on window.
Unify and harden editable-island behavior, crowd routing, mooring routing, and related UI/tooling. Key changes:

- Model/engines: align engine placement offsets, use shared lift-engine system for home/islands, preserve original rocket engine as 'heavy', adjust propeller shader tint/alpha and prop scales, and wire home engine build (buildHomeIslandEngines) so home engines are selectable/upgradeable.
- Editable islands: render island terrain per-cell (parity with home), add ensureEditableIslandCellTiles, and adjust selection rendering so editable-island cells always render correctly.
- Crowd/AI: expand walkable terrain set, introduce path-cell detection and a BFS grid pathfinder with segment/check/simplify utilities, implement path-biased wander routes (favor roads but roam land), and spawn logic that prefers paths while avoiding obstacles.
- Moorings: add hazard avoidance when routing cables (MOORING_HAZARD_CLEARANCE and avoidMooringHazards) so cables route around engine hazards instead of failing; remove previous blocker rejection for engine hazards.
- World logic & tools: fix neighbor-level checks to respect island-local GRID bounds, make island-placement anchor fallback to home, ensure new islands are created unselected (select:false), detect occupied visual slots, hide island ghost when no free slot, and add a menu item to clear all sky-islands.
- UI: reveal the agent panel when engine targets are selected so controls are visible.
- Assets: add three JPEG assets (home-default-view.jpeg, island-percell-terrain.jpeg, jet-original.jpeg).

These changes improve parity between home and editable islands, prevent routing/placement failures, and make crowd motion robust around obstacles.
Selection (Select tool): plain click selects one object and shows the
radial menu; Shift+click extends a contiguous range from the last anchor;
Cmd/Ctrl+Shift+click toggles individual cells (discontiguous); empty click
clears. Shift+drag marquee unchanged. Wires up the previously-unused
toggleCellSelection helper.

Radial menu: fix jitter during camera moves. updateCamera() never refreshed
matrixWorldInverse, so the menu projected against a one-frame-stale camera
while orbiting. Call camera.updateMatrixWorld() before projecting and position
via subpixel translate3d instead of snapped left/top.
Copilot AI review requested due to automatic review settings May 30, 2026 14:45
@netlify
Copy link
Copy Markdown

netlify Bot commented May 30, 2026

Deploy Preview for tiny-world-builder failed.

Name Link
🔨 Latest commit b3b89fb
🔍 Latest deploy log https://app.netlify.com/projects/tiny-world-builder/deploys/6a1af81930f3fe000850c478

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces an 8-slot grid-based sky island placement system with radial context menu, completely overhauls crowd navigation with tile-based walkability and BFS pathfinding, implements custom voxel-part materialization with URL-based world loading, and adds comprehensive client-side utilities for storage and file I/O. It also refines AI prompts for custom object generation and includes fork-improvement analysis.

Changes

Island Placement and Editable Island System

Layer / File(s) Summary
Home Island Model and Engine Infrastructure
engine/world/14-editable-islands-moorings.js, engine/world/09-model-stamp-loader.js
Models home world as an editable island for engine selection; implements per-cell terrain tile rendering; switches from legacy rocket engines to editable lift engines with shader color/alpha tuning and propeller scaling.
Island Tool UI and 8-Slot Placement System
engine/world/20-input-place-erase.js, engine/world/19-tools-toolbar.js, engine/world/18-scene-pick-xr.js
Adds 8-neighbor slot grid around anchor islands; snaps hologram to nearest free slot; integrates slot selection into input flow; triggers hologram snap on tool selection.
Click Selection and Marquee Input Rework
engine/world/20-input-place-erase.js
Refactors pointer input to snapshot hit and modifiers at pointerdown; supports Cmd/Ctrl+Shift cell toggle, Shift+click range extension, and modifier-aware click resolution on pointerup.
Mooring Hazard Avoidance and Island Neighbor Bounds
engine/world/14-editable-islands-moorings.js, engine/world/16-drop-anim-adjacency.js
Implements mooring cable routing that pushes intermediate control points away from engine hazards; fixes level-neighbor lookups to respect island-aligned board boundaries instead of global grid bounds.
Cell Mesh Rendering for Editable Islands
engine/world/12-selection-tool.js
Ensures editable-island cells always render per-cell meshes regardless of whether world data exists.

Radial Context Menu System

Layer / File(s) Summary
Radial Menu Implementation
engine/world/33-radial-menu.js
IIFE-based radial menu with SVG icons, root and color submenus, duplicate/rotate/scale/panel-open actions, click feedback, and per-frame camera projection for screen positioning.
Radial Menu Styling and Bootstrap
styles/tiny-world.css, tiny-world-builder.html, engine/world/25-animation-loop-schema.js
Adds CSS layout with pop/pulse animations, swatch variants, and stagger delays; wires tickRadialMenu into animation loop; includes script loading in HTML boot.

Crowd Navigation Walkability and Pathfinding Overhaul

Layer / File(s) Summary
Crowd Walkability Model and Terrain Allowlists
engine/world/11-vehicle-crowd.js
Introduces CROWD_WALKABLE_TERRAIN allowlist; implements isCrowdWalkableCell for bridge/terrain/kind logic; collects walkable and path-biased cell pools; validates straight-line segment walkability via sampled intermediate tiles.
BFS Grid Pathfinding and Waypoint Simplification
engine/world/11-vehicle-crowd.js
Implements crowdGridPath BFS router returning cell-center routes; adds crowdSimplifyPath to drop collinear waypoints; builds crowdWanderRoute chaining multiple BFS paths through random waypoints with jitter and closure validation.
Route Mode Dispatch and Crowd Seeding
engine/world/11-vehicle-crowd.js
Reworks crowdRouteAround to dispatch on mode (static/cross/circle/default) with straight-line or BFS fallback; seeds crowd from walkable/path pools preferring paths; generates routes via updated API.

Persistence, Custom Objects, and Asset Management

Layer / File(s) Summary
Client Storage and File I/O Utilities
engine/world/00-prelude.js
Implements twToast (fade notifications), twSafeSetItem (localStorage wrapper with quota handling), twDownloadJSON (JSON downloads), and twPickJSONFile (user file selection with JSON parsing).
Custom Voxel Parts Schema and Materialization
engine/world/25-animation-loop-schema.js, engine/world/29-persistence-api.js
Extends WORLD_SCHEMA with customName and customParts; materializes cells with customParts into voxel-build stamp references; registers embedded voxelBuildStamps on load; exposes referencedVoxelBuildStamps on window.
URL-Based World Loading and Boot Integration
engine/world/29-persistence-api.js, engine/world/30-ui-boot-wiring.js
Adds ?world= parameter parsing for inline JSON or same-origin remote URLs; implements loadWorldFromUrl with async fetch; integrates into bootApp with placeholder swap and history refresh.
AI Prompt Refinement for Custom Object Generation
engine/world/26-ai-generation.js
Frames AI as "creative level designer and builder"; expands hero/landmark guidance for later customization; adds customParts generation instructions; extends validateWorld with extras/transform/landscape field validation.
Voxel Build Defaults, Enhancement Prompts, and Asset Library
engine/world/21-object-transform-voxel-build.js, engine/world/30-ui-boot-wiring.js
Updates seed defaults to culturally-neutral IDs; refines "enhance selected object" prompt detection; allows brand-new object creation when requested; saves builds with voxelBuildStamps; implements asset library export/import.
Engine Panel Visibility
engine/world/28-generate-panel-agent.js
Unhides agent panel when engine UI target is present without selection summary.

Documentation and Build Infrastructure

Layer / File(s) Summary
Workspace Logs and Fork Improvement Analysis
.codesurf/DREAMING.md, fork-improvements-report.md
Refreshes workspace memory with recent VibeClaw/OpenClaw session logs; documents fork-improvement analysis with recommendations, traps, and architectural context.
Publish Data Directory and Minimap Touch Fix
publish.sh, styles/tiny-world.css
Extends publish.sh to recursively copy optional data/ directory; adds touch-action: none to .minimap-wrap to prevent scroll interference during touch drag.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A radial feast of menu cheer,
With islands placed without fear,
Custom voxels bloom and grow,
While crowds now safely roam below,
Toast notifications softly gleam.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and concisely describes the three main changes: single-click selection behavior, multi-select modifiers support, and radial menu jitter fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-object-selection-radial-menu

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands selection/radial-menu behavior, but the actual diff also includes broader changes to island placement, persistence, AI/schema support, crowd routing, asset export/import, and generated documentation.

Changes:

  • Adds plain-click selection, modifier multi-select behavior, and a new radial DOM menu anchored to the transform gizmo.
  • Adds custom voxel-build embedding/import/export and ?world= URL loading support.
  • Includes wider island, crowd, schema, rendering, and generated-report changes beyond the stated PR scope.

Reviewed changes

Copilot reviewed 23 out of 26 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tiny-world-builder.html Loads the new radial menu module.
styles/tiny-world.css Adds radial-menu styles and minimap touch handling.
engine/world/33-radial-menu.js Implements the new radial object action menu.
engine/world/20-input-place-erase.js Updates click selection and island placement interaction logic.
engine/world/19-tools-toolbar.js Hooks island-tool selection into default slot hologram behavior.
engine/world/18-scene-pick-xr.js Adds island hologram rendering support.
engine/world/12-selection-tool.js Changes editable-island cell mesh rendering behavior.
engine/world/14-editable-islands-moorings.js Adds home-island engine handling, per-cell island terrain, and mooring hazard routing.
engine/world/16-drop-anim-adjacency.js Adjusts island adjacency bounds for riser/water-fall behavior.
engine/world/15-ghost-generation-fade.js Changes ghost terrain shadow handling.
engine/world/11-vehicle-crowd.js Reworks crowd walkability and routing.
engine/world/09-model-stamp-loader.js Updates island engine visuals and home engine integration.
engine/world/08-voxel-stamp-renderer.js Routes custom voxel stamp saves through safe storage.
engine/world/00-prelude.js Adds toast, safe localStorage, JSON download, and JSON picker helpers.
engine/world/21-object-transform-voxel-build.js Updates object-enhancement seeds and AI prompts.
engine/world/25-animation-loop-schema.js Ticks the radial menu and extends embedded schema with custom parts.
engine/world/26-ai-generation.js Updates generation prompts and validation coverage.
engine/world/28-generate-panel-agent.js Reveals the agent panel for engine controls.
engine/world/29-persistence-api.js Adds custom voxel-build materialization/embedding and world URL loading.
engine/world/30-ui-boot-wiring.js Adds world URL boot handling and asset library import/export menu items.
publish.sh Copies optional data/ files into dist.
fork-improvements-report.md Adds fork-analysis documentation.
.codesurf/DREAMING.md Replaces generated workspace memory content.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +789 to +790
"customParts": {
"$ref": "#/$defs/customParts"
top.title = level === 'root' ? 'Close' : 'Back';
top.addEventListener('click', e => {
e.stopPropagation();
if (level === 'root') { if (typeof clearSelection === 'function') clearSelection(); }
Comment on lines +64 to +73
function ensureHomeIslandObject() {
if (homeIslandRef) return homeIslandRef;
homeIslandRef = {
id: 'home', __home: true,
boardX: 0, boardZ: 0,
positionX: 0, positionY: 0, positionZ: 0, rotationY: 0,
engines: (typeof defaultEditableIslandEngineStates === 'function') ? defaultEditableIslandEngineStates() : [],
baseGroup: null, group: null, contentGroup: null, lod: 'full',
};
editableIslandById.set('home', homeIslandRef);
Comment thread .codesurf/DREAMING.md
Comment on lines +1 to +8
[assistant turn failed before producing content]

_Generated: 2026-05-29 (eighteenth pass)_
### Session: Article generator cron run
- Source: OpenClaw
- Provider: openclaw (main)
- Updated: 2026-05-30T07:41:19.879Z

---
USER: You are the VibeClaw Article Generator...
Comment on lines +1 to +2
# Fork Improvement Harvest — tiny-world-builder
_Scope: forks pushed in the last week (since 2026-05-23). Only forks genuinely AHEAD of upstream were analyzed: limudim972/main (168 commits ahead, merge-base 2026-05-14) and yuxiaoli/develop (5 commits ahead, merge-base 2026-05-28). Other 10 recently-touched forks had zero original commits._
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@engine/world/00-prelude.js`:
- Around line 61-69: The toast suppression logic in the save-failure path uses
"__twStorageWarnedAt" and "now" so the first failure is mistakenly suppressed
when __twStorageWarnedAt starts at 0; update the condition in the block that
computes "now" (and currently checks "if (now - __twStorageWarnedAt > 8000)") to
also allow the toast when __twStorageWarnedAt === 0 (for example: "if
(__twStorageWarnedAt === 0 || now - __twStorageWarnedAt > 8000)") and continue
to set "__twStorageWarnedAt = now" and call "twToast(...)" as before.
- Around line 32-47: When creating the toast host (__twToastHost) ensure you
reset the cached host to null if attaching it to document.body fails so future
toasts will retry creating/attaching a fresh element; specifically, in the
try/catch block that creates and appends __twToastHost (and the code that
appends child to __twToastHost), catch any error from document.body.appendChild
(or from the overall toast creation) and set __twToastHost = null before exiting
the catch so a detached element is not reused by subsequent calls to the toast
logic.

In `@engine/world/11-vehicle-crowd.js`:
- Around line 991-994: The jitter helper crowdJitterCell can push border cells
outside the valid range; modify crowdJitterCell to clamp the resulting x and z
to the board bounds (0 to GRID-1) after adding random jitter, and then replace
the ad-hoc spawn jitter at the spawn site with a call to this same
crowdJitterCell so both uses share the clamping logic (referencing
crowdJitterCell and the GRID constant).
- Around line 1114-1122: The spawn selection reuses the same path cells when
paths.length < count, causing stacked starts; change the selection in the spawn
loop (where seed is chosen) to prefer unique unused starts by first taking all
path cells, then appending the remaining walkable cells that are not in paths,
and draw seeds from that ordered list (or track used indices) so each spawn uses
a unique cell until that list is exhausted before cycling; update the logic
around collectCrowdWalkableCells(), spawnCells, walkable, paths and the seed
computation used for crowdRouteAround() to ensure uniqueness until all
candidates are used.

In `@engine/world/14-editable-islands-moorings.js`:
- Around line 213-223: ensureEditableIslandCellTiles currently uses
island.cellTilesRendered and never invalidates when GRID changes, leaving stale
meshes; update the function to track the GRID used for the rendered cell tiles
(e.g. add island._cellGridSize) and invalidate when it differs: if
island.cellTilesRendered && island._cellGridSize === GRID return; if
island.cellTilesRendered && island._cellGridSize !== GRID call
disposeEditableIslandSurface(island) and set island.cellTilesRendered = false
before re-rendering; after rendering set island.cellTilesRendered = true and
island._cellGridSize = GRID so future calls correctly short-circuit only when
the GRID matches.
- Around line 64-74: ensureHomeIslandObject currently returns the cached
homeIslandRef after clearEditableIslands() may have emptied editableIslandById,
leaving the map missing the 'home' entry; update ensureHomeIslandObject so that
if homeIslandRef is already truthy it still ensures
editableIslandById.set('home', homeIslandRef) before returning (i.e.,
re-register the cached object), and keep the existing creation path (which
already sets the map) unchanged; reference ensureHomeIslandObject,
homeIslandRef, editableIslandById, and clearEditableIslands in your change.

In `@engine/world/20-input-place-erase.js`:
- Around line 919-921: The branch that falls back to legacy place/erase should
use the pointerDownHit press snapshot instead of currentHover because
setHoverFromCell clears currentHover when the island tool has no free slot;
replace the conditional and call to applyToolToCell(currentHover) so it resolves
the target cell from pointerDownHit's press snapshot (e.g., pointerDownHit.cell
or pointerDownHit.pressSnapshot) and then call applyToolToCell with that
resolved cell; keep the rest of the logic intact and ensure you reference
pointerDownHit, setHoverFromCell, currentHover, and applyToolToCell when making
the change.

In `@engine/world/26-ai-generation.js`:
- Around line 415-423: The transform validation block in
engine/world/26-ai-generation.js (within the cells[i].transform checks) is
incomplete: ensure array-form transforms are validated element-wise (verify
transform is Array of length 3 or 4 and that every element is a number) and
ensure object-form transforms validate all numeric fields (check rotationY,
offsetX, offsetY, offsetZ existings are numbers); update the error returns to
reference the specific failing cell (e.g., 'cells['+i+'].transform[index]
invalid' for array elements and 'cells['+i+'].transform.offsetY invalid' etc.)
so malformed transforms are rejected before proceeding.

In `@engine/world/29-persistence-api.js`:
- Around line 175-180: The loop that pushes embedded stamps currently skips
stamps whose id already exists (getVoxelBuildStamp) causing wrong cross-device
resolution; instead, when normalizeVoxelBuildStamp yields a stamp but
getVoxelBuildStamp(stamp.id) returns a different existing stamp, generate a
fresh unique id for the imported stamp, update stamp.id to that new id, push the
new stamp into VOXEL_BUILD_STAMPS and increment added, and then rewrite all
references in data.cells (appearance.voxelBuildId) that pointed to the original
embedded id to point to the new id before applyState(...) runs; locate this
logic around normalizeVoxelBuildStamp, getVoxelBuildStamp, VOXEL_BUILD_STAMPS
and ensure reference rewriting happens prior to calling applyState.
- Around line 145-151: The code currently deletes c.customParts and c.customName
before calling normalizeVoxelBuildStamp, so if normalization throws the authored
data is lost; change the flow in the loop that handles parts/c to only call
delete c.customParts and delete c.customName after a successful stamp is
obtained: move those delete statements to after stamp is truthy (i.e., after the
try/catch and after the if (!stamp) check passes), ensure
normalizeVoxelBuildStamp({ name, customParts: parts, custom: true, footprint:
2.0 }, 'Custom Object') is invoked with the original c.customParts intact, and
leave c.customParts/c.customName untouched when normalization fails so the
object is preserved or can be handled later.

In `@engine/world/30-ui-boot-wiring.js`:
- Around line 1717-1723: The success toast is shown unconditionally after
calling twDownloadJSON, causing a false "Exported…" message when the download
fails; change the call to only display twToast('Exported …') after
twDownloadJSON reports success by checking its return (or awaiting its Promise)
or by using its success callback, and ensure any thrown/rejected errors are
caught/handled so the success toast is not emitted on failure; target the
twDownloadJSON invocation and the subsequent twToast call to implement this
conditional success behavior.

In `@engine/world/33-radial-menu.js`:
- Around line 107-111: The click handler for buttons currently calls
runAction(b.action) and runAction reads window.event.shiftKey; update the
btn.addEventListener('click', e => { ... }) to call runAction(b.action, e) and
change runAction to accept the event parameter (e.g., runAction(action, evt))
and use evt.shiftKey for the "size" action branch instead of window.event;
update any internal callers of runAction (if any) to provide a suitable event or
null and guard evt before accessing shiftKey.

In `@styles/tiny-world.css`:
- Line 4122: The CSS uses the keyword with incorrect casing: in the rule setting
stroke: currentColor; (look for the stroke declaration in the tiny-world.css
rule around where stroke is defined) change the value to the lowercase keyword
"currentcolor" to satisfy the value-keyword-case lint rule and prevent lint
failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52f378d6-c1e7-4f9e-8137-e46fd44a9500

📥 Commits

Reviewing files that changed from the base of the PR and between 88f9972 and b3b89fb.

⛔ Files ignored due to path filters (3)
  • home-default-view.jpeg is excluded by !**/*.jpeg
  • island-percell-terrain.jpeg is excluded by !**/*.jpeg
  • jet-original.jpeg is excluded by !**/*.jpeg
📒 Files selected for processing (23)
  • .codesurf/DREAMING.md
  • engine/world/00-prelude.js
  • engine/world/08-voxel-stamp-renderer.js
  • engine/world/09-model-stamp-loader.js
  • engine/world/11-vehicle-crowd.js
  • engine/world/12-selection-tool.js
  • engine/world/14-editable-islands-moorings.js
  • engine/world/15-ghost-generation-fade.js
  • engine/world/16-drop-anim-adjacency.js
  • engine/world/18-scene-pick-xr.js
  • engine/world/19-tools-toolbar.js
  • engine/world/20-input-place-erase.js
  • engine/world/21-object-transform-voxel-build.js
  • engine/world/25-animation-loop-schema.js
  • engine/world/26-ai-generation.js
  • engine/world/28-generate-panel-agent.js
  • engine/world/29-persistence-api.js
  • engine/world/30-ui-boot-wiring.js
  • engine/world/33-radial-menu.js
  • fork-improvements-report.md
  • publish.sh
  • styles/tiny-world.css
  • tiny-world-builder.html

Comment on lines +32 to +47
if (!__twToastHost) {
__twToastHost = document.createElement('div');
__twToastHost.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
document.body.appendChild(__twToastHost);
}
const el = document.createElement('div');
const bg = kind === 'err' ? '#c0392b' : (kind === 'ok' ? '#2e7d32' : '#333');
el.style.cssText = 'pointer-events:auto;max-width:min(92vw,420px);padding:10px 14px;border-radius:8px;color:#fff;font:500 13px/1.4 system-ui,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.25);background:' + bg + ';opacity:0;transition:opacity .18s ease;';
el.textContent = String(message);
__twToastHost.appendChild(el);
requestAnimationFrame(() => { el.style.opacity = '1'; });
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 220);
}, kind === 'err' ? 6000 : 3000);
} catch (_) { /* DOM not ready — nothing we can do */ }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset the cached toast host when body attachment fails.

If Line 35 throws before the node is attached, __twToastHost still points at a detached element. Every later toast then renders off-DOM, so save/import failures stop surfacing for the rest of the session.

Proposed fix
-      if (!__twToastHost) {
-        __twToastHost = document.createElement('div');
-        __twToastHost.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
-        document.body.appendChild(__twToastHost);
+      if (!__twToastHost || !__twToastHost.isConnected) {
+        const host = document.createElement('div');
+        host.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
+        document.body.appendChild(host);
+        __twToastHost = host;
       }
@@
-    } catch (_) { /* DOM not ready — nothing we can do */ }
+    } catch (_) {
+      __twToastHost = null;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!__twToastHost) {
__twToastHost = document.createElement('div');
__twToastHost.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
document.body.appendChild(__twToastHost);
}
const el = document.createElement('div');
const bg = kind === 'err' ? '#c0392b' : (kind === 'ok' ? '#2e7d32' : '#333');
el.style.cssText = 'pointer-events:auto;max-width:min(92vw,420px);padding:10px 14px;border-radius:8px;color:#fff;font:500 13px/1.4 system-ui,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.25);background:' + bg + ';opacity:0;transition:opacity .18s ease;';
el.textContent = String(message);
__twToastHost.appendChild(el);
requestAnimationFrame(() => { el.style.opacity = '1'; });
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 220);
}, kind === 'err' ? 6000 : 3000);
} catch (_) { /* DOM not ready — nothing we can do */ }
if (!__twToastHost || !__twToastHost.isConnected) {
const host = document.createElement('div');
host.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
document.body.appendChild(host);
__twToastHost = host;
}
const el = document.createElement('div');
const bg = kind === 'err' ? '`#c0392b`' : (kind === 'ok' ? '`#2e7d32`' : '`#333`');
el.style.cssText = 'pointer-events:auto;max-width:min(92vw,420px);padding:10px 14px;border-radius:8px;color:`#fff`;font:500 13px/1.4 system-ui,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.25);background:' + bg + ';opacity:0;transition:opacity .18s ease;';
el.textContent = String(message);
__twToastHost.appendChild(el);
requestAnimationFrame(() => { el.style.opacity = '1'; });
setTimeout(() => {
el.style.opacity = '0';
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 220);
}, kind === 'err' ? 6000 : 3000);
} catch (_) {
__twToastHost = null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/00-prelude.js` around lines 32 - 47, When creating the toast
host (__twToastHost) ensure you reset the cached host to null if attaching it to
document.body fails so future toasts will retry creating/attaching a fresh
element; specifically, in the try/catch block that creates and appends
__twToastHost (and the code that appends child to __twToastHost), catch any
error from document.body.appendChild (or from the overall toast creation) and
set __twToastHost = null before exiting the catch so a detached element is not
reused by subsequent calls to the toast logic.

Comment on lines +61 to +69
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
if (now - __twStorageWarnedAt > 8000) {
__twStorageWarnedAt = now;
const quota = err && (err.name === 'QuotaExceededError' || err.code === 22 || err.code === 1014);
twToast(
(label ? label + ' could not be saved' : 'Save failed') +
(quota ? ' — browser storage is full. Export your assets/worlds to a file to avoid losing them.' : '.'),
'err'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Let the first storage failure show a toast.

Line 62 suppresses the first warning on a fresh page because __twStorageWarnedAt starts at 0 and performance.now() is usually still < 8000. That hides the most important save-failure notification.

Proposed fix
-      if (now - __twStorageWarnedAt > 8000) {
+      if (!__twStorageWarnedAt || now - __twStorageWarnedAt > 8000) {
         __twStorageWarnedAt = now;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
if (now - __twStorageWarnedAt > 8000) {
__twStorageWarnedAt = now;
const quota = err && (err.name === 'QuotaExceededError' || err.code === 22 || err.code === 1014);
twToast(
(label ? label + ' could not be saved' : 'Save failed') +
(quota ? ' — browser storage is full. Export your assets/worlds to a file to avoid losing them.' : '.'),
'err'
);
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
if (!__twStorageWarnedAt || now - __twStorageWarnedAt > 8000) {
__twStorageWarnedAt = now;
const quota = err && (err.name === 'QuotaExceededError' || err.code === 22 || err.code === 1014);
twToast(
(label ? label + ' could not be saved' : 'Save failed') +
(quota ? ' — browser storage is full. Export your assets/worlds to a file to avoid losing them.' : '.'),
'err'
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/00-prelude.js` around lines 61 - 69, The toast suppression logic
in the save-failure path uses "__twStorageWarnedAt" and "now" so the first
failure is mistakenly suppressed when __twStorageWarnedAt starts at 0; update
the condition in the block that computes "now" (and currently checks "if (now -
__twStorageWarnedAt > 8000)") to also allow the toast when __twStorageWarnedAt
=== 0 (for example: "if (__twStorageWarnedAt === 0 || now - __twStorageWarnedAt
> 8000)") and continue to set "__twStorageWarnedAt = now" and call
"twToast(...)" as before.

Comment on lines +991 to +994
const crowdJitterCell = cell => ({
x: cell.x + (Math.random() - 0.5) * 0.22,
z: cell.z + (Math.random() - 0.5) * 0.22,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp jittered positions at the board edge.

Lines 992-993 can produce x/z < 0 or x/z > GRID - 1 for border cells, so edge walkers occasionally step off the island. Clamp the jitter here, and reuse the same helper for the spawn jitter on Lines 1125-1126.

Proposed fix
+  function crowdJitterPos(x, z, amount = 0.22) {
+    const half = amount * 0.5
+    return {
+      x: Math.max(half, Math.min(GRID - 1 - half, x + (Math.random() - 0.5) * amount)),
+      z: Math.max(half, Math.min(GRID - 1 - half, z + (Math.random() - 0.5) * amount)),
+    }
+  }
+
-  const crowdJitterCell = cell => ({
-    x: cell.x + (Math.random() - 0.5) * 0.22,
-    z: cell.z + (Math.random() - 0.5) * 0.22,
-  });
+  const crowdJitterCell = cell => crowdJitterPos(cell.x, cell.z)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const crowdJitterCell = cell => ({
x: cell.x + (Math.random() - 0.5) * 0.22,
z: cell.z + (Math.random() - 0.5) * 0.22,
});
function crowdJitterPos(x, z, amount = 0.22) {
const half = amount * 0.5
return {
x: Math.max(half, Math.min(GRID - 1 - half, x + (Math.random() - 0.5) * amount)),
z: Math.max(half, Math.min(GRID - 1 - half, z + (Math.random() - 0.5) * amount)),
}
}
const crowdJitterCell = cell => crowdJitterPos(cell.x, cell.z)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/11-vehicle-crowd.js` around lines 991 - 994, The jitter helper
crowdJitterCell can push border cells outside the valid range; modify
crowdJitterCell to clamp the resulting x and z to the board bounds (0 to GRID-1)
after adding random jitter, and then replace the ad-hoc spawn jitter at the
spawn site with a call to this same crowdJitterCell so both uses share the
clamping logic (referencing crowdJitterCell and the GRID constant).

Comment on lines +1114 to +1122
const { walkable, paths } = collectCrowdWalkableCells();
// Spawn preferentially on paths so crowds start on roads, then wander out.
const spawnCells = paths.length ? paths : walkable;
const count = Math.min(crowdCount, walkable.length);
if (!count) return;
const characters = ['townie', 'little-girl', 'dad', 'grandfather', 'grandmother'];
for (let i = 0; i < count; i++) {
const seed = pathCells[(i * 3) % pathCells.length];
const route = crowdRouteAround(seed, pathCells, i);
const seed = spawnCells[(i * 3) % spawnCells.length];
const route = crowdRouteAround(seed, walkable, paths, i);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prefer unique path starts before reusing a cell.

When paths.length < count, Line 1121 cycles back through the same path cells even though walkable still has unused cells. That stacks several people onto the same start tile on sparse-road maps.

Proposed fix
     const { walkable, paths } = collectCrowdWalkableCells();
     // Spawn preferentially on paths so crowds start on roads, then wander out.
-    const spawnCells = paths.length ? paths : walkable;
+    const pathKeys = new Set(paths.map(cell => cell.x + ',' + cell.z))
+    const spawnCells = paths.length
+      ? [...paths, ...walkable.filter(cell => !pathKeys.has(cell.x + ',' + cell.z))]
+      : walkable
     const count = Math.min(crowdCount, walkable.length);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/11-vehicle-crowd.js` around lines 1114 - 1122, The spawn
selection reuses the same path cells when paths.length < count, causing stacked
starts; change the selection in the spawn loop (where seed is chosen) to prefer
unique unused starts by first taking all path cells, then appending the
remaining walkable cells that are not in paths, and draw seeds from that ordered
list (or track used indices) so each spawn uses a unique cell until that list is
exhausted before cycling; update the logic around collectCrowdWalkableCells(),
spawnCells, walkable, paths and the seed computation used for crowdRouteAround()
to ensure uniqueness until all candidates are used.

Comment on lines +64 to +74
function ensureHomeIslandObject() {
if (homeIslandRef) return homeIslandRef;
homeIslandRef = {
id: 'home', __home: true,
boardX: 0, boardZ: 0,
positionX: 0, positionY: 0, positionZ: 0, rotationY: 0,
engines: (typeof defaultEditableIslandEngineStates === 'function') ? defaultEditableIslandEngineStates() : [],
baseGroup: null, group: null, contentGroup: null, lod: 'full',
};
editableIslandById.set('home', homeIslandRef);
return homeIslandRef;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-register homeIslandRef after the island maps are cleared.

After clearEditableIslands() wipes editableIslandById, this function returns the cached homeIslandRef without putting it back into the map. Home engine picks/upgrades then stop resolving after clear/reset or any home-border rebuild.

Suggested fix
 function ensureHomeIslandObject() {
-  if (homeIslandRef) return homeIslandRef;
-  homeIslandRef = {
-    id: 'home', __home: true,
-    boardX: 0, boardZ: 0,
-    positionX: 0, positionY: 0, positionZ: 0, rotationY: 0,
-    engines: (typeof defaultEditableIslandEngineStates === 'function') ? defaultEditableIslandEngineStates() : [],
-    baseGroup: null, group: null, contentGroup: null, lod: 'full',
-  };
-  editableIslandById.set('home', homeIslandRef);
+  if (!homeIslandRef) {
+    homeIslandRef = {
+      id: 'home', __home: true,
+      boardX: 0, boardZ: 0,
+      positionX: 0, positionY: 0, positionZ: 0, rotationY: 0,
+      engines: (typeof defaultEditableIslandEngineStates === 'function') ? defaultEditableIslandEngineStates() : [],
+      baseGroup: null, group: null, contentGroup: null, lod: 'full',
+    };
+  }
+  editableIslandById.set('home', homeIslandRef);
   return homeIslandRef;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/14-editable-islands-moorings.js` around lines 64 - 74,
ensureHomeIslandObject currently returns the cached homeIslandRef after
clearEditableIslands() may have emptied editableIslandById, leaving the map
missing the 'home' entry; update ensureHomeIslandObject so that if homeIslandRef
is already truthy it still ensures editableIslandById.set('home', homeIslandRef)
before returning (i.e., re-register the cached object), and keep the existing
creation path (which already sets the map) unchanged; reference
ensureHomeIslandObject, homeIslandRef, editableIslandById, and
clearEditableIslands in your change.

Comment on lines +145 to +151
if (!Array.isArray(parts) || !parts.length) { if (c) { delete c.customParts; delete c.customName; } continue; }
const name = (typeof c.customName === 'string' && c.customName.trim()) ? c.customName.trim() : 'Custom Object';
delete c.customParts;
delete c.customName;
let stamp = null;
try { stamp = normalizeVoxelBuildStamp({ name, customParts: parts, custom: true, footprint: 2.0 }, 'Custom Object'); } catch (_) {}
if (!stamp) continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only strip customParts after stamp creation succeeds.

Lines 147-150 delete the only authored custom-object data before normalizeVoxelBuildStamp(...) is known to succeed. If normalization fails, the load continues with the object silently dropped instead of preserved or rejected.

Proposed fix
       const parts = c.customParts;
       if (!Array.isArray(parts) || !parts.length) { if (c) { delete c.customParts; delete c.customName; } continue; }
       const name = (typeof c.customName === 'string' && c.customName.trim()) ? c.customName.trim() : 'Custom Object';
-      delete c.customParts;
-      delete c.customName;
       let stamp = null;
       try { stamp = normalizeVoxelBuildStamp({ name, customParts: parts, custom: true, footprint: 2.0 }, 'Custom Object'); } catch (_) {}
       if (!stamp) continue;
+      delete c.customParts;
+      delete c.customName;
       if (typeof VOXEL_BUILD_STAMPS !== 'undefined' && typeof getVoxelBuildStamp === 'function' && !getVoxelBuildStamp(stamp.id)) {
         VOXEL_BUILD_STAMPS.push(stamp);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/29-persistence-api.js` around lines 145 - 151, The code
currently deletes c.customParts and c.customName before calling
normalizeVoxelBuildStamp, so if normalization throws the authored data is lost;
change the flow in the loop that handles parts/c to only call delete
c.customParts and delete c.customName after a successful stamp is obtained: move
those delete statements to after stamp is truthy (i.e., after the try/catch and
after the if (!stamp) check passes), ensure normalizeVoxelBuildStamp({ name,
customParts: parts, custom: true, footprint: 2.0 }, 'Custom Object') is invoked
with the original c.customParts intact, and leave c.customParts/c.customName
untouched when normalization fails so the object is preserved or can be handled
later.

Comment on lines +175 to +180
for (const item of list) {
if (!item || typeof item !== 'object') continue;
if (item.id && getVoxelBuildStamp(item.id)) continue;
let stamp = null;
try { stamp = normalizeVoxelBuildStamp(Object.assign({}, item, { custom: true }), item.name); } catch (_) {}
if (stamp && !getVoxelBuildStamp(stamp.id)) { VOXEL_BUILD_STAMPS.push(stamp); added++; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Handle embedded stamp ID collisions instead of skipping them.

If Line 177 finds an existing local stamp with the same id but different contents, this load path silently reuses the local asset and the imported world resolves voxelBuildId to the wrong build. That breaks the cross-device portability this PR is adding.

A robust fix needs to remap colliding embedded IDs to fresh ones and rewrite the corresponding appearance.voxelBuildId references in data.cells before applyState(...) consumes them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/29-persistence-api.js` around lines 175 - 180, The loop that
pushes embedded stamps currently skips stamps whose id already exists
(getVoxelBuildStamp) causing wrong cross-device resolution; instead, when
normalizeVoxelBuildStamp yields a stamp but getVoxelBuildStamp(stamp.id) returns
a different existing stamp, generate a fresh unique id for the imported stamp,
update stamp.id to that new id, push the new stamp into VOXEL_BUILD_STAMPS and
increment added, and then rewrite all references in data.cells
(appearance.voxelBuildId) that pointed to the original embedded id to point to
the new id before applyState(...) runs; locate this logic around
normalizeVoxelBuildStamp, getVoxelBuildStamp, VOXEL_BUILD_STAMPS and ensure
reference rewriting happens prior to calling applyState.

Comment on lines +1717 to +1723
twDownloadJSON('tinyworld-assets.json', {
tinyworldAssets: 1,
exportedAt: new Date().toISOString(),
voxelBuilds,
assetTemplates,
});
twToast('Exported ' + count + ' asset' + (count === 1 ? '' : 's') + ' → tinyworld-assets.json', 'ok');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Only show the success toast if the export actually started.

twDownloadJSON() already reports failures. As written, a download error produces both the error toast and the “Exported …” success toast, which tells the user their backup succeeded when it did not.

Proposed fix
-    twDownloadJSON('tinyworld-assets.json', {
+    if (!twDownloadJSON('tinyworld-assets.json', {
       tinyworldAssets: 1,
       exportedAt: new Date().toISOString(),
       voxelBuilds,
       assetTemplates,
-    });
+    })) return;
     twToast('Exported ' + count + ' asset' + (count === 1 ? '' : 's') + ' → tinyworld-assets.json', 'ok');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
twDownloadJSON('tinyworld-assets.json', {
tinyworldAssets: 1,
exportedAt: new Date().toISOString(),
voxelBuilds,
assetTemplates,
});
twToast('Exported ' + count + ' asset' + (count === 1 ? '' : 's') + ' → tinyworld-assets.json', 'ok');
if (!twDownloadJSON('tinyworld-assets.json', {
tinyworldAssets: 1,
exportedAt: new Date().toISOString(),
voxelBuilds,
assetTemplates,
})) return;
twToast('Exported ' + count + ' asset' + (count === 1 ? '' : 's') + ' → tinyworld-assets.json', 'ok');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/30-ui-boot-wiring.js` around lines 1717 - 1723, The success
toast is shown unconditionally after calling twDownloadJSON, causing a false
"Exported…" message when the download fails; change the call to only display
twToast('Exported …') after twDownloadJSON reports success by checking its
return (or awaiting its Promise) or by using its success callback, and ensure
any thrown/rejected errors are caught/handled so the success toast is not
emitted on failure; target the twDownloadJSON invocation and the subsequent
twToast call to implement this conditional success behavior.

Comment on lines +107 to +111
btn.addEventListener('click', e => {
e.stopPropagation();
flash(btn);
if (b.submenu) renderLevel(b.submenu);
else runAction(b.action);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="engine/world/33-radial-menu.js"

echo "== File info =="
wc -l "$FILE" || true
echo

echo "== Search for window.event usage =="
rg -n "window\.event" "$FILE" || true
echo

echo "== Inspect lines ~90-140 =="
nl -ba "$FILE" | sed -n '80,150p'
echo

echo "== Inspect lines ~140-220 =="
nl -ba "$FILE" | sed -n '140,230p'
echo

echo "== Find runAction() definition and call sites =="
rg -n "function runAction|runAction\(" "$FILE"
echo

Repository: jasonkneen/tiny-world-builder

Length of output: 349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="engine/world/33-radial-menu.js"

echo "== Search for window.event usage =="
rg -n "window\.event" "$FILE" || true
echo

echo "== Lines 90-150 (context around click handler) =="
sed -n '90,150p' "$FILE" | cat -n -w3 -s': '
echo

echo "== Lines 150-230 (context around runAction) =="
sed -n '150,230p' "$FILE" | cat -n -w3 -s': '
echo

echo "== Search for runAction definition and call sites =="
rg -n "function runAction|runAction\\(" "$FILE"

Repository: jasonkneen/tiny-world-builder

Length of output: 341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="engine/world/33-radial-menu.js"

python3 - <<'PY'
import itertools, pathlib
path = pathlib.Path("engine/world/33-radial-menu.js")
lines = path.read_text().splitlines()
def show(a,b):
    for i in range(a-1,b):
        if 0<=i<len(lines):
            print(f"{i+1:4d}: {lines[i]}")
        else:
            break
print("== Lines 90-150 ==")
show(90,150)
print("\n== Lines 150-230 ==")
show(150,230)
PY

echo
echo "== Search for runAction definition and call sites =="
rg -n "function runAction|runAction\\(" "$FILE" || true
echo
echo "== Search for scaleSelectedBoardObject usage =="
rg -n "scaleSelectedBoardObject" "$FILE" || true

Repository: jasonkneen/tiny-world-builder

Length of output: 7409


Avoid window.event in radial menu Shift handling; thread the click event into runAction().

engine/world/33-radial-menu.js calls runAction(b.action) from the click handler (107-112), but runAction() reads window.event.shiftKey for the size action (159-176). Pass the click event and use evt.shiftKey instead.

Suggested fix
-          btn.addEventListener('click', e => {
+          btn.addEventListener('click', e => {
             e.stopPropagation();
             flash(btn);
             if (b.submenu) renderLevel(b.submenu);
-            else runAction(b.action);
+            else runAction(b.action, e);
           });
@@
-    function runAction(id) {
+    function runAction(id, evt) {
       try {
@@
         } else if (id === 'size') {
           if (typeof scaleSelectedBoardObject === 'function') {
-            scaleSelectedBoardObject((window.event && window.event.shiftKey) ? 0.87 : 1.15);
+            scaleSelectedBoardObject((evt && evt.shiftKey) ? 0.87 : 1.15);
           }
         } else if (id === 'more' || id === 'style') {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/world/33-radial-menu.js` around lines 107 - 111, The click handler for
buttons currently calls runAction(b.action) and runAction reads
window.event.shiftKey; update the btn.addEventListener('click', e => { ... }) to
call runAction(b.action, e) and change runAction to accept the event parameter
(e.g., runAction(action, evt)) and use evt.shiftKey for the "size" action branch
instead of window.event; update any internal callers of runAction (if any) to
provide a suitable event or null and guard evt before accessing shiftKey.

Comment thread styles/tiny-world.css
width: 21px;
height: 21px;
fill: none;
stroke: currentColor;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Lowercase currentcolor to satisfy the existing lint rule.

This line trips value-keyword-case, so lint will keep failing until the keyword is lowercased.

Suggested fix
-    stroke: currentColor;
+    stroke: currentcolor;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stroke: currentColor;
stroke: currentcolor;
🧰 Tools
🪛 Stylelint (17.12.0)

[error] 4122-4122: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@styles/tiny-world.css` at line 4122, The CSS uses the keyword with incorrect
casing: in the rule setting stroke: currentColor; (look for the stroke
declaration in the tiny-world.css rule around where stroke is defined) change
the value to the lowercase keyword "currentcolor" to satisfy the
value-keyword-case lint rule and prevent lint failures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants