-
Notifications
You must be signed in to change notification settings - Fork 0
Add findNearestValidTileAround helper and use it for pet spawn/teleport; add tests #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| import { Position } from "../components/Position.js"; | ||
| import { ItemInfo } from "../components/ItemInfo.js"; | ||
| import { Player } from "../components/Player.js"; | ||
| import { Collider } from "../components/Collider.js"; | ||
| import { Vitality } from "../components/Vitality.js"; | ||
| import { isWalkable } from "../environment/dungeon/tileMap.js"; | ||
|
|
||
| export function itemsAt(world, x, y) { | ||
| const ids = []; | ||
|
|
@@ -21,3 +24,50 @@ export function playerEntity(world) { | |
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Find the nearest valid tile around a source point. | ||
| * Valid means walkable terrain and no solid/living occupant. | ||
| * | ||
| * @param {import('../../lib/ecs-js/index.js').World} world | ||
| * @param {{x:number, y:number}} source | ||
| * @param {{ | ||
| * maxDistance?: number, | ||
| * exclude?: Array<{x:number, y:number}> | ||
| * }} [opts] | ||
| */ | ||
| export function findNearestValidTileAround(world, source, opts = {}) { | ||
| const maxDistance = Math.max(0, opts.maxDistance ?? 1); | ||
| const excluded = new Set((opts.exclude || []).map((p) => `${p.x},${p.y}`)); | ||
| const blocked = new Set(); | ||
|
|
||
| for (const [id, pos] of world.query(Position)) { | ||
| const col = world.get(id, Collider); | ||
| if (col?.solid) blocked.add(`${pos.x},${pos.y}`); | ||
|
|
||
| const vit = world.get(id, Vitality); | ||
| if (vit && (vit.hp ?? 0) > 0) blocked.add(`${pos.x},${pos.y}`); | ||
| } | ||
|
|
||
| const candidates = []; | ||
| for (let dy = -maxDistance; dy <= maxDistance; dy++) { | ||
| for (let dx = -maxDistance; dx <= maxDistance; dx++) { | ||
| const x = source.x + dx; | ||
| const y = source.y + dy; | ||
| const dist = Math.abs(dx) + Math.abs(dy); | ||
| candidates.push({ x, y, dist, axisBias: (dx === 0 || dy === 0) ? 0 : 1 }); | ||
| } | ||
|
Comment on lines
+53
to
+59
|
||
| } | ||
|
|
||
| candidates.sort((a, b) => a.dist - b.dist || a.axisBias - b.axisBias); | ||
|
|
||
| for (const p of candidates) { | ||
| const key = `${p.x},${p.y}`; | ||
| if (excluded.has(key)) continue; | ||
| if (!isWalkable(p.x, p.y)) continue; | ||
| if (blocked.has(key)) continue; | ||
| return { x: p.x, y: p.y }; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { assert, assertEquals } from "jsr:@std/assert"; | ||
| import { World } from '../src/lib/ecs-js/index.js'; | ||
| import { findNearestValidTileAround } from '../src/rules/utils/queries.js'; | ||
| import { petFollowSystem } from '../src/rules/systems/petFollowSystem.js'; | ||
| import { loadChunk, clearAll } from '../src/rules/environment/dungeon/tileMap.js'; | ||
| import { CHUNK_SIZE, TILE_FLOOR, TILE_WALL } from '../src/rules/environment/dungeon/constants.js'; | ||
| import { Position } from '../src/rules/components/Position.js'; | ||
| import { Player } from '../src/rules/components/Player.js'; | ||
| import { Pet } from '../src/rules/components/Pet.js'; | ||
| import { Collider } from '../src/rules/components/Collider.js'; | ||
|
|
||
| function fillChunk(fill = TILE_FLOOR) { | ||
| const tiles = new Uint8Array(CHUNK_SIZE * CHUNK_SIZE); | ||
| tiles.fill(fill); | ||
| return tiles; | ||
| } | ||
|
|
||
| Deno.test('findNearestValidTileAround finds valid spawn near walls', () => { | ||
| clearAll(); | ||
| const tiles = fillChunk(TILE_WALL); | ||
| tiles[1 * CHUNK_SIZE + 1] = TILE_FLOOR; // source tile | ||
| tiles[2 * CHUNK_SIZE + 1] = TILE_FLOOR; // only valid adjacent tile (1,2) | ||
| loadChunk(0, 0, tiles); | ||
|
|
||
| const world = new World({ seed: 1 }); | ||
| const tile = findNearestValidTileAround(world, { x: 1, y: 1 }, { | ||
| maxDistance: 1, | ||
| exclude: [{ x: 1, y: 1 }], | ||
| }); | ||
|
|
||
| assert(tile, 'expected a valid adjacent tile'); | ||
| assertEquals(tile, { x: 1, y: 2 }); | ||
| clearAll(); | ||
| }); | ||
|
|
||
| Deno.test('pet teleport keeps current position when nearby tiles are blocked', () => { | ||
| clearAll(); | ||
|
Comment on lines
+18
to
+37
|
||
| loadChunk(0, 0, fillChunk(TILE_FLOOR)); | ||
|
|
||
| const world = new World({ seed: 2 }); | ||
| const playerId = world.create(); | ||
| world.add(playerId, Player); | ||
| world.add(playerId, Position, { x: 5, y: 5 }); | ||
|
|
||
| const petId = world.create(); | ||
| world.add(petId, Pet); | ||
| world.add(petId, Position, { x: 20, y: 20 }); | ||
|
|
||
| for (let dy = -1; dy <= 1; dy++) { | ||
| for (let dx = -1; dx <= 1; dx++) { | ||
| if (dx === 0 && dy === 0) continue; | ||
| const blocker = world.create(); | ||
| world.add(blocker, Position, { x: 5 + dx, y: 5 + dy }); | ||
| world.add(blocker, Collider, { solid: true, blocksSight: false }); | ||
| } | ||
| } | ||
|
|
||
| petFollowSystem(world); | ||
|
|
||
| const petPos = world.get(petId, Position); | ||
| assertEquals(petPos, { x: 20, y: 20 }); | ||
| clearAll(); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maxDistanceis treated as a coordinate offset (square area), but the code computes Manhattandistand will still consider/return tiles where|dx| + |dy| > maxDistance(e.g. dx=2,dy=2 when maxDistance=2). IfmaxDistanceis intended to be a Manhattan radius, filter candidates todist <= maxDistance(or iterate by distance rings) so the function never returns a tile beyond the requested distance.