Skip to content

Add survival/creative modes, inventory, and robust mouse input#2

Merged
MichaelFisher1997 merged 5 commits into
masterfrom
opencode/shiny-cabin
Jun 14, 2026
Merged

Add survival/creative modes, inventory, and robust mouse input#2
MichaelFisher1997 merged 5 commits into
masterfrom
opencode/shiny-cabin

Conversation

@MichaelFisher1997

Copy link
Copy Markdown
Contributor

Summary

Adds a full survival + creative gameplay loop with an inventory system, plus a Firefox-robust input layer. Built to Luanti/VoxeLibre conventions (per the task).

Gameplay (Tier 1+2)

  • Item registry (src/game/Items.ts): every block becomes a placeable item (b<id>) plus food items; drop table (grass→dirt, leaves/ice→nothing), per-block dig times, GameMode.
  • Inventory (src/game/Inventory.ts): 36 slots (9 hotbar + 27 backpack), stacks + merge, JSON serialize.
  • PlayerState (src/game/PlayerState.ts): 20 HP (10 hearts), hunger/saturation/exhaustion (overflow @4000), breath (10); fall & cactus damage, drowning, starvation, regen; death + respawn. Fall damage from a real fall-distance tracker.
  • Minetest-style inventory screen (src/ui/InventoryUI.ts): backpack + shared hotbar, crafting placeholder, searchable creative palette, trash slot, and a Survival/Creative toggle (press E).
  • Per-seed save/load (src/state/SaveData.ts): inventory + vitals persist in localStorage keyed by seed.

Mining / placing / eating

  • Creative: instant break, infinite placement.
  • Survival: hold left-click to mine (darkening overlay shows progress), drops go to inventory, placing consumes a stack; hold right-click with food to eat.

HUD (src/ui/HUD.ts + ui.css)

  • Hearts / hunger / air bars (SVG, no emoji), inventory-driven hotbar with counts.

Robust input (src/engine/Input.ts, src/game/Player.ts)

  • Cursor-aiming fallback via scene.createPickingRay when pointer lock is unavailable, so the game is playable without mouse-look.
  • Break/place also ride on click / auxclick / contextmenu (which fire reliably in Firefox even when mousedown flakes during pointer-lock transitions) — not just mousedown.
  • requestPointerLock wrapped in try/catch; pointerlockerror surfaced as a toast.
  • Steeper spawn pitch + REACH 8 so a block is always within targeting range.

Other

  • Debug logging (src/state/Debug.ts) gated behind localStorage.voxl.debugoff by default, enable with localStorage.setItem("voxl.debug","1").

Verification

  • bunx tsc --noEmit passes
  • vite build bundles clean (game + website sync)
  • Verified headlessly (Chromium/swiftshader) in both pointer-lock and cursor-aiming modes: break + place confirmed

Notes / follow-ups

  • Tier 3 (crafting grid is currently a labeled placeholder), tools/durability, and mobs are intentionally out of scope for this slice.
  • README.md/AGENTS.md still reference Three.js (stale from the Babylon migration) — left untouched here.

Controls

E inventory + mode toggle · hold L-click mine · R-click place · hold R-click (food) eat · 19/scroll/F hotbar · double-tap Space fly (creative).

Survival + creative gameplay loop (Luanti/VoxeLibre-flavoured):
- Item registry (block items + food), 36-slot inventory with stacks &
  merge, drops, and per-seed save/load (localStorage)
- Vitals: 20 HP (10 hearts), hunger/saturation/exhaustion, breath;
  fall & cactus damage, drowning, starvation, regen; death + respawn
- Minetest-style inventory screen: backpack + shared hotbar, crafting
  placeholder, searchable creative palette, trash, Survival/Creative
  toggle (E to open)
- Mining: instant in creative, timed hold-to-mine (with overlay) in
  survival; placing consumes a stack in survival, infinite in creative
- Eating (hold right-click with food)
- HUD: hearts/hunger/air bars, inventory-driven hotbar with counts

Robust input (Firefox-friendly):
- Cursor-aiming fallback via createPickingRay when pointer lock is
  unavailable; break/place also ride on click/auxclick/contextmenu
  (reliable in Firefox), not just mousedown
- Pointer-lock error handling + try/catch around requestPointerLock
- Steeper spawn pitch + reach 8 so a block is always targetable

Debug logging gated behind `voxl.debug` localStorage flag (off by default).
@github-actions

Copy link
Copy Markdown

opencode session  |  github run

@github-actions

Copy link
Copy Markdown

📋 Summary

No linked issues (e.g., Fixes #…) were found in the PR description. This PR implements the advertised survival/creative loop, inventory, save/load, and Firefox-robust input layer. Build verification: game typecheck and build pass; website build passes; website astro check fails only on a pre-existing optional playwright import in website/scripts/screenshot.ts (not modified here).

📌 Review Metadata

🔴 Critical Issues

None identified.

⚠️ High Priority Issues

**[HIGH]** `src/game/Player.ts:266-282` - Fall damage is not cancelled by water
**Confidence:** Medium
**Description:** `trackFall()` records the peak Y while airborne and computes `pendingFall` on landing, but it never resets `fallPeakY` when the player enters water. Falling into deep water and then touching the bottom charges the full fall distance.
**Impact:** Survival players take unexpected, often lethal fall damage when landing in water — contrary to typical voxel-sandbox expectations.
**Suggested Fix:** Reset `fallPeakY = null` inside `trackFall()` whenever `this.inWater` becomes true (or when `headSubmerged()` is true), so water cushions the fall.
**[HIGH]** `src/state/SaveData.ts:22-31` / `src/game/Game.ts:270-279` - Save `mode` value is not validated
**Confidence:** Medium
**Description:** `loadSave()` only checks that `parsed.inventory` is an array and `parsed.stats` exists; `parsed.mode` is passed straight into `applySettings({ mode: save.mode })`. A corrupted or hand-edited save can set `settings.mode` to an invalid string.
**Impact:** Mode-dependent branches (`this.player.canFly`, `digTime`, hunger/damage ticking, HUD labels) can silently behave like survival or fail the `=== "creative"` checks.
**Suggested Fix:** In `loadSave()`, validate `parsed.mode === "survival" || parsed.mode === "creative"` and fall back to the current global `settings.mode` or `"creative"` before returning.

💡 Medium Priority Issues

**[MEDIUM]** `src/game/Inventory.ts:117-123` - Loaded inventory slots are not validated against the item registry
**Confidence:** Low
**Description:** `Inventory.load()` writes `e.id` directly into a slot without checking `getItem(e.id)`. Corrupted saves can store arbitrary strings.
**Impact:** Invalid items render with default color, cannot be placed/eaten, and may confuse the HUD. Generally harmless but degrades save robustness.
**Suggested Fix:** Skip any entry where `getItem(e.id)` is undefined, or clamp the count and discard invalid ids.
**[MEDIUM]** `src/engine/Input.ts:104-109` / `src/game/Game.ts:177-181` - Escape cannot close inventory while the search box is focused
**Confidence:** High
**Description:** `handleKeyDown()` returns early when the active element is an `<input>`, so the `Escape` keydown never reaches `Game`'s inventory-close handler while the creative search field has focus.
**Impact:** Keyboard-driven players must click the Close button to exit the inventory after searching.
**Suggested Fix:** Add a `keydown` listener inside `InventoryUI` (on the inventory root or the search input) that closes the UI on `Escape`, or make `Input` dispatch Escape before the input-element early return.

ℹ️ Low Priority Suggestions

**[LOW]** `src/game/Blocks.ts:378` - `HOTBAR_BLOCKS` is now unused
**Description:** The export is no longer imported by `Game.ts` or `HUD.ts` after the inventory refactor.
**Suggested Fix:** Remove the dead export to reduce confusion.
**[LOW]** `src/game/Player.ts:285-291` / `src/game/Player.ts:295-313` - Magic block ids for water and cactus
**Description:** `headSubmerged()` compares to `7` and `touchingCactus()` compares to `19`. These are stable in the current registry but are brittle if ids ever shift.
**Suggested Fix:** Import named constants (e.g., `WATER_BLOCK = 7`, `CACTUS_BLOCK = 19`) from `Blocks.ts`.
**[LOW]** `src/game/Items.ts:129-133` - Water is breakable in survival
**Description:** `HARDNESS[7] = "instant"` makes water minable/removable with bare hands.
**Suggested Fix:** If water sources should not be deleted by punching, mark `7` as unbreakable or skip mining when the targeted block is water.

📊 SOLID Principles Score

Principle Score Notes
Single Responsibility 5 Game.ts remains a large orchestrator handling rendering, input, inventory, survival systems, and UI state.
Open/Closed 7 New item/survival systems extend the block registry without reordering or mutating BLOCKS.
Liskov Substitution 8 Few inheritance hierarchies; no subtype violations observed.
Interface Segregation 8 Small, focused interfaces (ClickEdges, ItemStack, SerializedStats).
Dependency Inversion 6 Game depends on concrete classes (Inventory, PlayerState) rather than abstractions, which is acceptable for this prototype scope.
Average 6.8

🎯 Final Assessment

Overall Confidence Score: 70%

How to interpret: 61-80%: Minor concerns, mostly ready with some fixes.

Confidence Breakdown:

  • Code Quality: 75% (clean TypeScript, follows repo patterns, builds pass; some validation gaps)
  • Completeness: 85% (covers all described Tier 1/2 features; crafting is intentionally placeholder)
  • Risk Level: 60% (large surface area across input/game loop/UI, but no observed crashes)
  • Test Coverage: 30% (no automated tests; relies on manual/build verification)

Merge Readiness:

  • All critical issues resolved
  • SOLID average score >= 6.0
  • Overall confidence >= 60%
  • No security concerns
  • Tests present and passing (no test suite exists)

Verdict:

MERGE WITH FIXES

Address the water fall-damage bug and save-data validation before merging; the remaining issues are minor polish/UX items.

New%20session%20-%202026-06-13T23%3A30%3A29.626Z
opencode session  |  github run

- [HIGH] Water now cushions falls: trackFall resets when inWater
- [HIGH] Validate saved `mode` in loadSave; invalid values are dropped
- [MED] Inventory.load skips unknown item ids and clamps counts to maxStack
- [MED] Escape closes the inventory even when the search field has focus
- [LOW] Remove unused HOTBAR_BLOCKS export
- [LOW] Named block constants (WATER/CACTUS/MUSHROOM) replace magic ids
- [LOW] Water is no longer breakable (punching fluids does nothing),
  and the unbreakable toast is now generic
@github-actions

Copy link
Copy Markdown

📋 Summary

No linked issues (e.g., Fixes #…) were found in the PR description. This PR implements the advertised survival/creative gameplay loop, inventory system, per-seed save/load, and Firefox-robust input layer. The author also pushed a follow-up commit (9eecf447) that addresses every issue raised in the previous automated review. Build verification: game typecheck and build pass; website build passes; website astro check fails only on the pre-existing optional playwright import in website/scripts/screenshot.ts (not modified here).

📌 Review Metadata

🔴 Critical Issues

✅ All previously reported critical issues have been resolved.
None identified.

⚠️ High Priority Issues

[FIXED] Previous issue: Fall damage not cancelled by water — Player.trackFall() now resets fallPeakY when this.inWater is true (src/game/Player.ts:267-271).
[FIXED] Previous issue: Save mode value not validated — SaveData.loadSave() now drops invalid mode values (src/state/SaveData.ts:1793-1798).
None identified.

💡 Medium Priority Issues

[FIXED] Previous issue: Loaded inventory slots not validated — Inventory.load() now checks getItem(e.id) and clamps counts to maxStack (src/game/Inventory.ts:1158-1167).
[FIXED] Previous issue: Escape cannot close inventory while search box focused — InventoryUI adds its own keydown Escape handler on the search input (src/ui/InventoryUI.ts:2118-2124).

**[MEDIUM]** `src/engine/Input.ts:176-180` - Scroll wheel hotbar cycling disabled in cursor-aiming fallback mode
**Confidence:** High
**Description:** `handleWheel()` returns early unless `this._locked` is true. In the cursor-aiming fallback (pointer lock unavailable), the wheel event is ignored, so the player cannot scroll through the hotbar.
**Impact:** Hotbar scrolling is broken for the exact fallback mode the PR is trying to make fully playable.
**Suggested Fix:** Allow wheel events when no visible UI overlay has focus, e.g.:
```ts
const t = e.target as Element | null;
if (t && t.closest && t.closest(".screen:not([hidden])")) return;
e.preventDefault();
this.onScroll?.(e.deltaY > 0 ? 1 : -1);

[MEDIUM] src/game/Game.ts:599-604 - Survival placement consumes an item even when the block is not placed
Confidence: High
Description: placeBlock() ignores the changed boolean returned by world.setBlock() and always consumes one item in survival mode. If the target position already contains the same block, no change occurs but the item is still consumed.
Impact: Players can waste blocks by spam-placing against an already-present block (e.g., non-solid blocks like tall grass).
Suggested Fix: Only consume the item when the placement actually changed the world:

const changed = this.world!.setBlock(t.px, t.py, t.pz, block);
if (changed && this.settings.mode === "survival") {
  this.inventory.consumeOne(this.selectedIndex);
  this.refreshHud();
}

## ℹ️ Low Priority Suggestions
✅ **[FIXED]** Previous issue: `HOTBAR_BLOCKS` unused — export removed from `Blocks.ts`.
✅ **[FIXED]** Previous issue: Magic block ids for water and cactus — now use `WATER_BLOCK` / `CACTUS_BLOCK` constants (`src/game/Blocks.ts:368-370`, `src/game/Player.ts:1358`).
✅ **[FIXED]** Previous issue: Water breakable in survival — `HARDNESS[WATER_BLOCK] = "unbreakable"` (`src/game/Items.ts:1311`).

[LOW] src/state/Settings.ts:16-25 - Corrupted mode in global settings is not validated
Description: loadSettings() spreads the parsed JSON directly into DEFAULT_SETTINGS without validating mode. A hand-edited or corrupted localStorage entry could set settings.mode to an invalid string, disabling flight/mode checks.
Impact: Rare edge case, but the PR introduced this field and validated it in SaveData.ts; the global settings path should match.
Suggested Fix: Validate parsed.mode and fall back to DEFAULT_SETTINGS.mode if it is not "survival" or "creative".


[LOW] src/ui/InventoryUI.ts:141 - Trash icon uses emoji
Description: The trash slot uses the emoji 🗑️, while the PR deliberately uses SVG icons for hearts/hunger/air to avoid emoji rendering differences across platforms.
Impact: Minor visual inconsistency.
Suggested Fix: Replace the emoji with a text label ("Trash") or an inline SVG icon.


## 📊 SOLID Principles Score
| Principle | Score | Notes |
|-----------|-------|-------|
| Single Responsibility | 5 | `Game.ts` remains a large orchestrator handling rendering, input, inventory, survival systems, and UI state. |
| Open/Closed | 7 | New item/survival systems extend the block registry without reordering or mutating `BLOCKS`. |
| Liskov Substitution | 8 | Few inheritance hierarchies; no subtype violations observed. |
| Interface Segregation | 8 | Small, focused interfaces (`ClickEdges`, `ItemStack`, `SerializedStats`). |
| Dependency Inversion | 6 | `Game` depends on concrete classes (`Inventory`, `PlayerState`) rather than abstractions, acceptable for this prototype scope. |
| **Average** | **6.8** | |

## 🎯 Final Assessment

### Overall Confidence Score: 78%
**How to interpret:** 61-80%: Minor concerns, mostly ready with some fixes.

### Confidence Breakdown:
- **Code Quality:** 80% (clean TypeScript, follows repo patterns, builds pass; two medium interaction bugs)
- **Completeness:** 85% (covers all described Tier 1/2 features; crafting is intentionally placeholder)
- **Risk Level:** 65% (large surface area across input/game loop/UI, but no observed crashes)
- **Test Coverage:** 30% (no automated tests; relies on manual/build verification)

### Merge Readiness:
- [x] All critical issues resolved
- [x] SOLID average score >= 6.0
- [x] Overall confidence >= 60%
- [x] No security concerns
- [ ] Tests present and passing (no test suite exists)

### Verdict:
**MERGE WITH FIXES**

Address the cursor-aiming scroll regression and the survival placement item-consumption bug; the remaining items are polish/edge cases.

<a href="https://opencode.ai/s/tn9YA1aF"><img width="200" alt="New%20session%20-%202026-06-13T23%3A48%3A44.582Z" src="https://social-cards.sst.dev/opencode-share/TmV3IHNlc3Npb24gLSAyMDI2LTA2LTEzVDIzOjQ4OjQ0LjU4Mlo=.png?model=kimi-for-coding/k2p7&version=1.17.6&id=tn9YA1aF" /></a>
[opencode session](https://opencode.ai/s/tn9YA1aF)&nbsp;&nbsp;|&nbsp;&nbsp;[github run](/OpenStaticFish/voxl/actions/runs/27482678520)

- [MED] Hotbar wheel cycling now works in cursor-aiming mode (was
  gated on pointer lock); UI overlays still scroll naturally
- [MED] Survival placement only consumes an item when setBlock reports
  a change (no more wasting blocks on no-op placements)
- [LOW] loadSettings validates `mode`, falling back to the default
- [LOW] Trash slot uses an inline SVG instead of an emoji (matches the
  SVG icon convention used for hearts/hunger/air)
@github-actions

Copy link
Copy Markdown

📋 Summary

No linked issues (e.g., Fixes #…) were found in the PR description. This PR implements the advertised survival/creative gameplay loop, inventory system, per-seed save/load, and Firefox-robust input layer. Build verification: game typecheck and build pass; website build passes; website astro check fails only on the pre-existing optional playwright import in website/scripts/screenshot.ts (not modified here).

📌 Review Metadata

This PR adds a large, coherent survival/creative slice: item registry, inventory, player vitals, save/load, inventory UI, HUD bars, and a multi-event input fallback. The author has addressed every issue raised in the two prior automated review rounds. One new functional issue remains in the input layer, plus a few minor robustness/cleanup items.

🔴 Critical Issues

✅ All previously reported critical issues have been resolved.
None identified.

⚠️ High Priority Issues

[FIXED] Previous issue: Fall damage not cancelled by water — Player.trackFall() resets fallPeakY when this.inWater is true (src/game/Player.ts:267-271).
[FIXED] Previous issue: Save mode value not validated — SaveData.loadSave() drops invalid mode values (src/state/SaveData.ts:31-33).
[FIXED] Previous issue: Survival placement consumes item even when block not placed — Game.placeBlock() now checks changed before consuming (src/game/Game.ts:599-604).

**[HIGH]** `src/engine/Input.ts:74-168` - Multiple mouse events can queue the same break/place action twice
**Confidence:** High
**Description:** `handleMouseDown`, `handleClick`, `handleAuxClick`, and `handleContext` all set the same `breakQueued` / `placeQueued` booleans. Because `consumeClicks()` resets those flags once per frame, a single physical click whose events span a frame boundary (e.g., `mousedown` in frame N and `click`/`auxclick` in frame N+1) will trigger two break/place actions. A single right-click can fire `mousedown`, `auxclick`, and `contextmenu`, making triple-place possible.
**Impact:** Players may accidentally break/place multiple blocks with one click, wasting survival items and undermining the PR's goal of reliable input.
**Suggested Fix:** Deduplicate within a short time window, e.g.:
```ts
private lastBreakTime = 0;
private queueBreak(): void {
  const now = performance.now();
  if (now - this.lastBreakTime < 50) return;
  this.lastBreakTime = now;
  this.breakQueued = true;
}

Call queueBreak() from handleMouseDown (button 0) and handleClick; do the same for place.


## 💡 Medium Priority Issues
✅ **[FIXED]** Previous issue: Loaded inventory slots not validated — `Inventory.load()` checks `getItem(e.id)` and clamps counts to `maxStack` (`src/game/Inventory.ts:120-123`).
✅ **[FIXED]** Previous issue: Escape cannot close inventory while search box focused — `InventoryUI` adds its own `keydown` Escape handler on the search input (`src/ui/InventoryUI.ts:114-120`).
✅ **[FIXED]** Previous issue: Scroll wheel hotbar cycling disabled in cursor-aiming fallback mode — `handleWheel()` now allows wheel events regardless of pointer-lock state (`src/engine/Input.ts:176-183`).

[MEDIUM] src/ui/InventoryUI.ts:311-318 - Held items are silently lost when closing a full inventory
Confidence: High
Description: close() returns the cursor-held stack to the inventory via this.inventory.add(this.held.id, this.held.count), but it ignores the returned leftover count. If the inventory has no free slots, the unreturned items vanish.
Impact: Players can accidentally destroy items by closing the inventory when it is full.
Suggested Fix: Surface the failure, e.g. show a toast when leftover > 0, or drop the items into an empty world slot if the game ever supports ground drops:

const leftover = this.inventory.add(this.held.id, this.held.count);
this.held = null;
if (leftover > 0) {
  this.hud?.showToast("Inventory full — some items lost");
  this.onRefresh?.();
}
**[MEDIUM]** `src/game/PlayerState.ts:164-172` - Loaded survival stats are not range-validated
**Confidence:** Medium
**Description:** `load()` uses `??` fallbacks but accepts any numeric value, including negative, NaN, or out-of-range values for `hp`, `hunger`, `saturation`, `exhaustion`, and `breath`.
**Impact:** A hand-edited or corrupted save can produce invalid HUD display, invulnerability, or unexpected death/respawn behavior.
**Suggested Fix:** Clamp each value to its valid range after loading:
```ts
this.hp = clamp(data.hp ?? MAX_HP, 0, MAX_HP);
this.hunger = clamp(data.hunger ?? MAX_HUNGER, 0, MAX_HUNGER);
this.saturation = clamp(data.saturation ?? 5, 0, MAX_SATURATION);
this.exhaustion = Math.max(0, data.exhaustion ?? 0);
this.breath = clamp(data.breath ?? MAX_BREATH, 0, MAX_BREATH);
this.dead = this.hp <= 0;

## ℹ️ Low Priority Suggestions
✅ **[FIXED]** Previous issue: `HOTBAR_BLOCKS` unused — export removed from `Blocks.ts`.
✅ **[FIXED]** Previous issue: Magic block ids for water and cactus — now use `WATER_BLOCK` / `CACTUS_BLOCK` constants (`src/game/Blocks.ts:368-369`, `src/game/Player.ts:16`).
✅ **[FIXED]** Previous issue: Water breakable in survival — `HARDNESS[WATER_BLOCK] = "unbreakable"` (`src/game/Items.ts:131`).
✅ **[FIXED]** Previous issue: Corrupted `mode` in global settings not validated — `Settings.loadSettings()` now validates `mode` (`src/state/Settings.ts:21-24`).
✅ **[FIXED]** Previous issue: Trash icon used emoji — now uses an inline SVG (`src/ui/InventoryUI.ts:141-142`).

[LOW] src/game/Items.ts:114-133 - Remaining magic block ids in drop/hardness tables
Description: DROP_TABLE (lines 114-120) and HARDNESS (lines 129-133) still use raw numeric ids such as 20, 21, 22. Block ids are stable, but named constants improve maintainability and match the WATER_BLOCK/CACTUS_BLOCK/MUSHROOM_BLOCK pattern already introduced.
Impact: Future refactors or additions are more error-prone.
Suggested Fix: Export constants like TALL_GRASS_BLOCK = 20, FLOWER_BLOCK = 21, etc., from Blocks.ts, or derive plantlike hardness from BLOCKS.filter(b => b.shape === "plantlike").


## 📊 SOLID Principles Score
| Principle | Score | Notes |
|-----------|-------|-------|
| Single Responsibility | 5 | `Game.ts` remains a large orchestrator handling rendering, input, inventory, survival systems, and UI state. |
| Open/Closed | 7 | New item/survival systems extend the block registry without reordering or mutating `BLOCKS`. |
| Liskov Substitution | 8 | Few inheritance hierarchies; no subtype violations observed. |
| Interface Segregation | 8 | Small, focused interfaces (`ClickEdges`, `ItemStack`, `SerializedStats`). |
| Dependency Inversion | 6 | `Game` depends on concrete classes (`Inventory`, `PlayerState`) rather than abstractions, acceptable for this prototype scope. |
| **Average** | **6.8** | |

## 🎯 Final Assessment

### Overall Confidence Score: 78%
**How to interpret:** 61-80%: Minor concerns, mostly ready with some fixes.

### Confidence Breakdown:
- **Code Quality:** 80% (clean TypeScript, follows repo patterns, builds pass; one input-layer deduplication bug)
- **Completeness:** 85% (covers all described Tier 1/2 features; crafting is intentionally placeholder)
- **Risk Level:** 65% (large surface area across input/game loop/UI, but no observed crashes)
- **Test Coverage:** 30% (no automated tests; relies on manual/build verification)

### Merge Readiness:
- [x] All critical issues resolved
- [x] SOLID average score >= 6.0
- [x] Overall confidence >= 60%
- [x] No security concerns
- [ ] Tests present and passing (no test suite exists)

### Verdict:
**MERGE WITH FIXES**

Address the mouse-event deduplication bug before merging; the held-item loss and stat-validation gaps are also worth fixing. The remaining item is minor cleanup.

<a href="https://opencode.ai/s/1apsNTiu"><img width="200" alt="New%20session%20-%202026-06-14T00%3A02%3A30.153Z" src="https://social-cards.sst.dev/opencode-share/TmV3IHNlc3Npb24gLSAyMDI2LTA2LTE0VDAwOjAyOjMwLjE1M1o=.png?model=kimi-for-coding/k2p7&version=1.17.6&id=1apsNTiu" /></a>
[opencode session](https://opencode.ai/s/1apsNTiu)&nbsp;&nbsp;|&nbsp;&nbsp;[github run](/OpenStaticFish/voxl/actions/runs/27482962729)

@MichaelFisher1997 MichaelFisher1997 merged commit 009a8c3 into master Jun 14, 2026
1 check passed
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.

1 participant