Add Homecoming scroll, canonical home anchor, and safe return portal flow#25
Add Homecoming scroll, canonical home anchor, and safe return portal flow#25
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a Homecoming scroll that allows players to instantly return to a canonical "home anchor" at depth 0 (overworld), with a temporary return portal that enables safe return to the departure location. The implementation includes comprehensive fallback logic for blocked tiles, proper event emissions, and visual effects for the portal.
Changes:
- Added homecoming spell and scroll item with loot table integration
- Implemented teleportation utilities with Chebyshev-distance fallback for safe placement
- Added DungeonState fields (homeAnchor, returnPortal) with automatic refresh on depth 0 transitions
- Created return portal interaction system with safe return placement
- Added purple swirling particle VFX and palette entries for visual feedback
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/homecomingScroll.test.mjs | Comprehensive regression tests covering successful return with fallback, invalid anchor failure, and blocked home tile scenarios |
| src/rules/utils/teleport.js | Teleport utilities including resolveTeleportDestination with fallback logic and findHomeAnchor for canonical home position |
| src/rules/scripts/spells.js | Homecoming spell implementation with safe teleportation, portal creation, and event emissions |
| src/rules/systems/interactionSystem.js | Return portal interaction handler with depth transition and fallback placement |
| src/rules/environment/dungeon/transition.js | Added skipPostTick option and homeAnchor refresh on depth 0 transitions |
| src/rules/environment/dungeon/index.js | Initialize homeAnchor during dungeon bootstrap |
| src/rules/data/spells.js | Homecoming spell definition |
| src/rules/data/lootTables.js | Added scroll_homecoming to scroll loot table |
| src/rules/data/itemCatalog.js | Scroll of Homecoming catalog entry |
| src/rules/components/DungeonState.js | Added homeAnchor and returnPortal fields |
| src/main.js | Portal VFX handler with purple swirling particles |
| src/display/palette/base.js | Palette entries for scroll and portal glyphs |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const fallback = resolveTeleportDestination(world, { x: fromPos.x, y: fromPos.y }, { | ||
| maxDistance: 3, | ||
| exclude: [{ x: fromPos.x, y: fromPos.y }], | ||
| }) || { x: fromPos.x, y: fromPos.y }; | ||
|
|
||
| world.set(actor, Position, fallback); | ||
| if (ds) ds.returnPortal = null; | ||
| try { world.destroy(targetId); } catch {} | ||
| world.emit?.("portal:returned", { actor, targetId, to: { depth: fromDepth, pos: fallback } }); |
There was a problem hiding this comment.
The fallback logic may place the player on a blocked tile. If resolveTeleportDestination returns null (when all tiles within maxDistance are blocked), the code falls back to fromPos, which could be occupied or blocked. This contradicts the test expectation on line 52 of homecomingScroll.test.mjs that expects the player to NOT be placed on a blocked departure tile. Consider either removing the || { x: fromPos.x, y: fromPos.y } fallback or handling the null case differently, such as emitting a failure event.
| const fallback = resolveTeleportDestination(world, { x: fromPos.x, y: fromPos.y }, { | |
| maxDistance: 3, | |
| exclude: [{ x: fromPos.x, y: fromPos.y }], | |
| }) || { x: fromPos.x, y: fromPos.y }; | |
| world.set(actor, Position, fallback); | |
| if (ds) ds.returnPortal = null; | |
| try { world.destroy(targetId); } catch {} | |
| world.emit?.("portal:returned", { actor, targetId, to: { depth: fromDepth, pos: fallback } }); | |
| const destination = resolveTeleportDestination(world, { x: fromPos.x, y: fromPos.y }, { | |
| maxDistance: 3, | |
| exclude: [{ x: fromPos.x, y: fromPos.y }], | |
| }); | |
| if (!destination) { | |
| world.emit?.("portal:failed", { actor, targetId, reason: "blocked-destination" }); | |
| break; | |
| } | |
| world.set(actor, Position, destination); | |
| if (ds) ds.returnPortal = null; | |
| try { world.destroy(targetId); } catch {} | |
| world.emit?.("portal:returned", { actor, targetId, to: { depth: fromDepth, pos: destination } }); |
| homecoming: { | ||
| id: 'homecoming', | ||
| name: 'Homecoming', | ||
| manaCost: 0, |
There was a problem hiding this comment.
The homecoming spell has manaCost set to 0, but the existing spell validation test in tests/dataIntegrity.test.mjs (line 13) expects all spells to have positive manaCost. This will cause the data integrity test to fail. Consider either setting manaCost to a positive value (e.g., 10 to represent the cost of a powerful teleportation spell) or updating the validation test to allow zero-cost spells if scrolls are meant to bypass mana costs entirely.
| manaCost: 0, | |
| manaCost: 10, |
Motivation
Description
homeAnchorandreturnPortaltoDungeonStateand initialize/refresh them during dungeon bootstrap and when transitioning to depth 0. (files:src/rules/components/DungeonState.js,src/rules/environment/dungeon/index.js,src/rules/environment/dungeon/transition.js)src/rules/utils/teleport.jsincludingresolveTeleportDestination(Chebyshev-radius fallback, excluded tiles, blocked/tile walkability) andfindHomeAnchor(compute canonical anchor from bed + nearest chest).homecomingand catalog itemscroll_homecoming, and add it to scroll loot tables so it can appear in the world. (files:src/rules/data/spells.js,src/rules/data/itemCatalog.js,src/rules/data/lootTables.js)home_return_portalentity storing return metadata, and emit semantic events (portal:opened,teleport:home,teleport:failed). (file:src/rules/scripts/spells.js)interactionSystemwhen the player interacts with the return portal: transition back to the source depth and place the player safely with fallback to nearby tiles if the exact tile is occupied. (file:src/rules/systems/interactionSystem.js)portal:openedto visualize the portal in-game. (files:src/display/palette/base.js,src/main.js)tests/homecomingScroll.test.mjs)Testing
node --checkon modified modules which succeeded (no syntax errors). (passed)deno testbutdenois not available in this environment so the Deno tests could not be executed here. (not run)bun testwhich errored due to environment differences aroundjsr:@std/assertimport resolution (test runtime mismatch); the new test file is authored for the project's Deno-based test runner. (failed under bun)Notes: new regression tests were added in
tests/homecomingScroll.test.mjs; they are written for the project's Deno test runner and should pass when executed in the project's normal Deno test environment.Codex Task