Add named Marks and cross-room NPC pathfinding
- Mark: a debug non-collidable named destination (pixel-art target icon), editor tool + name modal, globally-unique names, programmatic Room:addMark - NPCs walk to Marks: each RGB copy is a per-room resident (like the player), running its own A* on its own colour layer, transferring rooms as it crosses seams; rendered and collided by the room it currently stands in - Player copies are obstacles in NPC pathfinding - A* is footprint-aware for multi-cell NPCs - Editor: click an NPC to open a filterable destination picker - Ink: Dialogue.open binds walk_to; Jorge can be sent to "end"/"begin" - Fix: a blocked NPC now replans each tick and resumes instead of freezing - [npc] move diagnostics under DEBUG (start/goal/route or block reason) - Docs (adding-npcs) + CLAUDE.md LÖVE 11.x/float-colour corrections - Fix test harness loadSprite stub; movement tests green (28) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ad405ed590
commit
c348b81a93
17 changed files with 2755 additions and 43 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -15,10 +15,10 @@ lua tests/test_movement.lua # run the pure matrix-movement tests
|
||||||
|
|
||||||
`love` is not installed in this environment — you cannot run the game here; reason about the code statically.
|
`love` is not installed in this environment — you cannot run the game here; reason about the code statically.
|
||||||
|
|
||||||
**LÖVE version matters.** The code targets the **0.10.x** API, not 11.x. Signals of this that you must preserve when editing:
|
**LÖVE version matters.** The code targets the **11.x** API — `conf.lua` pins `t.version = "11.5"`. Preserve these when editing:
|
||||||
- Colors are `0–255` integers (`love.graphics.setColor(255, 255, 255)`), not `0–1` floats.
|
- Colors are normalized `0–1` floats (`love.graphics.setColor(1, 1, 1)`), not `0–255` integers — see `color.lua`.
|
||||||
- `sprite:getData()` / `img:getPixel` are used (see `sprite_manager.lua`); the 11.x replacements differ.
|
- Pixel inspection uses the 11.x ImageData API: `love.image.newImageData(path)` + `ImageData:getPixel` (see `getCellsFromSprite` in `_helpers.lua`). `Image:getData()` was removed in 11 — don't reintroduce it.
|
||||||
Do not "modernize" these to the 11.x API unless explicitly asked — it would break the whole render/color path.
|
Do not "downgrade" these to the 0.10.x API — it would break the whole render/color path.
|
||||||
|
|
||||||
## The core mechanic (read this before touching gameplay code)
|
## The core mechanic (read this before touching gameplay code)
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ The player is not one entity — it is **up to three simultaneous colored copies
|
||||||
- `entity.lua` — `Entity` base class (middleclass). Everything on the grid subclasses it: `Player`, `Box`, `Wall`, `Switch`, `Door`, `Artifact`, `Art`, `Ass` (assorted decor), `npc`. Grid↔pixel conversion, occupied-cell shapes, and the `sha()` jitter all live here.
|
- `entity.lua` — `Entity` base class (middleclass). Everything on the grid subclasses it: `Player`, `Box`, `Wall`, `Switch`, `Door`, `Artifact`, `Art`, `Ass` (assorted decor), `npc`. Grid↔pixel conversion, occupied-cell shapes, and the `sha()` jitter all live here.
|
||||||
- `room.lua` — the orchestrator: entity registries, per-color matrices, movement/push/pull, tick, switch/door logic, room transitions (`nextRoom`), and serialization.
|
- `room.lua` — the orchestrator: entity registries, per-color matrices, movement/push/pull, tick, switch/door logic, room transitions (`nextRoom`), and serialization.
|
||||||
- `color.lua` — `Color` class + the canonical RGB(W) table. `gColor` globals are created in `main._initGlobalColors`.
|
- `color.lua` — `Color` class + the canonical RGB(W) table. `gColor` globals are created in `main._initGlobalColors`.
|
||||||
- `sprite_manager.lua` — loads all images into the `sprites` global and precomputes `spriteCells` (which 16×16 cells of a sprite are non-empty, for multi-cell collision shapes). Sprites are `16px`-per-cell.
|
- Sprite loading: `loadSprite(path)` in `_helpers.lua` returns a cached `Image`. Per-asset occupied-cell shapes (which 16×16 cells are non-empty, for multi-cell collision) are precomputed by `getCellsFromSprite` (`_helpers.lua`) and gathered into `globalAssetProperties` by `getAllAssets` (`level_editor/object_attributes.lua`). Sprites are `16px`-per-cell. (An older `sprite_manager.lua` that populated `sprites`/`spriteCells` globals is gone; the stray `sprites.*` / `spriteCells[...]` references in stub files like `artifact.lua` / `assorted.lua` are vestigial.)
|
||||||
- `input.lua`, `shaders.lua` (`wiggle`, `glow` GLSL), `_helpers.lua` (`lerp`/`map`/`constrain`), `_debug_helpers.lua` (`printMatrix`, `shallowTablePrint`, `randomPosition`).
|
- `input.lua`, `shaders.lua` (`wiggle`, `glow` GLSL), `_helpers.lua` (`lerp`/`map`/`constrain`), `_debug_helpers.lua` (`printMatrix`, `shallowTablePrint`, `randomPosition`).
|
||||||
- `dialogue_manager.lua` is currently empty; `npc.lua`/`art.lua`/`artifact.lua` are stubs or reference art folders not present in `art/`.
|
- `dialogue_manager.lua` is currently empty; `npc.lua`/`art.lua`/`artifact.lua` are stubs or reference art folders not present in `art/`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,20 @@ Dialogable = {}
|
||||||
function Dialogable:onBump()
|
function Dialogable:onBump()
|
||||||
if self.book then
|
if self.book then
|
||||||
local wasOpen = Dialogue.isOpen()
|
local wasOpen = Dialogue.isOpen()
|
||||||
Dialogue.open(self.book)
|
Dialogue.open(self.book, self:dialogueBindings())
|
||||||
if Sound and not wasOpen then Sound.jabber() end
|
if Sound and not wasOpen then Sound.jabber() end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Ink external functions exposed to this talker's book. A walking NPC (one that
|
||||||
|
-- includes Walker) can be sent to a named Mark with `~ walk_to("mark_name")`,
|
||||||
|
-- e.g. to leave after a conversation.
|
||||||
|
function Dialogable:dialogueBindings()
|
||||||
|
local talker = self
|
||||||
|
return {
|
||||||
|
walk_to = function(markName)
|
||||||
|
if talker.sendTo then talker:sendTo(markName) end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,15 @@ local function advance()
|
||||||
selected = 1
|
selected = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
function Dialogue.open(book)
|
-- `bindings` (optional) is a {name = function} table wired in as Ink external
|
||||||
|
-- functions before the story starts, so a book can call e.g. `~ walk_to("gate")`
|
||||||
|
-- to drive game state. Dialogable supplies these per talker (see dialogable.lua).
|
||||||
|
function Dialogue.open(book, bindings)
|
||||||
if story then return end -- already talking: ignore repeat bumps
|
if story then return end -- already talking: ignore repeat bumps
|
||||||
story = narrator.init_story(book)
|
story = narrator.init_story(book)
|
||||||
|
if bindings then
|
||||||
|
for name, fn in pairs(bindings) do story:bind(name, fn) end
|
||||||
|
end
|
||||||
story:begin()
|
story:begin()
|
||||||
advance()
|
advance()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,14 @@ In `main.lua`, next to `startSignBook`:
|
||||||
|
|
||||||
## 4. Make the entity
|
## 4. Make the entity
|
||||||
|
|
||||||
In `npc.lua`, copy `Sign` — pick your art and name your book:
|
In `npc.lua`, copy `Sign` — pick your art and name your book. Include `Walker`
|
||||||
|
too (it's what lets the NPC path to a Mark; see "Making an NPC walk" below), and
|
||||||
|
call `self:initWalker()` after `Entity.initialize`:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Oracle = class("Oracle", Entity)
|
Oracle = class("Oracle", Entity)
|
||||||
Oracle:include(Dialogable)
|
Oracle:include(Dialogable)
|
||||||
|
Oracle:include(Walker)
|
||||||
|
|
||||||
function Oracle:initialize(x, y)
|
function Oracle:initialize(x, y)
|
||||||
self.book = oracleBook -- from step 3
|
self.book = oracleBook -- from step 3
|
||||||
|
|
@ -55,6 +58,7 @@ function Oracle:initialize(x, y)
|
||||||
blue = loadSprite("art/npc/oracle_b.png"),
|
blue = loadSprite("art/npc/oracle_b.png"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
self:initWalker()
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -82,3 +86,44 @@ Then add it to a room's `rooms/<room>.sav` `objects` list:
|
||||||
That's it — walk into it and it talks. NPCs are stamped into every colour's
|
That's it — walk into it and it talks. NPCs are stamped into every colour's
|
||||||
collision matrix, so they block movement and obey the world's colour rules (a
|
collision matrix, so they block movement and obey the world's colour rules (a
|
||||||
red-only player reads them in red).
|
red-only player reads them in red).
|
||||||
|
|
||||||
|
# Marks — named destinations
|
||||||
|
|
||||||
|
A **Mark** is a debug non-collidable: an editor-only annotation the game reads
|
||||||
|
back. It carries a globally-unique name, and it's the only thing an NPC can be
|
||||||
|
sent to. It is *not* a Note — it just shares the debug overlay toggle. In the
|
||||||
|
editor, click the **target** tool at the bottom of the panel, then click a cell
|
||||||
|
to drop one and type its name (colliding names auto-suffix `_2`, `_3`, …).
|
||||||
|
Left-click an existing mark to rename it; right-click to delete. Marks show only
|
||||||
|
under `DEBUG` while the overlay is on (`Tab`).
|
||||||
|
|
||||||
|
You can also create marks from code so they exist without living in a `.sav`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
room:addMark("gate", 5, 9) -- registers + renders in debug; skipped by serialize
|
||||||
|
```
|
||||||
|
|
||||||
|
# Making an NPC walk
|
||||||
|
|
||||||
|
Any NPC that includes `Walker` (see step 4) can be sent to a Mark. From Ink, call
|
||||||
|
the bound external function — e.g. to leave once the conversation ends:
|
||||||
|
|
||||||
|
```ink
|
||||||
|
Oracle: The way is open now. Go.
|
||||||
|
~ walk_to("gate")
|
||||||
|
-> END
|
||||||
|
```
|
||||||
|
|
||||||
|
`walk_to(name)` is wired for every Dialogable (`dialogable.lua`); it resolves the
|
||||||
|
Mark by name and calls `npc:sendTo`. Under the hood the NPC becomes the player's
|
||||||
|
RGB model — up to three colour copies, each running its **own** A* on its own
|
||||||
|
collision layer (`pathfinding.lua`), stepping in real time and pathing across
|
||||||
|
room seams over the world grid. Copies split apart and additively re-merge to
|
||||||
|
white on arrival. Pathfinding is footprint-aware, so a 2×2 NPC routes its whole
|
||||||
|
body around obstacles rather than threading a 1-wide gap.
|
||||||
|
|
||||||
|
## Testing without a story
|
||||||
|
|
||||||
|
You don't need an Ink book to try it: in the editor (`DEBUG`), **left-click an
|
||||||
|
NPC** to open the destination picker — a filterable list of every Mark. Type to
|
||||||
|
filter, click a row (or Enter for the top match) to send it; Esc cancels.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ require "level_editor/object_attributes"
|
||||||
require "level_editor/gui"
|
require "level_editor/gui"
|
||||||
require "level_editor/layout"
|
require "level_editor/layout"
|
||||||
require "level_editor/note_editor"
|
require "level_editor/note_editor"
|
||||||
|
require "mark" -- Mark.icon() supplies the MARK tool button art
|
||||||
|
|
||||||
editorView = "room" --can be "off", "room", "world", "asset"
|
editorView = "room" --can be "off", "room", "world", "asset"
|
||||||
selectedColor = "white"
|
selectedColor = "white"
|
||||||
|
|
@ -68,6 +69,14 @@ local boxPaletteSections = {}
|
||||||
local noteMode = nil
|
local noteMode = nil
|
||||||
local editingNote = nil
|
local editingNote = nil
|
||||||
|
|
||||||
|
-- Mark authoring is entirely independent of notes (they only share the debug
|
||||||
|
-- overlay toggle). `markPlacing` is true after the MARK tool is picked, until a
|
||||||
|
-- grid cell is clicked to drop one; naming then happens in a small screen-space
|
||||||
|
-- modal (`markNameMode` = "create" | "edit"), with `editingMark` the target.
|
||||||
|
local markPlacing = false
|
||||||
|
local markNameMode = nil
|
||||||
|
local editingMark = nil
|
||||||
|
|
||||||
-- Convert a pointer position into a valid room cell. Keeping brush state in
|
-- Convert a pointer position into a valid room cell. Keeping brush state in
|
||||||
-- cell coordinates means a click that jitters within a tile cannot place or
|
-- cell coordinates means a click that jitters within a tile cannot place or
|
||||||
-- erase that tile more than once.
|
-- erase that tile more than once.
|
||||||
|
|
@ -325,7 +334,7 @@ local function layoutRoomControls(c, s)
|
||||||
|
|
||||||
placeRow({ "white", "red", "green", "blue" }, 22)
|
placeRow({ "white", "red", "green", "blue" }, 22)
|
||||||
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22)
|
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22)
|
||||||
placeRow({ "save", "clearSaves", "world", "wire", "note" }, 28)
|
placeRow({ "save", "clearSaves", "world", "wire", "note", "mark" }, 28)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Build the full room-view layout (sections + a scroll list + a back/search
|
-- Build the full room-view layout (sections + a scroll list + a back/search
|
||||||
|
|
@ -761,6 +770,16 @@ function _initLevelEditor()
|
||||||
y = 0,
|
y = 0,
|
||||||
}, "note")
|
}, "note")
|
||||||
|
|
||||||
|
_addUIButton(createUIElement{
|
||||||
|
name = "mark",
|
||||||
|
sprite = Mark.icon(),
|
||||||
|
onClic = { left = function()
|
||||||
|
if editorView == "room" then beginMarkCreate() end
|
||||||
|
end },
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
}, "mark")
|
||||||
|
|
||||||
roomNameField = createUIElement{
|
roomNameField = createUIElement{
|
||||||
name = "roomName",
|
name = "roomName",
|
||||||
text = editorRoomName,
|
text = editorRoomName,
|
||||||
|
|
@ -822,6 +841,7 @@ function selectAsset(asset)
|
||||||
print("selecting", asset)
|
print("selecting", asset)
|
||||||
selectedAsset = asset
|
selectedAsset = asset
|
||||||
exitNoteMode() -- picking any real asset closes the note text field
|
exitNoteMode() -- picking any real asset closes the note text field
|
||||||
|
markPlacing = false -- ...and disarms a pending mark placement
|
||||||
print(selectedAsset)
|
print(selectedAsset)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -836,6 +856,7 @@ end
|
||||||
-- clicks a grid cell to drop the note there (that placement is the "done").
|
-- clicks a grid cell to drop the note there (that placement is the "done").
|
||||||
function beginNoteCreate()
|
function beginNoteCreate()
|
||||||
if editorView ~= "room" then return end
|
if editorView ~= "room" then return end
|
||||||
|
markPlacing = false
|
||||||
boxPaletteOpen = false
|
boxPaletteOpen = false
|
||||||
noteMode = "create"
|
noteMode = "create"
|
||||||
editingNote = nil
|
editingNote = nil
|
||||||
|
|
@ -865,6 +886,216 @@ function cancelNoteMode()
|
||||||
exitNoteMode()
|
exitNoteMode()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ---- Marks -------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Mark names must be globally unique (an NPC is sent to one by name). Keep the
|
||||||
|
-- typed name if free, otherwise suffix _2, _3, ... `exclude` is the mark being
|
||||||
|
-- renamed, so editing a mark without changing its name is a no-op.
|
||||||
|
local function uniqueMarkName(name, exclude)
|
||||||
|
if name == "" then return name end
|
||||||
|
local function taken(candidate)
|
||||||
|
if gameWorld and gameWorld.markByName then
|
||||||
|
local found = gameWorld:markByName(candidate)
|
||||||
|
return found ~= nil and found ~= exclude
|
||||||
|
end
|
||||||
|
for _, mk in ipairs(currentRoom.marks or {}) do
|
||||||
|
if mk ~= exclude and mk.markName == candidate then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if not taken(name) then return name end
|
||||||
|
local i = 2
|
||||||
|
while taken(name .. "_" .. i) do i = i + 1 end
|
||||||
|
return name .. "_" .. i
|
||||||
|
end
|
||||||
|
|
||||||
|
function markNameEditorActive() return markNameMode ~= nil end
|
||||||
|
|
||||||
|
-- Open the screen-space name modal for `mark`; `mode` is "create" (a just-placed
|
||||||
|
-- mark still needing a name) or "edit" (renaming an existing one).
|
||||||
|
local function openMarkNameEditor(mark, mode)
|
||||||
|
exitNoteMode()
|
||||||
|
markNameMode = mode
|
||||||
|
editingMark = mark
|
||||||
|
activeField = "markName"
|
||||||
|
inputText = mark.markName or ""
|
||||||
|
response = inputText
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resetMarkNameEditor()
|
||||||
|
markNameMode = nil
|
||||||
|
editingMark = nil
|
||||||
|
if activeField == "markName" then activeField = nil end
|
||||||
|
inputText = ""
|
||||||
|
response = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
function commitMarkName()
|
||||||
|
if editingMark then editingMark.markName = uniqueMarkName(inputText, editingMark) end
|
||||||
|
resetMarkNameEditor()
|
||||||
|
end
|
||||||
|
|
||||||
|
function cancelMarkName()
|
||||||
|
-- Abandoning a brand-new, still-unnamed mark removes it (it can't be sent to).
|
||||||
|
if markNameMode == "create" and editingMark and editingMark.markName == "" then
|
||||||
|
for i = #currentRoom.marks, 1, -1 do
|
||||||
|
if currentRoom.marks[i] == editingMark then table.remove(currentRoom.marks, i) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
resetMarkNameEditor()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The MARK tool: arm placement. The next grid click drops a mark and opens the
|
||||||
|
-- name modal on it.
|
||||||
|
function beginMarkCreate()
|
||||||
|
if editorView ~= "room" then return end
|
||||||
|
exitNoteMode()
|
||||||
|
boxPaletteOpen = false
|
||||||
|
markPlacing = true
|
||||||
|
editorStatus = "Click a cell to place a mark."
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Left-clicking an existing mark reopens its name modal.
|
||||||
|
function beginMarkEdit(mark)
|
||||||
|
openMarkNameEditor(mark, "edit")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Debug destination picker. Left-clicking an NPC in the room view opens a
|
||||||
|
-- filterable list of every Mark name; picking one sends that NPC there via its
|
||||||
|
-- Walker (Npc:sendTo). Lets you exercise NPC pathfinding without an Ink story.
|
||||||
|
-- Drawn over the play area in screen space (see editorDrawMarkPicker), so its
|
||||||
|
-- geometry uses window coordinates, not the sidebar's virtual space.
|
||||||
|
local markPicker = nil -- { npc = npc } while open
|
||||||
|
|
||||||
|
function markPickerActive() return markPicker ~= nil end
|
||||||
|
|
||||||
|
local function markNamesMatching(filter)
|
||||||
|
local seen, names = {}, {}
|
||||||
|
local rooms = (gameWorld and gameWorld.levels) or { currentRoom }
|
||||||
|
for _, room in pairs(rooms) do
|
||||||
|
for _, mk in ipairs(room.marks or {}) do
|
||||||
|
if mk.markName ~= "" and not seen[mk.markName] and fuzzyMatches(mk.markName, filter) then
|
||||||
|
seen[mk.markName] = true
|
||||||
|
table.insert(names, mk.markName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(names)
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
local function openMarkPicker(npc)
|
||||||
|
markPicker = { npc = npc }
|
||||||
|
activeField = "markFilter"
|
||||||
|
inputText = ""
|
||||||
|
response = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
function closeMarkPicker()
|
||||||
|
markPicker = nil
|
||||||
|
if activeField == "markFilter" then activeField = nil end
|
||||||
|
inputText = ""
|
||||||
|
response = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sendToMark(name)
|
||||||
|
if markPicker and markPicker.npc and markPicker.npc.sendTo then
|
||||||
|
markPicker.npc:sendTo(name)
|
||||||
|
editorStatus = "Sent NPC to '" .. name .. "'."
|
||||||
|
end
|
||||||
|
closeMarkPicker()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Panel + hit-tested rows, computed identically for draw and click, in screen
|
||||||
|
-- coordinates centered over the play area (left of the sidebar).
|
||||||
|
local function markPickerLayout()
|
||||||
|
local names = markNamesMatching(inputText)
|
||||||
|
local rowH, headerH, w = 26, 44, 320
|
||||||
|
local h = headerH + math.max(1, #names) * rowH + 12
|
||||||
|
local x = math.max(10, math.floor((sideBarBox.x - w) / 2))
|
||||||
|
local y = math.max(10, math.floor((height - h) / 2))
|
||||||
|
local rows = {}
|
||||||
|
for i, name in ipairs(names) do
|
||||||
|
rows[i] = { name = name, x = x + 8, y = y + headerH + (i - 1) * rowH, w = w - 16, h = rowH - 2 }
|
||||||
|
end
|
||||||
|
return { x = x, y = y, w = w, h = h, headerH = headerH, rows = rows, names = names }
|
||||||
|
end
|
||||||
|
|
||||||
|
function editorDrawMarkPicker()
|
||||||
|
if not (DEBUG and markPicker) then return end
|
||||||
|
local L = markPickerLayout()
|
||||||
|
local prevFont = love.graphics.getFont()
|
||||||
|
love.graphics.setFont(font)
|
||||||
|
love.graphics.setColor(0, 0, 0, 0.92)
|
||||||
|
love.graphics.rectangle("fill", L.x, L.y, L.w, L.h)
|
||||||
|
love.graphics.setColor(0.4, 1, 0.9, 1)
|
||||||
|
love.graphics.rectangle("line", L.x, L.y, L.w, L.h)
|
||||||
|
love.graphics.printf("SEND NPC TO (type to filter, Enter = top, Esc cancels)", L.x + 8, L.y + 6, L.w - 16, "left")
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.printf("> " .. inputText, L.x + 8, L.y + 24, L.w - 16, "left")
|
||||||
|
if #L.names == 0 then
|
||||||
|
love.graphics.setColor(0.7, 0.7, 0.7, 1)
|
||||||
|
love.graphics.printf("(no marks — place some with the MARK tool)", L.x + 8, L.y + L.headerH + 4, L.w - 16, "left")
|
||||||
|
love.graphics.setFont(prevFont)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local mx, my = love.mouse.getPosition()
|
||||||
|
for _, r in ipairs(L.rows) do
|
||||||
|
local hover = mx >= r.x and mx <= r.x + r.w and my >= r.y and my <= r.y + r.h
|
||||||
|
love.graphics.setColor(0.15, hover and 0.5 or 0.15, hover and 0.45 or 0.15, hover and 0.6 or 0.4)
|
||||||
|
love.graphics.rectangle("fill", r.x, r.y, r.w, r.h)
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.printf(r.name, r.x + 6, r.y + 5, r.w - 12, "left")
|
||||||
|
end
|
||||||
|
love.graphics.setFont(prevFont)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Screen-space click while the picker is open: choose a row, or dismiss on an
|
||||||
|
-- outside click. Returns true when the click was consumed.
|
||||||
|
function markPickerMousepressed(x, y, button)
|
||||||
|
if not markPicker then return false end
|
||||||
|
local L = markPickerLayout()
|
||||||
|
if button == 1 then
|
||||||
|
for _, r in ipairs(L.rows) do
|
||||||
|
if x >= r.x and x <= r.x + r.w and y >= r.y and y <= r.y + r.h then
|
||||||
|
sendToMark(r.name)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
closeMarkPicker()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The mark name modal, drawn over the play area in screen space. Independent of
|
||||||
|
-- the note editor; it just reads the shared `inputText` filled via editorText-
|
||||||
|
-- Handler. Target-red to match the mark icon.
|
||||||
|
function editorDrawMarkNameEditor()
|
||||||
|
if not (DEBUG and markNameMode) then return end
|
||||||
|
local w, h = 340, 92
|
||||||
|
local x = math.max(10, math.floor((sideBarBox.x - w) / 2))
|
||||||
|
local y = math.max(10, math.floor((height - h) / 2))
|
||||||
|
local prevFont = love.graphics.getFont()
|
||||||
|
love.graphics.setFont(font)
|
||||||
|
love.graphics.setColor(0, 0, 0, 0.92)
|
||||||
|
love.graphics.rectangle("fill", x, y, w, h)
|
||||||
|
love.graphics.setColor(1, 0.35, 0.25, 1)
|
||||||
|
love.graphics.rectangle("line", x, y, w, h)
|
||||||
|
local title = markNameMode == "edit" and "RENAME MARK" or "NAME THIS MARK"
|
||||||
|
love.graphics.printf(title .. " (Enter = ok, Esc cancels)", x + 10, y + 8, w - 20, "left")
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.rectangle("line", x + 10, y + 34, w - 20, 30)
|
||||||
|
love.graphics.printf("> " .. inputText, x + 16, y + 42, w - 32, "left")
|
||||||
|
love.graphics.setFont(prevFont)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Any click while naming commits the current text (a click is a blur = save).
|
||||||
|
function markNameEditorMousepressed(_, _, _)
|
||||||
|
if not markNameMode then return false end
|
||||||
|
commitMarkName()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
function selectQuickAsset(asset)
|
function selectQuickAsset(asset)
|
||||||
selectAsset(asset)
|
selectAsset(asset)
|
||||||
boxPaletteOpen = asset == "box"
|
boxPaletteOpen = asset == "box"
|
||||||
|
|
@ -1219,6 +1450,11 @@ local function openWorldRoom(cellX, cellY)
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorMouseHandler(x, y, button)
|
function editorMouseHandler(x, y, button)
|
||||||
|
-- The mark modals (destination picker, name entry) are drawn over the play
|
||||||
|
-- area in raw screen coords, so they get first refusal before any sidebar/grid
|
||||||
|
-- handling.
|
||||||
|
if markPicker and markPickerMousepressed(x, y, button) then return end
|
||||||
|
if markNameMode and markNameEditorMousepressed(x, y, button) then return end
|
||||||
if x >= sideBarBox.x and x < sideBarBox.x + sideBarBox.w then
|
if x >= sideBarBox.x and x < sideBarBox.x + sideBarBox.w then
|
||||||
-- map from real window coords into the sidebar's fixed virtual space
|
-- map from real window coords into the sidebar's fixed virtual space
|
||||||
-- (same transform used to scale the sidebar canvas up in love.draw)
|
-- (same transform used to scale the sidebar canvas up in love.draw)
|
||||||
|
|
@ -1282,7 +1518,7 @@ function editorMouseHandler(x, y, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- control cluster (only the buttons that live there)
|
-- control cluster (only the buttons that live there)
|
||||||
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire", "note" }) do
|
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire", "note", "mark" }) do
|
||||||
local b = uiButtons[name]
|
local b = uiButtons[name]
|
||||||
if b and b:clicIsInBox(x, y) then
|
if b and b:clicIsInBox(x, y) then
|
||||||
b:onClic(button)
|
b:onClic(button)
|
||||||
|
|
@ -1337,11 +1573,22 @@ function editorMouseHandler(x, y, button)
|
||||||
exitNoteMode()
|
exitNoteMode()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- Left-clicking an existing note opens it for editing instead of
|
-- Placing a new mark: drop a blank one, then open its name modal.
|
||||||
-- painting over it.
|
if cell and markPlacing then
|
||||||
|
markPlacing = false
|
||||||
|
local mark = currentRoom:attemptPlaceAtCell("mark", nil, cell, nil, "")
|
||||||
|
if mark then openMarkNameEditor(mark, "create") end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Left-clicking an existing note/mark opens it for editing instead of
|
||||||
|
-- painting over it; a Walker NPC opens the debug destination picker.
|
||||||
if cell then
|
if cell then
|
||||||
local note = currentRoom:noteAtCell(cell)
|
local note = currentRoom:noteAtCell(cell)
|
||||||
if note then beginNoteEdit(note); return end
|
if note then beginNoteEdit(note); return end
|
||||||
|
local mark = currentRoom:markAtCell(cell)
|
||||||
|
if mark then beginMarkEdit(mark); return end
|
||||||
|
local npc = currentRoom:npcAtCell(cell)
|
||||||
|
if npc and npc.sendTo then openMarkPicker(npc); return end
|
||||||
end
|
end
|
||||||
beginRoomBrush(x, y, button)
|
beginRoomBrush(x, y, button)
|
||||||
elseif editorView == "wire" then
|
elseif editorView == "wire" then
|
||||||
|
|
@ -1408,6 +1655,10 @@ function editorMouseReleased(_, _, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorTextHandler(text)
|
function editorTextHandler(text)
|
||||||
|
if markPicker or markNameMode then
|
||||||
|
inputText = text
|
||||||
|
return
|
||||||
|
end
|
||||||
if editorView == "asset" and activeField then
|
if editorView == "asset" and activeField then
|
||||||
inputText = text
|
inputText = text
|
||||||
editingAssetButtons[activeField]:editText(text)
|
editingAssetButtons[activeField]:editText(text)
|
||||||
|
|
@ -1429,6 +1680,12 @@ function editorTextHandler(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorCompleteResponse()
|
function editorCompleteResponse()
|
||||||
|
if markPicker then
|
||||||
|
local names = markNamesMatching(inputText)
|
||||||
|
if names[1] then sendToMark(names[1]) else closeMarkPicker() end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if markNameMode then commitMarkName(); return end
|
||||||
if editorView == "asset" and activeField then
|
if editorView == "asset" and activeField then
|
||||||
assetProperties[activeField] = inputText
|
assetProperties[activeField] = inputText
|
||||||
elseif editorView == "room" and activeField == "roomName" then
|
elseif editorView == "room" and activeField == "roomName" then
|
||||||
|
|
@ -1454,6 +1711,9 @@ function editorCompleteResponse()
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorCancelResponse()
|
function editorCancelResponse()
|
||||||
|
if markPicker then closeMarkPicker(); return true end
|
||||||
|
if markNameMode then cancelMarkName(); return true end
|
||||||
|
if markPlacing then markPlacing = false; editorStatus = "Mark placement cancelled."; return true end
|
||||||
if editorView == "wire" then discardWireRooms(); return true end
|
if editorView == "wire" then discardWireRooms(); return true end
|
||||||
if editorView ~= "world" or activeField ~= "cloneName" then return false end
|
if editorView ~= "world" or activeField ~= "cloneName" then return false end
|
||||||
activeField = nil
|
activeField = nil
|
||||||
|
|
@ -1554,7 +1814,7 @@ function editorDraw()
|
||||||
-- control cluster
|
-- control cluster
|
||||||
love.graphics.setColor(gColor.white:set())
|
love.graphics.setColor(gColor.white:set())
|
||||||
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center")
|
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center")
|
||||||
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire", "note" }) do
|
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire", "note", "mark" }) do
|
||||||
local button = uiButtons[name]
|
local button = uiButtons[name]
|
||||||
if button then
|
if button then
|
||||||
button:draw()
|
button:draw()
|
||||||
|
|
@ -1563,7 +1823,9 @@ function editorDraw()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.print("W", button.x + button.w - 7, button.y - 4)
|
love.graphics.print("W", button.x + button.w - 7, button.y - 4)
|
||||||
end
|
end
|
||||||
if (asset and selectedAsset == asset) or (name == "note" and noteMode ~= nil) then
|
if (asset and selectedAsset == asset)
|
||||||
|
or (name == "note" and noteMode ~= nil)
|
||||||
|
or (name == "mark" and (markPlacing or markNameMode ~= nil)) then
|
||||||
love.graphics.setColor(1, 0.85, 0.2, 1)
|
love.graphics.setColor(1, 0.85, 0.2, 1)
|
||||||
love.graphics.setLineWidth(2)
|
love.graphics.setLineWidth(2)
|
||||||
love.graphics.rectangle("line", button.x - 1, button.y - 1, button.w + 2, button.h + 2)
|
love.graphics.rectangle("line", button.x - 1, button.y - 1, button.w + 2, button.h + 2)
|
||||||
|
|
|
||||||
21
main.lua
21
main.lua
|
|
@ -100,6 +100,7 @@ function love.update(dt)
|
||||||
if repeatedMove and not Dialogue.isOpen() and not MapManager:isOpen() then gameWorld:movePlayer(currentRoom, repeatedMove) end
|
if repeatedMove and not Dialogue.isOpen() and not MapManager:isOpen() then gameWorld:movePlayer(currentRoom, repeatedMove) end
|
||||||
end
|
end
|
||||||
currentRoom:update(dt)
|
currentRoom:update(dt)
|
||||||
|
if gameWorld then gameWorld:update(dt) end
|
||||||
MapManager:record(currentRoom)
|
MapManager:record(currentRoom)
|
||||||
|
|
||||||
-- if love.keyboard.isDown("w") then worldSha = worldSha + 1 end
|
-- if love.keyboard.isDown("w") then worldSha = worldSha + 1 end
|
||||||
|
|
@ -174,6 +175,8 @@ function love.draw()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
if DEBUG then love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y) end
|
if DEBUG then love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y) end
|
||||||
if DEBUG then editorDrawNoteHover() end
|
if DEBUG then editorDrawNoteHover() end
|
||||||
|
if DEBUG then editorDrawMarkPicker() end
|
||||||
|
if DEBUG then editorDrawMarkNameEditor() end
|
||||||
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
||||||
PerfProfiler:draw()
|
PerfProfiler:draw()
|
||||||
Dialogue.draw()
|
Dialogue.draw()
|
||||||
|
|
@ -229,6 +232,24 @@ function love.keypressed(key)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The mark modals (destination picker, name entry) own the keyboard while open:
|
||||||
|
-- characters filter/type via love.textinput, Enter confirms, Esc cancels.
|
||||||
|
if DEBUG and (markPickerActive() or markNameEditorActive()) then
|
||||||
|
if key == "escape" then
|
||||||
|
editorCancelResponse()
|
||||||
|
elseif key == "return" then
|
||||||
|
response = ""
|
||||||
|
editorCompleteResponse()
|
||||||
|
elseif key == "backspace" then
|
||||||
|
local byteoffset = utf8.offset(response, -1)
|
||||||
|
if byteoffset then
|
||||||
|
response = string.sub(response, 1, byteoffset - 1)
|
||||||
|
editorTextHandler(response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Tab toggles the editor's note overlay (DEBUG-only; default on).
|
-- Tab toggles the editor's note overlay (DEBUG-only; default on).
|
||||||
if DEBUG and key == "tab" then
|
if DEBUG and key == "tab" then
|
||||||
NOTES_VISIBLE = not NOTES_VISIBLE
|
NOTES_VISIBLE = not NOTES_VISIBLE
|
||||||
|
|
|
||||||
78
mark.lua
Normal file
78
mark.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
require "entity"
|
||||||
|
|
||||||
|
-- A named destination on the grid. NPCs are sent to a Mark by name (see
|
||||||
|
-- Npc:sendTo / World:markByName), so the name is the whole point of the object.
|
||||||
|
--
|
||||||
|
-- A Mark is a *debug non-collidable*: an editor-only annotation that everything
|
||||||
|
-- passes through, shown only while the debug overlay is on. It is NOT a Note and
|
||||||
|
-- shares no code with one — the two just happen to be siblings in that category.
|
||||||
|
-- Its icon is a target, drawn procedurally so it needs no art asset.
|
||||||
|
Mark = class("Mark", Entity)
|
||||||
|
|
||||||
|
-- Entity only ever calls :getWidth/:getHeight on the sizing sprite (Mark:draw
|
||||||
|
-- paints a target instead of blitting it), so a lightweight stub is enough and
|
||||||
|
-- keeps Mark free of any borrowed artwork.
|
||||||
|
local sizeStub = { getWidth = function() return 16 end, getHeight = function() return 16 end }
|
||||||
|
|
||||||
|
-- The target is pixel art: a 16x16 bullseye (two rings + a centre dot) baked once
|
||||||
|
-- into a list of on-pixels, drawn as blocky rectangles so it stays crisp at the
|
||||||
|
-- game's nearest-neighbour scale instead of a smooth vector circle.
|
||||||
|
local GRID = 16
|
||||||
|
local targetPixels = {}
|
||||||
|
do
|
||||||
|
local c = (GRID - 1) / 2 -- 7.5, the sub-pixel centre
|
||||||
|
for row = 0, GRID - 1 do
|
||||||
|
for col = 0, GRID - 1 do
|
||||||
|
local dist = math.sqrt((col - c) ^ 2 + (row - c) ^ 2)
|
||||||
|
local on = math.abs(dist - 6.5) < 0.7 -- outer ring
|
||||||
|
or math.abs(dist - 3.5) < 0.7 -- inner ring
|
||||||
|
or dist < 1.3 -- centre dot
|
||||||
|
if on then targetPixels[#targetPixels + 1] = { col, row } end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Mark:initialize(x, y, name)
|
||||||
|
self.markName = name or ""
|
||||||
|
self.saveClass = "mark"
|
||||||
|
-- programmatic marks are created from code on every room load, so they are
|
||||||
|
-- skipped by Room:serialize (see there) to avoid duplicating on disk.
|
||||||
|
self.programmatic = false
|
||||||
|
Entity.initialize(self, {
|
||||||
|
x = x, y = y,
|
||||||
|
color = "white", -- representative only; Mark:draw is channel-agnostic
|
||||||
|
collision = "none",
|
||||||
|
sprite = sizeStub,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Drawn once (not per-channel) from the room's debug overlay pass, so it takes no
|
||||||
|
-- channel argument. A faint pixel-art red target with its name beside it, kept at
|
||||||
|
-- the same low opacity as a Note so it reads as an annotation, not a game object.
|
||||||
|
function Mark:draw()
|
||||||
|
local p = self:getDrawPos()
|
||||||
|
love.graphics.setColor(1, 0.3, 0.25, 0.85)
|
||||||
|
for _, px in ipairs(targetPixels) do
|
||||||
|
love.graphics.rectangle("fill", p.x + px[1] * drawScale, p.y + px[2] * drawScale, drawScale, drawScale)
|
||||||
|
end
|
||||||
|
if self.markName ~= "" then
|
||||||
|
love.graphics.setColor(1, 0.5, 0.42, 0.85)
|
||||||
|
love.graphics.print(self.markName, p.x, p.y - 12)
|
||||||
|
end
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The same pixel-art target baked into a 16x16 Image, for the editor toolbar
|
||||||
|
-- button (full opacity there — it's a control, not a faint overlay). Cached, and
|
||||||
|
-- built lazily so it only touches love.graphics once the editor needs it.
|
||||||
|
function Mark.static.icon()
|
||||||
|
if Mark.static._icon then return Mark.static._icon end
|
||||||
|
local data = love.image.newImageData(GRID, GRID)
|
||||||
|
for _, px in ipairs(targetPixels) do
|
||||||
|
data:setPixel(px[1], px[2], 1, 0.3, 0.25, 1)
|
||||||
|
end
|
||||||
|
local img = love.graphics.newImage(data)
|
||||||
|
img:setFilter("nearest", "nearest")
|
||||||
|
Mark.static._icon = img
|
||||||
|
return img
|
||||||
|
end
|
||||||
214
npc.lua
214
npc.lua
|
|
@ -1,11 +1,222 @@
|
||||||
require "entity"
|
require "entity"
|
||||||
require "dialogable"
|
require "dialogable"
|
||||||
|
|
||||||
|
local Pathfinding = require "pathfinding"
|
||||||
|
|
||||||
|
local channels = {"red", "green", "blue"}
|
||||||
|
|
||||||
|
-- An NpcCopy is one colour's presence of an NPC inside one room, in that room's
|
||||||
|
-- local coordinates — the exact analogue of room.player[color]. The room it lives
|
||||||
|
-- in draws it (Room:draw) and stamps it into collision (World:rebuildCollision),
|
||||||
|
-- so a copy obeys that room's colour view and collides there like any object.
|
||||||
|
-- An NPC has three copies, and while walking they may live in up to three
|
||||||
|
-- DIFFERENT rooms at once, exactly like the player's copies.
|
||||||
|
local NpcCopy = {}
|
||||||
|
NpcCopy.__index = NpcCopy
|
||||||
|
|
||||||
|
local function makeCopy(npc, color, room, x, y)
|
||||||
|
return setmetatable({ npc = npc, color = color, room = room, x = x, y = y }, NpcCopy)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The copy's cell in world coordinates (for pathfinding / collision).
|
||||||
|
function NpcCopy:worldCell()
|
||||||
|
if gameWorld then
|
||||||
|
local wc = gameWorld:worldCell(self.room, { x = self.x, y = self.y })
|
||||||
|
if wc then return wc end
|
||||||
|
end
|
||||||
|
return { x = self.x, y = self.y }
|
||||||
|
end
|
||||||
|
|
||||||
|
function NpcCopy:occupiesCell(cell)
|
||||||
|
for _, s in ipairs(self.npc:getCellShape()) do
|
||||||
|
if self.x + s.x - 1 == cell.x and self.y + s.y - 1 == cell.y then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Drawn by its room's per-channel pass, in that room's local coordinates — the
|
||||||
|
-- same call shape as every other object. `channel` always equals self.color.
|
||||||
|
function NpcCopy:draw(channel)
|
||||||
|
local sprite = self.npc.sprites[channel]
|
||||||
|
if not sprite then return end
|
||||||
|
local npc = self.npc
|
||||||
|
love.graphics.setColor(gColor[channel]:set())
|
||||||
|
local px = (self.x - 1) * width / gridWidth + npc:sha()
|
||||||
|
local py = (self.y - 1) * height / gridHeight + npc:sha()
|
||||||
|
love.graphics.draw(sprite, px + npc.drawOffset.x * drawScale, py + npc.drawOffset.y * drawScale, 0, drawScale, drawScale)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Walker: gives an NPC the player's RGB model. The NPC entity itself is only a
|
||||||
|
-- controller (kept in room.npcs for identity / dialogue / serialisation); its
|
||||||
|
-- visible, collidable presence is its three NpcCopy residents, which each belong
|
||||||
|
-- to whatever room they currently stand in and run their own A* toward a Mark.
|
||||||
|
Walker = {}
|
||||||
|
|
||||||
|
function Walker:initWalker()
|
||||||
|
self.paths = { red = nil, green = nil, blue = nil }
|
||||||
|
self.goal = nil
|
||||||
|
self.walking = false
|
||||||
|
self.moveTimer = 0
|
||||||
|
self.moveInterval = 0.15
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create the three copies at this NPC's home cell in `room` and register each
|
||||||
|
-- into that room's per-colour copy list. Called from Room:registerNpc.
|
||||||
|
function Walker:initCopies(room)
|
||||||
|
self.copies = {}
|
||||||
|
for _, color in ipairs(channels) do
|
||||||
|
local copy = makeCopy(self, color, room, self.x, self.y)
|
||||||
|
self.copies[color] = copy
|
||||||
|
table.insert(room.npcCopies[color], copy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Walker:isWalking() return self.walking end
|
||||||
|
|
||||||
|
-- Move one colour copy into `room` at local (x, y), transferring it between the
|
||||||
|
-- rooms' per-colour copy lists — the NPC equivalent of the player crossing a
|
||||||
|
-- room seam.
|
||||||
|
function Walker:placeCopy(copy, room, x, y)
|
||||||
|
if copy.room ~= room then
|
||||||
|
local from = copy.room.npcCopies[copy.color]
|
||||||
|
for i = #from, 1, -1 do if from[i] == copy then table.remove(from, i) end end
|
||||||
|
table.insert(room.npcCopies[copy.color], copy)
|
||||||
|
copy.room = room
|
||||||
|
end
|
||||||
|
copy.x, copy.y = x, y
|
||||||
|
end
|
||||||
|
|
||||||
|
local function npcLog(...)
|
||||||
|
if DEBUG then print("[npc]", ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Send this NPC to a named Mark. Each colour plots its own route on its own
|
||||||
|
-- collision layer, so the copies may take different paths and cross into
|
||||||
|
-- different rooms.
|
||||||
|
function Walker:sendTo(markName)
|
||||||
|
if not gameWorld then npcLog("sendTo abort: no gameWorld"); return end
|
||||||
|
if not self.copies then npcLog("sendTo abort: no copies (not registered?)"); return end
|
||||||
|
local mark, markRoom = gameWorld:markByName(markName)
|
||||||
|
if not mark then npcLog("sendTo abort: no mark named '" .. tostring(markName) .. "'"); return end
|
||||||
|
local goal = gameWorld:worldCell(markRoom, { x = mark.x, y = mark.y })
|
||||||
|
if not goal then npcLog("sendTo abort: mark's room '" .. tostring(markRoom.name) .. "' is not placed on the world map"); return end
|
||||||
|
|
||||||
|
self.goal = goal
|
||||||
|
self.walking = true
|
||||||
|
-- Route is computed against the live collision matrix, so rebuild first (the
|
||||||
|
-- player and other NPCs may have moved since the last tick).
|
||||||
|
if gameWorld.rebuildCollision then gameWorld:rebuildCollision() end
|
||||||
|
local shape = self:getCellShape()
|
||||||
|
for _, color in ipairs(channels) do
|
||||||
|
local start = self.copies[color]:worldCell()
|
||||||
|
self.paths[color] = Pathfinding.route(start, goal, color, self, shape)
|
||||||
|
npcLog(("sendTo %s: start=%d,%d goal=%d,%d -> %s"):format(color, start.x, start.y, goal.x, goal.y,
|
||||||
|
self.paths[color] and ("route len " .. #self.paths[color])
|
||||||
|
or ("NO ROUTE (goal blocked by " .. (Pathfinding.blockReason(goal, color, self, shape) or "a wall between here and there") .. ")")))
|
||||||
|
end
|
||||||
|
self.moveTimer = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Advance one grid step per colour when the timer elapses. Returns true if any
|
||||||
|
-- copy moved (so the caller can rebuild collision). Driven by World:update.
|
||||||
|
function Walker:step(dt)
|
||||||
|
if not self.walking then return false end
|
||||||
|
self.moveTimer = self.moveTimer + dt
|
||||||
|
if self.moveTimer < self.moveInterval then return false end
|
||||||
|
self.moveTimer = self.moveTimer - self.moveInterval
|
||||||
|
|
||||||
|
local shape = self:getCellShape()
|
||||||
|
local moved = false
|
||||||
|
for _, color in ipairs(channels) do
|
||||||
|
local copy = self.copies[color]
|
||||||
|
local start = copy:worldCell()
|
||||||
|
if start.x ~= self.goal.x or start.y ~= self.goal.y then
|
||||||
|
-- (Re)plan whenever we have no live path — so a channel blocked this tick
|
||||||
|
-- keeps trying and resumes the moment the obstacle (a box, the player)
|
||||||
|
-- clears, instead of freezing forever.
|
||||||
|
local path = self.paths[color]
|
||||||
|
if not path or #path == 0 then
|
||||||
|
path = Pathfinding.route(start, self.goal, color, self, shape)
|
||||||
|
self.paths[color] = path
|
||||||
|
end
|
||||||
|
if path and #path > 0 then
|
||||||
|
local nextCell = path[1]
|
||||||
|
if Pathfinding.walkable(nextCell, color, self, shape) then
|
||||||
|
local room, localCell = gameWorld:roomCell(nextCell)
|
||||||
|
if room then
|
||||||
|
self:placeCopy(copy, room, localCell.x, localCell.y)
|
||||||
|
table.remove(path, 1)
|
||||||
|
moved = true
|
||||||
|
else
|
||||||
|
self.paths[color] = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- blocked this tick: drop the stale path, replan next tick
|
||||||
|
if not self._blocked then self._blocked = {} end
|
||||||
|
local reason = Pathfinding.blockReason(nextCell, color, self, shape)
|
||||||
|
if self._blocked[color] ~= reason then
|
||||||
|
npcLog(("%s blocked at %d,%d by %s (waiting)"):format(color, nextCell.x, nextCell.y, reason or "?"))
|
||||||
|
self._blocked[color] = reason
|
||||||
|
end
|
||||||
|
self.paths[color] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self:allArrived() then self:arrive() end
|
||||||
|
return moved
|
||||||
|
end
|
||||||
|
|
||||||
|
function Walker:allArrived()
|
||||||
|
if not self.goal then return false end
|
||||||
|
for _, color in ipairs(channels) do
|
||||||
|
local wc = self.copies[color]:worldCell()
|
||||||
|
if wc.x ~= self.goal.x or wc.y ~= self.goal.y then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Every copy reached the destination. Stop, and move the controller (self, the
|
||||||
|
-- entity kept in room.npcs) into the room the copies ended up in, so a save
|
||||||
|
-- writes it in the right level and the editor finds it there.
|
||||||
|
function Walker:arrive()
|
||||||
|
self.walking = false
|
||||||
|
self.paths = { red = nil, green = nil, blue = nil }
|
||||||
|
self.goal = nil
|
||||||
|
local lead = self.copies[channels[1]]
|
||||||
|
if lead and lead.room then
|
||||||
|
if lead.room ~= self.room and self.room then
|
||||||
|
for i = #self.room.npcs, 1, -1 do if self.room.npcs[i] == self then table.remove(self.room.npcs, i) end end
|
||||||
|
table.insert(lead.room.npcs, self)
|
||||||
|
end
|
||||||
|
self.room = lead.room
|
||||||
|
self.x, self.y = lead.x, lead.y
|
||||||
|
end
|
||||||
|
if gameWorld and gameWorld.rebuildCollision then gameWorld:rebuildCollision() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove this NPC entirely: its copies from every room's copy lists and its
|
||||||
|
-- controller from its room's npc registry. Used by the editor's right-click.
|
||||||
|
function Walker:destroy()
|
||||||
|
if self.copies then
|
||||||
|
for _, color in ipairs(channels) do
|
||||||
|
local copy = self.copies[color]
|
||||||
|
local list = copy.room.npcCopies[color]
|
||||||
|
for i = #list, 1, -1 do if list[i] == copy then table.remove(list, i) end end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.room then
|
||||||
|
for i = #self.room.npcs, 1, -1 do if self.room.npcs[i] == self then table.remove(self.room.npcs, i) end end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- A dialogable placed in the world. Signs, npcs and artifacts will each be a
|
-- A dialogable placed in the world. Signs, npcs and artifacts will each be a
|
||||||
-- small Entity subclass that includes Dialogable and names its own book; Sign is
|
-- small Entity subclass that includes Dialogable and names its own book; Sign is
|
||||||
-- the first. It just picks art + a story — the talking lives in the mixin.
|
-- the first. It just picks art + a story — the talking lives in the mixin.
|
||||||
Sign = class("Sign", Entity)
|
Sign = class("Sign", Entity)
|
||||||
Sign:include(Dialogable)
|
Sign:include(Dialogable)
|
||||||
|
Sign:include(Walker)
|
||||||
|
|
||||||
function Sign:initialize(x, y)
|
function Sign:initialize(x, y)
|
||||||
self.book = startSignBook
|
self.book = startSignBook
|
||||||
|
|
@ -19,10 +230,12 @@ function Sign:initialize(x, y)
|
||||||
blue = loadSprite("art/npc/start_sign_b.png"),
|
blue = loadSprite("art/npc/start_sign_b.png"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
self:initWalker()
|
||||||
end
|
end
|
||||||
|
|
||||||
Jorge = class("Jorge", Entity)
|
Jorge = class("Jorge", Entity)
|
||||||
Jorge:include(Dialogable)
|
Jorge:include(Dialogable)
|
||||||
|
Jorge:include(Walker)
|
||||||
|
|
||||||
function Jorge:initialize(x, y)
|
function Jorge:initialize(x, y)
|
||||||
self.book = jorgeBook
|
self.book = jorgeBook
|
||||||
|
|
@ -37,4 +250,5 @@ function Jorge:initialize(x, y)
|
||||||
blue = loadSprite("art/npc/jorge_b.png"),
|
blue = loadSprite("art/npc/jorge_b.png"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
self:initWalker()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
125
pathfinding.lua
Normal file
125
pathfinding.lua
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
-- Grid A* over the world collidable grid, in WORLD coordinates, one color at a
|
||||||
|
-- time. NPC color copies each call this on their own channel (see Npc:sendTo),
|
||||||
|
-- so a red copy routes around only red obstacles — "red can only be hit by red".
|
||||||
|
--
|
||||||
|
-- The world grid is continuous across rooms (World:worldCell / World:roomCell),
|
||||||
|
-- so a route can cross a room seam without any special casing.
|
||||||
|
local Pathfinding = {}
|
||||||
|
|
||||||
|
local directions = {
|
||||||
|
{x = 1, y = 0}, {x = -1, y = 0}, {x = 0, y = 1}, {x = 0, y = -1},
|
||||||
|
}
|
||||||
|
|
||||||
|
local singleCell = {{x = 1, y = 1}}
|
||||||
|
|
||||||
|
local function key(cell) return cell.x .. ":" .. cell.y end
|
||||||
|
local function heuristic(a, b) return math.abs(a.x - b.x) + math.abs(a.y - b.y) end
|
||||||
|
|
||||||
|
-- Is one world cell free for `color`? Free means on the map and its collidable
|
||||||
|
-- slot is empty, holds the moving entity itself, or holds a passable object
|
||||||
|
-- (marks, notes). Everything else — walls, boxes, doors, other NPC copies on this
|
||||||
|
-- channel — blocks.
|
||||||
|
local function cellFree(cell, color, ignoreEntity)
|
||||||
|
if not gameWorld then return false end
|
||||||
|
if not gameWorld:roomCell(cell) then return false end
|
||||||
|
-- Player copies are solid to a walker (it can't push them), so they block.
|
||||||
|
local players = gameWorld.playerCells and gameWorld.playerCells[color]
|
||||||
|
if players and players[cell.x .. ":" .. cell.y] then return false end
|
||||||
|
local row = gameWorld.collidableMatrices[color] and gameWorld.collidableMatrices[color][cell.y]
|
||||||
|
local entity = row and row[cell.x]
|
||||||
|
if not entity then return true end
|
||||||
|
if entity == ignoreEntity then return true end
|
||||||
|
if entity.canPassOver and entity:canPassOver() then return true end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- An anchor cell is walkable only when EVERY cell of the entity's footprint is
|
||||||
|
-- free there. `shape` is an entity cellShape (1-based offsets); a nil shape means
|
||||||
|
-- a single 1x1 cell. This is what stops a 2x2 NPC squeezing through a 1-wide gap
|
||||||
|
-- as if it were its top-left cell alone.
|
||||||
|
local function walkable(anchor, color, ignoreEntity, shape)
|
||||||
|
shape = shape or singleCell
|
||||||
|
for _, offset in ipairs(shape) do
|
||||||
|
local cell = { x = anchor.x + offset.x - 1, y = anchor.y + offset.y - 1 }
|
||||||
|
if not cellFree(cell, color, ignoreEntity) then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
Pathfinding.walkable = walkable
|
||||||
|
|
||||||
|
-- Human-readable reason an anchor is not walkable (for diagnostics), or nil if it
|
||||||
|
-- is free. Reports the first offending footprint cell.
|
||||||
|
function Pathfinding.blockReason(anchor, color, ignoreEntity, shape)
|
||||||
|
shape = shape or singleCell
|
||||||
|
if not gameWorld then return "no gameWorld" end
|
||||||
|
for _, offset in ipairs(shape) do
|
||||||
|
local cell = { x = anchor.x + offset.x - 1, y = anchor.y + offset.y - 1 }
|
||||||
|
if not gameWorld:roomCell(cell) then return ("offmap@%d,%d"):format(cell.x, cell.y) end
|
||||||
|
local players = gameWorld.playerCells and gameWorld.playerCells[color]
|
||||||
|
if players and players[cell.x .. ":" .. cell.y] then return ("player@%d,%d"):format(cell.x, cell.y) end
|
||||||
|
local row = gameWorld.collidableMatrices[color] and gameWorld.collidableMatrices[color][cell.y]
|
||||||
|
local e = row and row[cell.x]
|
||||||
|
if e and e ~= ignoreEntity and not (e.canPassOver and e:canPassOver()) then
|
||||||
|
local what = e.getClass and e:getClass() or "obstacle"
|
||||||
|
return ("%s@%d,%d"):format(what, cell.x, cell.y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a list of anchor world cells from just-after `start` through `goal`
|
||||||
|
-- inclusive, or nil if no route exists. `shape` is the entity's cellShape, so
|
||||||
|
-- the whole footprint must fit at every step and at the goal.
|
||||||
|
function Pathfinding.route(start, goal, color, ignoreEntity, shape)
|
||||||
|
if not gameWorld then return nil end
|
||||||
|
if start.x == goal.x and start.y == goal.y then return {} end
|
||||||
|
if not walkable(goal, color, ignoreEntity, shape) then return nil end
|
||||||
|
|
||||||
|
local open = { start } -- frontier as a plain list
|
||||||
|
local cameFrom = {}
|
||||||
|
local gScore = { [key(start)] = 0 }
|
||||||
|
local inOpen = { [key(start)] = true }
|
||||||
|
|
||||||
|
while #open > 0 do
|
||||||
|
-- pick the open node with the lowest f = g + h (small grids: linear scan)
|
||||||
|
local bestIndex, best = 1, open[1]
|
||||||
|
local bestF = gScore[key(best)] + heuristic(best, goal)
|
||||||
|
for i = 2, #open do
|
||||||
|
local node = open[i]
|
||||||
|
local f = gScore[key(node)] + heuristic(node, goal)
|
||||||
|
if f < bestF then bestIndex, best, bestF = i, node, f end
|
||||||
|
end
|
||||||
|
table.remove(open, bestIndex)
|
||||||
|
inOpen[key(best)] = nil
|
||||||
|
|
||||||
|
if best.x == goal.x and best.y == goal.y then
|
||||||
|
local path, node = {}, best
|
||||||
|
while cameFrom[key(node)] do
|
||||||
|
table.insert(path, 1, node)
|
||||||
|
node = cameFrom[key(node)]
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, dir in ipairs(directions) do
|
||||||
|
local neighbor = { x = best.x + dir.x, y = best.y + dir.y }
|
||||||
|
if walkable(neighbor, color, ignoreEntity, shape) then
|
||||||
|
local tentative = gScore[key(best)] + 1
|
||||||
|
local nk = key(neighbor)
|
||||||
|
if not gScore[nk] or tentative < gScore[nk] then
|
||||||
|
cameFrom[nk] = best
|
||||||
|
gScore[nk] = tentative
|
||||||
|
if not inOpen[nk] then
|
||||||
|
table.insert(open, neighbor)
|
||||||
|
inOpen[nk] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return Pathfinding
|
||||||
107
room.lua
107
room.lua
|
|
@ -18,6 +18,7 @@ require "generic_spiky"
|
||||||
require "generic_unmoveable"
|
require "generic_unmoveable"
|
||||||
require "generic_player_blocker"
|
require "generic_player_blocker"
|
||||||
require "note"
|
require "note"
|
||||||
|
require "mark"
|
||||||
|
|
||||||
local Movement = require "movement"
|
local Movement = require "movement"
|
||||||
local Wire = require "wire"
|
local Wire = require "wire"
|
||||||
|
|
@ -32,7 +33,11 @@ function Room:initialize(t)
|
||||||
self.collideables = { red = {}, green = {}, blue = {} }
|
self.collideables = { red = {}, green = {}, blue = {} }
|
||||||
self.playerBlockers = { red = {}, green = {}, blue = {} }
|
self.playerBlockers = { red = {}, green = {}, blue = {} }
|
||||||
self.npcs = {}
|
self.npcs = {}
|
||||||
|
-- Per-colour NPC copy residents, mirroring self.player: each room draws and
|
||||||
|
-- collides the copies currently standing in it (see npc.lua / Walker).
|
||||||
|
self.npcCopies = { red = {}, green = {}, blue = {} }
|
||||||
self.notes = {}
|
self.notes = {}
|
||||||
|
self.marks = {}
|
||||||
self.activeColors = {red = false, green = false, blue = false}
|
self.activeColors = {red = false, green = false, blue = false}
|
||||||
self.colorsPlayerHas = {}
|
self.colorsPlayerHas = {}
|
||||||
|
|
||||||
|
|
@ -172,16 +177,21 @@ function Room:draw(drawChannels)
|
||||||
for _, blocker in pairs(self.playerBlockers[channel]) do blocker:draw(channel) end
|
for _, blocker in pairs(self.playerBlockers[channel]) do blocker:draw(channel) end
|
||||||
for _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
|
for _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
|
||||||
for _, switch in ipairs(self.wiredSwitches) do switch:draw(channel) end
|
for _, switch in ipairs(self.wiredSwitches) do switch:draw(channel) end
|
||||||
for _, npc in ipairs(self.npcs) do npc:draw(channel) end
|
-- NPC copies render exactly like player copies: each room draws only the
|
||||||
|
-- copies currently standing in it, on this channel. A copy that has walked
|
||||||
|
-- into a red-only room shows only its red layer there.
|
||||||
|
for _, copy in ipairs(self.npcCopies[channel]) do copy:draw(channel) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Notes are an editor-only overlay: shown only while the editor build is
|
-- Debug non-collidables (notes, marks, …) are editor-only overlays: shown only
|
||||||
-- running (DEBUG) and the NOTES_VISIBLE toggle is on. Drawn once (not per
|
-- under DEBUG while the overlay toggle is on. Drawn once (not per channel) in
|
||||||
-- channel) in alpha so the faint icon reads as itself instead of screen-adding.
|
-- alpha so each icon reads as itself instead of screen-adding. Each kind draws
|
||||||
if DEBUG and NOTES_VISIBLE and #self.notes > 0 then
|
-- itself in its own pass; they share the toggle, not any code.
|
||||||
|
if DEBUG and NOTES_VISIBLE then
|
||||||
local mode, alphaMode = love.graphics.getBlendMode()
|
local mode, alphaMode = love.graphics.getBlendMode()
|
||||||
love.graphics.setBlendMode("alpha")
|
love.graphics.setBlendMode("alpha")
|
||||||
for _, note in ipairs(self.notes) do note:draw() end
|
for _, note in ipairs(self.notes) do note:draw() end
|
||||||
|
for _, mark in ipairs(self.marks) do mark:draw() end
|
||||||
love.graphics.setBlendMode(mode, alphaMode)
|
love.graphics.setBlendMode(mode, alphaMode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -756,6 +766,11 @@ function Room:createEntity(class, color, x, y, data)
|
||||||
self:registerNote(entity)
|
self:registerNote(entity)
|
||||||
return entity
|
return entity
|
||||||
end
|
end
|
||||||
|
if class == "mark" then
|
||||||
|
local entity = Mark:new(x, y, data.name or data.markName)
|
||||||
|
self:registerMark(entity)
|
||||||
|
return entity
|
||||||
|
end
|
||||||
|
|
||||||
local props = globalAssetProperties and globalAssetProperties[class]
|
local props = globalAssetProperties and globalAssetProperties[class]
|
||||||
local behavior = props and props.class or "immoveable"
|
local behavior = props and props.class or "immoveable"
|
||||||
|
|
@ -868,9 +883,12 @@ function Room:registerPlayer(entityPointer, color)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:registerNpc(entity)
|
function Room:registerNpc(entity)
|
||||||
-- npcs are dialogable entities: they carry .x/.y like any Entity, so
|
-- The NPC entity is a controller kept here for identity / dialogue / serialize.
|
||||||
-- serialize's npc loop still round-trips them, and they get stamped into the
|
-- Its visible, collidable presence is three per-colour copies (Walker) that
|
||||||
-- collision matrix (see createCollideablesMatrix) so a bump is a rejected move.
|
-- each belong to whatever room they stand in; initCopies seeds them here at the
|
||||||
|
-- NPC's home cell, registered into this room's npcCopies lists.
|
||||||
|
entity.room = self
|
||||||
|
if entity.initCopies then entity:initCopies(self) end
|
||||||
table.insert(self.npcs, entity)
|
table.insert(self.npcs, entity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -890,6 +908,44 @@ function Room:noteAtCell(cell)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Room:registerMark(entity)
|
||||||
|
-- Marks, like notes, are passable annotations kept in their own registry so
|
||||||
|
-- serialize round-trips them and movement never stamps them into a matrix.
|
||||||
|
-- The difference is their name is meaningful: NPCs are sent here by it.
|
||||||
|
table.insert(self.marks, entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The mark (if any) under a given grid cell — used by the editor's hover/click.
|
||||||
|
function Room:markAtCell(cell)
|
||||||
|
if not cell then return nil end
|
||||||
|
for _, mark in ipairs(self.marks) do
|
||||||
|
if mark:occupiesCell(cell) then return mark end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The npc (if any) with a copy under a given grid cell in this room — used by the
|
||||||
|
-- editor's debug destination picker (click an NPC to send it to a mark). Checks
|
||||||
|
-- the visible copies, so clicking a walked-away copy still finds its controller.
|
||||||
|
function Room:npcAtCell(cell)
|
||||||
|
if not cell then return nil end
|
||||||
|
for _, color in ipairs({"red", "green", "blue"}) do
|
||||||
|
for _, copy in ipairs(self.npcCopies[color]) do
|
||||||
|
if copy:occupiesCell(cell) then return copy.npc end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Code-side mark creation: register a named destination through the same path
|
||||||
|
-- authored marks use, so it collides/renders in debug identically. Flagged
|
||||||
|
-- `programmatic` so Room:serialize skips it — code recreates it on every load.
|
||||||
|
function Room:addMark(name, x, y)
|
||||||
|
local entity = self:createEntity("mark", nil, x, y, { name = name })
|
||||||
|
if entity then entity.programmatic = true end
|
||||||
|
return entity
|
||||||
|
end
|
||||||
|
|
||||||
function Room:getCellFromMousePos(mouseX, mouseY)
|
function Room:getCellFromMousePos(mouseX, mouseY)
|
||||||
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self and
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self and
|
||||||
not gameWorld.drawingRoomNeighborhood then
|
not gameWorld.drawingRoomNeighborhood then
|
||||||
|
|
@ -960,17 +1016,21 @@ function Room:attemptDeleteAtCell(cell, color)
|
||||||
table.remove(self.wiredSwitches, index)
|
table.remove(self.wiredSwitches, index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- NPCs are stamped into every collision layer, but are kept in their own
|
-- An NPC is a controller plus per-colour copies spread across rooms; deleting
|
||||||
-- registry rather than in self.collideables. Remove them directly so a
|
-- one means tearing down all of them (Walker:destroy), regardless of the
|
||||||
-- right-click works no matter which editor colour is currently selected.
|
-- currently selected editor colour.
|
||||||
for index = #self.npcs, 1, -1 do
|
local npc = self:npcAtCell(cell)
|
||||||
if self.npcs[index]:occupiesCell(cell) then table.remove(self.npcs, index) end
|
if npc and npc.destroy then npc:destroy() end
|
||||||
end
|
|
||||||
-- Notes are non-collidable and colourless, so a right-click removes them
|
-- Notes are non-collidable and colourless, so a right-click removes them
|
||||||
-- regardless of the currently selected editor colour.
|
-- regardless of the currently selected editor colour.
|
||||||
for index = #self.notes, 1, -1 do
|
for index = #self.notes, 1, -1 do
|
||||||
if self.notes[index]:occupiesCell(cell) then table.remove(self.notes, index) end
|
if self.notes[index]:occupiesCell(cell) then table.remove(self.notes, index) end
|
||||||
end
|
end
|
||||||
|
-- Marks behave like notes for deletion: colourless, passable, removed on any
|
||||||
|
-- selected colour.
|
||||||
|
for index = #self.marks, 1, -1 do
|
||||||
|
if self.marks[index]:occupiesCell(cell) then table.remove(self.marks, index) end
|
||||||
|
end
|
||||||
self:tick()
|
self:tick()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -991,6 +1051,17 @@ function Room:attemptPlaceAtCell(name, props, cell, color, text)
|
||||||
self:tick()
|
self:tick()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
-- Marks are colourless, single-instance named destinations. One mark per cell:
|
||||||
|
-- drop any mark already here before adding the new one. `text` carries the name.
|
||||||
|
if name == "mark" then
|
||||||
|
if not cell then return end
|
||||||
|
for index = #self.marks, 1, -1 do
|
||||||
|
if self.marks[index]:occupiesCell(cell) then table.remove(self.marks, index) end
|
||||||
|
end
|
||||||
|
local mark = self:createEntity("mark", nil, cell.x, cell.y, { name = text })
|
||||||
|
self:tick()
|
||||||
|
return mark
|
||||||
|
end
|
||||||
if not props or not props.sprites then return end
|
if not props or not props.sprites then return end
|
||||||
if not cell then return end
|
if not cell then return end
|
||||||
-- Jorge is one dialogable NPC with three display channels, not three
|
-- Jorge is one dialogable NPC with three display channels, not three
|
||||||
|
|
@ -1155,6 +1226,14 @@ function Room:serialize(includePlayers)
|
||||||
table.insert(objects, { class = "note", x = note.x, y = note.y, text = note.text })
|
table.insert(objects, { class = "note", x = note.x, y = note.y, text = note.text })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Programmatic marks are recreated from code on every load, so persisting them
|
||||||
|
-- would duplicate them. Only authored (editor-placed) marks are written.
|
||||||
|
for _, mark in ipairs(self.marks) do
|
||||||
|
if not mark.programmatic then
|
||||||
|
table.insert(objects, { class = "mark", x = mark.x, y = mark.y, name = mark.markName })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return { objects = objects, wires = self:serializeWires() }
|
return { objects = objects, wires = self:serializeWires() }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
888
rooms/room_16_17.sav
Normal file
888
rooms/room_16_17.sav
Normal file
|
|
@ -0,0 +1,888 @@
|
||||||
|
{
|
||||||
|
objects={
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="mark",
|
||||||
|
name="end",
|
||||||
|
y=6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wires={
|
||||||
|
}
|
||||||
|
}
|
||||||
917
rooms/testing_movement.sav
Normal file
917
rooms/testing_movement.sav
Normal file
|
|
@ -0,0 +1,917 @@
|
||||||
|
{
|
||||||
|
objects={
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="blue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=1,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=6,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=1,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=2,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=3,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=4,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=6,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=7,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=9,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=10,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=11,
|
||||||
|
class="static_wall",
|
||||||
|
y=7,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=2,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=3,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=4,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=5,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=8,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=9,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=10,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=8,
|
||||||
|
class="static_wall",
|
||||||
|
y=11,
|
||||||
|
color="red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="jorge",
|
||||||
|
y=4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x=5,
|
||||||
|
class="mark",
|
||||||
|
name="begin",
|
||||||
|
y=4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wires={
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
|
width=18,
|
||||||
height=18,
|
height=18,
|
||||||
lastRoom="blocker_test",
|
lastRoom="room_16_17",
|
||||||
rooms={
|
rooms={
|
||||||
{
|
{
|
||||||
name="3",
|
name="3",
|
||||||
|
|
@ -281,7 +282,16 @@ rooms={
|
||||||
name="white_ok",
|
name="white_ok",
|
||||||
x=11,
|
x=11,
|
||||||
y=1
|
y=1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name="room_16_17",
|
||||||
|
x=16,
|
||||||
|
y=17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name="testing_movement",
|
||||||
|
x=15,
|
||||||
|
y=17
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
width=18
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,4 +4,10 @@ Jorge: I'm Jorge. I'm LGBTQ+, and I spent too long hiding that.
|
||||||
Jorge smiles. "Thank you. Being met with kindness makes this place feel safer."
|
Jorge smiles. "Thank you. Being met with kindness makes this place feel safer."
|
||||||
* [Mistreat Jorge] That's not something I respect.
|
* [Mistreat Jorge] That's not something I respect.
|
||||||
Jorge's smile fades. "You can choose cruelty, but you don't get to define me."
|
Jorge's smile fades. "You can choose cruelty, but you don't get to define me."
|
||||||
|
* [Send him to the end] Go wait for me at the end.
|
||||||
|
~ walk_to("end")
|
||||||
|
Jorge nods and heads for the end.
|
||||||
|
* [Send him to the start] Head back to the beginning.
|
||||||
|
~ walk_to("begin")
|
||||||
|
Jorge nods and heads back the way you came.
|
||||||
- -> END
|
- -> END
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
return {["constants"]={},["tree"]={["_"]={["_"]={"Jorge: I'm Jorge. I'm LGBTQ+, and I spent too long hiding that.",{["node"]={"Jorge smiles. \"Thank you. Being met with kindness makes this place feel safer.\""},["choice"]="Be kind",["text"]="I'm glad you told me."},{["node"]={"Jorge's smile fades. \"You can choose cruelty, but you don't get to define me.\""},["choice"]="Mistreat Jorge",["text"]="That's not something I respect."},{["divert"]={["path"]="END",["tunnel"]=false}}}}},["inclusions"]={},["params"]={},["version"]={["tree"]=1,["engine"]=2},["variables"]={},["lists"]={}}
|
return {["constants"]={},["tree"]={["_"]={["_"]={"Jorge: I'm Jorge. I'm LGBTQ+, and I spent too long hiding that.",{["node"]={"Jorge smiles. \"Thank you. Being met with kindness makes this place feel safer.\""},["choice"]="Be kind",["text"]="I'm glad you told me."},{["node"]={"Jorge's smile fades. \"You can choose cruelty, but you don't get to define me.\""},["choice"]="Mistreat Jorge",["text"]="That's not something I respect."},{["node"]={{["var"]="",["value"]="walk_to(\"end\")"},"Jorge nods and heads for the end."},["choice"]="Send him to the end",["text"]="Go wait for me at the end."},{["node"]={{["var"]="",["value"]="walk_to(\"begin\")"},"Jorge nods and heads back the way you came."},["choice"]="Send him to the start",["text"]="Head back to the beginning."},{["divert"]={["path"]="END",["tunnel"]=false}}}}},["inclusions"]={},["params"]={},["version"]={["tree"]=1,["engine"]=2},["variables"]={},["lists"]={}}
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,12 @@ local function loadRoomClass()
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
-- room pulls in note.lua/mark.lua, which call loadSprite at module-load time.
|
||||||
|
-- Provide a default stub so requiring room works; individual tests still
|
||||||
|
-- override loadSprite as needed.
|
||||||
|
loadSprite = loadSprite or function()
|
||||||
|
return {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
||||||
|
end
|
||||||
require "room"
|
require "room"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
63
world.lua
63
world.lua
|
|
@ -213,6 +213,9 @@ end
|
||||||
function World:rebuildCollision()
|
function World:rebuildCollision()
|
||||||
self.collidableMatrices = {red = {}, green = {}, blue = {}}
|
self.collidableMatrices = {red = {}, green = {}, blue = {}}
|
||||||
self.playerBlockerMatrices = {red = {}, green = {}, blue = {}}
|
self.playerBlockerMatrices = {red = {}, green = {}, blue = {}}
|
||||||
|
-- Per-colour set of world cells occupied by a player copy. NPC pathfinding
|
||||||
|
-- treats these as solid (a walker can't push the player) — see pathfinding.lua.
|
||||||
|
self.playerCells = {red = {}, green = {}, blue = {}}
|
||||||
self.entityRooms = {}
|
self.entityRooms = {}
|
||||||
local function put(matrix, cell, entity)
|
local function put(matrix, cell, entity)
|
||||||
matrix[cell.y] = matrix[cell.y] or {}
|
matrix[cell.y] = matrix[cell.y] or {}
|
||||||
|
|
@ -221,6 +224,13 @@ function World:rebuildCollision()
|
||||||
for name, room in pairs(self.levels) do
|
for name, room in pairs(self.levels) do
|
||||||
local location = self:roomLocation(name)
|
local location = self:roomLocation(name)
|
||||||
if location then
|
if location then
|
||||||
|
for color, player in pairs(room.player) do
|
||||||
|
for _, cell in ipairs(player:getOccupiedCells()) do
|
||||||
|
local x = (location.x - 1) * gridWidth + cell.x
|
||||||
|
local y = (location.y - 1) * gridHeight + cell.y
|
||||||
|
self.playerCells[color][x .. ":" .. y] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
for color, entities in pairs(room.collideables) do
|
for color, entities in pairs(room.collideables) do
|
||||||
for _, entity in pairs(entities) do
|
for _, entity in pairs(entities) do
|
||||||
self.entityRooms[entity] = room
|
self.entityRooms[entity] = room
|
||||||
|
|
@ -232,16 +242,19 @@ function World:rebuildCollision()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- NPCs are local room state, but globally collidable: a movement lookup
|
-- NPC copies are per-colour room residents (see npc.lua). Each colour
|
||||||
-- must find them so its normal bump() path can open dialogue.
|
-- stamps only its own copy's cells — "red can only be hit by red" — at the
|
||||||
for _, npc in ipairs(room.npcs) do
|
-- copy's position in the room it currently stands in. The controller
|
||||||
self.entityRooms[npc] = room
|
-- entity (copy.npc) is stored so a bump opens its dialogue.
|
||||||
for _, cell in ipairs(npc:getOccupiedCells()) do
|
for color, copies in pairs(room.npcCopies) do
|
||||||
local worldCell = {
|
for _, copy in ipairs(copies) do
|
||||||
x = (location.x - 1) * gridWidth + cell.x,
|
self.entityRooms[copy.npc] = room
|
||||||
y = (location.y - 1) * gridHeight + cell.y,
|
for _, shapeCell in ipairs(copy.npc:getCellShape()) do
|
||||||
}
|
put(self.collidableMatrices[color], {
|
||||||
for color in pairs(self.collidableMatrices) do put(self.collidableMatrices[color], worldCell, npc) end
|
x = (location.x - 1) * gridWidth + copy.x + shapeCell.x - 1,
|
||||||
|
y = (location.y - 1) * gridHeight + copy.y + shapeCell.y - 1,
|
||||||
|
}, copy.npc)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for color, entities in pairs(room.playerBlockers) do
|
for color, entities in pairs(room.playerBlockers) do
|
||||||
|
|
@ -271,6 +284,17 @@ function World:roomCell(cell)
|
||||||
return room, {x = cell.x - (roomX - 1) * gridWidth, y = cell.y - (roomY - 1) * gridHeight}
|
return room, {x = cell.x - (roomX - 1) * gridWidth, y = cell.y - (roomY - 1) * gridHeight}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Marks are globally unique named destinations. This resolves one for an NPC's
|
||||||
|
-- Walker:sendTo, scanning every loaded room's mark registry.
|
||||||
|
function World:markByName(name)
|
||||||
|
for _, room in pairs(self.levels or {}) do
|
||||||
|
for _, mark in ipairs(room.marks or {}) do
|
||||||
|
if mark.markName == name then return mark, room end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local function cellKey(cell)
|
local function cellKey(cell)
|
||||||
return cell.x .. ":" .. cell.y
|
return cell.x .. ":" .. cell.y
|
||||||
end
|
end
|
||||||
|
|
@ -795,6 +819,25 @@ function World:tick(room)
|
||||||
self:nextRoomCheck(room)
|
self:nextRoomCheck(room)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Real-time NPC movement across every placed room, independent of player turns.
|
||||||
|
-- Snapshot the NPC list first: Walker:arrive can hand an NPC to another room's
|
||||||
|
-- registry, which would otherwise mutate a list we are iterating.
|
||||||
|
function World:update(dt)
|
||||||
|
local walking = {}
|
||||||
|
for name, room in pairs(self.levels or {}) do
|
||||||
|
if self:roomLocation(name) then
|
||||||
|
for _, npc in ipairs(room.npcs) do
|
||||||
|
if npc.step then table.insert(walking, npc) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local moved = false
|
||||||
|
for _, npc in ipairs(walking) do
|
||||||
|
if npc:step(dt) then moved = true end
|
||||||
|
end
|
||||||
|
if moved then self:rebuildCollision() end
|
||||||
|
end
|
||||||
|
|
||||||
function World:nextRoomCheck(room)
|
function World:nextRoomCheck(room)
|
||||||
for _, color in ipairs(room:getActiveColors()) do
|
for _, color in ipairs(room:getActiveColors()) do
|
||||||
local player = room.player[color]
|
local player = room.player[color]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue