Compare commits

..

No commits in common. "e73f63b5d4ceda07319d7e53c73c9334a95495b4" and "e065ab83deab6d6b8360ab629dc6b23603b7a5a4" have entirely different histories.

44 changed files with 3867 additions and 4533 deletions

View file

@ -48,18 +48,10 @@ end
-- Runtime saves live in LÖVE's write directory. This deliberately uses only
-- love.filesystem operations: they cannot delete files from the source tree.
function clearRuntimeSaveData(roomName)
function clearRuntimeSaveData()
local removed = 0
if roomName then
-- love.filesystem.remove only affects the writable save directory, never
-- the shipped/source room with the same path.
if love.filesystem.remove("rooms/" .. roomName .. ".sav") then removed = 1 end
return removed
end
local function removeTree(path)
local info = love.filesystem.getInfo(path)
if info and info.type == "directory" then
if love.filesystem.isDirectory(path) then
for _, child in ipairs(love.filesystem.getDirectoryItems(path)) do
removeTree(path .. "/" .. child)
end
@ -68,7 +60,7 @@ function clearRuntimeSaveData(roomName)
end
-- These are the only files this game writes through love.filesystem: room
-- autosaves and the generated asset cache. Source levels are read-only to
-- autosaves and the generated asset cache. Source levels are read-only to
-- this API and cannot be removed here.
removeTree("rooms")
if love.filesystem.remove("assets.prop") then removed = removed + 1 end
@ -110,18 +102,16 @@ function cellShapeFromBox(box)
return cells
end
-- Which 16x16 cells of an image are non-empty, for multi-cell collision
-- shapes. LÖVE 11 no longer exposes Image:getData(), so inspect ImageData
-- directly instead of creating a GPU Image first.
function getCellsFromSprite(spritePath)
local img = love.image.newImageData(spritePath)
local w, h = img:getWidth(), img:getHeight()
-- Which 16x16 cells of a sprite are non-empty, for multi-cell collision shapes.
function getCellsFromSprite(sprite)
local w, h = sprite:getWidth(), sprite:getHeight()
local box = {w = w / 16, h = h / 16}
local boxCells = cellShapeFromBox(box)
if box.w < 1 or box.h < 1 then
return boxCells
end
local img = sprite:getData()
local cellsToRemove = {}
for _, cell in pairs(boxCells) do

View file

@ -1,3 +0,0 @@
{
class="immoveable"
}

View file

@ -1,3 +0,0 @@
{
class="immoveable"
}

View file

@ -1 +0,0 @@
{class="moveable"}

View file

@ -1 +0,0 @@
{class="moveable"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 879 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 890 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 879 B

Before After
Before After

View file

@ -1,3 +0,0 @@
{
class="moveable"
}

View file

@ -1,3 +0,0 @@
{
class="immoveable"
}

View file

@ -4,11 +4,10 @@
Color = class("Color")
Color.static.list = {
-- LÖVE 11 represents color components as normalized floats, not bytes.
red = {r = 1, g = 0, b = 0, a = 1},
green = {r = 0, g = 1, b = 0, a = 1},
blue = {r = 0, g = 0, b = 1, a = 1},
white = {r = 1, g = 1, b = 1, a = 1}
red = {r = 255, g = 0, b = 0, a = 255},
green = {r = 0, g = 255, b = 0, a = 255},
blue = {r = 0, g = 0, b = 255, a = 255},
white = {r = 255, g = 255, b = 255, a = 255}
}
function Color:initialize(inColor)
@ -28,4 +27,4 @@ end
function randomColor()
local colors = {"red", "green", "blue"}
return colors[math.random(1, 3)]
end
end

View file

@ -1,12 +1,5 @@
function love.conf(t)
-- This project uses the normalized-color and ImageData APIs introduced in
-- LÖVE 11. Pinning the intended runtime also gives a useful warning when a
-- player tries to launch it with an older installation.
t.version = "11.5"
t.window.width = 900
t.window.height = 640
t.window.title = "CHROMA SOLSTICE"
t.window.resizable = true
t.window.minwidth = 700
t.window.minheight = 500
end
end

View file

@ -1,11 +0,0 @@
-- Dialogable: a mixin that makes any Entity subclass talkable. `include` it and
-- give the entity a `self.book` (an Ink book); bumping the entity then opens the
-- modal Dialogue. npcs, signs, artifacts — anything you can talk to — are just
-- Dialogables. For now an entity carries one book; later it can swap books as it
-- progresses. All it does is route a bump to the dialogue manager.
Dialogable = {}
function Dialogable:onBump()
if self.book then Dialogue.open(self.book) end
return true
end

View file

@ -1,110 +0,0 @@
-- Dead-simple modal dialogue overlay driven by a narrator (Ink) story.
-- No variables, no saved state: open a book, walk its paragraphs, pick a
-- choice, then a keypress dismisses it. `narrator` is the global set up in
-- main.lua; Dialogable entities (see npc.lua) call Dialogue.open on bump.
Dialogue = {}
local story -- active narrator story, or nil when the box is closed
local paragraphs = {} -- text lines currently shown
local choices = {} -- current choices (empty once we reach an ending)
local selected = 1
function Dialogue.isOpen()
return story ~= nil
end
-- Run the story forward to the next choice point (or the end), gathering every
-- paragraph printed along the way.
local function advance()
paragraphs = {}
while story:can_continue() do
for _, p in ipairs(story:continue()) do
table.insert(paragraphs, p.text)
end
end
choices = story:can_choose() and story:get_choices() or {}
selected = 1
end
function Dialogue.open(book)
if story then return end -- already talking: ignore repeat bumps
story = narrator.init_story(book)
story:begin()
advance()
end
function Dialogue.close()
story, paragraphs, choices, selected = nil, {}, {}, 1
end
function Dialogue.keypressed(key)
if #choices == 0 then
-- an ending is on screen: any key dismisses the box
Dialogue.close()
elseif key == "up" then
selected = selected > 1 and selected - 1 or #choices
elseif key == "down" then
selected = selected < #choices and selected + 1 or 1
elseif key == "return" or key == "space" or key == "x" then
story:choose(selected)
advance()
end
end
-- LCD font sized to the current play area, rebuilt only when the size changes,
-- so dialogue text scales with the game like every sprite does.
local dlgFont, dlgFontSize
local function dialogueFont()
local size = math.max(8, math.floor(height / gridHeight * 0.4))
if not dlgFont or size ~= dlgFontSize then
dlgFont = love.graphics.newFont("font/LCD_Solid.ttf", size)
dlgFontSize = size
end
return dlgFont
end
function Dialogue.draw()
if not story then return end
local fnt = dialogueFont()
local fh = fnt:getHeight()
local pad, lineH = fh * 0.8, fh * 1.3
local boxH = height * 0.4
local y0 = height - boxH
local wrap = width - pad * 2
-- Dialogue obeys the world's colour rule: only the channels the player has
-- are rendered. One colour -> that colour; all three -> white (additive), so
-- a red-only player literally reads the sign in red.
local has = { red = false, green = false, blue = false }
for _, c in ipairs(currentRoom.colorsPlayerHas) do has[c] = true end
local r = has.red and 1 or 0
local g = has.green and 1 or 0
local b = has.blue and 1 or 0
local prevFont = love.graphics.getFont()
love.graphics.setFont(fnt)
love.graphics.setColor(0, 0, 0, 0.82)
love.graphics.rectangle("fill", 0, y0, width, boxH)
local y = y0 + pad
love.graphics.setColor(r, g, b, 1)
for _, text in ipairs(paragraphs) do
love.graphics.printf(text, pad, y, wrap)
local _, lines = fnt:getWrap(text, wrap)
y = y + math.max(1, #lines) * lineH
end
y = y + lineH * 0.5
for i, choice in ipairs(choices) do
local dim = (i == selected) and 1 or 0.5 -- selected is brighter + "> "
love.graphics.setColor(r * dim, g * dim, b * dim, 1)
local line = ((i == selected) and "> " or " ") .. choice.text
love.graphics.printf(line, pad, y, wrap)
local _, lines = fnt:getWrap(line, wrap)
y = y + math.max(1, #lines) * lineH
end
love.graphics.setFont(prevFont)
end

View file

@ -1,84 +0,0 @@
# Adding a talkable NPC
An NPC/sign is just an immovable `Entity` that mixes in `Dialogable`. Bumping it
(a rejected move) opens the dialogue box. Talking is the same everywhere — you
only supply **art** + **a story**.
## 1. Write the story
Create `stories/<name>.ink` — a line, a few choices, an ending:
```ink
yo wassup
* kick the sign
it doesnt budge.
* [say nothing] you have nothing to say to a sign.
- -> END
```
## 2. Compile it
The game can't parse `.ink` (LÖVE has no lpeg), so compile to a `.lua` book:
```sh
lua tools/compile_ink.lua <name>
```
Re-run this **every time you edit the `.ink`** — the game loads the `.lua`, not
the `.ink`. (Needs `lua` + `lua-lpeg` installed.)
## 3. Load the book
In `main.lua`, next to `startSignBook`:
```lua
<name>Book = require "stories.<name>"
```
## 4. Make the entity
In `npc.lua`, copy `Sign` — pick your art and name your book:
```lua
Oracle = class("Oracle", Entity)
Oracle:include(Dialogable)
function Oracle:initialize(x, y)
self.book = oracleBook -- from step 3
Entity.initialize(self, {
x = x, y = y, color = "red",
collision = "unmoveable",
sprites = {
red = loadSprite("art/npc/oracle_r.png"),
green = loadSprite("art/npc/oracle_g.png"),
blue = loadSprite("art/npc/oracle_b.png"),
},
})
end
```
Per-channel art lives in `art/npc/` as `<name>_r/_g/_b.png` (16px cells).
## 5. Place it in a room
Map a save class to your entity in `Room:createEntity` (`room.lua`), alongside
the `npc``Sign` branch:
```lua
if class == "oracle" then
local entity = Oracle:new(x, y)
self:registerNpc(entity)
return entity
end
```
Then add it to a room's `rooms/<room>.sav` `objects` list:
```lua
{ class = "oracle", x = 4, y = 8 }
```
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
red-only player reads them in red).

View file

@ -1,50 +0,0 @@
# Dialogue consequences — next steps
Design for dialogue trees with consequences across conversations ("talk again
does something else", NPC A affects NPC B, dialogue opens doors). Builds on the
existing narrator (Ink) setup — see `dialogue_manager.lua`, `dialogable.lua`.
## Core principle
**Books are disposable views; a global `WorldState` table is the single source
of truth.** Each Ink book reads flags in and writes flags out; it holds no
authority, so it can be re-`init`ed and thrown away without losing anything.
This resolves book-per-character's weakness (book A's visit counts are invisible
to book B) — B branches on a `WorldState` flag, not on A's runtime state.
Mirrors the existing world-vs-room split (`world.lua`: "the world is only a
layout; rooms own their puzzle state"). Narrative progress is a third, global
category with its own save file.
Keep book-per-character (`Dialogable.book`), add a stable `self.storyId`.
## The bug to fix first
`Dialogue.open` re-`init`s the story every bump (`dialogue_manager.lua:31-33`),
discarding all state — so there's no "again" to hang consequences on.
## Steps
1. Add a global `WorldState = {}` (flags + counters).
2. `saveProgress` / `loadProgress``save/progress.sav` via TSerial +
`readFromSource` / `writeToSource` (helpers already exist, used in
`world.lua` / `world_seed.lua`). Shape: `{ version = 1, flags = WorldState }`.
Flat table, no per-book blobs initially.
3. `Dialogue.bridge(story)` on open:
- `story:bind("flag", fn)` — read a WorldState flag from Ink.
- `story:bind("setflag", fn)` — write one back.
- gameplay binds, e.g. `give_color` / `open_door` calling into `currentRoom`.
4. Cache the live story per `storyId` in the Dialogue module (stop re-init'ing).
5. Hook `saveProgress` into the existing autosave points (near `room.lua:814`).
"Talk again does X" = increment an own `WorldState` counter (e.g.
`sage_talk_count`) on open, branch on it in Ink. No reliance on Ink
visit-count persistence.
## Deferred
Persisting `story:save_state()` (plain table, keyed by `storyId`) only buys
in-book niceties — consumed sticky choices, resume mid-conversation across a
hard quit. Skipped initially: it couples the save file to book structure and
forces narrator's `migrate` hook on every `.ink` edit. `WorldState` flags don't
— a renamed flag just reads `nil`/false.

View file

@ -16,18 +16,13 @@ function Entity:initialize(t)
self.collision = t.collision --"none", "moveable", "unmoveable"
self.sprite = repSprite
self.shaAmount = t.sha or 1
-- onBump is a method (see Entity:onBump) so mixins like Dialogable can
-- override it; a per-instance t.onBump still wins when supplied.
if t.onBump then self.onBump = t.onBump end
self.onBump = t.onBump or function(self) return true end
self.tick = t.tick or function(self) return true end
self.drawOffset = {x = 0, y = 0}
if self.size.w < 1 or self.size.h < 1 then
-- Small sprites (notably 10px switches) are drawn inside a 16px cell.
-- Offset by the leftover space in native sprite-pixels (e.g. (16-10)/2 = 3);
-- drawScale is applied at draw time so this stays correct across resizes.
self.drawOffset.x = (16 - self.spriteSize.w) / 2
self.drawOffset.y = (16 - self.spriteSize.h) / 2
self.drawOffset.x = (drawScale * 16 * self.size.w) / 2
self.drawOffset.y = (drawScale * 16 * self.size.h) / 2
end
end
@ -124,7 +119,7 @@ function Entity:draw(channel)
if not sprite then return end
love.graphics.setColor(gColor[channel]:set())
local p = self:getDrawPos()
love.graphics.draw(sprite, p.x + self.drawOffset.x * drawScale, p.y + self.drawOffset.y * drawScale, 0, drawScale, drawScale)
love.graphics.draw(sprite, p.x + self.drawOffset.x, p.y + self.drawOffset.y, 0, drawScale, drawScale)
end
function Entity:canMove()
@ -162,11 +157,7 @@ function Entity:setSprite(sprite)
end
function Entity:sha()
return math.random(-worldSha, worldSha) * .125 * drawScale
end
function Entity:onBump()
return true
return math.random(-worldSha, worldSha) * .5
end
function Entity:bump()

View file

@ -3,48 +3,8 @@ Input = {}
Input.playermovekeys = { up = "up", down = "down", left = "left", right = "right" }
Input.playerswitchkeys = { ["1"] = "1", ["2"] = "2", ["3"] = "3"}
Input.colorfromkey = {"red", "green", "blue"}
Input.moveRepeatDelay = .28
Input.moveRepeatInterval = .09
Input.heldMoveKey = nil
Input.moveRepeatElapsed = 0
Input.moveRepeatStarted = false
function Input:handler(dt)
local key = self.heldMoveKey
if key and not love.keyboard.isDown(key) then
key = nil
for _, candidate in ipairs({"up", "down", "left", "right"}) do
if love.keyboard.isDown(candidate) then key = candidate; break end
end
self.heldMoveKey = key
self.moveRepeatElapsed = 0
self.moveRepeatStarted = false
end
if not key then return nil end
self.moveRepeatElapsed = self.moveRepeatElapsed + dt
if not self.moveRepeatStarted then
if self.moveRepeatElapsed >= self.moveRepeatDelay then
self.moveRepeatElapsed = 0
self.moveRepeatStarted = true
return key
end
elseif self.moveRepeatElapsed >= self.moveRepeatInterval then
self.moveRepeatElapsed = 0
return key
end
end
function Input:beginMove(key)
self.heldMoveKey = key
self.moveRepeatElapsed = 0
self.moveRepeatStarted = false
end
function Input:stopMoveRepeat()
self.heldMoveKey = nil
self.moveRepeatElapsed = 0
self.moveRepeatStarted = false
end
@ -64,4 +24,4 @@ end
function Input.playerColor(key)
return Input.colorfromkey[tonumber(key)]
end
end

View file

@ -33,59 +33,12 @@ local editorRoomName = nil
local roomNameField = nil
local editorStatus = nil
local clearSaveConfirmationExpires = 0
local clearSaveConfirmationScope = nil
local worldSeedConfirmationExpires = 0
local worldMode = "place"
local worldRoomButtons = {}
local worldControls = {}
local worldPage = 1
local worldPageSize = 5
local assetSearchText = ""
local assetSearchButtons = {}
local worldSearchText = ""
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
local function fuzzyMatches(value, query)
query = (query or ""):lower():gsub("%s+", "")
if query == "" then return true end
local index = 1
for character in value:lower():gmatch(".") do
if character == query:sub(index, index) then
index = index + 1
if index > #query then return true end
end
end
return false
end
local function beginSearch(field)
activeField = field
inputText = field == "assetSearch" and assetSearchText or worldSearchText
response = inputText
end
local function rebuildAssetSearchButtons()
assetSearchButtons = {}
if assetSearchText == "" then return end
local names = {}
for asset in pairs(gameAssets) do
if fuzzyMatches(asset, assetSearchText) then table.insert(names, asset) end
end
table.sort(names)
for index, asset in ipairs(names) do
if index > 8 then break end
local sprites = {}
for color, location in pairs(gameAssets[asset].sprites) do sprites[color] = love.graphics.newImage(location) end
table.insert(assetSearchButtons, createUIElement{
sprites = sprites,
name = "asset",
text = asset,
x = 30,
y = 45 + (index - 1) * 62,
onClic = { left = function() selectAsset(asset) end }
})
end
end
local function buttonContains(button, x, y)
return x > button.x and x < button.x + button.w and y > button.y and y < button.y + button.h
@ -93,17 +46,14 @@ end
local function rebuildWorldRoomButtons()
worldRoomButtons = {}
local names = {}
for _, name in ipairs(gameWorld:listRoomNames()) do
if fuzzyMatches(name, worldSearchText) then table.insert(names, name) end
end
local names = gameWorld:listRoomNames()
local pages = math.max(1, math.ceil(#names / worldPageSize))
worldPage = constrain(worldPage, 1, pages)
local first = (worldPage - 1) * worldPageSize + 1
for index = first, math.min(first + worldPageSize - 1, #names) do
local roomName = names[index]
local row = index - first
table.insert(worldRoomButtons, { name = roomName, x = 10, y = 70 + row * 68, w = sideBarBox.w - 20, h = 60 })
table.insert(worldRoomButtons, { name = roomName, x = 10, y = 36 + row * 68, w = sideBarBox.w - 20, h = 60 })
end
worldControls = {
{ text = worldMode == "place" and "MODE: PLACE ROOMS" or "MODE: OPEN LEVELS", x = 10, y = sideBarBox.h - 194, w = sideBarBox.w - 20, h = 34,
@ -131,19 +81,16 @@ end
local function drawWorldSidebar()
love.graphics.setColor(gColor.white:set())
love.graphics.printf("ROOMS — " .. (worldMode == "place" and "select to place; re-click to clear" or "click a map room to open"), 8, 6, sideBarBox.w - 16, "center")
worldSearchBox.w = sideBarBox.w - 20
love.graphics.rectangle("line", worldSearchBox.x, worldSearchBox.y, worldSearchBox.w, worldSearchBox.h)
love.graphics.printf("SEARCH: " .. worldSearchText, worldSearchBox.x + 6, worldSearchBox.y + 7, worldSearchBox.w - 12, "left")
love.graphics.printf("ROOMS — " .. (worldMode == "place" and "select to place; re-click to clear" or "click a map room to open"), 8, 10, sideBarBox.w - 16, "center")
for _, button in ipairs(worldRoomButtons) do
love.graphics.setColor(button.name == selectedRoom and 1 or 100 / 255, button.name == selectedRoom and 1 or 100 / 255, button.name == selectedRoom and 1 or 100 / 255, 1)
love.graphics.setColor(button.name == selectedRoom and 255 or 100, button.name == selectedRoom and 255 or 100, button.name == selectedRoom and 255 or 100, 255)
love.graphics.rectangle("line", button.x, button.y, button.w, button.h)
gameWorld:drawRoomPreview(button.name, button.x + 4, button.y + 4, button.h - 8)
love.graphics.setColor(gColor.white:set())
love.graphics.printf(button.name, button.x + button.h + 4, button.y + 21, button.w - button.h - 8, "left")
end
for _, control in ipairs(worldControls) do
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setColor(255, 255, 255, 255)
love.graphics.rectangle("line", control.x, control.y, control.w, control.h)
love.graphics.printf(control.text, control.x, control.y + 9, control.w, "center")
end
@ -182,8 +129,7 @@ local function saveRoomFromEditor()
return
end
local savePlayerPositions = love.keyboard.isDown("lshift", "rshift")
if not currentRoom:saveToSource(name, savePlayerPositions, oldName) then
if not currentRoom:saveToSource(name) then
editorStatus = "Could not save '" .. name .. "'."
return
end
@ -200,28 +146,20 @@ local function saveRoomFromEditor()
editorRoomName = name
roomNameField:editText(name)
activeField = nil
editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or ".")
editorStatus = "Saved source level '" .. name .. ".'"
end
local function clearRuntimeSaves()
local now = love.timer.getTime()
local clearAll = love.keyboard.isDown("lshift", "rshift")
local roomName = currentRoom.name
local scope = clearAll and "all" or roomName
if now > clearSaveConfirmationExpires or clearSaveConfirmationScope ~= scope then
if now > clearSaveConfirmationExpires then
clearSaveConfirmationExpires = now + 3
clearSaveConfirmationScope = scope
if clearAll then
editorStatus = "Shift-click the clear-save icon again within 3 seconds to erase runtime saves for every room."
else
editorStatus = "Click the clear-save icon again within 3 seconds to erase the runtime save for '" .. (roomName or "this room") .. "'."
end
editorStatus = "Click the clear-save icon again within 3 seconds to erase runtime saves."
return
end
local removed = clearRuntimeSaveData(clearAll and nil or roomName)
local removed = clearRuntimeSaveData()
clearSaveConfirmationExpires = 0
clearSaveConfirmationScope = nil
local roomName = currentRoom.name
if roomName then
currentRoom = Room:new{ name = roomName }
setEditorRoomName(currentRoom.name)
@ -246,10 +184,7 @@ function _initLevelEditor()
table.insert(hierarchy[gameAssets[asset].directory], asset)
end
local folders = {}
for folder in pairs(hierarchy) do table.insert(folders, folder) end
table.sort(folders)
for _, folder in ipairs(folders) do
for folder, object in pairs(hierarchy) do
local button = createUIElement{
name = "folder",
text = folder,
@ -375,8 +310,7 @@ function selectFolder(folder)
local counter = 1
local limit = 10
table.sort(hierarchy[selectedFolder])
for _, asset in ipairs(hierarchy[selectedFolder]) do
for _, asset in pairs(hierarchy[selectedFolder]) do
if counter % limit == 0 then
counter = 1
@ -394,7 +328,7 @@ function selectFolder(folder)
end
}
}
table.insert(assetButtons[row], forwardButton)
table.insert(assetButtons[row], forwardbutton)
row = row + 1
table.insert(assetButtons, {})
@ -587,7 +521,6 @@ local function openWorldRoom(cellX, cellY)
currentRoom = Room:new{ name = name }
editorStatus = "Editing '" .. name .. ".'"
end
gameWorld:setLastRoom(currentRoom.name)
setEditorRoomName(currentRoom.name)
assetsView()
end
@ -598,17 +531,7 @@ function editorMouseHandler(x, y, button)
x = x - width
if editorView == "room" then
if buttonContains({ x = 10, y = 5, w = sideBarBox.w - 20, h = 28 }, x, y) then
beginSearch("assetSearch")
return
elseif assetSearchText ~= "" then
for _, uiElement in ipairs(assetSearchButtons) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
return
end
end
elseif not selectedFolder then
if not selectedFolder then
for _, uiElement in pairs(folderButtons) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
@ -636,10 +559,6 @@ function editorMouseHandler(x, y, button)
end
end
elseif editorView == "world" then
if buttonContains(worldSearchBox, x, y) then
beginSearch("worldSearch")
return
end
for _, roomButton in ipairs(worldRoomButtons) do
if buttonContains(roomButton, x, y) then
if selectedRoom == roomButton.name then
@ -654,7 +573,7 @@ function editorMouseHandler(x, y, button)
end
for _, control in ipairs(worldControls) do
if buttonContains(control, x, y) then
control.onClic(x)
control:onClic(x)
return
end
end
@ -687,15 +606,6 @@ function editorTextHandler(text)
elseif editorView == "room" and activeField == "roomName" then
inputText = text
roomNameField:editText(text)
elseif editorView == "room" and activeField == "assetSearch" then
inputText = text
assetSearchText = text
rebuildAssetSearchButtons()
elseif editorView == "world" and activeField == "worldSearch" then
inputText = text
worldSearchText = text
worldPage = 1
rebuildWorldRoomButtons()
end
end
@ -704,10 +614,6 @@ function editorCompleteResponse()
assetProperties[activeField] = inputText
elseif editorView == "room" and activeField == "roomName" then
editorRoomName = inputText
elseif editorView == "room" and activeField == "assetSearch" then
assetSearchText = inputText
elseif editorView == "world" and activeField == "worldSearch" then
worldSearchText = inputText
end
activeField = nil
end
@ -722,17 +628,12 @@ end
function editorDraw()
if editorView == "room" then
love.graphics.setColor(gColor.white:set())
love.graphics.rectangle("line", 10, 5, sideBarBox.w - 20, 28)
love.graphics.printf("ASSET SEARCH: " .. assetSearchText, 16, 12, sideBarBox.w - 32, "left")
love.graphics.setColor(gColor.white:set())
love.graphics.printf("LEVEL NAME", 0, sideBarBox.h * (7 / 10) - 24, sideBarBox.w, "center")
for _, button in pairs(uiButtons) do
button:draw()
end
if assetSearchText ~= "" then
for _, asset in ipairs(assetSearchButtons) do asset:draw() end
elseif not selectedFolder then
if not selectedFolder then
for _, folder in pairs(folderButtons) do
folder:draw()
end

View file

@ -1,6 +1,6 @@
GUI = class("level_editor/gui")
GUI.static.pressSound = love.audio.newSource("sounds/ui/button.wav", "static")
GUI.static.pressSound = love.audio.newSource("sounds/ui/button.wav")
GUI.static.defaultCallbac = function() print("this button has not been configured") end

View file

@ -34,7 +34,7 @@ function getAllAssets()
-- class comes from a <name>.meta sitting next to the art
-- (authoritative); default to immoveable if none exists
local metaPath = topFolder .. "/" .. folder .. "/" .. name .. ".meta"
if love.filesystem.getInfo(metaPath, "file") then
if love.filesystem.exists(metaPath) then
globalAssetProperties[name].class = TSerial.unpack(love.filesystem.read(metaPath)).class
elseif not globalAssetProperties[name].class then
globalAssetProperties[name].class = "immoveable"
@ -45,7 +45,7 @@ function getAllAssets()
if not globalAssetProperties[name].cells or reanalyzeCells then
globalAssetProperties[name].cells = {}
globalAssetProperties[name].cells[color] = getCellsFromSprite(sprite)
globalAssetProperties[name].cells[color] = getCellsFromSprite(love.graphics.newImage(sprite))
end
end
end
@ -54,4 +54,4 @@ function getAllAssets()
end
return globalAssetProperties
end
end

@ -1 +0,0 @@
Subproject commit 45a8b03553f2385716c24ec762757e418b2b73c1

View file

@ -15,12 +15,6 @@ require "world_seed"
require "shaders"
require "level_editor/editor"
-- Ink dialogue: ship the pre-parsed book (LÖVE has no lpeg to parse .ink at
-- runtime; the narrator parser degrades to false without it, runtime is fine).
package.path = package.path .. ";libs/narrator/?.lua"
narrator = require "narrator.narrator"
startSignBook = require "stories.start_sign"
function love.load()
globalAssetProperties = {} -- rebuilt from the filesystem (art/ + .meta) by getAllAssets
@ -31,39 +25,23 @@ function love.load()
gameWorld = World:new()
_initLevelEditor()
_initCanvases()
gameCanvas = love.graphics.newCanvas()
sideBarCanvas = love.graphics.newCanvas(sideBarBox.w, sideBarBox.h)
finalCanvas = love.graphics.newCanvas()
inputTimer = Timer.new()
-- Resume the game: prefer the runtime autosave so the last saved position is
-- restored, falling back to the authored source level. Editor room-opens go
-- through their own (source-only) load path, so this only affects resume.
currentRoom = Room:new{
name = gameWorld.lastRoom or "start",
runtime = true
name = "start" -- TEMP: load a migrated level to eyeball it; revert once the room picker lands
}
gameWorld:setLastRoom(currentRoom.name)
setEditorRoomName(currentRoom.name)
response = ""
end
-- Autosave on close so a position reached by moving within a room (short of a
-- room-to-room transition, which already autosaves in nextRoom) survives.
function love.quit()
if currentRoom then
currentRoom:save()
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
end
end
function love.update(dt)
if editorView == "world" then
Input:stopMoveRepeat()
else
local repeatedMove = Input:handler(dt)
if repeatedMove and not Dialogue.isOpen() then currentRoom:movePlayer(repeatedMove) end
end
Input:handler(dt)
currentRoom:update(dt)
-- if love.keyboard.isDown("w") then worldSha = worldSha + 1 end
@ -89,7 +67,7 @@ function love.draw()
love.graphics.push()
for i = 1, samples do
local c = 65 / 255 / (i * 1.75)
local c = 65 / (i * 1.75)
love.graphics.setColor(c, c, c)
love.graphics.scale(scaleCoefficient, scaleCoefficient)
love.graphics.translate((width / 2 - (width * scaleCoefficient) / 2) + 2, (width / 2 - (height * scaleCoefficient) / 2) + 2)
@ -97,7 +75,7 @@ function love.draw()
end
love.graphics.pop()
love.graphics.setColor(1, 1, 1)
love.graphics.setColor(255, 255, 255)
love.graphics.draw(gameCanvas, 0, 0)
love.graphics.setCanvas()
@ -107,23 +85,15 @@ function love.draw()
love.graphics.setCanvas()
love.graphics.setBlendMode("alpha")
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setColor(255, 255, 255, 255)
love.graphics.draw(finalCanvas, 0, 0)
love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y)
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
Dialogue.draw()
end
function love.keypressed(key)
-- Talking to a sign captures input: navigate/choose, don't move the player.
if Dialogue.isOpen() then
Dialogue.keypressed(key)
return
end
if editorView ~= "world" and Input.isPlayerMove(key) then
Input:beginMove(key)
currentRoom:movePlayer(key)
elseif editorView ~= "world" and Input.isPlayerSwitch(key) then
local c = Input.playerColor(key)
@ -164,32 +134,12 @@ function love.textinput(t)
editorTextHandler(response)
end
function love.resize(w, h)
_computeDimensions()
drawScale = width / gridWidth / 16
_initCanvases()
end
function _initDefaults()
--set filter
love.graphics.setDefaultFilter("linear", "nearest")
love.filesystem.createDirectory("rooms")
--set global width / height / sidebar variables
_computeDimensions()
--set randomseed
math.randomseed(os.time())
--font
font = love.graphics.newFont("font/LCD_Solid.ttf", 16)
love.graphics.setFont(font)
end
-- Derive the square play area (side = smaller window dimension) and the
-- sidebar that fills the remaining horizontal space. Safe to call any time
-- the window size changes.
function _computeDimensions()
--set global width / height variables
width = love.graphics.getWidth()
height = love.graphics.getHeight()
if width > height then
@ -198,14 +148,15 @@ function _computeDimensions()
height = width
end
sideBarBox = {x = width, y = 0, w = love.graphics.getWidth() - width, h = height}
end
--set randomseed
math.randomseed(os.time())
-- (Re)create the render targets at the current window size.
function _initCanvases()
gameCanvas = love.graphics.newCanvas()
finalCanvas = love.graphics.newCanvas()
sideBarCanvas = love.graphics.newCanvas(math.max(1, sideBarBox.w), math.max(1, sideBarBox.h))
--font
font = love.graphics.newFont("font/LCD_Solid.ttf", 16)
love.graphics.setFont(font)
--sidebar
sideBarBox = {x = width, y = 0, w = love.graphics.getWidth() - width, h = height}
end
function _initGridVars()

28
npc.lua
View file

@ -1,22 +1,14 @@
require "entity"
require "dialogable"
npc = class("npc", Entity)
-- 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
-- the first. It just picks art + a story — the talking lives in the mixin.
Sign = class("Sign", Entity)
Sign:include(Dialogable)
function Sign:initialize(x, y)
self.book = startSignBook
function npc:initialize(x, y, color, name)
Entity.initialize(self, {
x = x, y = y,
color = "red", -- representative only; sprites are per-channel
x = x,
y = y,
color = color,
collision = "unmoveable",
sprites = {
red = loadSprite("art/npc/start_sign_r.png"),
green = loadSprite("art/npc/start_sign_g.png"),
blue = loadSprite("art/npc/start_sign_b.png"),
},
sprite = sprites.npc[name][color],
onBump = function(self)
end,
})
end
end

View file

@ -3,8 +3,8 @@ require "entity"
Player = class("Player", Entity)
Player.static.sounds = {
switchFail = love.audio.newSource("sounds/switch_fail.wav", "static"),
switchSuccess = love.audio.newSource("sounds/switch_success.wav", "static")
switchFail = love.audio.newSource("sounds/switch_fail.wav"),
switchSuccess = love.audio.newSource("sounds/switch_success.wav")
}
function Player:initialize(x, y, color)

107
room.lua
View file

@ -27,10 +27,6 @@ function Room:initialize(t)
print(t.name)
if t.player then
-- The carried colors are the sole source of truth for what's active.
-- Copies that live in this room (loaded from the sav) start INACTIVE —
-- you must walk onto one to merge it; it never joins you automatically.
self.activeColors = {red = false, green = false, blue = false}
-- entering from another room: place the carried colors at the entry point
for _, color in ipairs(t.player.activeColors) do
if self.player[color] then
@ -40,6 +36,11 @@ function Room:initialize(t)
end
self:setActiveColor(color, true)
end
elseif not next(self.player) then
-- fresh load of a room with no saved player: default red spawn so it's
-- playable (the player starts with only red)
self:registerPlayer(Player:new(1, 1, "red"), "red")
self:setActiveColor("red", true)
end
self.name = t.name
@ -101,11 +102,6 @@ function Room:initialize(t)
self.collidableMatrices = {} --{red = {{...}{...}{...}}, ...
self:createSwitchMatrix()
self:createCollideablesMatrix()
-- Resolve switches/doors up front so the very first frame renders correct
-- lock state: a block already sitting on a switch presses it before anything
-- is drawn, instead of waiting for the first move's tick.
self:resolveSwitchesAndDoors()
end
@ -120,11 +116,19 @@ function Room:draw()
-- draw only the channels the player has. Each object just exposes sprites per
-- channel via Entity:draw(channel); a door registered on all three channels
-- naturally draws its red sprite on the red pass, etc. No per-object logic.
local has = {}
for _, channel in ipairs(self.colorsPlayerHas) do
has[channel] = true
if self.player[channel] then self.player[channel]:draw(channel) end
for _, entity in pairs(self.collideables[channel]) do entity:draw(channel) end
for _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
for _, npc in ipairs(self.npcs) do npc:draw(channel) end
end
-- npcs (old "sages") are markers for now: a "?" tinted to the player's channels
local cellW, cellH = width / gridWidth, height / gridHeight
love.graphics.setColor(has.red and 255 or 0, has.green and 255 or 0, has.blue and 255 or 0)
for _, npc in ipairs(self.npcs) do
love.graphics.print("?", (npc.x - 1) * cellW + cellW * 0.3, (npc.y - 1) * cellH + cellH * 0.1, 0, 2, 2)
end
end
@ -132,14 +136,6 @@ function Room:createCollideablesMatrix()
for color, entities in pairs(self.collideables) do
self.collidableMatrices[color] = self:createEntityMatrix(entities)
end
-- Dialogables (npcs) block every channel, so stamp them into each matrix.
for _, npc in ipairs(self.npcs) do
for _, cell in ipairs(npc:getOccupiedCells()) do
for color in pairs(self.collidableMatrices) do
self.collidableMatrices[color][cell.y][cell.x] = npc
end
end
end
end
function Room:createSwitchMatrix()
@ -423,17 +419,11 @@ function Room:tick()
end
self:createCollideablesMatrix()
self:createSwitchMatrix()
self:resolveSwitchesAndDoors()
self:nextRoomCheck()
end
-- Run the switch press check and push the result to every door. Assumes the
-- collidable/switch matrices are already current.
function Room:resolveSwitchesAndDoors()
local setDoorColors = self:switchCheck()
for _, door in pairs(self.doors) do
door:setColors(setDoorColors)
end
self:nextRoomCheck()
end
function Room:playerJoinCheck(previousPositions, newPositions)
@ -499,10 +489,7 @@ function Room:switchCheck()
local onSwitches = {}
for _, switch in pairs(switchArray) do
local targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
local player = self.player[color]
-- Players are not stored in collidableMatrices, but a colour copy
-- standing on its matching switch presses it just like a block does.
if targetCell or (player and player:occupiesCell(switch:getGridPos())) then
if targetCell then
--switch is on
switch:activate()
table.insert(onSwitches, switch) --doesn't rly matter what we put here as long as it's true-y
@ -541,9 +528,8 @@ end
-- keyword handled directly since it has no art.
function Room:createEntity(class, color, x, y)
if class == "npc" then
local entity = Sign:new(x, y)
self:registerNpc(entity)
return entity
self:registerNpc(x, y)
return
end
local props = globalAssetProperties and globalAssetProperties[class]
@ -603,11 +589,8 @@ function Room:registerPlayer(entityPointer, color)
self.player[color] = entityPointer
end
function Room:registerNpc(entity)
-- npcs are dialogable entities: they carry .x/.y like any Entity, so
-- serialize's npc loop still round-trips them, and they get stamped into the
-- collision matrix (see createCollideablesMatrix) so a bump is a rejected move.
table.insert(self.npcs, entity)
function Room:registerNpc(x, y)
table.insert(self.npcs, { x = x, y = y })
end
function Room:getCellFromMousePos(mouseX, mouseY)
@ -702,12 +685,6 @@ end
function Room:deserialize(saveTable)
for _, obj in ipairs(saveTable.objects or {}) do
self:createEntity(obj.class, obj.color, obj.x, obj.y)
-- createEntity turns every loaded player on; honour a saved active flag so
-- copies that were off at save time stay off (older savs lack the field and
-- keep the old all-on behaviour).
if obj.class == "player" and obj.active ~= nil then
self:setActiveColor(obj.color, obj.active)
end
end
end
@ -722,37 +699,13 @@ end
-- This is intentionally separate from Room:save. It is the editor's explicit
-- "write this level into the project" action; gameplay autosaves must never
-- call it.
function Room:saveToSource(name, savePlayerPositions, playerSourceName)
function Room:saveToSource(name)
name = name or self.name or "test"
local saveTable = self:serialize(savePlayerPositions ~= false)
if savePlayerPositions == false then
-- A normal editor save keeps the authored spawn positions from the
-- existing source level. During a rename that source is still oldName.
local sourceName = playerSourceName or name
local existing = readFromSource("rooms/" .. sourceName .. ".sav")
local keptPlayer = false
if existing then
local previous = TSerial.unpack(existing)
for _, object in ipairs(previous.objects or {}) do
if object.class == "player" then
table.insert(saveTable.objects, object)
keptPlayer = true
end
end
end
-- A newly named blank level has no prior source spawn to retain.
if not keptPlayer then
for color, player in pairs(self.player) do
local pos = player:getGridPos()
table.insert(saveTable.objects, { class = "player", color = color, x = pos.x, y = pos.y })
end
end
end
local room = TSerial.pack(saveTable, nil, true)
local room = TSerial.pack(self:serialize(), nil, true)
return writeToSource("rooms/" .. name .. ".sav", room)
end
function Room:serialize(includePlayers)
function Room:serialize()
local objects = {}
local function emit(class, color, entity)
@ -760,17 +713,8 @@ function Room:serialize(includePlayers)
table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y })
end
if includePlayers ~= false then
for color, player in pairs(self.player) do
local pos = player:getGridPos()
-- Persist which copies were active so a resume restores exactly the
-- channels that were on at save time. A colour deactivated during play
-- (see playerJoinCheck) must come back as an inactive copy, not turn on.
table.insert(objects, {
class = "player", color = color, x = pos.x, y = pos.y,
active = self.activeColors[color] and true or false,
})
end
for color, player in pairs(self.player) do
emit("player", color, player)
end
for color, collideableArray in pairs(self.collideables) do
@ -822,7 +766,6 @@ function nextRoom(pos)
x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()
}
}
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
if currentRoom.name then setEditorRoomName(currentRoom.name) end
end

View file

@ -1,76 +0,0 @@
{
objects={
{
color="red",
y=4,
x=8,
class="toilet"
},
{
color="red",
y=4,
x=6,
class="toilet"
},
{
color="red",
y=4,
x=4,
class="toilet"
},
{
color="green",
y=4,
x=8,
class="toilet"
},
{
color="green",
y=4,
x=6,
class="toilet"
},
{
color="green",
y=4,
x=4,
class="toilet"
},
{
color="blue",
y=4,
x=8,
class="toilet"
},
{
color="blue",
y=4,
x=6,
class="toilet"
},
{
color="blue",
y=4,
x=4,
class="toilet"
},
{
color="red",
y=8,
x=6,
class="player"
},
{
color="green",
y=8,
x=6,
class="player"
},
{
color="blue",
y=8,
x=6,
class="player"
}
}
}

View file

@ -1,4 +0,0 @@
{
objects={
}
}

View file

@ -1,538 +1,538 @@
{
objects={
{
color="green",
class="static_wall",
x=1,
y=1
},
{
color="blue",
y=1,
x=1
class="static_wall",
x=1,
y=1
},
{
color="red",
class="static_wall",
x=1,
y=1
},
{
color="green",
class="static_wall",
x=11,
y=1
},
{
color="blue",
y=1,
x=11
class="static_wall",
x=11,
y=1
},
{
color="red",
class="static_wall",
x=11,
y=1
},
{
color="blue",
y=2,
x=1
class="static_wall",
x=1,
y=2
},
{
color="green",
class="static_wall",
x=1,
y=2
},
{
color="red",
class="static_wall",
x=1,
y=2
},
{
color="red",
class="static_wall",
x=11,
y=2
},
{
color="green",
class="static_wall",
x=11,
y=2
},
{
color="blue",
y=2,
x=11
class="static_wall",
x=11,
y=2
},
{
class="static_wall",
color="blue",
y=3,
x=1
class="static_wall",
x=1,
y=3
},
{
color="red",
class="static_wall",
x=1,
y=3
},
{
color="green",
class="static_wall",
x=1,
y=3
},
{
color="blue",
y=3,
x=11
class="static_wall",
x=11,
y=3
},
{
color="green",
class="static_wall",
x=11,
y=3
},
{
color="red",
class="static_wall",
x=11,
y=3
},
{
color="green",
class="static_wall",
x=1,
y=4
},
{
color="blue",
y=4,
x=1
class="static_wall",
x=1,
y=4
},
{
color="red",
class="static_wall",
x=1,
y=4
},
{
color="green",
class="static_wall",
x=11,
y=4
},
{
color="blue",
y=4,
x=11
class="static_wall",
x=11,
y=4
},
{
color="red",
class="static_wall",
x=11,
y=4
},
{
color="blue",
y=5,
x=1
class="static_wall",
x=1,
y=5
},
{
color="green",
class="static_wall",
x=1,
y=5
},
{
color="red",
class="static_wall",
x=1,
y=5
},
{
color="green",
class="static_wall",
x=11,
y=5
},
{
color="red",
class="static_wall",
x=11,
y=5
},
{
color="green",
class="static_wall",
x=1,
y=6
},
{
color="red",
class="static_wall",
x=1,
y=6
},
{
color="blue",
y=6,
x=1
class="static_wall",
x=1,
y=6
},
{
color="red",
class="static_wall",
x=11,
y=6
},
{
color="green",
class="static_wall",
x=11,
y=6
},
{
color="blue",
y=6,
x=11
class="static_wall",
x=11,
y=6
},
{
color="red",
class="static_wall",
x=1,
y=7
},
{
color="green",
class="static_wall",
x=1,
y=7
},
{
color="blue",
y=7,
x=1
class="static_wall",
x=1,
y=7
},
{
color="red",
class="static_wall",
x=11,
y=7
},
{
color="green",
class="static_wall",
x=11,
y=7
},
{
color="blue",
y=7,
x=11
class="static_wall",
x=11,
y=7
},
{
color="green",
class="static_wall",
x=1,
y=8
},
{
color="blue",
y=8,
x=1
class="static_wall",
x=1,
y=8
},
{
color="red",
class="static_wall",
x=1,
y=8
},
{
color="red",
class="static_wall",
x=11,
y=8
},
{
color="green",
class="static_wall",
x=11,
y=8
},
{
color="blue",
y=8,
x=11
class="static_wall",
x=11,
y=8
},
{
color="red",
class="static_wall",
x=1,
y=9
},
{
color="blue",
y=9,
x=1
class="static_wall",
x=1,
y=9
},
{
color="green",
class="static_wall",
x=1,
y=9
},
{
color="blue",
y=9,
x=11
class="static_wall",
x=11,
y=9
},
{
color="green",
class="static_wall",
x=11,
y=9
},
{
color="red",
class="static_wall",
x=11,
y=9
},
{
color="red",
class="static_wall",
x=1,
y=10
},
{
color="blue",
y=10,
x=1
class="static_wall",
x=1,
y=10
},
{
color="green",
class="static_wall",
x=1,
y=10
},
{
color="red",
class="static_wall",
x=11,
y=10
},
{
color="green",
class="static_wall",
x=11,
y=10
},
{
color="blue",
y=10,
x=11
class="static_wall",
x=11,
y=10
},
{
color="red",
class="static_wall",
x=1,
y=11
},
{
color="green",
class="static_wall",
x=1,
y=11
},
{
color="blue",
y=11,
x=1
class="static_wall",
x=1,
y=11
},
{
color="green",
class="static_wall",
x=2,
y=11
},
{
color="blue",
y=11,
x=2
class="static_wall",
x=2,
y=11
},
{
color="red",
class="static_wall",
x=2,
y=11
},
{
color="blue",
y=11,
x=3
class="static_wall",
x=3,
y=11
},
{
color="green",
class="static_wall",
x=3,
y=11
},
{
color="red",
class="static_wall",
x=3,
y=11
},
{
color="green",
class="static_wall",
x=4,
y=11
},
{
color="red",
class="static_wall",
x=4,
y=11
},
{
color="blue",
y=11,
x=4
class="static_wall",
x=4,
y=11
},
{
class="static_wall",
color="blue",
y=11,
x=5
class="static_wall",
x=5,
y=11
},
{
color="green",
class="static_wall",
x=5,
y=11
},
{
color="red",
class="static_wall",
x=5,
y=11
},
{
color="red",
class="static_wall",
x=7,
y=11
},
{
color="blue",
y=11,
x=7
class="static_wall",
x=7,
y=11
},
{
color="green",
class="static_wall",
x=7,
y=11
},
{
color="blue",
y=11,
x=8
class="static_wall",
x=8,
y=11
},
{
color="red",
class="static_wall",
x=8,
y=11
},
{
color="green",
class="static_wall",
x=8,
y=11
},
{
color="blue",
y=11,
x=9
class="static_wall",
x=9,
y=11
},
{
color="red",
class="static_wall",
x=9,
y=11
},
{
color="green",
class="static_wall",
x=9,
y=11
},
{
color="red",
class="static_wall",
x=10,
y=11
},
{
color="green",
class="static_wall",
x=10,
y=11
},
{
color="blue",
y=11,
x=10
class="static_wall",
x=10,
y=11
},
{
class="static_wall",
color="blue",
y=11,
x=11
class="static_wall",
x=11,
y=11
},
{
class="static_wall",
color="red",
y=1,
x=1
},
{
class="static_wall",
color="red",
y=1,
x=11
},
{
class="static_wall",
color="red",
y=2,
x=1
},
{
class="static_wall",
color="red",
y=2,
x=11
},
{
class="static_wall",
color="red",
y=3,
x=1
},
{
class="static_wall",
color="red",
y=3,
x=11
},
{
class="static_wall",
color="red",
y=4,
x=1
},
{
class="static_wall",
color="red",
y=4,
x=11
},
{
class="static_wall",
color="red",
y=5,
x=1
},
{
class="static_wall",
color="red",
y=5,
x=11
},
{
class="static_wall",
color="red",
y=6,
x=1
},
{
class="static_wall",
color="red",
y=6,
x=11
},
{
class="static_wall",
color="red",
y=7,
x=1
},
{
class="static_wall",
color="red",
y=7,
x=11
},
{
class="static_wall",
color="red",
y=8,
x=1
},
{
class="static_wall",
color="red",
y=8,
x=11
},
{
class="static_wall",
color="red",
y=9,
x=1
},
{
class="static_wall",
color="red",
y=9,
x=11
},
{
class="static_wall",
color="red",
y=10,
x=1
},
{
class="static_wall",
color="red",
y=10,
x=11
},
{
class="static_wall",
color="red",
y=11,
x=1
},
{
class="static_wall",
color="red",
y=11,
x=2
},
{
class="static_wall",
color="red",
y=11,
x=3
},
{
class="static_wall",
color="red",
y=11,
x=4
},
{
class="static_wall",
color="red",
y=11,
x=5
},
{
class="static_wall",
color="red",
y=11,
x=7
},
{
class="static_wall",
color="red",
y=11,
x=8
},
{
class="static_wall",
color="red",
y=11,
x=9
},
{
class="static_wall",
color="red",
y=11,
x=10
},
{
class="static_wall",
color="red",
y=11,
x=11
},
{
class="static_wall",
color="green",
y=1,
x=1
class="static_wall",
x=11,
y=11
},
{
color="red",
class="static_wall",
color="green",
y=1,
x=11
},
{
class="static_wall",
color="green",
y=2,
x=1
},
{
class="static_wall",
color="green",
y=2,
x=11
},
{
class="static_wall",
color="green",
y=3,
x=1
},
{
class="static_wall",
color="green",
y=3,
x=11
},
{
class="static_wall",
color="green",
y=4,
x=1
},
{
class="static_wall",
color="green",
y=4,
x=11
},
{
class="static_wall",
color="green",
y=5,
x=1
},
{
class="static_wall",
color="green",
y=5,
x=11
},
{
class="static_wall",
color="green",
y=6,
x=1
},
{
class="static_wall",
color="green",
y=6,
x=11
},
{
class="static_wall",
color="green",
y=7,
x=1
},
{
class="static_wall",
color="green",
y=7,
x=11
},
{
class="static_wall",
color="green",
y=8,
x=1
},
{
class="static_wall",
color="green",
y=8,
x=11
},
{
class="static_wall",
color="green",
y=9,
x=1
},
{
class="static_wall",
color="green",
y=9,
x=11
},
{
class="static_wall",
color="green",
y=10,
x=1
},
{
class="static_wall",
color="green",
y=10,
x=11
},
{
class="static_wall",
color="green",
y=11,
x=1
},
{
class="static_wall",
color="green",
y=11,
x=2
},
{
class="static_wall",
color="green",
y=11,
x=3
},
{
class="static_wall",
color="green",
y=11,
x=4
},
{
class="static_wall",
color="green",
y=11,
x=5
},
{
class="static_wall",
color="green",
y=11,
x=7
},
{
class="static_wall",
color="green",
y=11,
x=8
},
{
class="static_wall",
color="green",
y=11,
x=9
},
{
class="static_wall",
color="green",
y=11,
x=10
},
{
class="static_wall",
color="green",
y=11,
x=11
x=11,
y=11
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,24 @@ objects={
x=6,
color="green"
},
{
y=11,
class="player",
x=6,
color="blue"
},
{
y=11,
class="player",
x=6,
color="red"
},
{
y=11,
class="player",
x=6,
color="green"
},
{
y=1,
class="static_wall",
@ -1189,4 +1207,4 @@ objects={
color="red"
}
}
}
}

View file

@ -1,5 +1,23 @@
{
objects={
{
y=2,
class="player",
x=6,
color="blue"
},
{
y=3,
class="player",
x=2,
color="red"
},
{
y=3,
class="player",
x=7,
color="green"
},
{
y=1,
class="static_wall",
@ -991,4 +1009,4 @@ objects={
color="green"
}
}
}
}

View file

@ -1,157 +1,146 @@
{
width=18,
height=18,
rooms={
{
name="start",
y=11,
x=9
x=9,
name="start"
},
{
name="3",
y=10,
x=10
x=10,
name="3"
},
{
name="5",
y=10,
x=12
x=11,
name="4"
},
{
y=10,
x=12,
name="5"
},
{
name="village_1",
y=9,
x=12
x=12,
name="village_1"
},
{
name="village_2",
y=8,
x=12
x=12,
name="village_2"
},
{
name="village_3",
y=8,
x=13
x=13,
name="village_3"
},
{
name="village_4",
y=8,
x=11
x=11,
name="village_4"
},
{
name="yellow_1",
y=7,
x=11
x=11,
name="yellow_1"
},
{
name="yellow_3",
y=6,
x=11
x=11,
name="yellow_3"
},
{
name="yellow_2",
y=7,
x=12
x=12,
name="yellow_2"
},
{
name="yellow_4",
y=5,
x=11
x=11,
name="yellow_4"
},
{
name="yellow_5",
y=4,
x=11
x=11,
name="yellow_5"
},
{
name="yellow_6",
y=5,
x=10
x=10,
name="yellow_6"
},
{
name="default",
y=1,
x=1
x=1,
name="default"
},
{
name="old_yellow_3",
y=1,
x=3
x=3,
name="old_yellow_3"
},
{
name="test_patt",
y=1,
x=5
x=5,
name="test_patt"
},
{
name="test_patt_update",
y=1,
x=7
x=7,
name="test_patt_update"
},
{
name="white_asymmetrical",
y=1,
x=9
x=9,
name="white_asymmetrical"
},
{
name="white_ok",
y=1,
x=11
x=11,
name="white_ok"
},
{
name="white_primary",
y=1,
x=13
x=13,
name="white_primary"
},
{
name="yellow_arches",
y=1,
x=15
x=15,
name="yellow_arches"
},
{
name="yellow_asymmetrical",
y=1,
x=17
x=17,
name="yellow_asymmetrical"
},
{
name="yellow_cc_moustache",
y=3,
x=1
x=1,
name="yellow_cc_moustache"
},
{
name="yellow_color_changing_levvel_1",
y=3,
x=3
x=3,
name="yellow_color_changing_levvel_1"
},
{
name="yellow_smile",
y=3,
x=7
x=5,
name="yellow_skull"
},
{
y=3,
x=7,
name="yellow_smile"
},
{
name="2",
y=10,
x=9
},
{
name="room_11_09",
y=9,
x=11
},
{
name="yellow_skull",
y=3,
x=9
},
{
name="jorge_room",
y=5,
x=5
},
{
name="4",
y=10,
x=11
x=9,
name="2"
}
},
lastRoom="yellow_4",
width=18,
height=18
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,18 @@ objects={
x=13,
color="blue"
},
{
y=11,
class="player",
x=6,
color="red"
},
{
y=11,
class="player",
x=6,
color="green"
},
{
y=1,
class="static_wall",
@ -1027,4 +1039,4 @@ objects={
color="green"
}
}
}
}

View file

@ -12,6 +12,12 @@ objects={
x=6,
color="red"
},
{
y=11,
class="player",
x=6,
color="green"
},
{
y=1,
class="static_wall",
@ -973,4 +979,4 @@ objects={
color="red"
}
}
}
}

View file

@ -1,8 +0,0 @@
yo wassup
* kick the sign
it doesnt budge.
* kiss the sign
You step away, a little embarrassed.
* [say nothing] you have nothing to say to a sign.
- -> END

View file

@ -1 +0,0 @@
return {["version"]={["engine"]=2,["tree"]=1},["inclusions"]={},["lists"]={},["tree"]={["_"]={["_"]={"yo wassup",{["node"]={"it doesnt budge."},["text"]="kick the sign",["choice"]="kick the sign"},{["node"]={"You step away, a little embarrassed."},["text"]="kiss the sign",["choice"]="kiss the sign"},{["text"]="you have nothing to say to a sign.",["choice"]="say nothing"},{["divert"]={["path"]="END",["tunnel"]=false}}}}},["constants"]={},["params"]={},["variables"]={}}

View file

@ -1,31 +0,0 @@
-- Compile Ink source (stories/*.ink) into the pre-parsed .lua books the game
-- loads. LÖVE ships no lpeg, so parsing happens here, once, at author time;
-- the game only ever requires the compiled .lua. Needs a standalone `lua` with
-- lpeg installed (Arch: pacman -S lua-lpeg).
--
-- Run from the project root:
-- lua tools/compile_ink.lua # compile every stories/*.ink
-- lua tools/compile_ink.lua start_sign # compile stories/start_sign.ink
-- lua tools/compile_ink.lua stories/foo.ink # explicit path
package.path = package.path .. ";libs/narrator/?.lua"
local narrator = require("narrator.narrator")
local function compile(path)
narrator.parse_file(path, { save = true })
print("compiled " .. path .. " -> " .. (path:gsub("%.ink$", "") .. ".lua"))
end
local targets = {}
if #arg == 0 then
local ls = io.popen("ls stories/*.ink 2>/dev/null")
for line in ls:lines() do table.insert(targets, line) end
ls:close()
else
for _, a in ipairs(arg) do
table.insert(targets, a:match("%.ink$") and a or ("stories/" .. a .. ".ink"))
end
end
assert(#targets > 0, "no .ink files to compile")
for _, t in ipairs(targets) do compile(t) end

View file

@ -1,29 +0,0 @@
#!/usr/bin/env sh
# Delete Chroma Solstice's *runtime* LÖVE save data. Project levels in
# ./rooms are not affected.
set -eu
DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"}
SAVE_DIR="$DATA_HOME/love/chroma-solstice"
if [ "${1:-}" != "--yes" ]; then
printf '%s\n' "This deletes only the local LÖVE runtime saves at:"
printf '%s\n' " $SAVE_DIR"
printf '%s\n' "Repository files, including ./rooms/*.sav, are not touched."
printf '%s\n' "Run: $0 --yes"
exit 1
fi
if [ ! -e "$SAVE_DIR" ]; then
printf '%s\n' "No local LÖVE saves found at: $SAVE_DIR"
exit 0
fi
# Keep the destructive target exact even if this script is changed later.
case "$SAVE_DIR" in
"$DATA_HOME/love/chroma-solstice") ;;
*) printf '%s\n' "Refusing unexpected save path: $SAVE_DIR" >&2; exit 2 ;;
esac
rm -rf -- "$SAVE_DIR"
printf '%s\n' "Deleted local LÖVE saves: $SAVE_DIR"

View file

@ -8,7 +8,6 @@ function World:initialize()
self.rooms = {}
self.roomByName = {}
self.previewCache = {}
self.lastRoom = nil
self:load()
end
@ -27,7 +26,6 @@ function World:load()
if not raw then return end
local ok, save = pcall(TSerial.unpack, raw)
if not ok or type(save) ~= "table" then return end
self.lastRoom = save.lastRoom
for _, room in ipairs(save.rooms or {}) do
if room.name and self:isInBounds(room.x, room.y) then
self:_putRoom(room.name, room.x, room.y)
@ -40,7 +38,7 @@ function World:save()
for _, room in ipairs(self.rooms) do
table.insert(rooms, { name = room.name, x = room.x, y = room.y })
end
return writeToSource("rooms/world.sav", TSerial.pack({ width = self.width, height = self.height, lastRoom = self.lastRoom, rooms = rooms }, nil, true))
return writeToSource("rooms/world.sav", TSerial.pack({ width = self.width, height = self.height, rooms = rooms }, nil, true))
end
function World:isInBounds(x, y)
@ -89,7 +87,6 @@ function World:removeRoom(x, y)
if room.x == x and room.y == y then
self.roomByName[room.name] = nil
table.remove(self.rooms, i)
if self.lastRoom == room.name then self.lastRoom = nil end
return self:save(), room.name
end
end
@ -102,20 +99,11 @@ function World:renameRoom(oldName, newName)
self.roomByName[oldName] = nil
room.name = newName
self.roomByName[newName] = room
if self.lastRoom == oldName then self.lastRoom = newName end
self:invalidatePreview(oldName)
self:invalidatePreview(newName)
return self:save()
end
function World:setLastRoom(name)
if name and self.lastRoom ~= name then
self.lastRoom = name
return self:save()
end
return true
end
function World:getNeighbor(name, exit)
local room = self:roomLocation(name)
if not room then return nil end
@ -156,17 +144,12 @@ function World:drawRoomPreview(name, x, y, size)
love.graphics.setScissor(x, y, size, size)
for _, object in ipairs(room.objects or {}) do
if object.class == "npc" then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setColor(255, 255, 255, 255)
love.graphics.print("?", x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
else
local props = globalAssetProperties[object.class]
if props and props.sprites then
local colors
if object.class == "door" then
colors = object.color and { object.color } or { "red", "green", "blue" }
else
colors = { object.color }
end
local colors = object.class == "door" and { "red", "green", "blue" } or { object.color }
for _, color in ipairs(colors) do
local path = props.sprites[color]
if path then
@ -182,14 +165,14 @@ end
function World:draw()
local cell = width / self.width
love.graphics.setColor(12 / 255, 12 / 255, 20 / 255, 1)
love.graphics.setColor(12, 12, 20, 255)
love.graphics.rectangle("fill", 0, 0, width, height)
for y = 1, self.height do
for x = 1, self.width do
local screenX, screenY = (x - 1) * cell, (y - 1) * cell
local name = self:roomAt(x, y)
if name then self:drawRoomPreview(name, screenX, screenY, cell) end
love.graphics.setColor(name and 1 or 70 / 255, name and 1 or 70 / 255, name and 1 or 90 / 255, 1)
love.graphics.setColor(name and 255 or 70, name and 255 or 70, name and 255 or 90, 255)
love.graphics.rectangle("line", screenX, screenY, cell, cell)
end
end