fix(level-links): prevent infinite teleport bounce at seam arrival
Eliminate fallback to trigger tile (x, y) when no walkable tile is found within initial radius. Expand search to full grid extent and guarantee that the arrival destination is strictly offset inward away from the seam trigger box. TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
5f484e5921
commit
27e0db38c8
|
|
@ -394,7 +394,8 @@ export function seamArrivalSpot(
|
|||
const step = inwardStep(side)
|
||||
const targetX = x + step.x * (SEAM_TRIGGER_SUBTILES + 3)
|
||||
const targetY = y + step.y * (SEAM_TRIGGER_SUBTILES + 3)
|
||||
for (let radius = 0; radius <= 32; radius += 1) {
|
||||
const maxRadius = Math.max(grid.gridWidth, grid.gridHeight, 64)
|
||||
for (let radius = 0; radius <= maxRadius; radius += 1) {
|
||||
for (let dy = -radius; dy <= radius; dy += 1) {
|
||||
for (let dx = -radius; dx <= radius; dx += 1) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dy)) !== radius) continue
|
||||
|
|
@ -406,7 +407,14 @@ export function seamArrivalSpot(
|
|||
}
|
||||
}
|
||||
}
|
||||
return nearestWalkable(grid, targetX, targetY, 32, region) ?? { x, y }
|
||||
// Never place the player directly back on or inside the trigger box around (x, y),
|
||||
// which would trigger an immediate bounce back in an infinite ping-pong loop.
|
||||
const clampedX = Math.max(0, Math.min(grid.gridWidth - 1, targetX))
|
||||
const clampedY = Math.max(0, Math.min(grid.gridHeight - 1, targetY))
|
||||
if (Math.abs(clampedX - x) <= SEAM_TRIGGER_SUBTILES && Math.abs(clampedY - y) <= SEAM_TRIGGER_SUBTILES) {
|
||||
return { x: targetX, y: targetY }
|
||||
}
|
||||
return { x: clampedX, y: clampedY }
|
||||
}
|
||||
|
||||
/** Where an edge of a level opens, and how convincing the opening is. */
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ import {
|
|||
DROPPED_VIS_EDGES,
|
||||
} from '../src/game/world-graph.ts'
|
||||
import type { Side } from '../src/game/world-graph.ts'
|
||||
import {
|
||||
seamArrivalSpot,
|
||||
SEAM_TRIGGER_SUBTILES,
|
||||
type LinkGrid,
|
||||
} from '../src/game/level-links.ts'
|
||||
|
||||
/**
|
||||
* Build a `Levels.txt`-shaped table from sparse overrides.
|
||||
|
|
@ -376,3 +381,66 @@ describe('parseWarpGeometry', () => {
|
|||
expect(findWarpGeometry(parseWarpGeometry(lvlwarp), 71)?.id).toBe(71)
|
||||
})
|
||||
})
|
||||
|
||||
describe('seamArrivalSpot', () => {
|
||||
function makeGrid(width: number, height: number, blockedIndices: Set<number> = new Set()): LinkGrid {
|
||||
const blocked = new Uint8Array(width * height)
|
||||
for (const idx of blockedIndices) {
|
||||
blocked[idx] = 1
|
||||
}
|
||||
return {
|
||||
cellsX: Math.ceil(width / 5),
|
||||
cellsY: Math.ceil(height / 5),
|
||||
gridWidth: width,
|
||||
gridHeight: height,
|
||||
blocked,
|
||||
}
|
||||
}
|
||||
|
||||
it('places arrival strictly outside trigger box on all 4 sides', () => {
|
||||
const grid = makeGrid(50, 50)
|
||||
const sides: Side[] = ['north', 'south', 'east', 'west']
|
||||
const x = 25
|
||||
const y = 25
|
||||
|
||||
for (const side of sides) {
|
||||
const spot = seamArrivalSpot(grid, x, y, side)
|
||||
const insideTriggerBox =
|
||||
Math.abs(spot.x - x) <= SEAM_TRIGGER_SUBTILES &&
|
||||
Math.abs(spot.y - y) <= SEAM_TRIGGER_SUBTILES
|
||||
expect(insideTriggerBox).toBe(false)
|
||||
expect(spot).not.toEqual({ x, y })
|
||||
}
|
||||
})
|
||||
|
||||
it('never bounces back to trigger tile (x, y) even when completely blocked', () => {
|
||||
// A grid where every single tile is impassable
|
||||
const blockedSet = new Set<number>()
|
||||
for (let i = 0; i < 50 * 50; i++) blockedSet.add(i)
|
||||
const grid = makeGrid(50, 50, blockedSet)
|
||||
|
||||
const spot = seamArrivalSpot(grid, 20, 0, 'north')
|
||||
// Must never fall back to trigger tile (20, 0)
|
||||
expect(spot).not.toEqual({ x: 20, y: 0 })
|
||||
const insideTriggerBox =
|
||||
Math.abs(spot.x - 20) <= SEAM_TRIGGER_SUBTILES &&
|
||||
Math.abs(spot.y - 0) <= SEAM_TRIGGER_SUBTILES
|
||||
expect(insideTriggerBox).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores walkable tiles inside trigger radius and finds the nearest valid tile outside', () => {
|
||||
// Only the trigger tile itself and its immediate neighbors (radius 2) are walkable
|
||||
const blockedSet = new Set<number>()
|
||||
for (let i = 0; i < 50 * 50; i++) blockedSet.add(i)
|
||||
// Clear trigger tile and one tile far inward
|
||||
blockedSet.delete(0 * 50 + 20) // trigger (20, 0)
|
||||
blockedSet.delete(1 * 50 + 20) // (20, 1) inside trigger box
|
||||
blockedSet.delete(10 * 50 + 20) // (20, 10) valid outside trigger box
|
||||
|
||||
const grid = makeGrid(50, 50, blockedSet)
|
||||
const spot = seamArrivalSpot(grid, 20, 0, 'north')
|
||||
|
||||
expect(spot).toEqual({ x: 20, y: 10 })
|
||||
expect(Math.abs(spot.y - 0)).toBeGreaterThan(SEAM_TRIGGER_SUBTILES)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue