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:
troytt 2026-09-17 13:27:27 +00:00
parent 5f484e5921
commit 27e0db38c8
2 changed files with 78 additions and 2 deletions

View File

@ -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. */

View File

@ -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)
})
})