add dialog
This commit is contained in:
parent
c92c0f6ff7
commit
5413823b26
13 changed files with 390 additions and 32 deletions
11
dialogable.lua
Normal file
11
dialogable.lua
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- 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
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
-- 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
|
||||||
84
docs/adding-npcs.md
Normal file
84
docs/adding-npcs.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# 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).
|
||||||
|
|
@ -16,7 +16,9 @@ function Entity:initialize(t)
|
||||||
self.collision = t.collision --"none", "moveable", "unmoveable"
|
self.collision = t.collision --"none", "moveable", "unmoveable"
|
||||||
self.sprite = repSprite
|
self.sprite = repSprite
|
||||||
self.shaAmount = t.sha or 1
|
self.shaAmount = t.sha or 1
|
||||||
self.onBump = t.onBump or function(self) return true end
|
-- 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.tick = t.tick or function(self) return true end
|
self.tick = t.tick or function(self) return true end
|
||||||
|
|
||||||
self.drawOffset = {x = 0, y = 0}
|
self.drawOffset = {x = 0, y = 0}
|
||||||
|
|
@ -163,6 +165,10 @@ function Entity:sha()
|
||||||
return math.random(-worldSha, worldSha) * .125 * drawScale
|
return math.random(-worldSha, worldSha) * .125 * drawScale
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Entity:onBump()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
function Entity:bump()
|
function Entity:bump()
|
||||||
return self:onBump()
|
return self:onBump()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
1
libs/narrator
Submodule
1
libs/narrator
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 45a8b03553f2385716c24ec762757e418b2b73c1
|
||||||
15
main.lua
15
main.lua
|
|
@ -15,6 +15,12 @@ require "world_seed"
|
||||||
require "shaders"
|
require "shaders"
|
||||||
require "level_editor/editor"
|
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()
|
function love.load()
|
||||||
globalAssetProperties = {} -- rebuilt from the filesystem (art/ + .meta) by getAllAssets
|
globalAssetProperties = {} -- rebuilt from the filesystem (art/ + .meta) by getAllAssets
|
||||||
|
|
||||||
|
|
@ -56,7 +62,7 @@ function love.update(dt)
|
||||||
Input:stopMoveRepeat()
|
Input:stopMoveRepeat()
|
||||||
else
|
else
|
||||||
local repeatedMove = Input:handler(dt)
|
local repeatedMove = Input:handler(dt)
|
||||||
if repeatedMove then currentRoom:movePlayer(repeatedMove) end
|
if repeatedMove and not Dialogue.isOpen() then currentRoom:movePlayer(repeatedMove) end
|
||||||
end
|
end
|
||||||
currentRoom:update(dt)
|
currentRoom:update(dt)
|
||||||
|
|
||||||
|
|
@ -106,9 +112,16 @@ function love.draw()
|
||||||
love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y)
|
love.graphics.draw(sideBarCanvas, sideBarBox.x, sideBarBox.y)
|
||||||
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
love.graphics.print("Current FPS: "..tostring(love.timer.getFPS( )), 10, 10)
|
||||||
|
|
||||||
|
Dialogue.draw()
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.keypressed(key)
|
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
|
if editorView ~= "world" and Input.isPlayerMove(key) then
|
||||||
Input:beginMove(key)
|
Input:beginMove(key)
|
||||||
currentRoom:movePlayer(key)
|
currentRoom:movePlayer(key)
|
||||||
|
|
|
||||||
28
npc.lua
28
npc.lua
|
|
@ -1,14 +1,22 @@
|
||||||
npc = class("npc", Entity)
|
require "entity"
|
||||||
|
require "dialogable"
|
||||||
|
|
||||||
function npc:initialize(x, y, color, name)
|
-- 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
|
||||||
Entity.initialize(self, {
|
Entity.initialize(self, {
|
||||||
x = x,
|
x = x, y = y,
|
||||||
y = y,
|
color = "red", -- representative only; sprites are per-channel
|
||||||
color = color,
|
|
||||||
collision = "unmoveable",
|
collision = "unmoveable",
|
||||||
sprite = sprites.npc[name][color],
|
sprites = {
|
||||||
onBump = function(self)
|
red = loadSprite("art/npc/start_sign_r.png"),
|
||||||
|
green = loadSprite("art/npc/start_sign_g.png"),
|
||||||
end,
|
blue = loadSprite("art/npc/start_sign_b.png"),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
||||||
30
room.lua
30
room.lua
|
|
@ -120,19 +120,11 @@ function Room:draw()
|
||||||
-- draw only the channels the player has. Each object just exposes sprites per
|
-- 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
|
-- 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.
|
-- naturally draws its red sprite on the red pass, etc. No per-object logic.
|
||||||
local has = {}
|
|
||||||
for _, channel in ipairs(self.colorsPlayerHas) do
|
for _, channel in ipairs(self.colorsPlayerHas) do
|
||||||
has[channel] = true
|
|
||||||
if self.player[channel] then self.player[channel]:draw(channel) end
|
if self.player[channel] then self.player[channel]:draw(channel) end
|
||||||
for _, entity in pairs(self.collideables[channel]) do entity: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 _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
|
||||||
end
|
for _, npc in ipairs(self.npcs) do npc:draw(channel) 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 1 or 0, has.green and 1 or 0, has.blue and 1 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -140,6 +132,14 @@ function Room:createCollideablesMatrix()
|
||||||
for color, entities in pairs(self.collideables) do
|
for color, entities in pairs(self.collideables) do
|
||||||
self.collidableMatrices[color] = self:createEntityMatrix(entities)
|
self.collidableMatrices[color] = self:createEntityMatrix(entities)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
function Room:createSwitchMatrix()
|
function Room:createSwitchMatrix()
|
||||||
|
|
@ -541,8 +541,9 @@ end
|
||||||
-- keyword handled directly since it has no art.
|
-- keyword handled directly since it has no art.
|
||||||
function Room:createEntity(class, color, x, y)
|
function Room:createEntity(class, color, x, y)
|
||||||
if class == "npc" then
|
if class == "npc" then
|
||||||
self:registerNpc(x, y)
|
local entity = Sign:new(x, y)
|
||||||
return
|
self:registerNpc(entity)
|
||||||
|
return entity
|
||||||
end
|
end
|
||||||
|
|
||||||
local props = globalAssetProperties and globalAssetProperties[class]
|
local props = globalAssetProperties and globalAssetProperties[class]
|
||||||
|
|
@ -602,8 +603,11 @@ function Room:registerPlayer(entityPointer, color)
|
||||||
self.player[color] = entityPointer
|
self.player[color] = entityPointer
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:registerNpc(x, y)
|
function Room:registerNpc(entity)
|
||||||
table.insert(self.npcs, { x = x, y = y })
|
-- 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)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:getCellFromMousePos(mouseX, mouseY)
|
function Room:getCellFromMousePos(mouseX, mouseY)
|
||||||
|
|
|
||||||
76
rooms/jorge_room.sav
Normal file
76
rooms/jorge_room.sav
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -135,18 +135,23 @@ rooms={
|
||||||
y=9,
|
y=9,
|
||||||
x=11
|
x=11
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name="4",
|
|
||||||
y=3,
|
|
||||||
x=5
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name="yellow_skull",
|
name="yellow_skull",
|
||||||
y=3,
|
y=3,
|
||||||
x=9
|
x=9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name="jorge_room",
|
||||||
|
y=5,
|
||||||
|
x=5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name="4",
|
||||||
|
y=10,
|
||||||
|
x=11
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lastRoom="start",
|
lastRoom="yellow_4",
|
||||||
width=18,
|
width=18,
|
||||||
height=18
|
height=18
|
||||||
}
|
}
|
||||||
8
stories/start_sign.ink
Normal file
8
stories/start_sign.ink
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
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
|
||||||
1
stories/start_sign.lua
Normal file
1
stories/start_sign.lua
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
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"]={}}
|
||||||
31
tools/compile_ink.lua
Normal file
31
tools/compile_ink.lua
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue