fix(drop): prevent items and gold from dropping into player-unreachable locations #492

Closed
opened 2026-09-27 05:46:49 +00:00 by troytt · 1 comment
Owner

Problem Description

When monsters die, objects/chests open, or items/gold are dropped, items can currently land in coordinates that the player character cannot physically reach to pick up:

  1. No Player Path-Connectivity / Reachability Verification:
    • Player movement (moveWithCollision in src/game/combat.ts) traverses 4-connected walkable sub-tiles where terrain.overlap(x, y) === 0.
    • findIsometricDropPosition and findSafeDropPosition in src/game/ground-items.ts do not verify whether a drop candidate lies in the same 4-connected walkable component as the player (reachableFrom) or drop anchor. Monsters dying across rivers/moats/walls/chasms or near thin barriers can drop items onto isolated islands or sealed rooms.
  2. Blocked Drop Origin Poisons rayTraceScene and Forces Fallback to the Blocked Origin:
    • When a drop origin is inside a wall, cliff, door, water, or obstacle of thickness >= 2 sub-tiles (e.g., flying monster dying over water/void or monster dying overlapping a wall), rayTraceScene(grid, originX, originY, candX, candY, COLLIDE_MASK_SPAWN_LOS) immediately collides at steps === 1 for every candidate outside the obstacle.
    • Because every candidate fails LoS from the blocked origin, firstWalkableCandidate stays null and findIsometricDropPosition falls back to returning { x: originX, y: originY } — placing the item directly inside the unreachable wall/void!
  3. Bresenham Diagonal Corner-Clipping Across Orthogonal Wall Sub-tiles:
    • collisionRayTrace allows diagonal steps (x, y) -> (x + sx, y + sy) without checking the two shared orthogonal neighbors (x + sx, y) and (x, y + sy). Items can be placed across diagonal wall corners where the player cannot walk through.
  4. Incomplete Collision Mask & Unwrapped CountingTerrain in findSafeDropPosition:
    • COLLIDE_MASK_SPAWN (0x3E01) omits COLLIDE_BLANK (0x0020), COLLIDE_NOPLAYER (0x0008), COLLIDE_WATER (0x00c0), and COLLIDE_MASK_INVALID (0x0027).
    • findSafeDropPosition unwraps grid.grid before checking grid.overlap(wx, wy), bypassing the player footprint clearance check, and only checks 0x0027 on collisionMasks.
  5. Insufficient Search Radius Fallback:
    • If the drop origin is more than 10 sub-tiles (2 cells) inside a blocked region or across an obstacle from the player, the fixed spiral radius exhausts and falls back to the blocked origin instead of expanding to the nearest reachable walkable floor coordinate.

Solution (Diablo II v1.13c Parity)

  • Enforce complete player-walkability and spawn collision masks (COLLIDE_MASK_SPAWN | COLLIDE_MASK_PLAYER_PATH | COLLIDE_BLANK | COLLIDE_WATER | COLLIDE_MASK_INVALID, blocked === 1, and overlap(x, y) === 0) in both findIsometricDropPosition and findSafeDropPosition.
  • Prevent diagonal corner-clipping in drop LoS / walkability checks and verify 4-connected walkable path reachability.
  • Support reachableFrom (player coordinates) in findIsometricDropPosition and findSafeDropPosition, snapping blocked/unreachable origins to the nearest player-reachable walkable floor coordinate and expanding search rings when necessary so items never fall back to unreachable tiles.
  • Wire reachableFrom: { x: this.player.x, y: this.player.y } across Engine.triggerMonsterDrop, Engine.dropItem, Engine.dropGold, and NetScene item drop handlers.
## Problem Description When monsters die, objects/chests open, or items/gold are dropped, items can currently land in coordinates that the player character cannot physically reach to pick up: 1. **No Player Path-Connectivity / Reachability Verification**: - Player movement (`moveWithCollision` in `src/game/combat.ts`) traverses 4-connected walkable sub-tiles where `terrain.overlap(x, y) === 0`. - `findIsometricDropPosition` and `findSafeDropPosition` in `src/game/ground-items.ts` do not verify whether a drop candidate lies in the same 4-connected walkable component as the player (`reachableFrom`) or drop anchor. Monsters dying across rivers/moats/walls/chasms or near thin barriers can drop items onto isolated islands or sealed rooms. 2. **Blocked Drop Origin Poisons `rayTraceScene` and Forces Fallback to the Blocked Origin**: - When a drop origin is inside a wall, cliff, door, water, or obstacle of thickness >= 2 sub-tiles (e.g., flying monster dying over water/void or monster dying overlapping a wall), `rayTraceScene(grid, originX, originY, candX, candY, COLLIDE_MASK_SPAWN_LOS)` immediately collides at `steps === 1` for every candidate outside the obstacle. - Because every candidate fails LoS from the blocked origin, `firstWalkableCandidate` stays `null` and `findIsometricDropPosition` falls back to returning `{ x: originX, y: originY }` — placing the item directly inside the unreachable wall/void! 3. **Bresenham Diagonal Corner-Clipping Across Orthogonal Wall Sub-tiles**: - `collisionRayTrace` allows diagonal steps `(x, y) -> (x + sx, y + sy)` without checking the two shared orthogonal neighbors `(x + sx, y)` and `(x, y + sy)`. Items can be placed across diagonal wall corners where the player cannot walk through. 4. **Incomplete Collision Mask & Unwrapped `CountingTerrain` in `findSafeDropPosition`**: - `COLLIDE_MASK_SPAWN` (`0x3E01`) omits `COLLIDE_BLANK` (`0x0020`), `COLLIDE_NOPLAYER` (`0x0008`), `COLLIDE_WATER` (`0x00c0`), and `COLLIDE_MASK_INVALID` (`0x0027`). - `findSafeDropPosition` unwraps `grid.grid` before checking `grid.overlap(wx, wy)`, bypassing the player footprint clearance check, and only checks `0x0027` on `collisionMasks`. 5. **Insufficient Search Radius Fallback**: - If the drop origin is more than 10 sub-tiles (2 cells) inside a blocked region or across an obstacle from the player, the fixed spiral radius exhausts and falls back to the blocked origin instead of expanding to the nearest reachable walkable floor coordinate. ## Solution (Diablo II v1.13c Parity) - Enforce complete player-walkability and spawn collision masks (`COLLIDE_MASK_SPAWN | COLLIDE_MASK_PLAYER_PATH | COLLIDE_BLANK | COLLIDE_WATER | COLLIDE_MASK_INVALID`, `blocked === 1`, and `overlap(x, y) === 0`) in both `findIsometricDropPosition` and `findSafeDropPosition`. - Prevent diagonal corner-clipping in drop LoS / walkability checks and verify 4-connected walkable path reachability. - Support `reachableFrom` (player coordinates) in `findIsometricDropPosition` and `findSafeDropPosition`, snapping blocked/unreachable origins to the nearest player-reachable walkable floor coordinate and expanding search rings when necessary so items never fall back to unreachable tiles. - Wire `reachableFrom: { x: this.player.x, y: this.player.y }` across `Engine.triggerMonsterDrop`, `Engine.dropItem`, `Engine.dropGold`, and `NetScene` item drop handlers.
Author
Owner

Fixed in commit 9c2e3ca on main.

Summary of Changes

  • Added COLLIDE_MASK_DROP_BLOCK and isTerrainPointBlocked in src/game/ground-items.ts to reject all player-unwalkable sub-tiles (COLLIDE_WALL, COLLIDE_BLANK, COLLIDE_WATER, COLLIDE_NOPLAYER, COLLIDE_MASK_INVALID, COLLIDE_NO_PATH, COLLIDE_DOOR, COLLIDE_OBJECT) as well as player footprint overlaps (CountingTerrain.overlap(x, y) !== 0).
  • Prevented diagonal corner-clipping across orthogonal wall corners in isSubTileRayBlocked / isDropPositionBlocked and findSafeDropPosition.
  • Added 4-connected walkable path reachability and reachableFrom support in findIsometricDropPosition and findSafeDropPosition, snapping blocked or isolated drop origins (inside thick walls, over water/void, or across rivers/walls from the player) to the nearest player-reachable walkable floor coordinate.
  • Wired reachableFrom in GameEngine.triggerMonsterDrop, GameEngine.dropItem, GameEngine.dropGold, and NetScene.
  • Added regression and E2E walk-and-pickup tests in tests/ground-items-reachability.test.ts (all passing, npm run typecheck 0 errors, and 136-level tests/full-spectrum-drop-parity.test.ts 100% passing).
Fixed in commit `9c2e3ca` on `main`. ### Summary of Changes - Added `COLLIDE_MASK_DROP_BLOCK` and `isTerrainPointBlocked` in `src/game/ground-items.ts` to reject all player-unwalkable sub-tiles (`COLLIDE_WALL`, `COLLIDE_BLANK`, `COLLIDE_WATER`, `COLLIDE_NOPLAYER`, `COLLIDE_MASK_INVALID`, `COLLIDE_NO_PATH`, `COLLIDE_DOOR`, `COLLIDE_OBJECT`) as well as player footprint overlaps (`CountingTerrain.overlap(x, y) !== 0`). - Prevented diagonal corner-clipping across orthogonal wall corners in `isSubTileRayBlocked` / `isDropPositionBlocked` and `findSafeDropPosition`. - Added 4-connected walkable path reachability and `reachableFrom` support in `findIsometricDropPosition` and `findSafeDropPosition`, snapping blocked or isolated drop origins (inside thick walls, over water/void, or across rivers/walls from the player) to the nearest player-reachable walkable floor coordinate. - Wired `reachableFrom` in `GameEngine.triggerMonsterDrop`, `GameEngine.dropItem`, `GameEngine.dropGold`, and `NetScene`. - Added regression and E2E walk-and-pickup tests in `tests/ground-items-reachability.test.ts` (all passing, `npm run typecheck` 0 errors, and 136-level `tests/full-spectrum-drop-parity.test.ts` 100% passing).
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#492
No description provided.