Skip to content

Gameplay correctness: quest/state/economy fixes, combat+boss wiring, client truth pass, and a working Fracture Rift#2

Merged
georgeglarson merged 31 commits into
masterfrom
feat-gameplay-correctness
Jul 20, 2026
Merged

Gameplay correctness: quest/state/economy fixes, combat+boss wiring, client truth pass, and a working Fracture Rift#2
georgeglarson merged 31 commits into
masterfrom
feat-gameplay-correctness

Conversation

@georgeglarson

@georgeglarson georgeglarson commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the 2026-07-20 gameplay-correctness scan (6-lane end-to-end trace). The scan found the codebase's signature defect at scale: fully-built features never wired, and features advertising behavior they don't have. This PR fixes the high/medium findings in three server/client lanes plus the namesake feature itself.

Quest UX (was the 60-second CEO experience, broken):

  • Quest progress streams to the client (Messages.QuestStatus after offers and each increment, both AI and no-AI branches)
  • Quest rewards are actually granted (XP + item, drop-at-feet when inventory full)
  • Real abandon: new QUEST_ABANDON (105) routed to the quest service + QuestStatus(null) ack

State-wipe bugs:

  • Death no longer wipes up to 60s of progress: respawn-HELLO skips the storage reload for an already-authenticated dead player
  • Achievements no longer wiped on login (init is idempotent)
  • AI players no longer respawn as unkillable ghosts

Economy/inventory: cake + firepotion purchases no longer phantom, shop stock no longer leaks on abort, unequip preserves properties + notifies owner, stackable drops no longer desync

Combat/progression (advertised-but-fake class):

  • Nemesis system resurrected (killer attribution was never passed)
  • Roaming bosses broadcast movement, use config stats, can drop legendaries (bossId now set)
  • Zone XP/gold bonuses applied (were display-only), ascension multipliers applied, gray-mob XP protection sees the real level (was inverted), rested XP depletes, skill unlocks fire on level-up, level-up no longer refills HP on a dead player, party HP/position sync live, roaming-boss leash exemption, map-edge spatial-group clamp

Client truth pass: #tap-debug overlay removed from production, Venice branding retired, boss spawn announcements display, boss leaderboard reachable (L key), HUD equipment icons refresh, minimap fixes (incl. returning-player auto-show), glitch1 sound, quest offer de-duplicated, NPC dialogue keyed by kind, I/J keys guarded, ESC closes rift panels before exiting a run

The Fracture Rift (was completely unreachable): format registration, entry via status-bar button + R key + ESC, positional Messages.* classes, world spawn tick with map-validated positions, kill/death/disconnect wiring (disconnect restores entry position BEFORE save — panel finding), off-owner kills free the owner's spawn slot (panel finding), door-path teleport, SQLite-backed leaderboard, FORTIFIED/EMPOWERED modifiers at spawn

Test plan

  • pnpm test: 70 files / 3,321 tests green (+122 vs master)
  • pnpm build:server (tsc) + pnpm build:client (webpack) both clean; eslint parity on touched files
  • Rift implementation delegated to a MiniMax-M3 headless session (flat-rate lane) then hand-verified: full diff review, suite, builds; two panel rounds of findings fixed with tests (disposition record in the PR conversation)
  • Deploy note: no env/config changes vs current production; straight pull + build + restart

Summary by Sourcery

Restore advertised gameplay features and wiring across quests, combat, progression, AI, rifts, and client UI, including a fully playable Fracture Rift with persistent leaderboard and correct server-client messaging.

New Features:

  • Implement a full Fracture Rift experience with arena teleport, mob spawning, run progression, rewards, disconnect handling, and a client-side menu/HUD/leaderboard UI.
  • Add boss spawn world-event announcements and a boss kill leaderboard accessible via keybinds and UI.
  • Introduce quest abandon handling on server and client, including status clearing and confirmation messaging.

Bug Fixes:

  • Ensure quest progress and completion are streamed to the client, rewards are actually granted (including full-inventory ground drops), and abandon requests clear server-side state.
  • Prevent death-triggered respawns from reloading stale storage state, preserving in-memory progression while re-registering the player cleanly in the world.
  • Fix AI players respawning as invisible ghosts by re-adding them to the world entity registry on respawn.
  • Correct economy handling so consumable purchases like cake and firepotion grant items and shop stock does not leak when inventory is full.
  • Make inventory drops and unequips preserve item properties and keep client inventory stacks in sync.
  • Apply ascension, nemesis, zone, and roaming-boss multipliers correctly to combat rewards and damage, including gray-mob XP protection using the real player level.
  • Keep party HP and positions in sync by updating party state on damage, regen ticks, and movement.
  • Exempt roaming bosses from leash resets so they do not rubber-band to spawn mid-fight and broadcast their movement properly.
  • Clamp map spatial groups at edges so all tiles belong to a valid group and entities remain visible.
  • Make achievements initialization idempotent so persistence-loaded progress is not wiped on login.
  • Deplete rested XP correctly during XP gains and avoid refilling HP on level-up for dead players.
  • Guard inventory and achievements keybindings behind game readiness to prevent UI toggling when the game is not started.
  • Retire production debug overlays and Venice-specific client state while keeping static content intact.

Enhancements:

  • Refactor rift messaging to use positional Messages classes that match client parsers and add richer tests around the rift lifecycle and leaderboard.
  • Extend RiftManager with walkability-aware spawning, spawned-mob tracking, off-owner kill handling, and SQLite-backed leaderboard seeding/persistence.
  • Expand combat-system tests and logic around reward distribution, party sharing, and nemesis/zone/boss interactions.
  • Improve quest controller, Venice handler, and message router to keep quest state server-authoritative while exposing abandon and progress flows to the client.
  • Enhance minimap rendering to classify entities by kind rather than ad-hoc types and adjust visibility defaults for better UX.
  • Wire progression callbacks so skill unlocks trigger on level-up and ascension bonuses affect HP and damage consistently.
  • Tighten message format checking and add new message types for rift operations, leaderboards, and quest abandon.

Deployment:

  • Integrate rift leaderboard persistence with the existing SQLite storage at server boot while keeping the rest of the deployment flow unchanged.

Tests:

  • Add extensive unit tests for RiftHandler, RiftManager, combat and shop systems, Venice quest handling, map grouping, achievements, inventory, auth/respawn, AI-player lifecycle, roaming bosses, progression, storage, and player rested XP to lock in corrected behaviors and messaging contracts.
  • Increase overall test coverage for world ticks (regen, aggro), rift lifecycle, and new persistence paths like the rift leaderboard.

Summary by cubic

Fixes core gameplay wiring and ships a fully working Fracture Rift. Quests, combat, bosses, economy, and client UI now behave as advertised, and player progress no longer gets wiped on respawn/login.

  • New Features

    • Fracture Rift is playable: enter via status bar or R, exit with ESC; periodic spawns at map-validated positions; door-path teleport; FORTIFIED/EMPOWERED modifiers; correct kill/death/disconnect handling; off-owner kills free spawn slots; SQLite-backed leaderboard seeded at boot.
    • Boss leaderboard (L key) and world event announcements for boss spawns.
  • Bug Fixes

    • Quests: stream status to the client, grant XP and item rewards (drop at feet when full), support server-side abandon with an ack, and send a final status update before completion to keep the tracker in sync.
    • Progress/state: respawn no longer reloads from storage (no progress loss); achievements init is idempotent; AI bots no longer respawn as ghosts.
    • Economy/inventory: cake/firepotion purchases grant items; shop stock no longer leaks on full-inventory abort; unequip preserves properties and updates the owner; stack drops send correct counts.
    • Combat/progression: killer attribution flows end-to-end; zone and nemesis multipliers applied; ascension damage/HP bonuses applied; rested XP actually depletes; skill unlocks fire on level-up; level-up doesn’t refill HP on a dead player.
    • Bosses/world/party: roaming bosses broadcast movement, use config stats, and can drop legendaries; map-edge group clamp prevents invisible entities; party position/HP kept in sync.
    • Client: removed #tap-debug; retired Venice branding; minimap visibility/dots fixed; HUD equipment icons refresh on live equip; NPC talks keyed by kind; I/J keys gated until game start; ESC exits a rift only when no window or panel was closed (rift panels close first); failed rift entries show the server’s reason; boss spawn announcements and leaderboard reachable; rift UI escapes leaderboard names and modifier text to prevent XSS.

Written for commit 85c45fe. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added the Fracture Rift menu, lifecycle controls, progress updates, rewards, and leaderboard access.
    • Added keyboard shortcuts: R for the Rift menu and L for the boss leaderboard.
    • Added persistent Rift leaderboard rankings.
    • Added quest abandonment and improved quest progress, completion, and reward tracking.
    • Added world-event notifications and improved minimap visibility and entity indicators.
  • Bug Fixes

    • Improved inventory drop and unequip handling, including stack counts and item properties.
    • Prevented purchases when inventory is full.
    • Improved respawn behavior, party updates, boss movement, and Rift cleanup.
    • Added ascension bonuses to player health and combat damage.
  • Documentation

    • Updated in-game messaging to reflect AI integration retirement and static fallback content.

…n damage multipliers

Finding 1 (nemesis dead end-to-end): handleHurt called
world.handleHurtEntity(player) with no attacker, so player:died carried
killerId:undefined and nemesis.service never fired. Now passes (player,
mob, dmg) — the mob reaches handlePlayerDeath and the event carries
killerId/killerType. Also applies mob.nemesisPowerLevel (set by
nemesis.service but never consumed) as a mob->player damage multiplier.

Finding 6 (ascension): getAscensionDamageMultiplier had zero callers;
handleHit now scales player->mob damage by it (+5% per ascension, as
PROGRESSION_INIT advertises).
…istribution

distributePartyRewards fixes from the gameplay-correctness scan:

- Finding 1c: wire nemesisService.getRevengeMultipliers (zero callers)
  in when the dead mob was the attacker's nemesis (2.5x XP / 2x gold).
- Finding 4: consume RoamingBoss.getLootMultipliers (zero callers) so
  zone bosses pay with their config xp/gold multipliers.
- Finding 5: apply the mob's-zone xpBonus/goldBonus via the ZoneManager
  singleton (applyXpBonus/applyGoldBonus had zero callers while
  ZONE_INFO advertises 5-50% bonuses). Uses the singleton because the
  CombatSystem world-context literal (world.ts) does not carry
  zoneManager; zone is looked up at the MOB position.
- Finding 7: pass the attacker's real level to Formulas.xpFromMob so
  xpLevelModifier no longer compares against level 1 (level-40 players
  farming level-5 mobs got a bonus instead of ~0). Party shares use
  each member's own level.
- Finding 9: call player.updatePartyHp?.() in handleHurtEntity so
  party HP bars track damage.
- Finding 2: spawnBoss never called boss.onMove(...) (unlike
  mobarea.ts / spawn-manager.ts), so clients saw zone bosses frozen
  while the server roamed them. Now wired to
  world.onMobMoveCallback; BossWorld interface gains the method.
- Finding 3: world.getDroppedItem gates legendary drops on mob.bossId,
  which RoamingBoss never set — zone-boss legendaries were impossible.
  Constructor now sets this.bossId = config.id.
- Finding 4: bosses fought and paid as their base sprite kind while
  getBaseDamage/getArmor sat unused. Constructor now overrides
  weaponLevel/armorLevel from config and level from the zone maxLevel.
- Replace lazy require('./message') with a top-level import (no import
  cycle; message.ts only depends on gametypes/items) so the module is
  loadable under the ESM test runner.
Finding 6: getAscensionHpMultiplier (progression.handler) had zero
callers, so PROGRESSION_INIT's advertised +5% max HP per ascension was
never applied. All max-HP recalcs (equipment-driven player update, mob
spawn) route through Character.resetHitPoints, so the multiplier is
applied there; mobs carry no ascensionCount and are unaffected.
Finding 8: getGroupIdFromPosition used floor() group dims, so on the
172x314 map (6x26 full groups) tiles x in {169..171} and y in {312,313}
resolved to nonexistent groups — addToGroup skipped those entities and
removeFromGroups nulled entity.group (invisible players at the east
beach edge, ~2.3% ghost zone-boss spawns). gx/gy are now clamped into
[0, groupWidth-1]/[0, groupHeight-1]; a full-map sweep test asserts
every tile resolves to a group forEachGroup creates.
…s the first (finding 11); also wires sendQuestAbandon dep (finding 7)
- Send Messages.QuestStatus after quest offer (progress 0) and after
  every non-completing kill/area progress tick, in both Venice and
  no-AI branches (finding 1: nothing sent QuestStatus).
- On quest completion, grant quest.xp via player.grantXP and add the
  reward item to inventory, dropping at the player's feet when full
  (finding 2: rewards were never granted).
- Route QUEST_ABANDON (105) to the active quest service and confirm
  with QuestStatus(null) so the client clears server-side
  (finding 3).
- Respawn (second HELLO on the same connection while dead) no longer
  reloads from storage, which clobbered up to 60s of progress with the
  last autosave. Keeps in-memory state, restores full HP, respawns at
  the last checkpoint, and resends WELCOME + EQUIP from live state
  (finding 4). First-login behavior is unchanged.
- AchievementService.initPlayer is now idempotent: when the
  persistence layer already loaded saved state during login, the later
  init from auth.handler no longer overwrites it with a blank state
  (finding 5).
- ProgressionService.levelUp now invokes a checkSkillUnlocks callback,
  wired in Player to SkillHandler.checkSkillUnlock, so SKILL_UNLOCK
  reaches the client mid-session (finding 7: checkSkillUnlocks had no
  callers).
- Player.grantXP writes the rested XP burn back from the progression
  context copy, so the rested bonus actually depletes (finding 8).
- EconomyService classifies consumables with Types.isExpendableItem,
  so CAKE and FIREPOTION take the inventory path instead of charging
  no gold and granting no item while stock still decremented
  (finding 9).
- handleShopBuy checks inventory room BEFORE processPurchase so
  limited stock is no longer decremented when the buy then aborts on
  a full inventory (finding 10).
- handleUnequipToInventory now carries the equipped item's properties
  into inventory (and into INVENTORY_ADD) instead of null (finding 11).
- The owner is sent the equip change directly after unequip — the
  broadcast ignores self (finding 12, mirrors the equip path).
- Dropping part of a stack sends INVENTORY_UPDATE with the remaining
  count instead of INVENTORY_REMOVE while the server keeps the
  remainder (finding 13).
- AIPlayer.respawn re-registers the bot with the entity registry and
  players map — die() removed it, so bots respawned as invisible
  ghosts (finding 6).
- MOVE handler calls player.updatePartyPosition after a successful
  move so party XP range checks and the party UI use live positions
  (finding 14: updatePartyPosition had no callers).
- World regen tick calls player.updatePartyHp after healing so party
  HP bars don't freeze between damage events (finding 15).
- RoamingBoss mobs (config present) are exempt from spawn-anchor
  leash resets in both the aggro tick and the move callback — they
  roam beyond leash range by design and rubber-banded mid-fight
  (finding 16).
Covers saveRiftEntry upsert, getRiftLeaderboard sort/limit, and
uninitialized-db guards for both functions. The wiring on the
rift-manager side (setLeaderboardSink + loadLeaderboard on boot) was
already landed on this branch; these tests lock the storage layer
that backs it.
The namesake feature was unreachable and unwired at every layer (scan
finding D2). Closes the full gap list:

- format.ts: register RIFT_ENTER/RIFT_EXIT/RIFT_LEADERBOARD_REQ (the
  checker used to disconnect on these)
- Entry: status-bar rift button + R key + ESC (RIFT_PORTAL has no client
  sprite, so the menu-button path); in-rift exit via menu/ESC
- Protocol: Messages.RiftStart/Progress/Advance/End/Leaderboard with
  positional serialize() matching the existing client parsers
- Spawning: world 1s rift tick -> riftManager.getMobToSpawn (3s cadence,
  max 5 alive, map-validated positions) -> addMob -> registerSpawnedMob
- Kills/death/disconnect: handleRiftKill/handleRiftDeath from
  combat-system, handleRiftDisconnect from world onExit (despawns
  leftover mobs)
- Teleport: save position on enter, door-path teleport to the arena,
  restore on exit/death
- Leaderboard: seeded from SQLite at boot + persisted per ranked run
  (memory-only fallback if storage unavailable)
- Modifiers: FORTIFIED/EMPOWERED applied at spawn; the rest documented
  display-only (minimal modifier system)

Built via delegated MiniMax-M3 session; verified by hand (full diff
review + suite). Tests 70f/3317 green (+26), tsc + webpack clean.
- player.ts/world.ts: handleRiftDisconnect now restores the rift entry
  position and runs BEFORE saveToStorage — a disconnect inside the rift
  used to persist the arena position (codex)
- combat-system.ts + rift-manager.ts: freeSpawnSlotForMob frees the
  owning run's spawn slot when a rift mob dies to a non-owner (party
  member / another player) — off-owner kills no longer stall the run at
  the spawn cap; no progress credit (codex)
- world.ts: rift mob ids are '6' + zero-padded session counter — entity.ts
  parses ids with parseInt, so kind-encoding or separators break/collide
  (qwen, amplified: the kind+counter string concatenation was ambiguous
  and the separator variant parsed to a constant)
- qwen leaderboard-handler finding: FALSE POSITIVE — handler exists at
  message-router.ts:388 (pre-existing; the PR only added format+client)
- qwen equip() double-call finding: harmless — equip() is a pure message
  builder (player.ts 'return new Messages.EquipItem(this, item)')

Tests: +4 (position restore, no-op disconnect, freeSpawnSlot ownership
+ unknown mob). Suite 70f/3321 green, tsc clean.
- progression.service: level-up skips the HP refill and HitPoints packet
  when the player is dead (rift death-reward level-up would resurrect the
  corpse's HP bar and confuse hitPoints<=0 cleanup) (codex)
- client ESC: closes open rift panels (menu/leaderboard) before falling
  through to exitRift — closing an overlay no longer ends the run (codex)
- minimap: returning players get auto-show too, matching the old default
  (qwen P3)
- qwen unknown-reward-kind finding: SILENCED by design — an unresolvable
  kind string cannot be materialized or dropped; XP always lands, all 16
  shipped templates have valid kinds (pinned by quest tests), the warn
  log is the audit trail
@sourcery-ai

sourcery-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Server and client gameplay correctness pass wiring previously standalone systems (quests, progression, combat, bosses, rift) to actual state, messages, and storage: Fracture Rift is now fully enterable, progresses and rewards correctly with positional protocol, quest progress and rewards are granted and abandon is handled, combat/XP/economy systems respect nemesis, zones, ascension, party, and boss configs, and several persistence/AI/world bugs are fixed.

Sequence diagram for quest progress, completion, and reward delivery

sequenceDiagram
  actor P as Player
  participant GC as GameClient
  participant VH as VeniceHandler
  participant QS as QuestServiceLike
  participant Inv as Inventory
  participant W as World

  P->>GC: kill mob / enter area
  GC->>VH: handleKill / handleAreaChange
  VH->>QS: checkQuestProgress(playerId, type, target)
  alt quest completed
    QS-->>VH: QuestResult(completed=true, xp, reward)
    VH->>GC: Messages.QuestComplete
    VH->>VH: grantQuestRewards(ctx, result)
    opt XP reward
      VH->>P: grantXP(result.xp)
    end
    VH->>Inv: getInventory()
    alt inventory hasRoom(kind)
      VH->>Inv: addItem(kind, properties, 1)
      VH->>GC: INVENTORY_ADD(slotIndex, kind, properties, count)
    else inventory full or no inventory
      VH->>W: createItemWithProperties(kind, ctx.x, ctx.y, null)
      W-->>VH: SpawnableItem
      VH->>W: addItem(item)
      VH->>W: broadcast(Messages.Spawn)
    end
  else quest not yet complete
    QS-->>VH: null
    VH->>VH: sendQuestProgress(ctx, quests, type, target)
    VH->>GC: Messages.QuestStatus(quest)
  end

  note over P,QS: QUEST_ABANDON
  P->>GC: QUEST_ABANDON
  GC->>VH: handleAbandonQuest
  VH->>QS: abandonQuest(playerId)
  VH->>GC: Messages.QuestStatus(null)
Loading

File-Level Changes

Change Details Files
Fracture Rift server protocol, lifecycle, and world integration are fully wired and aligned with positional client parsers, including enter/kill/advance/end/leaderboard, teleport/save/restore, mob spawning/cleanup, and leaderboard persistence.
  • Refactor RiftHandler to use Messages.* positional classes for RIFT_START/PROGRESS/ADVANCE/END/LEADERBOARD and strictly match client receiveRift* field order.
  • Extend RiftPlayerContext and Player to support saving entry position, door-path teleport into arena, teleport back on exit, and despawning leftover rift mobs on exit/death/disconnect.
  • Enhance RiftManager with spawn position validation against the world map, arena center calculation, freeSpawnSlotForMob for off-owner kills, cleanupDisconnectedPlayer return value, and SQLite-backed leaderboard sink/load.
  • Add world rift spawn tick that spawns scaled rift mobs via addMob on walkable tiles, registers them with riftManager, and exempts roaming bosses from leash resets.
  • Update tests for rift handler, rift manager, world, and message serialization to cover positional messages, new lifecycle behaviors, and mob cleanup/identification.
server/ts/player/rift.handler.ts
server/ts/rifts/rift-manager.ts
server/ts/world.ts
server/ts/player.ts
server/ts/message.ts
server/ts/__tests__/rift.handler.test.ts
server/ts/__tests__/rift-manager.test.ts
server/ts/__tests__/world.test.ts
server/ts/main.ts
server/ts/__tests__/message.test.ts
Quest system now streams progress/status to the client, grants XP and item rewards (including drop-at-feet when inventory is full), and supports explicit quest abandonment across static and Venice modes.
  • Extend VenicePlayerContext with optional inventory/world/reward methods and implement sendQuestProgress and grantQuestRewards helpers that use Messages.QuestStatus/QuestComplete and INVENTORY_ADD or ground drops.
  • Wire quest progress updates for kill and explore quests in both static and Venice services and send QuestStatus on offer initialization and partial progress.
  • Implement handleAbandonQuest on the server and route QUEST_ABANDON in message router, clearing active quests in static/Venice services and acknowledging with QuestStatus(null).
  • Update client quest controller to send QUEST_ABANDON, track repeated QuestStatus updates, and tolerate server-side status null after local abandon.
  • Expand Venice/quest-related tests to cover progress messaging, reward granting behavior, drop-on-full, unknown reward strings, and abandon flows.
server/ts/player/venice.handler.ts
server/ts/player/message-router.ts
server/ts/__tests__/venice.handler.test.ts
server/ts/__tests__/message-router.test.ts
client/ts/quest/quest-controller.ts
client/ts/__tests__/quest-controller.test.ts
shared/ts/gametypes.ts
server/ts/ai/achievements/achievement.service.ts
Combat, progression, and party systems now correctly apply gray-mob XP protection, nemesis revenge multipliers, zone and boss loot bonuses, ascension damage/HP bonuses, party HP sync, and rift kill/death hooks, with killer attribution flowing through to player:died.
  • Wire nemesisService, zoneManager, riftManager, and RoamingBoss loot multipliers into CombatSystem reward distribution, with per-member XP based on each member’s level and zone/party modifiers.
  • Add party HP sync on both damage (combat-system) and regen ticks (world), and guard optional updatePartyHp for AI players.
  • Update CombatHandler to apply nemesisPowerLevel as a damage multiplier, use ascension-based damage multiplier via progression handler, and pass the attacking mob into world.handleHurtEntity so death events carry killerId.
  • Extend Character.resetHitPoints and progression service to apply ascension HP multiplier and trigger skill unlock checks and level-up HP behavior that respects dead players.
  • Integrate rift kill/death hooks in CombatSystem and Player so only rift mobs count toward rift progress, off-owner kills free spawn slots, and player death ends the rift run with partial rewards.
  • Add extensive tests in combat-system, combat-handler, character, progression.service, and roaming-boss to validate XP/gold math, multipliers stacking, party splits, nemesis behavior, boss stats/loot multipliers, and HP/skill unlock semantics.
server/ts/combat/combat-system.ts
server/ts/player/combat.handler.ts
server/ts/character.ts
server/ts/player/progression.service.ts
server/ts/player.ts
server/ts/roaming-boss.ts
server/ts/__tests__/combat-system.test.ts
server/ts/__tests__/combat.handler.test.ts
server/ts/__tests__/character.test.ts
server/ts/__tests__/progression.service.test.ts
server/ts/__tests__/roaming-boss.test.ts
Economy, inventory, shop, and map systems are corrected so purchases are non-phantom, shop stock doesn’t leak on abort, unequip preserves item properties and notifies the owner, stackable drops sync correctly, and spatial groups clamp at map edges.
  • Fix EconomyService consumable classification to treat all expendables (cake, firepotion, flask, burger) as consumables with correct heal amounts/prices.
  • Change ShopHandler.handleShopBuy to pre-check inventory capacity before calling processPurchase, avoiding stock decrement for aborted purchases, and add tests for cake/firepotion and stock-leak pre-checks.
  • Update inventory unequip to carry properties into inventory, serialize them in INVENTORY_ADD, and send self equip messages in addition to broadcast, and adjust inventory drop to use INVENTORY_UPDATE when a stack remains instead of INVENTORY_REMOVE.
  • Correct Map.getGroupIdFromPosition to clamp group indices within valid bounds so entities in truncated edge strips are assigned to existing groups, and add tests covering edge clamping and group coverage.
  • Extend inventory handler tests for unequip properties, owner notifications, and stackable drop behavior.
server/ts/player/economy.service.ts
server/ts/player/shop.handler.ts
server/ts/player/inventory.handler.ts
server/ts/map.ts
server/ts/__tests__/shop.handler.test.ts
server/ts/__tests__/inventory.handler.test.ts
server/ts/__tests__/economy.service.test.ts
server/ts/__tests__/map.test.ts
Auth, achievements, AI players, world exit, and misc client UI/input flows are hardened to prevent state wipes and ghost states, retire debugging overlays and Venice branding, and wire new boss/rift UIs, keyboard shortcuts, and minimap behavior.
  • Implement a respawn-specific HELLO path that skips storage reload for already-authenticated dead players, restores HP/position, re-registers with world, resends WELCOME/EQUIP from in-memory state, and add tests to guarantee achievements and progress are preserved.
  • Make AchievementService.initPlayer idempotent so persistence-layer loaded state isn’t wiped by auth, and test load-order behavior and reward duplication avoidance.
  • Fix AIPlayer die/respawn to properly remove from and re-add to world entity registries and players map, preventing unkillable ghost bots; add tests for this lifecycle.
  • Update world onExit to call player.handleRiftDisconnect before saving, ensuring rift exit restores entry position pre-save and leftover mobs despawn; add world tests for regen party HP behavior and roaming boss leash exemption.
  • On the client, remove tap-debug overlay and Venice button/branding, add boss leaderboard and rift menu/HUD/leaderboard UI (including ESC behavior and keyboard guards), change NPC talk tracking to be keyed by npcKind, and ensure minimap initializes hidden but is auto-shown for new/returning players and uses Types-based kind classification for dots.
  • Wire new client events and handlers for world events (boss spawn announcements), boss leaderboard, rift entry/exit/leaderboard routes, quest abandon message, and equipment HUD refresh; adjust audio to include glitch1 sound and add CSS/HTML for rift status bar button and AI-retired note.
server/ts/player/auth.handler.ts
server/ts/achievements/achievement.service.ts
server/ts/ai/aiplayer.ts
server/ts/world.ts
server/ts/player/message-router.ts
server/ts/__tests__/auth.handler.test.ts
server/ts/__tests__/achievement.handler.test.ts
server/ts/__tests__/aiplayer.test.ts
client/ts/game.ts
client/ts/main.ts
client/ts/ui/rift-ui.ts
client/ts/handlers/game-event.handler.ts
client/ts/player/interaction-controller.ts
client/ts/network/message-handlers.ts
client/ts/network/client-events.ts
client/ts/network/gameclient.ts
client/ts/bootstrap/game-bootstrap.ts
client/ts/ui/minimap-ui.ts
client/css/main.css
client/index.html
client/ts/audio.ts
client/ts/handlers/rift-ui.handler.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Fracture Rift gameplay, client controls, server messaging, spawning, cleanup, and leaderboard persistence. It also synchronizes quest progress and abandonment, replaces retired AI presentation, and updates combat, progression, economy, inventory, map, and world behavior with regression coverage.

Changes

Fracture Rift

Layer / File(s) Summary
Rift protocol and lifecycle
server/ts/message.ts, server/ts/player/rift.handler.ts, server/ts/rifts/rift-manager.ts
Rift messages use serialized positional payloads, lifecycle handlers return cleanup data, and Rift mobs and leaderboard entries are tracked.
Rift runtime
server/ts/player.ts, server/ts/world.ts, server/ts/combat/*, server/ts/roaming-boss.ts
Rift spawning, teleportation, death/disconnect cleanup, combat hooks, and boss movement integration are implemented.
Rift client interface
client/ts/ui/rift-ui.ts, client/ts/game.ts, client/ts/main.ts, client/index.html
The Rift menu, status-bar button, hotkeys, leaderboard action, ESC handling, notifications, and escaped rendered strings are added.
Rift validation and persistence
server/ts/__tests__/rift*.test.ts, server/ts/__tests__/message.test.ts, server/ts/__tests__/storage.service.test.ts
Rift lifecycle payloads, cleanup, spawn-slot handling, serialization, and leaderboard storage are tested.

Quest synchronization and AI retirement

Layer / File(s) Summary
Quest contracts and transport
shared/ts/*, client/ts/network/*, server/ts/format.ts, server/ts/player/message-router.ts
Quest payloads are typed, QUEST_ABANDON is added, and the server routes abandonment requests.
Quest progress, rewards, and abandonment
server/ts/player/venice.handler.ts, server/ts/ai/quest.service.ts, server/ts/ai/types.ts, client/ts/quest/*
Quest trackers receive initial and intermediate status, completion syncs final state and grants rewards, and abandonment clears the tracker.
NPC state and retired AI presentation
client/ts/bootstrap/*, client/ts/game.ts, client/ts/player/interaction-controller.ts, client/css/main.css, client/index.html
NPC talk state is keyed by NPC kind, the AI retirement note replaces the former promotion, and related presentation is updated.
Quest and state validation
client/ts/__tests__/quest-controller.test.ts, server/ts/__tests__/venice.handler.test.ts, server/ts/__tests__/achievement.handler.test.ts, server/ts/__tests__/auth.handler.test.ts
Quest progress, abandonment, rewards, achievement initialization, and respawn flows receive expanded coverage.

Gameplay systems and regressions

Layer / File(s) Summary
Combat, rewards, and party state
server/ts/combat/*, server/ts/player/combat.handler.ts, server/ts/world.ts
Ascension and nemesis modifiers, zone and roaming-boss reward scaling, party HP synchronization, and killer context are added.
Progression, HP, and respawn
server/ts/character.ts, server/ts/player.ts, server/ts/player/progression.service.ts, server/ts/ai/aiplayer.ts
Ascension HP, rested-XP persistence, level-up callbacks, and player/AI re-registration after death are updated.
Economy and inventory
server/ts/player/economy.service.ts, server/ts/player/shop.handler.ts, server/ts/player/inventory.handler.ts
Expendable purchases, inventory capacity checks, stack drops, and equipped-item property preservation are corrected.
World, map, and movement
server/ts/map.ts, server/ts/player/message-router.ts, server/ts/world.ts, server/ts/roaming-boss.ts
Map coordinates clamp to valid groups, movement updates party state, roaming bosses bypass leash resets, and regeneration updates party health.
Gameplay regression coverage
server/ts/__tests__/*
Combat, progression, economy, inventory, map, movement, roaming-boss, and world-tick behavior are covered by additional tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant Client
  participant GameServer
  participant RiftManager
  participant Storage
  Player->>Client: open Rift menu
  Client->>GameServer: enter Rift
  GameServer->>RiftManager: start run and select spawn
  RiftManager-->>GameServer: run state and mob spawn data
  GameServer-->>Client: RiftStart or RiftProgress
  Player->>GameServer: exit or die
  GameServer->>RiftManager: end run
  RiftManager->>Storage: persist leaderboard entry
  GameServer-->>Client: RiftEnd and cleanup state
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is a single clear sentence that summarizes the main gameplay, client, and Fracture Rift changes in the pull request.
✨ 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 feat-gameplay-correctness

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 security issues

Security issues:

  • User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities (link)
  • User controlled data in a this.menuElement.innerHTML is an anti-pattern that can lead to XSS vulnerabilities (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="client/ts/ui/rift-ui.ts" line_range="193" />
<code_context>
    this.menuElement.innerHTML = html;
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-document-method):** User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

### Comment 2
<location path="client/ts/ui/rift-ui.ts" line_range="193" />
<code_context>
    this.menuElement.innerHTML = html;
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-innerhtml):** User controlled data in a `this.menuElement.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread client/ts/ui/rift-ui.ts
html += `<button id="rift-menu-leaderboard" style="${buttonStyle}">Leaderboard</button>`;
html += `<button id="rift-menu-close" style="${buttonStyle} background: #444;">Close</button>`;

this.menuElement.innerHTML = html;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.browser.security.insecure-document-method): User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

Source: opengrep

Comment thread client/ts/ui/rift-ui.ts
html += `<button id="rift-menu-leaderboard" style="${buttonStyle}">Leaderboard</button>`;
html += `<button id="rift-menu-close" style="${buttonStyle} background: #444;">Close</button>`;

this.menuElement.innerHTML = html;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.browser.security.insecure-innerhtml): User controlled data in a this.menuElement.innerHTML is an anti-pattern that can lead to XSS vulnerabilities

Source: opengrep

…eedback

- venice.handler/quest.service: QuestResult carries the completed quest;
  a final QuestStatus (N/N) is sent before QuestComplete in all four
  branches — the tracker no longer jumps from a partial count to the
  completion toast (qwen)
- client ESC: an active rift only exits when ESC closed nothing — any
  open window/panel (achievements, help, credits, rift panels) makes ESC
  a close gesture, not a run-ender (codex)
- client rift-ui.handler: failed entries and other non-death failures
  now show the server's reason (was silent) (codex P3)
- codex multi-world rift-mob id collision: dormant — nb_worlds=1 in
  production; noted with the existing boss-id-collision caveat in the
  scan doc
@georgeglarson

Copy link
Copy Markdown
Owner Author

Panel round 3 (review-223559) dispositions — FIXED in 62db492: codex ESC-exits-rift-with-window-open (ESC now only exits when it closed nothing); codex silent rift-entry failure (client shows the reason); qwen quest completion final progress sync (QuestResult carries the quest; final N/N QuestStatus precedes QuestComplete in all four branches, test updated). SILENCED: codex multi-world rift-mob id collision — dormant (nb_worlds=1 in production; noted alongside the existing boss-id-collision caveat). No fourth round: findings from the disposition run itself were micro-class and are all resolved above. Suite 70f/3321 green, tsc + webpack clean.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

11 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="client/ts/handlers/game-event.handler.ts">

<violation number="1" location="client/ts/handlers/game-event.handler.ts:127">
P3: showNotification is called twice back-to-back for title then description; since the notification UI appears to be a single-slot display, the title message risks being clobbered before it's visible. Consider staggering the description with a short setTimeout (similar to the warning in handleZoneEnter) or combining title+description into one notification.</violation>
</file>

<file name="client/ts/player/interaction-controller.ts">

<violation number="1" location="client/ts/player/interaction-controller.ts:220">
P2: Keying pending NPC talk purely by `npc.kind` (not by NPC instance) means talking to two NPCs of the same kind in quick succession (e.g. the map's 10 'guard' NPCs) can cross-attribute the AI response to the wrong NPC, and the first NPC's '...' bubble never resolves since its fallback-timeout equality check fails. Consider keying by a unique NPC id (or kind+id) instead of kind alone.</violation>
</file>

<file name="server/ts/player/shop.handler.ts">

<violation number="1" location="server/ts/player/shop.handler.ts:52">
P3: Now that inventory room is verified upfront before processPurchase, the duplicate hasRoom checks inside the isWeapon/isArmor and isConsumable branches further down are unreachable in the normal synchronous flow and could be removed for clarity.</violation>
</file>

<file name="server/ts/rifts/rift-manager.ts">

<violation number="1" location="server/ts/rifts/rift-manager.ts:341">
P3: `freeSpawnSlotForMob` does a linear scan across all active rift runs for every kill in the game (it's invoked unconditionally from combat-system.ts, not gated on `isRiftMob`). Consider maintaining a `mobId -> playerId` index in the manager so this lookup is O(1) instead of O(active runs) per kill.</violation>
</file>

<file name="server/ts/player/auth.handler.ts">

<violation number="1" location="server/ts/player/auth.handler.ts:42">
P2: If `world.addPlayer(player)` fails on respawn, the client still receives WELCOME/EQUIP and believes it has respawned, even though the player was never re-registered with the entity manager — producing exactly the kind of ghost/desynced player state this PR fixes elsewhere for AI respawns. Consider checking the boolean return and handling the failure path.</violation>
</file>

<file name="server/ts/player/venice.handler.ts">

<violation number="1" location="server/ts/player/venice.handler.ts:103">
P3: The drop-at-feet block in grantQuestRewards duplicates the create/addItem/broadcast/log pattern already used in handleInventoryDrop (inventory.handler.ts) and equipment.handler.ts; the comment even calls out that it 'mirrors' that pattern. Consider extracting a shared `dropItemAtFeet(world, ctx, kind, properties)` helper to avoid the three near-identical copies drifting apart over time.</violation>
</file>

<file name="server/ts/achievements/achievement.service.ts">

<violation number="1" location="server/ts/achievements/achievement.service.ts:47">
P2: The new idempotent early-return in initPlayer assumes cleanupPlayer() reliably clears a player's map entry before the id is reused; Player.cleanupAchievements() doesn't appear to be wired into any disconnect handler, so a reused player id could now inherit a previous session's achievement state instead of starting fresh (previously the old create-new-empty-state path acted as an implicit safety net). Worth confirming disconnect wiring calls player.cleanupAchievements(), or clearing playerAchievements on disconnect explicitly.</violation>
</file>

<file name="server/ts/world.ts">

<violation number="1" location="server/ts/world.ts:592">
P2: The rift mob spawn tick wraps the entire `forEachPlayer` loop in a single try/catch, so one player's spawn failure aborts spawning for every other player in that tick — unlike the periodic-save loop just above it, which catches per-player. Move the try/catch inside the per-player callback so one player's error doesn't stall rift mob spawns for everyone else in the world.</violation>
</file>

<file name="client/index.html">

<violation number="1" location="client/index.html:206">
P3: The Fracture Rift is accessible via the R key (visible in the button tooltip and wired in main.ts), but the R key shortcut is missing from the keyboard shortcuts help list in index.html. The L key for the boss leaderboard was added alongside it, so it looks like an omission. Adding an entry for R would make the help text consistent with the actual key bindings.</violation>
</file>

<file name="client/ts/network/message-handlers.ts">

<violation number="1" location="client/ts/network/message-handlers.ts:592">
P2: When a quest is completed, the player sees a double notification because both the old legacy handler in message-handlers.ts and the new handler in quest.handler.ts react to the same QUEST_COMPLETE event. The old handler calls `showNotification` with 'Quest Complete! Reward: X' and the new handler calls `showNotification` with 'Reward: X, +Y XP', so two toast-like messages appear. Remove the old QUEST_COMPLETE handler from message-handlers.ts (it was already done correctly for QUEST_OFFER in the same function) — the quest.handler.ts + QuestController.handleQuestComplete already handles the user-facing feedback.</violation>
</file>

<file name="client/ts/ui/minimap-ui.ts">

<violation number="1" location="client/ts/ui/minimap-ui.ts:280">
P3: Minimap item check does not exclude chests from the gold dot rendering. The new code uses `Types.isObject(entity.kind)` which includes chests (CHEST, CHEST_CRATE, etc.), but the game type system considers chests as non-items — `Types.isItem()` explicitly filters them out with `!isChest(kind)`. This means chests now render as gold "item" dots on the minimap when they didn't before. Consider using `Types.isItem(entity.kind)` for consistency with the rest of the game's type classification.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

// Store current NPC for response handling
this.deps.setCurrentNpcTalk(npc);
// Register pending talk (keyed by NPC kind) for response handling
this.deps.setPendingNpcTalk(npc);

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Keying pending NPC talk purely by npc.kind (not by NPC instance) means talking to two NPCs of the same kind in quick succession (e.g. the map's 10 'guard' NPCs) can cross-attribute the AI response to the wrong NPC, and the first NPC's '...' bubble never resolves since its fallback-timeout equality check fails. Consider keying by a unique NPC id (or kind+id) instead of kind alone.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/ts/player/interaction-controller.ts, line 220:

<comment>Keying pending NPC talk purely by `npc.kind` (not by NPC instance) means talking to two NPCs of the same kind in quick succession (e.g. the map's 10 'guard' NPCs) can cross-attribute the AI response to the wrong NPC, and the first NPC's '...' bubble never resolves since its fallback-timeout equality check fails. Consider keying by a unique NPC id (or kind+id) instead of kind alone.</comment>

<file context>
@@ -213,8 +216,8 @@ export class InteractionController {
-    // Store current NPC for response handling
-    this.deps.setCurrentNpcTalk(npc);
+    // Register pending talk (keyed by NPC kind) for response handling
+    this.deps.setPendingNpcTalk(npc);
 
     // Show thinking indicator
</file context>
Fix with cubic

// entity list — the client rebuilt its entity grid on respawn.
// (No enterCallback: callbacks persist from first login, and a respawn
// is not a fresh 'join' world event.)
world.addPlayer(player);

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: If world.addPlayer(player) fails on respawn, the client still receives WELCOME/EQUIP and believes it has respawned, even though the player was never re-registered with the entity manager — producing exactly the kind of ghost/desynced player state this PR fixes elsewhere for AI respawns. Consider checking the boolean return and handling the failure path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/ts/player/auth.handler.ts, line 42:

<comment>If `world.addPlayer(player)` fails on respawn, the client still receives WELCOME/EQUIP and believes it has respawned, even though the player was never re-registered with the entity manager — producing exactly the kind of ghost/desynced player state this PR fixes elsewhere for AI respawns. Consider checking the boolean return and handling the failure path.</comment>

<file context>
@@ -19,13 +19,61 @@ const log = createModuleLogger('AuthHandler');
+    // entity list — the client rebuilt its entity grid on respawn.
+    // (No enterCallback: callbacks persist from first login, and a respawn
+    // is not a fresh 'join' world event.)
+    world.addPlayer(player);
+    world.pushRelevantEntityListTo(player);
+
</file context>
Fix with cubic

* the existing entry instead of wiping it with a blank state.
*/
initPlayer(playerId: string, savedData?: PlayerAchievements): PlayerAchievements {
if (!savedData) {

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The new idempotent early-return in initPlayer assumes cleanupPlayer() reliably clears a player's map entry before the id is reused; Player.cleanupAchievements() doesn't appear to be wired into any disconnect handler, so a reused player id could now inherit a previous session's achievement state instead of starting fresh (previously the old create-new-empty-state path acted as an implicit safety net). Worth confirming disconnect wiring calls player.cleanupAchievements(), or clearing playerAchievements on disconnect explicitly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/ts/achievements/achievement.service.ts, line 47:

<comment>The new idempotent early-return in initPlayer assumes cleanupPlayer() reliably clears a player's map entry before the id is reused; Player.cleanupAchievements() doesn't appear to be wired into any disconnect handler, so a reused player id could now inherit a previous session's achievement state instead of starting fresh (previously the old create-new-empty-state path acted as an implicit safety net). Worth confirming disconnect wiring calls player.cleanupAchievements(), or clearing playerAchievements on disconnect explicitly.</comment>

<file context>
@@ -38,8 +38,19 @@ export class AchievementService {
+   * the existing entry instead of wiping it with a blank state.
    */
   initPlayer(playerId: string, savedData?: PlayerAchievements): PlayerAchievements {
+    if (!savedData) {
+      const existing = this.playerAchievements.get(playerId);
+      if (existing) {
</file context>
Fix with cubic

Comment thread server/ts/world.ts
// The manager owns cadence (SPAWN_INTERVAL), mob selection and difficulty
// scaling; the world spawns through the normal addMob path so rift mobs
// aggro, move, drop loot and broadcast like any other mob.
this.riftSpawnInterval = setInterval(() => {

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The rift mob spawn tick wraps the entire forEachPlayer loop in a single try/catch, so one player's spawn failure aborts spawning for every other player in that tick — unlike the periodic-save loop just above it, which catches per-player. Move the try/catch inside the per-player callback so one player's error doesn't stall rift mob spawns for everyone else in the world.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/ts/world.ts, line 592:

<comment>The rift mob spawn tick wraps the entire `forEachPlayer` loop in a single try/catch, so one player's spawn failure aborts spawning for every other player in that tick — unlike the periodic-save loop just above it, which catches per-player. Move the try/catch inside the per-player callback so one player's error doesn't stall rift mob spawns for everyone else in the world.</comment>

<file context>
@@ -557,6 +584,39 @@ export class World {
+      // The manager owns cadence (SPAWN_INTERVAL), mob selection and difficulty
+      // scaling; the world spawns through the normal addMob path so rift mobs
+      // aggro, move, drop loot and broadcast like any other mob.
+      this.riftSpawnInterval = setInterval(() => {
+        try {
+          this.forEachPlayer((player: Player) => {
</file context>
Fix with cubic

// second NPC before the first answers no longer eats the first's line
var npc = game.pendingNpcTalks[npcKind];
if (npc) {
delete game.pendingNpcTalks[npcKind];

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a quest is completed, the player sees a double notification because both the old legacy handler in message-handlers.ts and the new handler in quest.handler.ts react to the same QUEST_COMPLETE event. The old handler calls showNotification with 'Quest Complete! Reward: X' and the new handler calls showNotification with 'Reward: X, +Y XP', so two toast-like messages appear. Remove the old QUEST_COMPLETE handler from message-handlers.ts (it was already done correctly for QUEST_OFFER in the same function) — the quest.handler.ts + QuestController.handleQuestComplete already handles the user-facing feedback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/ts/network/message-handlers.ts, line 592:

<comment>When a quest is completed, the player sees a double notification because both the old legacy handler in message-handlers.ts and the new handler in quest.handler.ts react to the same QUEST_COMPLETE event. The old handler calls `showNotification` with 'Quest Complete! Reward: X' and the new handler calls `showNotification` with 'Reward: X, +Y XP', so two toast-like messages appear. Remove the old QUEST_COMPLETE handler from message-handlers.ts (it was already done correctly for QUEST_OFFER in the same function) — the quest.handler.ts + QuestController.handleQuestComplete already handles the user-facing feedback.</comment>

<file context>
@@ -580,9 +585,11 @@ function setupCombatHandlers(game: Game, client: GameClient): void {
+    // second NPC before the first answers no longer eats the first's line
+    var npc = game.pendingNpcTalks[npcKind];
+    if (npc) {
+      delete game.pendingNpcTalks[npcKind];
       if (response) {
         game.showBubbleFor(npc, response);
</file context>
Fix with cubic

// BEFORE processPurchase so limited stock isn't decremented for a
// purchase we then abort (stock leak).
const goesToInventory = Types.isWeapon(itemKind) || Types.isArmor(itemKind) || Types.isExpendableItem(itemKind);
if (goesToInventory && !ctx.getInventory().hasRoom(itemKind)) {

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Now that inventory room is verified upfront before processPurchase, the duplicate hasRoom checks inside the isWeapon/isArmor and isConsumable branches further down are unreachable in the normal synchronous flow and could be removed for clarity.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/ts/player/shop.handler.ts, line 52:

<comment>Now that inventory room is verified upfront before processPurchase, the duplicate hasRoom checks inside the isWeapon/isArmor and isConsumable branches further down are unreachable in the normal synchronous flow and could be removed for clarity.</comment>

<file context>
@@ -45,6 +45,16 @@ export interface ShopPlayerContext {
+  // BEFORE processPurchase so limited stock isn't decremented for a
+  // purchase we then abort (stock leak).
+  const goesToInventory = Types.isWeapon(itemKind) || Types.isArmor(itemKind) || Types.isExpendableItem(itemKind);
+  if (goesToInventory && !ctx.getInventory().hasRoom(itemKind)) {
+    log.info({ player: ctx.name, itemKind }, 'Inventory is full');
+    ctx.send(new Messages.ShopBuyResult(false, itemKind, ctx.gold, 'Inventory is full').serialize());
</file context>
Fix with cubic

* kills (panel review finding). Owner kills already free the slot via
* recordKill, so this is a no-op for them.
*/
freeSpawnSlotForMob(mobId: number): boolean {

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: freeSpawnSlotForMob does a linear scan across all active rift runs for every kill in the game (it's invoked unconditionally from combat-system.ts, not gated on isRiftMob). Consider maintaining a mobId -> playerId index in the manager so this lookup is O(1) instead of O(active runs) per kill.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/ts/rifts/rift-manager.ts, line 341:

<comment>`freeSpawnSlotForMob` does a linear scan across all active rift runs for every kill in the game (it's invoked unconditionally from combat-system.ts, not gated on `isRiftMob`). Consider maintaining a `mobId -> playerId` index in the manager so this lookup is O(1) instead of O(active runs) per kill.</comment>

<file context>
@@ -278,6 +322,65 @@ export class RiftManager {
+   * kills (panel review finding). Owner kills already free the slot via
+   * recordKill, so this is a no-op for them.
+   */
+  freeSpawnSlotForMob(mobId: number): boolean {
+    for (const run of this.activeRuns.values()) {
+      if (run.spawnedMobs.delete(mobId)) return true;
</file context>
Fix with cubic

}

// No inventory surface or inventory full — drop at the player's feet
const world = ctx.getWorld?.();

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The drop-at-feet block in grantQuestRewards duplicates the create/addItem/broadcast/log pattern already used in handleInventoryDrop (inventory.handler.ts) and equipment.handler.ts; the comment even calls out that it 'mirrors' that pattern. Consider extracting a shared dropItemAtFeet(world, ctx, kind, properties) helper to avoid the three near-identical copies drifting apart over time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/ts/player/venice.handler.ts, line 103:

<comment>The drop-at-feet block in grantQuestRewards duplicates the create/addItem/broadcast/log pattern already used in handleInventoryDrop (inventory.handler.ts) and equipment.handler.ts; the comment even calls out that it 'mirrors' that pattern. Consider extracting a shared `dropItemAtFeet(world, ctx, kind, properties)` helper to avoid the three near-identical copies drifting apart over time.</comment>

<file context>
@@ -11,17 +11,104 @@ import { getVeniceService, getFishAudioService } from '../ai';
+  }
+
+  // No inventory surface or inventory full — drop at the player's feet
+  const world = ctx.getWorld?.();
+  if (world && ctx.x !== undefined && ctx.y !== undefined) {
+    const item = world.createItemWithProperties(kind, ctx.x, ctx.y, null);
</file context>
Fix with cubic

Comment thread client/index.html
<li><span class="icon"></span>I - Inventory</li>
<li><span class="icon"></span>Q - Use health item</li>
<li><span class="icon"></span>J - Achievements</li>
<li><span class="icon"></span>L - Boss leaderboard</li>

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The Fracture Rift is accessible via the R key (visible in the button tooltip and wired in main.ts), but the R key shortcut is missing from the keyboard shortcuts help list in index.html. The L key for the boss leaderboard was added alongside it, so it looks like an omission. Adding an entry for R would make the help text consistent with the actual key bindings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/index.html, line 206:

<comment>The Fracture Rift is accessible via the R key (visible in the button tooltip and wired in main.ts), but the R key shortcut is missing from the keyboard shortcuts help list in index.html. The L key for the boss leaderboard was added alongside it, so it looks like an omission. Adding an entry for R would make the help text consistent with the actual key bindings.</comment>

<file context>
@@ -203,6 +203,7 @@ <h1>
         <li><span class="icon"></span>I - Inventory</li>
         <li><span class="icon"></span>Q - Use health item</li>
         <li><span class="icon"></span>J - Achievements</li>
+        <li><span class="icon"></span>L - Boss leaderboard</li>
         <li><span class="icon"></span>M - Minimap</li>
         <li><span class="icon"></span>N - Newspaper</li>
</file context>
Fix with cubic

// Other players - dark green ink
ctx.fillStyle = '#3a6b3a';
} else if (entity.type === 'item') {
} else if (Types.isWeapon(entity.kind) || Types.isArmor(entity.kind) || Types.isObject(entity.kind)) {

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Minimap item check does not exclude chests from the gold dot rendering. The new code uses Types.isObject(entity.kind) which includes chests (CHEST, CHEST_CRATE, etc.), but the game type system considers chests as non-items — Types.isItem() explicitly filters them out with !isChest(kind). This means chests now render as gold "item" dots on the minimap when they didn't before. Consider using Types.isItem(entity.kind) for consistency with the rest of the game's type classification.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/ts/ui/minimap-ui.ts, line 280:

<comment>Minimap item check does not exclude chests from the gold dot rendering. The new code uses `Types.isObject(entity.kind)` which includes chests (CHEST, CHEST_CRATE, etc.), but the game type system considers chests as non-items — `Types.isItem()` explicitly filters them out with `!isChest(kind)`. This means chests now render as gold "item" dots on the minimap when they didn't before. Consider using `Types.isItem(entity.kind)` for consistency with the rest of the game's type classification.</comment>

<file context>
@@ -264,15 +268,16 @@ export class MinimapUI {
           // Other players - dark green ink
           ctx.fillStyle = '#3a6b3a';
-        } else if (entity.type === 'item') {
+        } else if (Types.isWeapon(entity.kind) || Types.isArmor(entity.kind) || Types.isObject(entity.kind)) {
           ctx.fillStyle = '#8b7a2a'; // Gold ink for items
         } else {
</file context>
Suggested change
} else if (Types.isWeapon(entity.kind) || Types.isArmor(entity.kind) || Types.isObject(entity.kind)) {
} else if (Types.isItem(entity.kind)) {
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="client/ts/main.ts">

<violation number="1" location="client/ts/main.ts:450">
P2: `hadOpenUi` doesn't account for shop/inventory/newspaper/quest-tracker panels that `app.hideWindows()` also closes, so ESC will still incorrectly call `game.exitRift()` when one of those panels (not achievements/instructions/credits/rift) was open during an active rift — reintroducing the same 'ESC exits rift with window open' bug this change is meant to fix for those other UI panels.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread client/ts/main.ts
Comment on lines +450 to +453
const hadOpenUi = !!(game.achievementUI?.isVisible?.()) ||
$('#instructions').hasClass('active') ||
$('body').hasClass('credits') ||
!!(game.riftUI?.hasOpenPanel?.());

@cubic-dev-ai cubic-dev-ai Bot Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: hadOpenUi doesn't account for shop/inventory/newspaper/quest-tracker panels that app.hideWindows() also closes, so ESC will still incorrectly call game.exitRift() when one of those panels (not achievements/instructions/credits/rift) was open during an active rift — reintroducing the same 'ESC exits rift with window open' bug this change is meant to fix for those other UI panels.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/ts/main.ts, line 450:

<comment>`hadOpenUi` doesn't account for shop/inventory/newspaper/quest-tracker panels that `app.hideWindows()` also closes, so ESC will still incorrectly call `game.exitRift()` when one of those panels (not achievements/instructions/credits/rift) was open during an active rift — reintroducing the same 'ESC exits rift with window open' bug this change is meant to fix for those other UI panels.</comment>

<file context>
@@ -445,10 +445,16 @@ var initGame = function () {
       if (key === 27) { // ESC
+        // Only exit an active rift when ESC closed nothing: if any window or
+        // rift panel was open, ESC's job was to close it, not end the run
+        const hadOpenUi = !!(game.achievementUI?.isVisible?.()) ||
+          $('#instructions').hasClass('active') ||
+          $('body').hasClass('credits') ||
</file context>
Suggested change
const hadOpenUi = !!(game.achievementUI?.isVisible?.()) ||
$('#instructions').hasClass('active') ||
$('body').hasClass('credits') ||
!!(game.riftUI?.hasOpenPanel?.());
const hadOpenUi = !!(game.achievementUI?.isVisible?.()) ||
$('#instructions').hasClass('active') ||
$('body').hasClass('credits') ||
$('body').hasClass('about') ||
!!(game.shopUI?.isOpen?.()) ||
!!(game.inventoryUI?.isVisible?.()) ||
!!(game.uiManager?.isNewspaperVisible?.()) ||
!!(game.questUI?.isTrackerVisible?.()) ||
!!(game.riftUI?.hasOpenPanel?.());
Fix with cubic

Leaderboard playerName is user-controlled and went raw into innerHTML —
a crafted character name would XSS every leaderboard viewer. Modifier
name/description get the same treatment (title-attribute context).
Colors stay raw: they come from the static MODIFIERS table, not users.
@georgeglarson
georgeglarson merged commit 642c00a into master Jul 20, 2026
4 of 6 checks passed
@georgeglarson
georgeglarson deleted the feat-gameplay-correctness branch July 20, 2026 23:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/ts/player/interaction-controller.ts (1)

267-270: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the remaining production click-debug logging.

The visible overlay was removed, but the [Click Debug] console.log still runs on every click. Delete it or gate it behind a debug flag so the client no longer ships production debug output.

🤖 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 `@client/ts/player/interaction-controller.ts` around lines 267 - 270, Remove
the “[Click Debug]” console.log from the click handling flow in
interaction-controller.ts, or gate it behind the existing debug flag so
production builds emit no click-debug output. Preserve the surrounding entity
and position handling.
🧹 Nitpick comments (3)
server/ts/__tests__/message-router.test.ts (1)

604-623: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

QUEST_ABANDON tests share static quest-service state across test order.

getStaticServices().quests is a module-level singleton. The "no active quest" test (Line 618) implicitly relies on the preceding test (Line 604) having already abandoned the quest for the same ctx.id; there's no explicit cleanup between tests. If test execution order changes, this becomes an ordering dependency rather than an independent assertion. Consider abandoning/clearing the quest for ctx.id in an afterEach/beforeEach to make each test self-contained.

🤖 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 `@server/ts/__tests__/message-router.test.ts` around lines 604 - 623, Make the
QUEST_ABANDON tests independent of execution order by clearing the static quest
state for ctx.id in shared setup or teardown, using getStaticServices().quests
and its existing quest-clearing/abandon operation. Ensure each test starts with
no active quest while preserving both assertions.
server/ts/player/auth.handler.ts (1)

22-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test coverage for the respawn branch.

The graph evidence for auth.handler.test.ts only shows calls exercising the normal (non-respawn) HELLO path (new/existing character, wrong password, etc.). This new respawn branch (hasEnteredGame && isDead) is a large behavioral fork — it skips storage reload, achievement/inventory/skill re-init, and enterCallback, while re-adding the player and resending WELCOME/EQUIP. A dedicated test (set hasEnteredGame=true, isDead=true, call handleHello again, assert world.addPlayer was called, isDead is now false, WELCOME/EQUIP were resent, and storage/achievement init were not called) would guard this path against regressions.

🤖 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 `@server/ts/player/auth.handler.ts` around lines 22 - 76, Add a dedicated
auth.handler test for the hasEnteredGame && isDead respawn branch, configuring
an authenticated dead player before calling handleHello again. Assert the player
is revived, world.addPlayer is called, WELCOME and available EQUIP messages are
resent, and storage reload plus achievement/inventory/skill initialization and
enterCallback are not invoked.
server/ts/player/rift.handler.ts (1)

52-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Rift-entry failure reason is not discriminated.

riftManager.startRun returns null for two different causes (already in a rift vs. level too low), so handleRiftEnter always sends the same combined string regardless of which one applied. Since the PR's stated goal is showing the server-provided reason to the player, consider having startRun return a discriminated failure so the client message is precise.

♻️ Suggested direction
-  startRun(playerId: number, playerName: string, playerLevel: number): ActiveRun | null {
+  startRun(playerId: number, playerName: string, playerLevel: number): { run: ActiveRun } | { error: 'already_in_rift' | 'level_too_low' } {
     if (this.activeRuns.has(playerId)) {
-      return null;
+      return { error: 'already_in_rift' };
     }
     ...
     if (playerLevel < tier.minLevel) {
-      return null;
+      return { error: 'level_too_low' };
     }
-    return run;
+    return { run };

Then map the discriminant to a specific message in handleRiftEnter.

🤖 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 `@server/ts/player/rift.handler.ts` around lines 52 - 63, Update
riftManager.startRun and handleRiftEnter to return and consume a discriminated
failure result instead of using null for both cases. Map the discriminant to a
precise player-facing message for already-in-rift versus insufficient-level
failures, while preserving the existing successful run path and failure response
structure.
🤖 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 `@client/ts/audio.ts`:
- Line 20: Add the missing client/audio/sounds/glitch1.ogg asset corresponding
to the existing glitch1 entry in soundNames, ensuring the fallback preload path
can load it and complete the audio startup flow.

In `@client/ts/main.ts`:
- Around line 448-459: Update the hadOpenUi guard in the ESC handler to also
detect the shop, inventory, newspaper, and quest interfaces that
app.hideWindows() closes. Preserve the existing checks and ensure any of these
windows being open prevents game.exitRift() after ESC.

In `@client/ts/player/interaction-controller.ts`:
- Around line 49-53: Update the pending NPC-talk accessor types in the
interaction-controller interface: change setPendingNpcTalk to accept Character
or Npc, and change getPendingNpcTalk to return Character | undefined. Keep
clearPendingNpcTalk’s npcKind parameter unchanged and ensure the implementations
and callers conform to the typed pendingNpcTalks record.

In `@server/ts/rifts/rift-manager.ts`:
- Around line 66-71: Update the `RIFT_BOUNDS` coordinates so the rift arena lies
outside the lavaland zone and rift mobs cannot receive normal zone bonuses
through their spawn positions; preserve the arena’s existing bounds structure
and dimensions.

---

Outside diff comments:
In `@client/ts/player/interaction-controller.ts`:
- Around line 267-270: Remove the “[Click Debug]” console.log from the click
handling flow in interaction-controller.ts, or gate it behind the existing debug
flag so production builds emit no click-debug output. Preserve the surrounding
entity and position handling.

---

Nitpick comments:
In `@server/ts/__tests__/message-router.test.ts`:
- Around line 604-623: Make the QUEST_ABANDON tests independent of execution
order by clearing the static quest state for ctx.id in shared setup or teardown,
using getStaticServices().quests and its existing quest-clearing/abandon
operation. Ensure each test starts with no active quest while preserving both
assertions.

In `@server/ts/player/auth.handler.ts`:
- Around line 22-76: Add a dedicated auth.handler test for the hasEnteredGame &&
isDead respawn branch, configuring an authenticated dead player before calling
handleHello again. Assert the player is revived, world.addPlayer is called,
WELCOME and available EQUIP messages are resent, and storage reload plus
achievement/inventory/skill initialization and enterCallback are not invoked.

In `@server/ts/player/rift.handler.ts`:
- Around line 52-63: Update riftManager.startRun and handleRiftEnter to return
and consume a discriminated failure result instead of using null for both cases.
Map the discriminant to a precise player-facing message for already-in-rift
versus insufficient-level failures, while preserving the existing successful run
path and failure response structure.
🪄 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: 5f67fdd7-3e2e-4a9d-a652-283d1b5fd2cb

📥 Commits

Reviewing files that changed from the base of the PR and between 151615d and 85c45fe.

📒 Files selected for processing (62)
  • client/css/main.css
  • client/index.html
  • client/ts/__tests__/quest-controller.test.ts
  • client/ts/audio.ts
  • client/ts/bootstrap/game-bootstrap.ts
  • client/ts/game.ts
  • client/ts/handlers/game-event.handler.ts
  • client/ts/handlers/rift-ui.handler.ts
  • client/ts/main.ts
  • client/ts/network/client-events.ts
  • client/ts/network/gameclient.ts
  • client/ts/network/message-handlers.ts
  • client/ts/player/interaction-controller.ts
  • client/ts/quest/quest-controller.ts
  • client/ts/ui/minimap-ui.ts
  • client/ts/ui/rift-ui.ts
  • server/ts/__tests__/achievement.handler.test.ts
  • server/ts/__tests__/aiplayer.test.ts
  • server/ts/__tests__/auth.handler.test.ts
  • server/ts/__tests__/character.test.ts
  • server/ts/__tests__/combat-system.test.ts
  • server/ts/__tests__/combat.handler.test.ts
  • server/ts/__tests__/economy.service.test.ts
  • server/ts/__tests__/format.test.ts
  • server/ts/__tests__/inventory.handler.test.ts
  • server/ts/__tests__/map.test.ts
  • server/ts/__tests__/message-router.test.ts
  • server/ts/__tests__/message.test.ts
  • server/ts/__tests__/player.test.ts
  • server/ts/__tests__/progression.service.test.ts
  • server/ts/__tests__/rift-manager.test.ts
  • server/ts/__tests__/rift.handler.test.ts
  • server/ts/__tests__/roaming-boss.test.ts
  • server/ts/__tests__/shop.handler.test.ts
  • server/ts/__tests__/storage.service.test.ts
  • server/ts/__tests__/venice.handler.test.ts
  • server/ts/__tests__/world.test.ts
  • server/ts/achievements/achievement.service.ts
  • server/ts/ai/aiplayer.ts
  • server/ts/ai/quest.service.ts
  • server/ts/ai/types.ts
  • server/ts/character.ts
  • server/ts/combat/combat-system.ts
  • server/ts/format.ts
  • server/ts/main.ts
  • server/ts/map.ts
  • server/ts/message.ts
  • server/ts/player.ts
  • server/ts/player/auth.handler.ts
  • server/ts/player/combat.handler.ts
  • server/ts/player/economy.service.ts
  • server/ts/player/inventory.handler.ts
  • server/ts/player/message-router.ts
  • server/ts/player/progression.service.ts
  • server/ts/player/rift.handler.ts
  • server/ts/player/shop.handler.ts
  • server/ts/player/venice.handler.ts
  • server/ts/rifts/rift-manager.ts
  • server/ts/roaming-boss.ts
  • server/ts/world.ts
  • shared/ts/domain/game-constants.ts
  • shared/ts/gametypes.ts

Comment thread client/ts/audio.ts
areas = [];
musicNames = ['village', 'beach', 'forest', 'cave', 'desert', 'lavaland', 'boss', 'graveyard'];
soundNames = ['loot', 'hit1', 'hit2', 'hurt', 'heal', 'chat', 'revive', 'death', 'firefox', 'achievement', 'kill1', 'kill2', 'noloot', 'teleport', 'chest', 'npc', 'npc-end', 'levelup', 'gold', 'equip', 'quest'];
soundNames = ['loot', 'hit1', 'hit2', 'hurt', 'heal', 'chat', 'revive', 'death', 'firefox', 'achievement', 'kill1', 'kill2', 'noloot', 'teleport', 'chest', 'npc', 'npc-end', 'levelup', 'gold', 'equip', 'quest', 'glitch1'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

for ext in mp3 ogg; do
  if ! git ls-files | grep -Eiq "(^|/)glitch1\.${ext}$"; then
    echo "Missing tracked glitch1.${ext} asset"
    exit 1
  fi
done

Repository: georgeglarson/Fracture

Length of output: 194


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n-- audio.ts --\n'
cat -n client/ts/audio.ts | sed -n '1,220p'

printf '\n-- glitch1 assets tracked --\n'
git ls-files | rg -n '(^|/)glitch1\.(mp3|ogg)$' || true

printf '\n-- any references to glitch1 --\n'
rg -n 'glitch1' client -S

Repository: georgeglarson/Fracture

Length of output: 8516


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Show the loading helpers in client/ts/audio.ts
cat -n client/ts/audio.ts | sed -n '220,360p'

# Find the load callbacks for sounds/music
rg -n "canplaythrough|error|loaded_callback|loadSound|loadMusic|load\(" client/ts/audio.ts -n -S

Repository: georgeglarson/Fracture

Length of output: 6154


Add client/audio/sounds/glitch1.ogg
client/ts/audio.ts falls back to ogg when MP3 isn’t supported, but only client/audio/sounds/glitch1.mp3 is tracked. On those browsers the preload counter never clears, so music startup stalls.

🤖 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 `@client/ts/audio.ts` at line 20, Add the missing
client/audio/sounds/glitch1.ogg asset corresponding to the existing glitch1
entry in soundNames, ensuring the fallback preload path can load it and complete
the audio startup flow.

Comment thread client/ts/main.ts
Comment on lines +448 to +459
// Only exit an active rift when ESC closed nothing: if any window or
// rift panel was open, ESC's job was to close it, not end the run
const hadOpenUi = !!(game.achievementUI?.isVisible?.()) ||
$('#instructions').hasClass('active') ||
$('body').hasClass('credits') ||
!!(game.riftUI?.hasOpenPanel?.());
app.hideWindows();
if (game.riftUI?.hasOpenPanel?.()) {
game.riftUI.closePanels();
} else if (!hadOpenUi && game.riftUI?.isActive()) {
game.exitRift();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "hideWindows" --type=ts client/ts

Repository: georgeglarson/Fracture

Length of output: 435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the hideWindows implementation and nearby UI APIs.
sed -n '360,460p' client/ts/app.ts

printf '\n---\n'

# Find visibility/open-state methods on relevant UI managers.
rg -n "isVisible\\(|hasOpenPanel\\(|hideWindows\\(|showWindows\\(|closePanels\\(" client/ts

Repository: georgeglarson/Fracture

Length of output: 4950


Include the other ESC-closable windows in hadOpenUi
hadOpenUi only accounts for achievements, instructions, credits, and rift panels. Since app.hideWindows() also closes shop, inventory, newspaper, and quest UIs, opening any of those while in a rift still falls through to game.exitRift() after ESC closes the window. Add those checks to the guard so ESC doesn’t end the run after closing another panel.

🤖 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 `@client/ts/main.ts` around lines 448 - 459, Update the hadOpenUi guard in the
ESC handler to also detect the shop, inventory, newspaper, and quest interfaces
that app.hideWindows() closes. Preserve the existing checks and ensure any of
these windows being open prevents game.exitRift() after ESC.

Comment on lines +49 to +53
// Pending NPC talks, keyed by NPC kind (the server echoes the kind in
// NPCTALK_RESPONSE, so responses can be attributed to the right NPC)
setPendingNpcTalk: (npc: any) => void;
getPendingNpcTalk: (npcKind: number) => any;
clearPendingNpcTalk: (npcKind: number) => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files client/ts/player/interaction-controller.ts client/ts/**/message-handlers.ts client/ts/**/game*.ts client/ts/**/player*.ts | sed -n '1,200p'

Repository: georgeglarson/Fracture

Length of output: 421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  client/ts/player/interaction-controller.ts
  client/ts/game.ts
  client/ts/player/message-handlers.ts
)

for f in "${files[@]}"; do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    echo
    cat -n "$f" | sed -n '1,240p'
    echo
  fi
done

Repository: georgeglarson/Fracture

Length of output: 19955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import os, subprocess, textwrap, sys

paths = [
    "client/ts/player/interaction-controller.ts",
    "client/ts/game.ts",
    "client/ts/player/message-handlers.ts",
]
for p in paths:
    if os.path.exists(p):
        print(f"### {p}")
        with open(p, "r", encoding="utf-8") as f:
            lines = f.readlines()
        print(f"lines={len(lines)}")
        for i, line in enumerate(lines[:260], start=1):
            print(f"{i:4d}: {line.rstrip()}")
        print()
PY

Repository: georgeglarson/Fracture

Length of output: 20671


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' client/ts/player/interaction-controller.ts

Repository: georgeglarson/Fracture

Length of output: 6477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '### pendingNpcTalk references'
rg -n "pendingNpcTalk|setPendingNpcTalk|getPendingNpcTalk|clearPendingNpcTalk" client/ts shared/ts -S

printf '\n%s\n' '### interaction-controller surrounding code'
sed -n '200,245p' client/ts/player/interaction-controller.ts

printf '\n%s\n' '### message-handlers around NPC talk handling'
sed -n '1,260p' client/ts/network/message-handlers.ts

printf '\n%s\n' '### game.ts pending-talk related methods'
sed -n '520,760p' client/ts/game.ts

Repository: georgeglarson/Fracture

Length of output: 19951


Type the pending NPC-talk accessors
Game.pendingNpcTalks is already Record<number, Character>, so setPendingNpcTalk should accept Character/Npc and getPendingNpcTalk should return Character | undefined instead of any.

🧰 Tools
🪛 ESLint

[error] 51-51: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 52-52: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 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 `@client/ts/player/interaction-controller.ts` around lines 49 - 53, Update the
pending NPC-talk accessor types in the interaction-controller interface: change
setPendingNpcTalk to accept Character or Npc, and change getPendingNpcTalk to
return Character | undefined. Keep clearPendingNpcTalk’s npcKind parameter
unchanged and ensure the implementations and callers conform to the typed
pendingNpcTalks record.

Source: Linters/SAST tools

Comment on lines 66 to 71
private readonly RIFT_BOUNDS = { // Virtual rift arena bounds
minX: 10,
maxX: 30,
minY: 10,
maxY: 30
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the map/zone data for whatever occupies (10-30, 10-30)
rg -n "resolveZoneId|ZONE_DATA" server/ts/zones -n
fd . server/ts/zones --type f

Repository: georgeglarson/Fracture

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect zone definitions and the logic that maps coordinates to zones.
ast-grep outline server/ts/zones/zone-manager.ts --view expanded || true
ast-grep outline server/ts/zones/index.ts --view expanded || true

echo '--- zone-manager.ts ---'
cat -n server/ts/zones/zone-manager.ts | sed -n '1,260p'

echo '--- zones/index.ts ---'
cat -n server/ts/zones/index.ts | sed -n '1,260p'

Repository: georgeglarson/Fracture

Length of output: 6428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any zone rectangle/coordinate data in the repo.
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  "minX|maxX|minY|maxY|rect|rectangle|bounds|zone.*x|zone.*y|zoneId|resolveZoneId|walkable|arena" server/ts

Repository: georgeglarson/Fracture

Length of output: 40705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the shared zone definitions and the coordinate resolver.
ast-grep outline shared/ts/zones.ts --view expanded || true
ast-grep outline server/ts/combat/aggro-policy.ts --view expanded || true

echo '--- shared/ts/zones.ts ---'
cat -n shared/ts/zones.ts | sed -n '1,320p'

echo '--- server/ts/combat/aggro-policy.ts ---'
cat -n server/ts/combat/aggro-policy.ts | sed -n '1,280p'

Repository: georgeglarson/Fracture

Length of output: 1330


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the shared zone definition file and inspect the resolver implementation.
fd -a "zones" . | sed -n '1,50p'
rg -n "export const ZONE_DATA|interface ZoneDefinition|getZoneAtPosition|resolveZoneId" . --glob '!node_modules' --glob '!dist' --glob '!build'

# Print the relevant section of the resolver file.
cat -n server/ts/combat/aggro-policy.ts | sed -n '200,280p'

Repository: georgeglarson/Fracture

Length of output: 4373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the zone tests around the coordinates and boundaries that matter here.
cat -n server/ts/__tests__/zone.handler.test.ts | sed -n '220,340p'
cat -n server/ts/__tests__/aggro-policy.test.ts | sed -n '450,490p'

Repository: georgeglarson/Fracture

Length of output: 6600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the rift arena is explicitly carved out as a separate walkable pocket.
rg -n "rift arena|RIFT_BOUNDS|walkable tiles|teleport-in point|arena center|pocket|isolated" server/ts server/ts/__tests__ --glob '!node_modules' --glob '!dist' --glob '!build'

Repository: georgeglarson/Fracture

Length of output: 5144


Rift arena overlaps lavalandRIFT_BOUNDS falls inside the lavaland zone, so rift mobs can inherit normal zone XP/gold/drop bonuses through their real spawn coordinates. If the arena is meant to be isolated, move it outside the live map zone or bypass zone lookup for rift runs.

🤖 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 `@server/ts/rifts/rift-manager.ts` around lines 66 - 71, Update the
`RIFT_BOUNDS` coordinates so the rift arena lies outside the lavaland zone and
rift mobs cannot receive normal zone bonuses through their spawn positions;
preserve the arena’s existing bounds structure and dimensions.

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