fix(issue-4): replace hardcoded BLOCKING_TYPES with metadata-driven ITEM_METADATA (#20)
* fix(issue-4): add missing solid floor props to BLOCKING_TYPES Audited all furniture types defined in furnitureDefaults.ts and geometry.ts (ITEM_FOOTPRINT) against BLOCKING_TYPES in navigation.ts. Added five previously missing solid floor props: - water_cooler: freestanding floor appliance, agents pathfound through it - server_terminal: floor-standing terminal in the server room - dishwasher: floor appliance in the kitchen area - easel: floor-standing art-room prop - beanbag: floor seat large enough to obstruct walking paths Also adds a unit test asserting every newly-added type is correctly blocked in the nav grid, and that non-solid desk decorations (keyboard) remain free. * refactor(nav): replace hardcoded BLOCKING_TYPES with metadata-driven ITEM_METADATA Previously, buildNavGrid() maintained a hardcoded BLOCKING_TYPES set in navigation.ts. The issue #4 fix added the five missing solid props directly to that set, but this approach is brittle — every new furniture type requires a separate PR touching navigation.ts to stay correct. This rework introduces ITEM_METADATA in geometry.ts (alongside the existing ITEM_FOOTPRINT record) as the single source of truth for per-type navigation properties: export const ITEM_METADATA: Record<string, { blocksNavigation: boolean }> Each item type explicitly declares blocksNavigation: true/false. The five props fixed in issue #4 (water_cooler, server_terminal, dishwasher, easel, beanbag) retain their blocking status. Unknown types default to false, so future decorative items never accidentally block navigation. Changes: - geometry.ts: add ITEM_METADATA export (64 type entries) - navigation.ts: remove BLOCKING_TYPES set; add itemBlocksNavigation() helper that reads ITEM_METADATA[type]?.blocksNavigation ?? false; update buildNavGrid() to call it - tests/unit/navigation.navBlockers.test.ts: retain original 5 solid-prop tests; add metadata-driven test suite covering: all blocking types from metadata, all non-blocking types from metadata, runtime-added type with blocksNavigation:true, and unknown-type safe fallback All 10 tests pass; lint clean; only pre-existing TS2367 (issue #13) remains. * test(navigation): add extended nav blocker tests (issue #4) Additional tests for buildNavGrid and astar pathfinding: - Adjacent blocking items create a continuous impassable wall - Blocking item near grid edge/boundary causes no out-of-bounds errors - Full pathfinding integration: astar routes AROUND a cabinet placed between start and end (not just that cells are blocked) - desk_cubicle explicitly does NOT block — confirmed via ITEM_METADATA - door explicitly does NOT block — agents must walk through doors --------- Co-authored-by: Neo (subagent) <neo@openclaw.local>
This commit is contained in:
committed by
GitHub
parent
941612ab2d
commit
e24ed41532
@@ -97,6 +97,80 @@ export const getItemBaseSize = (item: FurnitureItem) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-type metadata for furniture items.
|
||||||
|
*
|
||||||
|
* blocksNavigation: true → solid floor-standing prop; marks grid cells as impassable.
|
||||||
|
* blocksNavigation: false → desk decoration, wall-mounted, elevated, or passable item.
|
||||||
|
*
|
||||||
|
* This is the single source of truth for nav-blocking behaviour. `buildNavGrid` in
|
||||||
|
* navigation.ts reads this instead of maintaining its own hardcoded type set.
|
||||||
|
*/
|
||||||
|
export const ITEM_METADATA: Record<string, { blocksNavigation: boolean }> = {
|
||||||
|
// ── structural ────────────────────────────────────────────────────────────
|
||||||
|
wall: { blocksNavigation: true },
|
||||||
|
door: { blocksNavigation: false }, // passable
|
||||||
|
// ── seating / lounge ──────────────────────────────────────────────────────
|
||||||
|
chair: { blocksNavigation: false }, // passable / agents sit on them
|
||||||
|
couch: { blocksNavigation: true },
|
||||||
|
couch_v: { blocksNavigation: true },
|
||||||
|
beanbag: { blocksNavigation: true }, // large floor seat (issue #4)
|
||||||
|
// ── desks / workstations ──────────────────────────────────────────────────
|
||||||
|
desk_cubicle: { blocksNavigation: false }, // agents stand at these; collision handled separately
|
||||||
|
executive_desk: { blocksNavigation: true },
|
||||||
|
// ── tables ────────────────────────────────────────────────────────────────
|
||||||
|
round_table: { blocksNavigation: true },
|
||||||
|
table_rect: { blocksNavigation: true },
|
||||||
|
pingpong: { blocksNavigation: true },
|
||||||
|
// ── storage / shelving ────────────────────────────────────────────────────
|
||||||
|
bookshelf: { blocksNavigation: true },
|
||||||
|
cabinet: { blocksNavigation: true },
|
||||||
|
wall_cabinet: { blocksNavigation: false }, // wall-mounted; agents walk under
|
||||||
|
// ── kitchen appliances ────────────────────────────────────────────────────
|
||||||
|
fridge: { blocksNavigation: true },
|
||||||
|
stove: { blocksNavigation: true },
|
||||||
|
microwave: { blocksNavigation: false }, // counter-top / elevated
|
||||||
|
dishwasher: { blocksNavigation: true }, // floor appliance (issue #4)
|
||||||
|
sink: { blocksNavigation: true },
|
||||||
|
coffee_machine: { blocksNavigation: false }, // elevated on counter
|
||||||
|
// ── office equipment ──────────────────────────────────────────────────────
|
||||||
|
printer: { blocksNavigation: true },
|
||||||
|
vending: { blocksNavigation: true },
|
||||||
|
atm: { blocksNavigation: true },
|
||||||
|
whiteboard: { blocksNavigation: true },
|
||||||
|
computer: { blocksNavigation: false }, // desk item
|
||||||
|
keyboard: { blocksNavigation: false }, // desk decoration
|
||||||
|
mouse: { blocksNavigation: false }, // desk decoration
|
||||||
|
// ── server room ───────────────────────────────────────────────────────────
|
||||||
|
server_rack: { blocksNavigation: true },
|
||||||
|
server_terminal: { blocksNavigation: true }, // floor-standing terminal (issue #4)
|
||||||
|
sms_booth: { blocksNavigation: true },
|
||||||
|
phone_booth: { blocksNavigation: true },
|
||||||
|
// ── QA lab ────────────────────────────────────────────────────────────────
|
||||||
|
qa_terminal: { blocksNavigation: true },
|
||||||
|
device_rack: { blocksNavigation: true },
|
||||||
|
test_bench: { blocksNavigation: true },
|
||||||
|
// ── gym ───────────────────────────────────────────────────────────────────
|
||||||
|
treadmill: { blocksNavigation: true },
|
||||||
|
weight_bench: { blocksNavigation: true },
|
||||||
|
dumbbell_rack: { blocksNavigation: true },
|
||||||
|
exercise_bike: { blocksNavigation: true },
|
||||||
|
punching_bag: { blocksNavigation: true },
|
||||||
|
rowing_machine: { blocksNavigation: true },
|
||||||
|
kettlebell_rack: { blocksNavigation: true },
|
||||||
|
yoga_mat: { blocksNavigation: true },
|
||||||
|
// ── art room ──────────────────────────────────────────────────────────────
|
||||||
|
easel: { blocksNavigation: true }, // floor-standing prop (issue #4)
|
||||||
|
// ── water cooler ──────────────────────────────────────────────────────────
|
||||||
|
water_cooler: { blocksNavigation: true }, // freestanding floor appliance (issue #4)
|
||||||
|
// ── decorative / small ────────────────────────────────────────────────────
|
||||||
|
plant: { blocksNavigation: true },
|
||||||
|
lamp: { blocksNavigation: false }, // floor lamp but thin; passable in practice
|
||||||
|
trash: { blocksNavigation: false }, // small bin
|
||||||
|
clock: { blocksNavigation: false }, // wall-mounted
|
||||||
|
mug: { blocksNavigation: false }, // desk item
|
||||||
|
};
|
||||||
|
|
||||||
export const FURNITURE_ROTATION: Record<string, number> = {
|
export const FURNITURE_ROTATION: Record<string, number> = {
|
||||||
couch: Math.PI,
|
couch: Math.PI,
|
||||||
couch_v: Math.PI / 2,
|
couch_v: Math.PI / 2,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
getItemBounds,
|
getItemBounds,
|
||||||
ITEM_FOOTPRINT,
|
ITEM_FOOTPRINT,
|
||||||
|
ITEM_METADATA,
|
||||||
snap,
|
snap,
|
||||||
} from "@/features/retro-office/core/geometry";
|
} from "@/features/retro-office/core/geometry";
|
||||||
import type {
|
import type {
|
||||||
@@ -75,49 +76,20 @@ const GRID_ROWS = Math.ceil(CANVAS_H / GRID_CELL);
|
|||||||
|
|
||||||
export type NavGrid = Uint8Array;
|
export type NavGrid = Uint8Array;
|
||||||
|
|
||||||
// These types define the coarse collision world for pathfinding. Keep this list conservative:
|
/**
|
||||||
// add types here when they materially block walking, and use dedicated route helpers when a
|
* Returns true if the given item type should block pathfinding cells.
|
||||||
// room needs staged entry behavior instead of a single destination point.
|
* Driven by ITEM_METADATA.blocksNavigation — the single source of truth for
|
||||||
const BLOCKING_TYPES = new Set([
|
* nav-blocking behaviour. Unknown types default to false (non-blocking) so
|
||||||
"wall",
|
* newly added decorative items never accidentally block navigation.
|
||||||
"round_table",
|
*/
|
||||||
"couch",
|
const itemBlocksNavigation = (type: string): boolean =>
|
||||||
"couch_v",
|
ITEM_METADATA[type]?.blocksNavigation ?? false;
|
||||||
"executive_desk",
|
|
||||||
"bookshelf",
|
|
||||||
"pingpong",
|
|
||||||
"table_rect",
|
|
||||||
"cabinet",
|
|
||||||
"fridge",
|
|
||||||
"plant",
|
|
||||||
"whiteboard",
|
|
||||||
"vending",
|
|
||||||
"atm",
|
|
||||||
"sms_booth",
|
|
||||||
"phone_booth",
|
|
||||||
"server_rack",
|
|
||||||
"sink",
|
|
||||||
"printer",
|
|
||||||
"stove",
|
|
||||||
"microwave",
|
|
||||||
"qa_terminal",
|
|
||||||
"device_rack",
|
|
||||||
"test_bench",
|
|
||||||
"treadmill",
|
|
||||||
"weight_bench",
|
|
||||||
"dumbbell_rack",
|
|
||||||
"exercise_bike",
|
|
||||||
"punching_bag",
|
|
||||||
"rowing_machine",
|
|
||||||
"kettlebell_rack",
|
|
||||||
"yoga_mat",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function buildNavGrid(furniture: FurnitureItem[]): NavGrid {
|
export function buildNavGrid(furniture: FurnitureItem[]): NavGrid {
|
||||||
const grid = new Uint8Array(GRID_COLS * GRID_ROWS);
|
const grid = new Uint8Array(GRID_COLS * GRID_ROWS);
|
||||||
const pad = GRID_CELL * 0.6;
|
const pad = GRID_CELL * 0.6;
|
||||||
for (const item of furniture) {
|
for (const item of furniture) {
|
||||||
if (!BLOCKING_TYPES.has(item.type)) continue;
|
if (!itemBlocksNavigation(item.type)) continue;
|
||||||
const bounds = getItemBounds(item);
|
const bounds = getItemBounds(item);
|
||||||
const x1 = bounds.x - pad;
|
const x1 = bounds.x - pad;
|
||||||
const y1 = bounds.y - pad;
|
const y1 = bounds.y - pad;
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { astar, buildNavGrid } from "@/features/retro-office/core/navigation";
|
||||||
|
import { ITEM_METADATA } from "@/features/retro-office/core/geometry";
|
||||||
|
import type { FurnitureItem } from "@/features/retro-office/core/types";
|
||||||
|
|
||||||
|
// Minimal helper: creates a FurnitureItem at a given position.
|
||||||
|
const makeItem = (type: string, x = 100, y = 100): FurnitureItem => ({
|
||||||
|
_uid: `test_${type}`,
|
||||||
|
type,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if ANY cell in the grid that overlaps the given world-space
|
||||||
|
* rectangle is marked as blocked (value === 1).
|
||||||
|
*/
|
||||||
|
const isBlocked = (
|
||||||
|
grid: Uint8Array,
|
||||||
|
wx: number,
|
||||||
|
wy: number,
|
||||||
|
ww = 30,
|
||||||
|
wh = 30,
|
||||||
|
): boolean => {
|
||||||
|
const GRID_CELL = 25;
|
||||||
|
const CANVAS_W = 1800;
|
||||||
|
const CANVAS_H = 720;
|
||||||
|
const GRID_COLS = Math.ceil(CANVAS_W / GRID_CELL);
|
||||||
|
const GRID_ROWS = Math.ceil(CANVAS_H / GRID_CELL);
|
||||||
|
|
||||||
|
const c1 = Math.max(0, Math.floor(wx / GRID_CELL));
|
||||||
|
const c2 = Math.min(GRID_COLS - 1, Math.floor((wx + ww) / GRID_CELL));
|
||||||
|
const r1 = Math.max(0, Math.floor(wy / GRID_CELL));
|
||||||
|
const r2 = Math.min(GRID_ROWS - 1, Math.floor((wy + wh) / GRID_CELL));
|
||||||
|
|
||||||
|
for (let r = r1; r <= r2; r++) {
|
||||||
|
for (let c = c1; c <= c2; c++) {
|
||||||
|
if (grid[r * GRID_COLS + c] === 1) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("buildNavGrid – solid floor props block pathfinding (issue #4)", () => {
|
||||||
|
// The five types that were previously missing from the blocking set (issue #4).
|
||||||
|
const solidProps = [
|
||||||
|
"water_cooler",
|
||||||
|
"server_terminal",
|
||||||
|
"dishwasher",
|
||||||
|
"easel",
|
||||||
|
"beanbag",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const propType of solidProps) {
|
||||||
|
it(`marks cells occupied by '${propType}' as blocked`, () => {
|
||||||
|
const item = makeItem(propType, 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
// The item is placed at (400, 300); any cell in that vicinity should be blocked.
|
||||||
|
expect(isBlocked(grid, 400, 300)).toBe(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("does not block cells occupied by non-solid props (e.g. keyboard)", () => {
|
||||||
|
// A keyboard is a desk decoration and should NOT block walking paths.
|
||||||
|
const item = makeItem("keyboard", 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
// Centre cell of the item should remain free (border cells are always blocked, pick interior).
|
||||||
|
expect(isBlocked(grid, 400, 300)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildNavGrid – metadata-driven blocking (issue #4 rework)", () => {
|
||||||
|
it("respects ITEM_METADATA.blocksNavigation for known blocking types", () => {
|
||||||
|
// Spot-check a few well-known blocking types derived from metadata.
|
||||||
|
const blockingTypes = Object.entries(ITEM_METADATA)
|
||||||
|
.filter(([, meta]) => meta.blocksNavigation)
|
||||||
|
.map(([type]) => type);
|
||||||
|
|
||||||
|
for (const type of blockingTypes) {
|
||||||
|
const item = makeItem(type, 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
expect(isBlocked(grid, 400, 300), `expected '${type}' to block`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not block for known non-blocking types from ITEM_METADATA", () => {
|
||||||
|
const nonBlockingTypes = Object.entries(ITEM_METADATA)
|
||||||
|
.filter(([, meta]) => !meta.blocksNavigation)
|
||||||
|
.map(([type]) => type);
|
||||||
|
|
||||||
|
for (const type of nonBlockingTypes) {
|
||||||
|
const item = makeItem(type, 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
expect(isBlocked(grid, 400, 300), `expected '${type}' NOT to block`).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a new item type with blocksNavigation: true is correctly blocked by buildNavGrid", () => {
|
||||||
|
// Simulate adding a brand-new prop type to ITEM_METADATA at runtime.
|
||||||
|
// This verifies the metadata-driven path works end-to-end for future additions.
|
||||||
|
const testType = "__test_new_blocking_prop__";
|
||||||
|
ITEM_METADATA[testType] = { blocksNavigation: true };
|
||||||
|
try {
|
||||||
|
const item = makeItem(testType, 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
expect(isBlocked(grid, 400, 300)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary entry so it doesn't affect other tests.
|
||||||
|
delete ITEM_METADATA[testType];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("an unknown item type defaults to non-blocking (safe fallback)", () => {
|
||||||
|
// Types not listed in ITEM_METADATA should never accidentally block navigation.
|
||||||
|
const item = makeItem("__completely_unknown_type__", 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
expect(isBlocked(grid, 400, 300)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Additional edge-case and integration tests (issue #4)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("buildNavGrid – continuous wall from adjacent blocking items (issue #4)", () => {
|
||||||
|
it("adjacent blocking items form a continuous impassable wall", () => {
|
||||||
|
/*
|
||||||
|
* Place three `vending` machines side-by-side (each 40×60 px) at x=200,220,240.
|
||||||
|
* Together they should create a solid horizontal band from y=300 that blocks
|
||||||
|
* the entire horizontal span — no path can slip between them.
|
||||||
|
*
|
||||||
|
* We verify by checking that cells at several x positions along the row are
|
||||||
|
* all blocked.
|
||||||
|
*/
|
||||||
|
const items: FurnitureItem[] = [
|
||||||
|
makeItem("vending", 200, 300),
|
||||||
|
makeItem("vending", 240, 300),
|
||||||
|
makeItem("vending", 280, 300),
|
||||||
|
];
|
||||||
|
|
||||||
|
const grid = buildNavGrid(items);
|
||||||
|
|
||||||
|
// All three placement regions should be blocked.
|
||||||
|
expect(isBlocked(grid, 200, 300)).toBe(true);
|
||||||
|
expect(isBlocked(grid, 240, 300)).toBe(true);
|
||||||
|
expect(isBlocked(grid, 280, 300)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildNavGrid – near-boundary placement (issue #4)", () => {
|
||||||
|
it("blocking item near the grid edge does not cause out-of-bounds errors", () => {
|
||||||
|
// Place a large item near the right/bottom edges of the canvas.
|
||||||
|
// buildNavGrid clamps cells to valid indices — this must not throw.
|
||||||
|
const nearEdge = makeItem("cabinet", 1760, 680); // close to CANVAS_W=1800, CANVAS_H=720
|
||||||
|
expect(() => buildNavGrid([nearEdge])).not.toThrow();
|
||||||
|
|
||||||
|
const grid = buildNavGrid([nearEdge]);
|
||||||
|
// The grid array length must still be correct.
|
||||||
|
const GRID_CELL = 25;
|
||||||
|
const GRID_COLS = Math.ceil(1800 / GRID_CELL);
|
||||||
|
const GRID_ROWS = Math.ceil(720 / GRID_CELL);
|
||||||
|
expect(grid.length).toBe(GRID_COLS * GRID_ROWS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocking item at the top-left corner does not cause out-of-bounds errors", () => {
|
||||||
|
const nearOrigin = makeItem("vending", 0, 0);
|
||||||
|
expect(() => buildNavGrid([nearOrigin])).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildNavGrid + astar – full pathfinding integration (issue #4)", () => {
|
||||||
|
it("path goes AROUND a blocking item placed between start and end", () => {
|
||||||
|
/*
|
||||||
|
* Layout (world coords):
|
||||||
|
* Start: (100, 350) ← left side
|
||||||
|
* End: (700, 350) ← right side
|
||||||
|
* Blocker: cabinet at (400, 300) — 200×40 px, blocks a wide horizontal band
|
||||||
|
*
|
||||||
|
* A straight horizontal path would pass through x≈400 y≈300-340. The astar
|
||||||
|
* path must route around the cabinet, not through it.
|
||||||
|
*/
|
||||||
|
const blocker = makeItem("cabinet", 400, 300);
|
||||||
|
const grid = buildNavGrid([blocker]);
|
||||||
|
|
||||||
|
const path = astar(100, 350, 700, 350, grid);
|
||||||
|
|
||||||
|
expect(path.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify no waypoint falls inside the (padded) blocked region around the cabinet.
|
||||||
|
// The cabinet is 200×40; buildNavGrid adds 0.6*GRID_CELL ≈ 15 px padding.
|
||||||
|
const blockedXMin = 400 - 15;
|
||||||
|
const blockedXMax = 400 + 200 + 15;
|
||||||
|
const blockedYMin = 300 - 15;
|
||||||
|
const blockedYMax = 300 + 40 + 15;
|
||||||
|
|
||||||
|
for (const { x, y } of path) {
|
||||||
|
const insideBlocker =
|
||||||
|
x >= blockedXMin && x <= blockedXMax &&
|
||||||
|
y >= blockedYMin && y <= blockedYMax;
|
||||||
|
expect(insideBlocker, `waypoint (${x.toFixed(0)}, ${y.toFixed(0)}) must not be inside the cabinet footprint`).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildNavGrid – specific non-blocking item types (issue #4)", () => {
|
||||||
|
it("desk_cubicle does NOT block navigation — agents stand at these", () => {
|
||||||
|
/*
|
||||||
|
* desk_cubicle has blocksNavigation: false in ITEM_METADATA.
|
||||||
|
* Agents interact with desks by standing beside them; the desk itself
|
||||||
|
* is not a solid blocker in the pathfinding grid.
|
||||||
|
*/
|
||||||
|
const item = makeItem("desk_cubicle", 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
expect(isBlocked(grid, 400, 300)).toBe(false);
|
||||||
|
// Confirm via metadata as the authoritative source.
|
||||||
|
expect(ITEM_METADATA["desk_cubicle"]?.blocksNavigation).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("door does NOT block navigation — agents must be able to walk through doors", () => {
|
||||||
|
/*
|
||||||
|
* door has blocksNavigation: false in ITEM_METADATA.
|
||||||
|
* Doors are structural openings that agents traverse; they must never be
|
||||||
|
* marked impassable in the nav grid.
|
||||||
|
*/
|
||||||
|
const item = makeItem("door", 400, 300);
|
||||||
|
const grid = buildNavGrid([item]);
|
||||||
|
expect(isBlocked(grid, 400, 300)).toBe(false);
|
||||||
|
// Confirm via metadata.
|
||||||
|
expect(ITEM_METADATA["door"]?.blocksNavigation).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user