Lua works; narrator works
This commit is contained in:
37
lua-scripts/narrator/annotations.lua
Normal file
37
lua-scripts/narrator/annotations.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
---@class Narrator.Book.Version
|
||||
---@field engine number
|
||||
---@field tree number
|
||||
|
||||
---@class Narrator.Book
|
||||
---@field version Narrator.Book.Version
|
||||
---@field inclusions string[]
|
||||
---@field lists table
|
||||
---@field constants table
|
||||
---@field variables table
|
||||
---@field params table
|
||||
---@field tree table
|
||||
|
||||
---@class Narrator.ParsingParams
|
||||
---@field save boolean Save a parsed book to the lua file
|
||||
|
||||
---@class Narrator.Paragraph
|
||||
---@field text string
|
||||
---@field tags string[]|nil
|
||||
|
||||
---@class Narrator.Choice
|
||||
---@field text string
|
||||
---@field tags string[]|nil
|
||||
|
||||
---@class Narrator.State
|
||||
---@field version number
|
||||
---@field temp table
|
||||
---@field seeds table
|
||||
---@field variables table
|
||||
---@field params table|nil
|
||||
---@field visits table
|
||||
---@field current_path table
|
||||
---@field paragraphs table
|
||||
---@field choices table
|
||||
---@field output table
|
||||
---@field tunnels table|nil
|
||||
---@field path table
|
||||
32
lua-scripts/narrator/enums.lua
Normal file
32
lua-scripts/narrator/enums.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
local enums = {
|
||||
|
||||
---Bump it when the state structure is changed
|
||||
engine_version = 2,
|
||||
|
||||
---@enum Narrator.ItemType
|
||||
item = {
|
||||
text = 1,
|
||||
alts = 2,
|
||||
choice = 3,
|
||||
condition = 4,
|
||||
variable = 5
|
||||
},
|
||||
|
||||
---@enum Narrator.Sequence
|
||||
sequence = {
|
||||
cycle = 1,
|
||||
stopping = 2,
|
||||
once = 3
|
||||
},
|
||||
|
||||
---@enum Narrator.ReadMode
|
||||
read_mode = {
|
||||
text = 1,
|
||||
choices = 2,
|
||||
gathers = 3,
|
||||
quit = 4
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return enums
|
||||
68
lua-scripts/narrator/libs/classic.lua
Executable file
68
lua-scripts/narrator/libs/classic.lua
Executable file
@@ -0,0 +1,68 @@
|
||||
--
|
||||
-- classic
|
||||
--
|
||||
-- Copyright (c) 2014, rxi
|
||||
--
|
||||
-- This module is free software; you can redistribute it and/or modify it under
|
||||
-- the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
|
||||
local Object = {}
|
||||
Object.__index = Object
|
||||
|
||||
|
||||
function Object:new()
|
||||
end
|
||||
|
||||
|
||||
function Object:extend()
|
||||
local cls = {}
|
||||
for k, v in pairs(self) do
|
||||
if k:find("__") == 1 then
|
||||
cls[k] = v
|
||||
end
|
||||
end
|
||||
cls.__index = cls
|
||||
cls.super = self
|
||||
setmetatable(cls, self)
|
||||
return cls
|
||||
end
|
||||
|
||||
|
||||
function Object:implement(...)
|
||||
for _, cls in pairs({...}) do
|
||||
for k, v in pairs(cls) do
|
||||
if self[k] == nil and type(v) == "function" then
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Object:is(T)
|
||||
local mt = getmetatable(self)
|
||||
while mt do
|
||||
if mt == T then
|
||||
return true
|
||||
end
|
||||
mt = getmetatable(mt)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function Object:__tostring()
|
||||
return "Object"
|
||||
end
|
||||
|
||||
|
||||
function Object:__call(...)
|
||||
local obj = setmetatable({}, self)
|
||||
obj:new(...)
|
||||
return obj
|
||||
end
|
||||
|
||||
|
||||
return Object
|
||||
780
lua-scripts/narrator/libs/lume.lua
Executable file
780
lua-scripts/narrator/libs/lume.lua
Executable file
@@ -0,0 +1,780 @@
|
||||
--
|
||||
-- lume
|
||||
--
|
||||
-- Copyright (c) 2020 rxi
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
-- this software and associated documentation files (the "Software"), to deal in
|
||||
-- the Software without restriction, including without limitation the rights to
|
||||
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||||
-- so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
--
|
||||
|
||||
local lume = { _version = "2.3.0" }
|
||||
|
||||
local pairs, ipairs = pairs, ipairs
|
||||
local type, assert, unpack = type, assert, unpack or table.unpack
|
||||
local tostring, tonumber = tostring, tonumber
|
||||
local math_floor = math.floor
|
||||
local math_ceil = math.ceil
|
||||
local math_atan2 = math.atan2 or math.atan
|
||||
local math_sqrt = math.sqrt
|
||||
local math_abs = math.abs
|
||||
|
||||
local noop = function()
|
||||
end
|
||||
|
||||
local identity = function(x)
|
||||
return x
|
||||
end
|
||||
|
||||
local patternescape = function(str)
|
||||
return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
|
||||
end
|
||||
|
||||
local absindex = function(len, i)
|
||||
return i < 0 and (len + i + 1) or i
|
||||
end
|
||||
|
||||
local iscallable = function(x)
|
||||
if type(x) == "function" then return true end
|
||||
local mt = getmetatable(x)
|
||||
return mt and mt.__call ~= nil
|
||||
end
|
||||
|
||||
local getiter = function(x)
|
||||
if lume.isarray(x) then
|
||||
return ipairs
|
||||
elseif type(x) == "table" then
|
||||
return pairs
|
||||
end
|
||||
error("expected table", 3)
|
||||
end
|
||||
|
||||
local iteratee = function(x)
|
||||
if x == nil then return identity end
|
||||
if iscallable(x) then return x end
|
||||
if type(x) == "table" then
|
||||
return function(z)
|
||||
for k, v in pairs(x) do
|
||||
if z[k] ~= v then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
return function(z) return z[x] end
|
||||
end
|
||||
|
||||
|
||||
|
||||
function lume.clamp(x, min, max)
|
||||
return x < min and min or (x > max and max or x)
|
||||
end
|
||||
|
||||
|
||||
function lume.round(x, increment)
|
||||
if increment then return lume.round(x / increment) * increment end
|
||||
return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
|
||||
end
|
||||
|
||||
|
||||
function lume.sign(x)
|
||||
return x < 0 and -1 or 1
|
||||
end
|
||||
|
||||
|
||||
function lume.lerp(a, b, amount)
|
||||
return a + (b - a) * lume.clamp(amount, 0, 1)
|
||||
end
|
||||
|
||||
|
||||
function lume.smooth(a, b, amount)
|
||||
local t = lume.clamp(amount, 0, 1)
|
||||
local m = t * t * (3 - 2 * t)
|
||||
return a + (b - a) * m
|
||||
end
|
||||
|
||||
|
||||
function lume.pingpong(x)
|
||||
return 1 - math_abs(1 - x % 2)
|
||||
end
|
||||
|
||||
|
||||
function lume.distance(x1, y1, x2, y2, squared)
|
||||
local dx = x1 - x2
|
||||
local dy = y1 - y2
|
||||
local s = dx * dx + dy * dy
|
||||
return squared and s or math_sqrt(s)
|
||||
end
|
||||
|
||||
|
||||
function lume.angle(x1, y1, x2, y2)
|
||||
return math_atan2(y2 - y1, x2 - x1)
|
||||
end
|
||||
|
||||
|
||||
function lume.vector(angle, magnitude)
|
||||
return math.cos(angle) * magnitude, math.sin(angle) * magnitude
|
||||
end
|
||||
|
||||
|
||||
function lume.random(a, b)
|
||||
if not a then a, b = 0, 1 end
|
||||
if not b then b = 0 end
|
||||
return a + math.random() * (b - a)
|
||||
end
|
||||
|
||||
|
||||
function lume.randomchoice(t)
|
||||
return t[math.random(#t)]
|
||||
end
|
||||
|
||||
|
||||
function lume.weightedchoice(t)
|
||||
local sum = 0
|
||||
for _, v in pairs(t) do
|
||||
assert(v >= 0, "weight value less than zero")
|
||||
sum = sum + v
|
||||
end
|
||||
assert(sum ~= 0, "all weights are zero")
|
||||
local rnd = lume.random(sum)
|
||||
for k, v in pairs(t) do
|
||||
if rnd < v then return k end
|
||||
rnd = rnd - v
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lume.isarray(x)
|
||||
return type(x) == "table" and x[1] ~= nil
|
||||
end
|
||||
|
||||
|
||||
function lume.push(t, ...)
|
||||
local n = select("#", ...)
|
||||
for i = 1, n do
|
||||
t[#t + 1] = select(i, ...)
|
||||
end
|
||||
return ...
|
||||
end
|
||||
|
||||
|
||||
function lume.remove(t, x)
|
||||
local iter = getiter(t)
|
||||
for i, v in iter(t) do
|
||||
if v == x then
|
||||
if lume.isarray(t) then
|
||||
table.remove(t, i)
|
||||
break
|
||||
else
|
||||
t[i] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return x
|
||||
end
|
||||
|
||||
|
||||
function lume.clear(t)
|
||||
local iter = getiter(t)
|
||||
for k in iter(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function lume.extend(t, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
local x = select(i, ...)
|
||||
if x then
|
||||
for k, v in pairs(x) do
|
||||
t[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function lume.shuffle(t)
|
||||
local rtn = {}
|
||||
for i = 1, #t do
|
||||
local r = math.random(i)
|
||||
if r ~= i then
|
||||
rtn[i] = rtn[r]
|
||||
end
|
||||
rtn[r] = t[i]
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.sort(t, comp)
|
||||
local rtn = lume.clone(t)
|
||||
if comp then
|
||||
if type(comp) == "string" then
|
||||
table.sort(rtn, function(a, b) return a[comp] < b[comp] end)
|
||||
else
|
||||
table.sort(rtn, comp)
|
||||
end
|
||||
else
|
||||
table.sort(rtn)
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.array(...)
|
||||
local t = {}
|
||||
for x in ... do t[#t + 1] = x end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function lume.each(t, fn, ...)
|
||||
local iter = getiter(t)
|
||||
if type(fn) == "string" then
|
||||
for _, v in iter(t) do v[fn](v, ...) end
|
||||
else
|
||||
for _, v in iter(t) do fn(v, ...) end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function lume.map(t, fn)
|
||||
fn = iteratee(fn)
|
||||
local iter = getiter(t)
|
||||
local rtn = {}
|
||||
for k, v in iter(t) do rtn[k] = fn(v) end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.all(t, fn)
|
||||
fn = iteratee(fn)
|
||||
local iter = getiter(t)
|
||||
for _, v in iter(t) do
|
||||
if not fn(v) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function lume.any(t, fn)
|
||||
fn = iteratee(fn)
|
||||
local iter = getiter(t)
|
||||
for _, v in iter(t) do
|
||||
if fn(v) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function lume.reduce(t, fn, first)
|
||||
local started = first ~= nil
|
||||
local acc = first
|
||||
local iter = getiter(t)
|
||||
for _, v in iter(t) do
|
||||
if started then
|
||||
acc = fn(acc, v)
|
||||
else
|
||||
acc = v
|
||||
started = true
|
||||
end
|
||||
end
|
||||
assert(started, "reduce of an empty table with no first value")
|
||||
return acc
|
||||
end
|
||||
|
||||
|
||||
function lume.unique(t)
|
||||
local rtn = {}
|
||||
for k in pairs(lume.invert(t)) do
|
||||
rtn[#rtn + 1] = k
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.filter(t, fn, retainkeys)
|
||||
fn = iteratee(fn)
|
||||
local iter = getiter(t)
|
||||
local rtn = {}
|
||||
if retainkeys then
|
||||
for k, v in iter(t) do
|
||||
if fn(v) then rtn[k] = v end
|
||||
end
|
||||
else
|
||||
for _, v in iter(t) do
|
||||
if fn(v) then rtn[#rtn + 1] = v end
|
||||
end
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.reject(t, fn, retainkeys)
|
||||
fn = iteratee(fn)
|
||||
local iter = getiter(t)
|
||||
local rtn = {}
|
||||
if retainkeys then
|
||||
for k, v in iter(t) do
|
||||
if not fn(v) then rtn[k] = v end
|
||||
end
|
||||
else
|
||||
for _, v in iter(t) do
|
||||
if not fn(v) then rtn[#rtn + 1] = v end
|
||||
end
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.merge(...)
|
||||
local rtn = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local t = select(i, ...)
|
||||
local iter = getiter(t)
|
||||
for k, v in iter(t) do
|
||||
rtn[k] = v
|
||||
end
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.concat(...)
|
||||
local rtn = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local t = select(i, ...)
|
||||
if t ~= nil then
|
||||
local iter = getiter(t)
|
||||
for _, v in iter(t) do
|
||||
rtn[#rtn + 1] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.find(t, value)
|
||||
local iter = getiter(t)
|
||||
for k, v in iter(t) do
|
||||
if v == value then return k end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
function lume.match(t, fn)
|
||||
fn = iteratee(fn)
|
||||
local iter = getiter(t)
|
||||
for k, v in iter(t) do
|
||||
if fn(v) then return v, k end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
function lume.count(t, fn)
|
||||
local count = 0
|
||||
local iter = getiter(t)
|
||||
if fn then
|
||||
fn = iteratee(fn)
|
||||
for _, v in iter(t) do
|
||||
if fn(v) then count = count + 1 end
|
||||
end
|
||||
else
|
||||
if lume.isarray(t) then
|
||||
return #t
|
||||
end
|
||||
for _ in iter(t) do count = count + 1 end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
|
||||
function lume.slice(t, i, j)
|
||||
i = i and absindex(#t, i) or 1
|
||||
j = j and absindex(#t, j) or #t
|
||||
local rtn = {}
|
||||
for x = i < 1 and 1 or i, j > #t and #t or j do
|
||||
rtn[#rtn + 1] = t[x]
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.first(t, n)
|
||||
if not n then return t[1] end
|
||||
return lume.slice(t, 1, n)
|
||||
end
|
||||
|
||||
|
||||
function lume.last(t, n)
|
||||
if not n then return t[#t] end
|
||||
return lume.slice(t, -n, -1)
|
||||
end
|
||||
|
||||
|
||||
function lume.invert(t)
|
||||
local rtn = {}
|
||||
for k, v in pairs(t) do rtn[v] = k end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.pick(t, ...)
|
||||
local rtn = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local k = select(i, ...)
|
||||
rtn[k] = t[k]
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.keys(t)
|
||||
local rtn = {}
|
||||
local iter = getiter(t)
|
||||
for k in iter(t) do rtn[#rtn + 1] = k end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.clone(t)
|
||||
local rtn = {}
|
||||
for k, v in pairs(t) do rtn[k] = v end
|
||||
return rtn
|
||||
end
|
||||
|
||||
|
||||
function lume.fn(fn, ...)
|
||||
assert(iscallable(fn), "expected a function as the first argument")
|
||||
local args = { ... }
|
||||
return function(...)
|
||||
local a = lume.concat(args, { ... })
|
||||
return fn(unpack(a))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lume.once(fn, ...)
|
||||
local f = lume.fn(fn, ...)
|
||||
local done = false
|
||||
return function(...)
|
||||
if done then return end
|
||||
done = true
|
||||
return f(...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local memoize_fnkey = {}
|
||||
local memoize_nil = {}
|
||||
|
||||
function lume.memoize(fn)
|
||||
local cache = {}
|
||||
return function(...)
|
||||
local c = cache
|
||||
for i = 1, select("#", ...) do
|
||||
local a = select(i, ...) or memoize_nil
|
||||
c[a] = c[a] or {}
|
||||
c = c[a]
|
||||
end
|
||||
c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
|
||||
return unpack(c[memoize_fnkey])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lume.combine(...)
|
||||
local n = select('#', ...)
|
||||
if n == 0 then return noop end
|
||||
if n == 1 then
|
||||
local fn = select(1, ...)
|
||||
if not fn then return noop end
|
||||
assert(iscallable(fn), "expected a function or nil")
|
||||
return fn
|
||||
end
|
||||
local funcs = {}
|
||||
for i = 1, n do
|
||||
local fn = select(i, ...)
|
||||
if fn ~= nil then
|
||||
assert(iscallable(fn), "expected a function or nil")
|
||||
funcs[#funcs + 1] = fn
|
||||
end
|
||||
end
|
||||
return function(...)
|
||||
for _, f in ipairs(funcs) do f(...) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lume.call(fn, ...)
|
||||
if fn then
|
||||
return fn(...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lume.time(fn, ...)
|
||||
local start = os.clock()
|
||||
local rtn = {fn(...)}
|
||||
return (os.clock() - start), unpack(rtn)
|
||||
end
|
||||
|
||||
|
||||
local lambda_cache = {}
|
||||
|
||||
function lume.lambda(str)
|
||||
if not lambda_cache[str] then
|
||||
local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
|
||||
assert(args and body, "bad string lambda")
|
||||
local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
|
||||
lambda_cache[str] = lume.dostring(s)
|
||||
end
|
||||
return lambda_cache[str]
|
||||
end
|
||||
|
||||
|
||||
local serialize
|
||||
|
||||
local serialize_map = {
|
||||
[ "boolean" ] = tostring,
|
||||
[ "nil" ] = tostring,
|
||||
[ "string" ] = function(v) return string.format("%q", v) end,
|
||||
[ "number" ] = function(v)
|
||||
if v ~= v then return "0/0" -- nan
|
||||
elseif v == 1 / 0 then return "1/0" -- inf
|
||||
elseif v == -1 / 0 then return "-1/0" end -- -inf
|
||||
return tostring(v)
|
||||
end,
|
||||
[ "table" ] = function(t, stk)
|
||||
stk = stk or {}
|
||||
if stk[t] then error("circular reference") end
|
||||
local rtn = {}
|
||||
stk[t] = true
|
||||
for k, v in pairs(t) do
|
||||
rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk)
|
||||
end
|
||||
stk[t] = nil
|
||||
return "{" .. table.concat(rtn, ",") .. "}"
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(serialize_map, {
|
||||
__index = function(_, k) error("unsupported serialize type: " .. k) end
|
||||
})
|
||||
|
||||
serialize = function(x, stk)
|
||||
return serialize_map[type(x)](x, stk)
|
||||
end
|
||||
|
||||
function lume.serialize(x)
|
||||
return serialize(x)
|
||||
end
|
||||
|
||||
|
||||
function lume.deserialize(str)
|
||||
return lume.dostring("return " .. str)
|
||||
end
|
||||
|
||||
|
||||
function lume.split(str, sep)
|
||||
if not sep then
|
||||
return lume.array(str:gmatch("([%S]+)"))
|
||||
else
|
||||
assert(sep ~= "", "empty separator")
|
||||
local psep = patternescape(sep)
|
||||
return lume.array((str..sep):gmatch("(.-)("..psep..")"))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lume.trim(str, chars)
|
||||
if not chars then return str:match("^[%s]*(.-)[%s]*$") end
|
||||
chars = patternescape(chars)
|
||||
return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
|
||||
end
|
||||
|
||||
|
||||
function lume.wordwrap(str, limit)
|
||||
limit = limit or 72
|
||||
local check
|
||||
if type(limit) == "number" then
|
||||
check = function(s) return #s >= limit end
|
||||
else
|
||||
check = limit
|
||||
end
|
||||
local rtn = {}
|
||||
local line = ""
|
||||
for word, spaces in str:gmatch("(%S+)(%s*)") do
|
||||
local s = line .. word
|
||||
if check(s) then
|
||||
table.insert(rtn, line .. "\n")
|
||||
line = word
|
||||
else
|
||||
line = s
|
||||
end
|
||||
for c in spaces:gmatch(".") do
|
||||
if c == "\n" then
|
||||
table.insert(rtn, line .. "\n")
|
||||
line = ""
|
||||
else
|
||||
line = line .. c
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(rtn, line)
|
||||
return table.concat(rtn)
|
||||
end
|
||||
|
||||
|
||||
function lume.format(str, vars)
|
||||
if not vars then return str end
|
||||
local f = function(x)
|
||||
return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
|
||||
end
|
||||
return (str:gsub("{(.-)}", f))
|
||||
end
|
||||
|
||||
|
||||
function lume.trace(...)
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
local t = { info.short_src .. ":" .. info.currentline .. ":" }
|
||||
for i = 1, select("#", ...) do
|
||||
local x = select(i, ...)
|
||||
if type(x) == "number" then
|
||||
x = string.format("%g", lume.round(x, .01))
|
||||
end
|
||||
t[#t + 1] = tostring(x)
|
||||
end
|
||||
print(table.concat(t, " "))
|
||||
end
|
||||
|
||||
|
||||
function lume.dostring(str)
|
||||
return assert((loadstring or load)(str))()
|
||||
end
|
||||
|
||||
|
||||
function lume.uuid()
|
||||
local fn = function(x)
|
||||
local r = math.random(16) - 1
|
||||
r = (x == "x") and (r + 1) or (r % 4) + 9
|
||||
return ("0123456789abcdef"):sub(r, r)
|
||||
end
|
||||
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
|
||||
end
|
||||
|
||||
|
||||
function lume.hotswap(modname)
|
||||
local oldglobal = lume.clone(_G)
|
||||
local updated = {}
|
||||
local function update(old, new)
|
||||
if updated[old] then return end
|
||||
updated[old] = true
|
||||
local oldmt, newmt = getmetatable(old), getmetatable(new)
|
||||
if oldmt and newmt then update(oldmt, newmt) end
|
||||
for k, v in pairs(new) do
|
||||
if type(v) == "table" then update(old[k], v) else old[k] = v end
|
||||
end
|
||||
end
|
||||
local err = nil
|
||||
local function onerror(e)
|
||||
for k in pairs(_G) do _G[k] = oldglobal[k] end
|
||||
err = lume.trim(e)
|
||||
end
|
||||
local ok, oldmod = pcall(require, modname)
|
||||
oldmod = ok and oldmod or nil
|
||||
xpcall(function()
|
||||
package.loaded[modname] = nil
|
||||
local newmod = require(modname)
|
||||
if type(oldmod) == "table" then update(oldmod, newmod) end
|
||||
for k, v in pairs(oldglobal) do
|
||||
if v ~= _G[k] and type(v) == "table" then
|
||||
update(v, _G[k])
|
||||
_G[k] = v
|
||||
end
|
||||
end
|
||||
end, onerror)
|
||||
package.loaded[modname] = oldmod
|
||||
if err then return nil, err end
|
||||
return oldmod
|
||||
end
|
||||
|
||||
|
||||
local ripairs_iter = function(t, i)
|
||||
i = i - 1
|
||||
local v = t[i]
|
||||
if v ~= nil then
|
||||
return i, v
|
||||
end
|
||||
end
|
||||
|
||||
function lume.ripairs(t)
|
||||
return ripairs_iter, t, (#t + 1)
|
||||
end
|
||||
|
||||
|
||||
function lume.color(str, mul)
|
||||
mul = mul or 1
|
||||
local r, g, b, a
|
||||
r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
|
||||
if r then
|
||||
r = tonumber(r, 16) / 0xff
|
||||
g = tonumber(g, 16) / 0xff
|
||||
b = tonumber(b, 16) / 0xff
|
||||
a = 1
|
||||
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
|
||||
local f = str:gmatch("[%d.]+")
|
||||
r = (f() or 0) / 0xff
|
||||
g = (f() or 0) / 0xff
|
||||
b = (f() or 0) / 0xff
|
||||
a = f() or 1
|
||||
else
|
||||
error(("bad color string '%s'"):format(str))
|
||||
end
|
||||
return r * mul, g * mul, b * mul, a * mul
|
||||
end
|
||||
|
||||
|
||||
local chain_mt = {}
|
||||
chain_mt.__index = lume.map(lume.filter(lume, iscallable, true),
|
||||
function(fn)
|
||||
return function(self, ...)
|
||||
self._value = fn(self._value, ...)
|
||||
return self
|
||||
end
|
||||
end)
|
||||
chain_mt.__index.result = function(x) return x._value end
|
||||
|
||||
function lume.chain(value)
|
||||
return setmetatable({ _value = value }, chain_mt)
|
||||
end
|
||||
|
||||
setmetatable(lume, {
|
||||
__call = function(_, ...)
|
||||
return lume.chain(...)
|
||||
end
|
||||
})
|
||||
|
||||
|
||||
return lume
|
||||
401
lua-scripts/narrator/list/mt.lua
Normal file
401
lua-scripts/narrator/list/mt.lua
Normal file
@@ -0,0 +1,401 @@
|
||||
--
|
||||
-- Dependencies
|
||||
|
||||
local lume = require('narrator.libs.lume')
|
||||
|
||||
--
|
||||
-- Metatable
|
||||
|
||||
local mt = { lists = { } }
|
||||
|
||||
function mt.__tostring(self)
|
||||
local pool = { }
|
||||
|
||||
local list_keys = { }
|
||||
for key, _ in pairs(self) do
|
||||
table.insert(list_keys, key)
|
||||
end
|
||||
table.sort(list_keys)
|
||||
|
||||
for i = 1, #list_keys do
|
||||
local list_name = list_keys[i]
|
||||
local list_items = self[list_name]
|
||||
for index = 1, #mt.lists[list_name] do
|
||||
pool[index] = pool[index] or { }
|
||||
local item_name = mt.lists[list_name][index]
|
||||
if list_items[item_name] == true then
|
||||
table.insert(pool[index], 1, item_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local items = { }
|
||||
|
||||
for _, titles in ipairs(pool) do
|
||||
for _, title in ipairs(titles) do
|
||||
table.insert(items, title)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(items, ', ')
|
||||
end
|
||||
|
||||
--
|
||||
-- Operators
|
||||
|
||||
function mt.__add(lhs, rhs) -- +
|
||||
if type(rhs) == 'table' then
|
||||
return mt.__add_list(lhs, rhs)
|
||||
elseif type(rhs) == 'number' then
|
||||
return mt.__shift_by_number(lhs, rhs)
|
||||
else
|
||||
error('Attempt to sum the list with ' .. type(rhs))
|
||||
end
|
||||
end
|
||||
|
||||
function mt.__sub(lhs, rhs) -- -
|
||||
if type(rhs) == 'table' then
|
||||
return mt.__subList(lhs, rhs)
|
||||
elseif type(rhs) == 'number' then
|
||||
return mt.__shift_by_number(lhs, -rhs)
|
||||
else
|
||||
error('Attempt to sub the list with ' .. type(rhs))
|
||||
end
|
||||
end
|
||||
|
||||
function mt.__mod(lhs, rhs) -- % (contain)
|
||||
if type(rhs) ~= 'table' then
|
||||
error('Attempt to check content of the list for ' .. type(rhs))
|
||||
end
|
||||
|
||||
for list_name, list_items in pairs(rhs) do
|
||||
if lhs[list_name] == nil then return false end
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
if (lhs[list_name][item_name] or false) ~= item_value then return false end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function mt.__pow(lhs, rhs) -- ^ (intersection)
|
||||
if type(rhs) ~= 'table' then
|
||||
error('Attempt to interselect the list with ' .. type(rhs))
|
||||
end
|
||||
|
||||
local intersection = { }
|
||||
|
||||
for list_name, list_items in pairs(lhs) do
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
local left = lhs[list_name][item_name]
|
||||
local right = (rhs[list_name] or { })[item_name]
|
||||
if left == true and right == true then
|
||||
intersection[list_name] = intersection[list_name] or { }
|
||||
intersection[list_name][item_name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(intersection, mt)
|
||||
return intersection
|
||||
end
|
||||
|
||||
function mt.__len(self) -- #
|
||||
local len = 0
|
||||
|
||||
for list_name, list_items in pairs(self) do
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
if item_value == true then len = len + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
return len
|
||||
end
|
||||
|
||||
function mt.__eq(lhs, rhs) -- ==
|
||||
if type(rhs) ~= 'table' then
|
||||
error('Attempt to compare the list with ' .. type(rhs))
|
||||
end
|
||||
|
||||
local function keys_count(object)
|
||||
local count = 0
|
||||
for _, _ in pairs(object) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local left_lists_count = keys_count(lhs)
|
||||
local right_lists_count = keys_count(rhs)
|
||||
if left_lists_count ~= right_lists_count then
|
||||
return false
|
||||
end
|
||||
|
||||
for list_name, left_items in pairs(lhs) do
|
||||
local right_items = rhs[list_name]
|
||||
if right_items == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local left_items_count = keys_count(left_items)
|
||||
local right_items_count = keys_count(right_items)
|
||||
|
||||
if left_items_count ~= right_items_count then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return mt.__mod(lhs, rhs)
|
||||
end
|
||||
|
||||
function mt.__lt(lhs, rhs) -- <
|
||||
if type(rhs) ~= 'table' then
|
||||
error('Attempt to compare the list with ' .. type(rhs))
|
||||
end
|
||||
|
||||
-- LEFT < RIGHT means "the smallest value in RIGHT is bigger than the largest values in LEFT"
|
||||
|
||||
local minLeft = mt.min_value_of(lhs, true)
|
||||
local maxRight = mt.max_value_of(rhs, true)
|
||||
|
||||
return minLeft < maxRight
|
||||
end
|
||||
|
||||
function mt.__le(lhs, rhs) -- <=
|
||||
if type(rhs) ~= 'table' then
|
||||
error('Attempt to compare the list with ' .. type(rhs))
|
||||
end
|
||||
|
||||
-- LEFT => RIGHT means "the smallest value in RIGHT is at least the smallest value in LEFT,
|
||||
-- and the largest value in RIGHT is at least the largest value in LEFT".
|
||||
|
||||
local minRight = mt.min_value_of(rhs, true)
|
||||
local minLeft = mt.min_value_of(lhs, true)
|
||||
local maxRight = mt.max_value_of(rhs, true)
|
||||
local maxLeft = mt.max_value_of(lhs, true)
|
||||
|
||||
return minRight >= minLeft and maxRight >= maxLeft
|
||||
end
|
||||
|
||||
--
|
||||
-- Custom operators
|
||||
|
||||
function mt.__add_list(lhs, rhs)
|
||||
local result = lume.clone(lhs)
|
||||
|
||||
for list_name, list_items in pairs(rhs) do
|
||||
result[list_name] = result[list_name] or { }
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
result[list_name][item_name] = item_value
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function mt.__subList(lhs, rhs)
|
||||
local result = lume.clone(lhs)
|
||||
|
||||
for list_name, list_items in pairs(rhs) do
|
||||
if lhs[list_name] ~= nil then
|
||||
for item_name, _ in pairs(list_items) do
|
||||
lhs[list_name][item_name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return mt.remove_empties_in_list(result)
|
||||
end
|
||||
|
||||
function mt.__shift_by_number(list, number)
|
||||
local result = { }
|
||||
|
||||
for list_name, list_items in pairs(list) do
|
||||
result[list_name] = { }
|
||||
for index, item_name in ipairs(mt.lists[list_name]) do
|
||||
if list_items[item_name] == true then
|
||||
local nextItem = mt.lists[list_name][index + number]
|
||||
if nextItem ~= nil then
|
||||
result[list_name][nextItem] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return mt.remove_empties_in_list(result)
|
||||
end
|
||||
|
||||
--
|
||||
-- Helpers
|
||||
|
||||
function mt.remove_empties_in_list(list)
|
||||
local result = lume.clone(list)
|
||||
|
||||
for list_name, list_items in pairs(list) do
|
||||
if next(list_items) == nil then
|
||||
result[list_name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function mt.min_value_of(list, raw)
|
||||
local min_index = 0
|
||||
local min_value = { }
|
||||
|
||||
local list_keys = { }
|
||||
for key, _ in pairs(list) do
|
||||
table.insert(list_keys, key)
|
||||
end
|
||||
table.sort(list_keys)
|
||||
|
||||
for i = 1, #list_keys do
|
||||
local list_name = list_keys[i]
|
||||
local list_items = list[list_name]
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
if item_value == true then
|
||||
local index = lume.find(mt.lists[list_name], item_name)
|
||||
if index and index < min_index or min_index == 0 then
|
||||
min_index = index
|
||||
min_value = { [list_name] = { [item_name] = true } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return raw and min_index or min_value
|
||||
end
|
||||
|
||||
function mt.max_value_of(list, raw)
|
||||
local max_index = 0
|
||||
local max_value = { }
|
||||
|
||||
local list_keys = { }
|
||||
for key, _ in pairs(list) do
|
||||
table.insert(list_keys, key)
|
||||
end
|
||||
table.sort(list_keys)
|
||||
|
||||
for i = 1, #list_keys do
|
||||
local list_name = list_keys[i]
|
||||
local list_items = list[list_name]
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
if item_value == true then
|
||||
local index = lume.find(mt.lists[list_name], item_name)
|
||||
if index and index > max_index or max_index == 0 then
|
||||
max_index = index
|
||||
max_value = { [list_name] = { [item_name] = true } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return raw and max_index or max_value
|
||||
end
|
||||
|
||||
function mt.random_value_of(list)
|
||||
local items = { }
|
||||
|
||||
local list_keys = { }
|
||||
for key, _ in pairs(list) do
|
||||
table.insert(list_keys, key)
|
||||
end
|
||||
table.sort(list_keys)
|
||||
|
||||
for i = 1, #list_keys do
|
||||
local list_name = list_keys[i]
|
||||
local list_items = list[list_name]
|
||||
local items_keys = { }
|
||||
for key, _ in pairs(list_items) do
|
||||
table.insert(items_keys, key)
|
||||
end
|
||||
table.sort(items_keys)
|
||||
|
||||
for i = 1, #items_keys do
|
||||
local item_name = items_keys[i]
|
||||
local item_value = list_items[item_name]
|
||||
if item_value == true then
|
||||
local result = { [list_name] = { [item_name] = true } }
|
||||
table.insert(items, result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local random_index = math.random(1, #items)
|
||||
return items[random_index]
|
||||
end
|
||||
|
||||
function mt.first_raw_value_of(list)
|
||||
local result = 0
|
||||
|
||||
for list_name, list_items in pairs(list) do
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
if item_value == true then
|
||||
local index = lume.find(mt.lists[list_name], item_name)
|
||||
if index then
|
||||
result = index
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function mt.posible_values_of(list)
|
||||
local result = { }
|
||||
|
||||
for list_name, list_items in pairs(list) do
|
||||
local subList = { }
|
||||
for _, item_name in ipairs(mt.lists[list_name]) do
|
||||
subList[item_name] = true
|
||||
end
|
||||
result[list_name] = subList
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function mt.range_of(list, min, max)
|
||||
if type(min) ~= 'table' and type(min) ~= 'number' then
|
||||
error('Attempt to get a range with incorrect min value of type ' .. type(min))
|
||||
end
|
||||
if type(max) ~= 'table' and type(max) ~= 'number' then
|
||||
error('Attempt to get a range with incorrect max value of type ' .. type(max))
|
||||
end
|
||||
|
||||
local result = { }
|
||||
local allList = mt.posible_values_of(list)
|
||||
local min_index = type(min) == 'number' and min or mt.first_raw_value_of(min)
|
||||
local max_index = type(max) == 'number' and max or mt.first_raw_value_of(max)
|
||||
|
||||
for list_name, list_items in pairs(allList) do
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
local index = lume.find(mt.lists[list_name], item_name)
|
||||
if index and index >= min_index and index <= max_index and list[list_name][item_name] == true then
|
||||
result[list_name] = result[list_name] or { }
|
||||
result[list_name][item_name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function mt.invert(list)
|
||||
local result = mt.posible_values_of(list)
|
||||
|
||||
for list_name, list_items in pairs(list) do
|
||||
for item_name, item_value in pairs(list_items) do
|
||||
if item_value == true then
|
||||
result[list_name][item_name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return mt
|
||||
150
lua-scripts/narrator/narrator.lua
Normal file
150
lua-scripts/narrator/narrator.lua
Normal file
@@ -0,0 +1,150 @@
|
||||
local lume = require('narrator.libs.lume')
|
||||
local enums = require('narrator.enums')
|
||||
local parser = require('narrator.parser')
|
||||
local Story = require('narrator.story')
|
||||
|
||||
--
|
||||
-- Local
|
||||
|
||||
local folder_separator = package.config:sub(1, 1)
|
||||
|
||||
---Clear path from '.lua' and '.ink' extensions and replace '.' to '/' or '\'
|
||||
---@param path string
|
||||
---@return string normalized_path
|
||||
local function normalize_path(path)
|
||||
local path = path:gsub('.lua$', '')
|
||||
local path = path:gsub('.ink$', '')
|
||||
|
||||
if path:match('%.') and not path:match(folder_separator) then
|
||||
path = path:gsub('%.', folder_separator)
|
||||
end
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
---Parse an .ink file to the content string.
|
||||
---@param path string
|
||||
---@return string content
|
||||
local function read_ink_file(path)
|
||||
local path = normalize_path(path) .. '.ink'
|
||||
|
||||
local file = io.open(path, 'r')
|
||||
assert(file, 'File doesn\'t exist: ' .. path)
|
||||
|
||||
local content = file:read('*all')
|
||||
file:close()
|
||||
|
||||
return content
|
||||
end
|
||||
|
||||
---Save a book to the lua module
|
||||
---@param book Narrator.Book
|
||||
---@param path string
|
||||
---@return boolean success
|
||||
local function save_book(book, path)
|
||||
local path = normalize_path(path) .. '.lua'
|
||||
|
||||
local data = lume.serialize(book)
|
||||
data = data:gsub('%[%d+%]=', '')
|
||||
data = data:gsub('[\'[%w_]+\']', function(match) return
|
||||
match:sub(3, #match - 2)
|
||||
end)
|
||||
|
||||
local file = io.open(path, 'w')
|
||||
if file == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
file:write('return ' .. data)
|
||||
file:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Merge a chapter to the book
|
||||
---@param book Narrator.Book
|
||||
---@param chapter Narrator.Book
|
||||
---@return Narrator.Book
|
||||
local function merge_chapter_to_book(book, chapter)
|
||||
-- Check a engine version compatibility
|
||||
if chapter.version.engine and chapter.version.engine ~= enums.engine_version then
|
||||
assert('Version ' .. chapter.version.engine .. ' of book isn\'t equal to the version ' .. enums.engine_version .. ' of Narrator.')
|
||||
end
|
||||
|
||||
--Merge the root knot and it's stitch
|
||||
book.tree._._ = lume.concat(chapter.tree._._, book.tree._._)
|
||||
chapter.tree._._ = nil
|
||||
book.tree._ = lume.merge(chapter.tree._, book.tree._)
|
||||
chapter.tree._ = nil
|
||||
|
||||
--Merge a chapter to the book
|
||||
book.tree = lume.merge(book.tree or { }, chapter.tree or { })
|
||||
book.constants = lume.merge(book.constants or { }, chapter.constants or { })
|
||||
book.lists = lume.merge(book.lists or { }, chapter.lists or { })
|
||||
book.variables = lume.merge(book.variables or { }, chapter.variables or { })
|
||||
book.params = lume.merge(book.params or { }, chapter.params or { })
|
||||
|
||||
return book
|
||||
end
|
||||
|
||||
--
|
||||
-- Public
|
||||
|
||||
local narrator = { }
|
||||
|
||||
---Parse a book from an Ink file
|
||||
---Use it during development, but prefer already parsed and stored books in production
|
||||
---Requires `lpeg` and `io`.
|
||||
---@param path string
|
||||
---@param params Narrator.ParsingParams|nil
|
||||
---@return Narrator.Book
|
||||
function narrator.parse_file(path, params)
|
||||
local params = params or { save = false }
|
||||
assert(parser, 'Can\'t parse anything without lpeg, sorry.')
|
||||
|
||||
local content = read_ink_file(path)
|
||||
local book = parser.parse(content)
|
||||
|
||||
for _, inclusion in ipairs(book.inclusions) do
|
||||
local folder_path = normalize_path(path):match('(.*' .. folder_separator .. ')')
|
||||
local inclusion_path = folder_path .. normalize_path(inclusion) .. '.ink'
|
||||
local chapter = narrator.parse_file(inclusion_path)
|
||||
|
||||
merge_chapter_to_book(book, chapter)
|
||||
end
|
||||
|
||||
if params.save then
|
||||
save_book(book, path)
|
||||
end
|
||||
|
||||
return book
|
||||
end
|
||||
|
||||
---Parse a book from the ink content string
|
||||
---Use it during development, but prefer already parsed and stored books in production
|
||||
---Requires `lpeg`
|
||||
---@param content string
|
||||
---@param inclusions string[]
|
||||
---@return Narrator.Book
|
||||
function narrator.parse_content(content, inclusions)
|
||||
local inclusions = inclusions or { }
|
||||
assert(parser, 'Can\'t parse anything without a parser.')
|
||||
|
||||
local book = parser.parse(content)
|
||||
|
||||
for _, inclusion in ipairs(inclusions) do
|
||||
local chapter = parser.parse(inclusion)
|
||||
merge_chapter_to_book(book, chapter)
|
||||
end
|
||||
|
||||
return book
|
||||
end
|
||||
|
||||
---Init a story based on the book
|
||||
---@param book Narrator.Book
|
||||
---@return Narrator.Story
|
||||
function narrator.init_story(book)
|
||||
return Story(book)
|
||||
end
|
||||
|
||||
return narrator
|
||||
789
lua-scripts/narrator/parser.lua
Normal file
789
lua-scripts/narrator/parser.lua
Normal file
@@ -0,0 +1,789 @@
|
||||
local lume = require('narrator.libs.lume')
|
||||
local enums = require('narrator.enums')
|
||||
|
||||
--
|
||||
-- LPeg
|
||||
|
||||
-- To allow to build in Defold
|
||||
local lpeg_name = 'lpeg'
|
||||
|
||||
if not pcall(require, lpeg_name) then
|
||||
return false
|
||||
end
|
||||
|
||||
local lpeg = require(lpeg_name)
|
||||
|
||||
local S, C, P, V = lpeg.S, lpeg.C, lpeg.P, lpeg.V
|
||||
local Cb, Ct, Cc, Cg = lpeg.Cb, lpeg.Ct, lpeg.Cc, lpeg.Cg
|
||||
local Cmt = lpeg.Cmt
|
||||
|
||||
lpeg.locale(lpeg)
|
||||
|
||||
--
|
||||
-- Parser
|
||||
|
||||
local parser = { }
|
||||
local constructor = { }
|
||||
|
||||
---Parse ink content string
|
||||
---@param content string
|
||||
---@return Narrator.Book
|
||||
function parser.parse(content)
|
||||
|
||||
--
|
||||
-- Basic patterns
|
||||
|
||||
local function get_length(array) return
|
||||
#array
|
||||
end
|
||||
|
||||
local eof = -1
|
||||
local sp = S(' \t') ^ 0
|
||||
local ws = S(' \t\r\n') ^ 0
|
||||
local nl = S('\r\n') ^ 1
|
||||
local none = Cc(nil)
|
||||
|
||||
local divert_sign = P'->'
|
||||
local gather_mark = sp * C('-' - divert_sign)
|
||||
local gather_level = Cg(Ct(gather_mark ^ 1) / get_length + none, 'level')
|
||||
|
||||
local sticky_marks = Cg(Ct((sp * C('+')) ^ 1) / get_length, 'level') * Cg(Cc(true), 'sticky')
|
||||
local choice_marks = Cg(Ct((sp * C('*')) ^ 1) / get_length, 'level') * Cg(Cc(false), 'sticky')
|
||||
local choice_level = sticky_marks + choice_marks
|
||||
|
||||
local id = (lpeg.alpha + '_') * (lpeg.alnum + '_') ^ 0
|
||||
local label = Cg('(' * sp * C(id) * sp * ')', 'label')
|
||||
local address = id * ('.' * id) ^ -2
|
||||
|
||||
---Something for tunnels
|
||||
local function check_tunnel(s, i, a)
|
||||
local r = lpeg.match (sp * divert_sign, s, i)
|
||||
return i, r ~= nil
|
||||
end
|
||||
|
||||
-- TODO: Clean divert expression to divert and tunnel
|
||||
local divert = divert_sign * sp * Cg(address, 'path') -- base search for divert symbol and path to follow
|
||||
local check_tunnel = Cg(Cmt(Cb('path'), check_tunnel), 'tunnel') -- a weird way to to check tunnel
|
||||
local opt_tunnel_sign = (sp * divert_sign * sp * (#nl + #S'#') ) ^ -1 -- tunnel sign in end of string, keep newline not consumed
|
||||
divert = Cg(Ct(divert * sp * check_tunnel * opt_tunnel_sign), 'divert')
|
||||
|
||||
local divert_to_nothing = divert_sign * none
|
||||
local exit_tunnel = Cg(divert_sign * divert_sign, 'exit')
|
||||
local tag = '#' * sp * V'text'
|
||||
local tags = Cg(Ct(tag * (sp * tag) ^ 0), 'tags')
|
||||
|
||||
local todo = sp * 'TODO:' * (1 - nl) ^ 0
|
||||
local comment_line = sp * '//' * sp * (1 - nl) ^ 0
|
||||
local comment_multi = sp * '/*' * ((P(1) - '*/') ^ 0) * '*/'
|
||||
local comment = comment_line + comment_multi
|
||||
|
||||
local multiline_end = ws * '}'
|
||||
|
||||
--
|
||||
-- Dynamic patterns and evaluation helpers
|
||||
|
||||
local function item_type(type)
|
||||
return Cg(Cc(type), 'type')
|
||||
end
|
||||
|
||||
local function balanced_multiline_item(is_restricted)
|
||||
local is_restricted = is_restricted ~= nil and is_restricted or false
|
||||
local paragraph = is_restricted and V'restricted_paragraph' or V'paragraph'
|
||||
return sp * paragraph ^ -1 * sp * V'multiline_item' * sp * paragraph ^ -1 * ws
|
||||
end
|
||||
|
||||
local function sentence_before(excluded, tailed)
|
||||
local tailed = tailed or false
|
||||
local character = P(1 - S(' \t')) - excluded
|
||||
local pattern = (sp * character ^ 1) ^ 1
|
||||
local with_tail = C(pattern * sp)
|
||||
local without_tail = C(pattern) * sp
|
||||
local without_tail_always = C(pattern) * sp * #(tags + nl)
|
||||
return without_tail_always + (tailed and with_tail or without_tail)
|
||||
end
|
||||
|
||||
local function unwrap_assignment(assignment)
|
||||
local unwrapped = assignment
|
||||
unwrapped = unwrapped:gsub('([%w_]*)%s*([%+%-])[%+%-]', '%1 = %1 %2 1')
|
||||
unwrapped = unwrapped:gsub('([%w_]*)%s*([%+%-])=%s*(.*)', '%1 = %1 %2 %3')
|
||||
local name, value = unwrapped:match('([%w_]*)%s*=%s*(.*)')
|
||||
return name or '', value or assignment
|
||||
end
|
||||
|
||||
local function check_special_escape(s, i, a)
|
||||
if string.sub(s, i - 2, i - 2) == '\\' then
|
||||
return
|
||||
end
|
||||
|
||||
return i
|
||||
end
|
||||
|
||||
--
|
||||
-- Grammar rules
|
||||
|
||||
local ink_grammar = P({ 'root',
|
||||
|
||||
-- Root
|
||||
|
||||
root = ws * V'items' + eof,
|
||||
items = Ct(V'item' ^ 0),
|
||||
|
||||
item = balanced_multiline_item() + V'singleline_item',
|
||||
singleline_item = sp * (V'global' + V'statement' + V'paragraph' + V'gatherPoint') * ws,
|
||||
multiline_item = ('{' * sp * (V'sequence' + V'switch') * sp * multiline_end) - V'inline_condition',
|
||||
|
||||
-- Gather points
|
||||
gatherPoint = Ct(gather_level * sp * nl * item_type('gather')),
|
||||
|
||||
-- Global declarations
|
||||
|
||||
global =
|
||||
Ct(V'inclusion' * item_type('inclusion')) +
|
||||
Ct(V'list' * item_type('list')) +
|
||||
Ct(V'constant' * item_type('constant')) +
|
||||
Ct(V'variable' * item_type('variable'))
|
||||
,
|
||||
|
||||
inclusion = 'INCLUDE ' * sp * Cg(sentence_before(nl + comment), 'filename'),
|
||||
list = 'LIST ' * sp * V'assignment_pair',
|
||||
constant = 'CONST ' * sp * V'assignment_pair',
|
||||
variable = 'VAR ' * sp * V'assignment_pair',
|
||||
|
||||
-- Statements
|
||||
|
||||
statement =
|
||||
Ct(V'return_from_func' * item_type('return')) +
|
||||
Ct(V'assignment' * item_type('assignment')) +
|
||||
Ct(V'func' * item_type('func')) +
|
||||
Ct(V'knot' * item_type('knot')) +
|
||||
Ct(V'stitch' * item_type('stitch')) +
|
||||
Ct(V'choice' * item_type('choice')) +
|
||||
comment + todo
|
||||
,
|
||||
|
||||
section_name = C(id) * sp * P'=' ^ 0,
|
||||
knot = P'==' * (P'=' ^ 0) * sp * Cg(V'section_name', 'knot'),
|
||||
stitch = '=' * sp * Cg(V'section_name', 'stitch'),
|
||||
|
||||
func_param = sp * C(id) * sp * S','^0,
|
||||
func_params = P'(' * Cg(Ct(V'func_param'^0), 'params') * P')',
|
||||
function_name = P'function' * sp * Cg(id, 'name') * sp * V'func_params' * sp * P'=' ^ 0,
|
||||
func = P'==' * (P'=' ^ 0) * sp * Cg(Ct(V'function_name'), 'func'),
|
||||
|
||||
return_from_func = sp * '~' * sp * P('return') * sp * Cg((P(1) - nl)^0, 'value') * nl ^ 0,
|
||||
|
||||
assignment = gather_level * sp * '~' * sp * V'assignment_temp' * sp * V'assignment_pair',
|
||||
assignment_temp = Cg('temp' * Cc(true) + Cc(false), 'temp'),
|
||||
assignment_pair = Cg(sentence_before(nl + comment) / unwrap_assignment, 'name') * Cg(Cb('name') / 2, 'value'),
|
||||
|
||||
choice_condition = Cg(V'expression' + none, 'condition'),
|
||||
choice_fallback = choice_level * sp * V'label_optional' * sp * V'choice_condition' * sp * (divert + divert_to_nothing) * sp * V'tags_optional',
|
||||
choice_normal = choice_level * sp * V'label_optional' * sp * V'choice_condition' * sp * Cg(V'text', 'text') * divert ^ -1 * sp * V'tags_optional',
|
||||
choice = V'choice_fallback' + V'choice_normal',
|
||||
|
||||
-- Paragraph
|
||||
|
||||
paragraph = Ct(gather_level * sp * (V'paragraph_label' + V'paragraph_text' + V'paragraph_tags') * item_type('paragraph')),
|
||||
paragraph_label = label * sp * Cg(V'text_optional', 'parts') * sp * V'tags_optional',
|
||||
paragraph_text = V'label_optional' * sp * Cg(V'text_complex', 'parts') * sp * V'tags_optional',
|
||||
paragraph_tags = V'label_optional' * sp * Cg(V'text_optional', 'parts') * sp * tags,
|
||||
|
||||
label_optional = label + none,
|
||||
text_optional = V'text_complex' + none,
|
||||
tags_optional = tags + none,
|
||||
|
||||
text_complex = Ct((Ct(
|
||||
Cg(V'inline_condition', 'condition') +
|
||||
Cg(V'inline_sequence', 'sequence') +
|
||||
Cg(V'expression', 'expression') +
|
||||
Cg(V'text' + ' ', 'text') * (exit_tunnel ^ -1) * (divert ^ -1) + exit_tunnel + divert
|
||||
) - V'multiline_item') ^ 1),
|
||||
|
||||
special_check_escape = Cmt(S("{|}"), check_special_escape),
|
||||
|
||||
text = sentence_before(nl + exit_tunnel + divert + comment + tag + V'special_check_escape', true) - V'statement',
|
||||
-- Inline expressions, conditions, sequences
|
||||
|
||||
expression = '{' * sp * sentence_before('}' + nl) * sp * '}',
|
||||
|
||||
inline_condition = '{' * sp * Ct(V'inline_if_else' + V'inline_if') * sp * '}',
|
||||
inline_if = Cg(sentence_before(S':}' + nl), 'condition') * sp * ':' * sp * Cg(V'text_complex', 'success'),
|
||||
inline_if_else = (V'inline_if') * sp * '|' * sp * Cg(V'text_complex', 'failure'),
|
||||
|
||||
inline_alt_empty = Ct(Ct(Cg(sp * Cc'', 'text') * sp * divert ^ -1)),
|
||||
inline_alt = V'text_complex' + V'inline_alt_empty',
|
||||
inline_alts = Ct(((sp * V'inline_alt' * sp * '|') ^ 1) * sp * V'inline_alt'),
|
||||
inline_sequence = '{' * sp * (
|
||||
'!' * sp * Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('once'), 'sequence')) +
|
||||
'&' * sp * Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('cycle'), 'sequence')) +
|
||||
'~' * sp * Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('stopping'), 'sequence') * Cg(Cc(true), 'shuffle')) +
|
||||
Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('stopping'), 'sequence'))
|
||||
) * sp * '}',
|
||||
|
||||
-- Multiline conditions and switches
|
||||
|
||||
switch = Ct((V'switch_comparative' + V'switch_conditional') * item_type('switch')),
|
||||
|
||||
switch_comparative = Cg(V'switch_condition', 'expression') * ws * Cg(Ct((sp * V'switch_case') ^ 1), 'cases'),
|
||||
switch_conditional = Cg(Ct(V'switch_cases_headed' + V'switch_cases_only'), 'cases'),
|
||||
|
||||
switch_cases_headed = V'switch_if' * ((sp * V'switch_case') ^ 0),
|
||||
switch_cases_only = ws * ((sp * V'switch_case') ^ 1),
|
||||
|
||||
switch_if = Ct(Cg(V'switch_condition', 'condition') * ws * Cg(Ct(V'switch_items'), 'node')),
|
||||
switch_case = ('-' - divert_sign) * sp * V'switch_if',
|
||||
switch_condition = sentence_before(':' + nl) * sp * ':' * sp * comment ^ -1,
|
||||
switch_items = (V'restricted_item' - V'switch_case') ^ 1,
|
||||
|
||||
-- Multiline sequences
|
||||
|
||||
sequence = Ct((V'sequence_params' * sp * nl * sp * V'sequence_alts') * item_type('sequence')),
|
||||
|
||||
sequence_params = (
|
||||
V'sequence_shuffle_optional' * sp * V'sequence_type' +
|
||||
V'sequence_shuffle' * sp * V'sequence_type' +
|
||||
V'sequence_shuffle' * sp * V'sequence_type_optional'
|
||||
) * sp * ':' * sp * comment ^ -1,
|
||||
|
||||
sequence_shuffle_optional = V'sequence_shuffle' + Cg(Cc(false), 'shuffle'),
|
||||
sequence_shuffle = Cg(P'shuffle' / function() return true end, 'shuffle'),
|
||||
|
||||
sequence_type_optional = V'sequence_type' + Cg(Cc'cycle', 'sequence'),
|
||||
sequence_type = Cg(P'cycle' + 'stopping' + 'once', 'sequence'),
|
||||
|
||||
sequence_alts = Cg(Ct((sp * V'sequence_alt') ^ 1), 'alts'),
|
||||
sequence_alt = ('-' - divert_sign) * ws * Ct(V'sequence_items'),
|
||||
sequence_items = (V'restricted_item' - V'sequence_alt') ^ 1,
|
||||
|
||||
-- Restricted items inside multiline items
|
||||
|
||||
restricted_item = balanced_multiline_item(true) + V'restricted_singleline_item',
|
||||
restricted_singleline_item = sp * (V'global' + V'restricted_statement' + V'restricted_paragraph' - multiline_end) * ws,
|
||||
|
||||
restricted_statement = Ct(
|
||||
V'choice' * item_type('choice') +
|
||||
V'assignment' * item_type('assignment')
|
||||
) + comment + todo,
|
||||
|
||||
restricted_paragraph = Ct((
|
||||
Cg(V'text_complex', 'parts') * sp * V'tags_optional' +
|
||||
Cg(V'text_optional', 'parts') * sp * tags
|
||||
) * item_type('paragraph'))
|
||||
|
||||
})
|
||||
|
||||
--
|
||||
-- Result
|
||||
|
||||
local parsed_items = ink_grammar:match(content)
|
||||
local book = constructor.construct_book(parsed_items)
|
||||
return book
|
||||
end
|
||||
|
||||
--
|
||||
-- A book construction
|
||||
|
||||
function constructor.unescape(text)
|
||||
local result = text
|
||||
|
||||
result = result:gsub('\\|', '|')
|
||||
result = result:gsub('\\{', '{')
|
||||
result = result:gsub('\\}', '}')
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function constructor.construct_book(items)
|
||||
|
||||
local construction = {
|
||||
current_knot = '_',
|
||||
current_stitch = '_',
|
||||
variables_to_compute = { }
|
||||
}
|
||||
|
||||
construction.book = {
|
||||
inclusions = { },
|
||||
lists = { },
|
||||
constants = { },
|
||||
variables = { },
|
||||
params = { },
|
||||
tree = { _ = { _ = { } } }
|
||||
}
|
||||
|
||||
construction.book.version = {
|
||||
engine = enums.engine_version,
|
||||
tree = 1
|
||||
}
|
||||
|
||||
construction.nodes_chain = {
|
||||
construction.book.tree[construction.current_knot][construction.current_stitch]
|
||||
}
|
||||
|
||||
constructor.add_node(construction, items)
|
||||
constructor.clear(construction.book.tree)
|
||||
constructor.compute_variables(construction)
|
||||
|
||||
return construction.book
|
||||
end
|
||||
|
||||
function constructor:add_node(items, is_restricted)
|
||||
local is_restricted = is_restricted ~= nil and is_restricted or false
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
if is_restricted then
|
||||
-- Are not allowed inside multiline blocks by Ink rules:
|
||||
-- a) nesting levels
|
||||
-- b) choices without diverts
|
||||
|
||||
item.level = nil
|
||||
if item.type == 'choice' and item.divert == nil then
|
||||
item.type = nil
|
||||
end
|
||||
end
|
||||
|
||||
if item.type == 'inclusion' then
|
||||
-- filename
|
||||
constructor.add_inclusion(self, item.filename)
|
||||
elseif item.type == 'list' then
|
||||
-- name, value
|
||||
constructor.add_list(self, item.name, item.value)
|
||||
elseif item.type == 'constant' then
|
||||
-- name, value
|
||||
constructor.add_constant(self, item.name, item.value)
|
||||
elseif item.type == 'variable' then
|
||||
-- name, value
|
||||
constructor.add_variable(self, item.name, item.value)
|
||||
elseif item.type == 'func' then
|
||||
-- function
|
||||
constructor.add_function(self, item.func.name, item.func.params)
|
||||
elseif item.type == 'knot' then
|
||||
-- knot
|
||||
constructor.add_knot(self, item.knot)
|
||||
elseif item.type == 'stitch' then
|
||||
-- stitch
|
||||
constructor.add_stitch(self, item.stitch)
|
||||
elseif item.type == 'switch' then
|
||||
-- expression, cases
|
||||
constructor.add_switch(self, item.expression, item.cases)
|
||||
elseif item.type == 'sequence' then
|
||||
-- sequence, shuffle, alts
|
||||
constructor.add_sequence(self, item.sequence, item.shuffle, item.alts)
|
||||
elseif item.type == 'assignment' then
|
||||
-- level, name, value, temp
|
||||
constructor.add_assignment(self, item.level, item.name, item.value, item.temp)
|
||||
elseif item.type == 'return' then
|
||||
constructor.add_return(self, item.value)
|
||||
elseif item.type == 'paragraph' then
|
||||
-- level, label, parts, tags
|
||||
constructor.add_paragraph(self, item.level, item.label, item.parts, item.tags)
|
||||
elseif item.type == 'gather' then
|
||||
constructor.add_paragraph(self, item.level, "", nil, item.tags)
|
||||
elseif item.type == 'choice' then
|
||||
-- level, sticky, label, condition, text, divert, tags
|
||||
constructor.add_choice(self, item.level, item.sticky, item.label, item.condition, item.text, item.divert, item.tags)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function constructor:add_inclusion(filename)
|
||||
table.insert(self.book.inclusions, filename)
|
||||
end
|
||||
|
||||
function constructor:add_list(name, value)
|
||||
local items = lume.array(value:gmatch('[%w_%.]+'))
|
||||
self.book.lists[name] = items
|
||||
|
||||
local switched = lume.array(value:gmatch('%b()'))
|
||||
switched = lume.map(switched, function(item) return item:sub(2, #item - 1) end)
|
||||
self.book.variables[name] = { [name] = { } }
|
||||
lume.each(switched, function(item) self.book.variables[name][name][item] = true end)
|
||||
end
|
||||
|
||||
function constructor:add_constant(constant, value)
|
||||
local value = lume.deserialize(value)
|
||||
self.book.constants[constant] = value
|
||||
end
|
||||
|
||||
function constructor:add_variable(variable, value)
|
||||
self.variables_to_compute[variable] = value
|
||||
end
|
||||
|
||||
function constructor:add_function(fname, params)
|
||||
local node = { }
|
||||
self.book.tree[fname] = { ['_'] = node }
|
||||
self.book.params[fname] = params
|
||||
self.nodes_chain = { node }
|
||||
end
|
||||
|
||||
function constructor:add_knot(knot)
|
||||
self.current_knot = knot
|
||||
self.current_stitch = '_'
|
||||
|
||||
local node = { }
|
||||
self.book.tree[self.current_knot] = { [self.current_stitch] = node }
|
||||
self.nodes_chain = { node }
|
||||
end
|
||||
|
||||
function constructor:add_stitch(stitch)
|
||||
-- If a root stitch is empty we need to add a divert to the first stitch in the ink file.
|
||||
if self.current_stitch == '_' then
|
||||
local root_stitch_node = self.book.tree[self.current_knot]._
|
||||
if #root_stitch_node == 0 then
|
||||
local divertItem = { divert = { path = stitch } }
|
||||
table.insert(root_stitch_node, divertItem)
|
||||
end
|
||||
end
|
||||
|
||||
self.current_stitch = stitch
|
||||
|
||||
local node = { }
|
||||
self.book.tree[self.current_knot][self.current_stitch] = node
|
||||
self.nodes_chain = { node }
|
||||
end
|
||||
|
||||
function constructor:add_switch(expression, cases)
|
||||
if expression then
|
||||
-- Convert switch cases to comparing conditions with expression
|
||||
for _, case in ipairs(cases) do
|
||||
if case.condition ~= 'else' then
|
||||
case.condition = expression .. '==' .. case.condition
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local item = {
|
||||
condition = { },
|
||||
success = { }
|
||||
}
|
||||
|
||||
for _, case in ipairs(cases) do
|
||||
if case.condition == 'else' then
|
||||
local failure_node = { }
|
||||
table.insert(self.nodes_chain, failure_node)
|
||||
constructor.add_node(self, case.node, true)
|
||||
table.remove(self.nodes_chain)
|
||||
item.failure = failure_node
|
||||
else
|
||||
local success_node = { }
|
||||
table.insert(self.nodes_chain, success_node)
|
||||
constructor.add_node(self, case.node, true)
|
||||
table.remove(self.nodes_chain)
|
||||
table.insert(item.success, success_node)
|
||||
table.insert(item.condition, case.condition)
|
||||
end
|
||||
end
|
||||
|
||||
constructor.add_item(self, nil, item)
|
||||
end
|
||||
|
||||
function constructor:add_sequence(sequence, shuffle, alts)
|
||||
local item = {
|
||||
sequence = sequence,
|
||||
shuffle = shuffle and true or nil,
|
||||
alts = { }
|
||||
}
|
||||
|
||||
for _, alt in ipairs(alts) do
|
||||
local alt_node = { }
|
||||
table.insert(self.nodes_chain, alt_node)
|
||||
constructor.add_node(self, alt, true)
|
||||
table.remove(self.nodes_chain)
|
||||
table.insert(item.alts, alt_node)
|
||||
end
|
||||
|
||||
constructor.add_item(self, nil, item)
|
||||
end
|
||||
|
||||
function constructor:add_return(value)
|
||||
local item = {
|
||||
return_value = value
|
||||
}
|
||||
|
||||
constructor.add_item(self, nil, item)
|
||||
end
|
||||
|
||||
function constructor:add_assignment(level, name, value, temp)
|
||||
local item = {
|
||||
temp = temp or nil,
|
||||
var = name,
|
||||
value = value
|
||||
}
|
||||
|
||||
constructor.add_item(self, level, item)
|
||||
end
|
||||
|
||||
function constructor:add_paragraph(level, label, parts, tags)
|
||||
local items = constructor.convert_paragraph_parts_to_items(parts, true)
|
||||
items = items or { }
|
||||
|
||||
-- If the paragraph has a label or tags we need to place them as the first text item.
|
||||
if label ~= nil or tags ~= nil then
|
||||
local first_item
|
||||
|
||||
if #items > 0 and items[1].condition == nil then
|
||||
first_item = items[1]
|
||||
else
|
||||
first_item = { }
|
||||
table.insert(items, first_item)
|
||||
end
|
||||
|
||||
first_item.label = label
|
||||
first_item.tags = tags
|
||||
end
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
constructor.add_item(self, level, item)
|
||||
end
|
||||
end
|
||||
|
||||
function constructor.convert_paragraph_parts_to_items(parts, is_root)
|
||||
if parts == nil then return nil end
|
||||
|
||||
local is_root = is_root ~= nil and is_root or false
|
||||
local items = { }
|
||||
local item
|
||||
|
||||
for index, part in ipairs(parts) do
|
||||
|
||||
if part.condition then -- Inline condition part
|
||||
|
||||
item = {
|
||||
condition = part.condition.condition,
|
||||
success = constructor.convert_paragraph_parts_to_items(part.condition.success),
|
||||
failure = constructor.convert_paragraph_parts_to_items(part.condition.failure)
|
||||
}
|
||||
|
||||
table.insert(items, item)
|
||||
item = nil
|
||||
|
||||
elseif part.sequence then -- Inline sequence part
|
||||
|
||||
item = {
|
||||
sequence = part.sequence.sequence,
|
||||
shuffle = part.sequence.shuffle and true or nil,
|
||||
alts = { }
|
||||
}
|
||||
|
||||
for _, alt in ipairs(part.sequence.alts) do
|
||||
table.insert(item.alts, constructor.convert_paragraph_parts_to_items(alt))
|
||||
end
|
||||
|
||||
table.insert(items, item)
|
||||
item = nil
|
||||
|
||||
else -- Text, expression and divert may be
|
||||
|
||||
local is_divert_only = part.divert ~= nil and part.text == nil
|
||||
|
||||
if item == nil then
|
||||
item = { text = (is_root or is_divert_only) and '' or '<>' }
|
||||
end
|
||||
|
||||
if part.text then
|
||||
item.text = item.text .. part.text:gsub('%s+', ' ')
|
||||
item.text = constructor.unescape(item.text)
|
||||
elseif part.expression then
|
||||
item.text = item.text .. '#' .. part.expression .. '#'
|
||||
end
|
||||
|
||||
if part.divert or part.exit then
|
||||
item.exit = part.exit and true or nil
|
||||
item.divert = part.divert
|
||||
item.text = #item.text > 0 and (item.text .. '<>') or nil
|
||||
table.insert(items, item)
|
||||
item = nil
|
||||
else
|
||||
local next = parts[index + 1]
|
||||
local next_is_block = next and not (next.text or next.expression)
|
||||
|
||||
if not next or next_is_block then
|
||||
if not is_root or next_is_block then
|
||||
item.text = item.text .. '<>'
|
||||
end
|
||||
table.insert(items, item)
|
||||
item = nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
if is_root then
|
||||
-- Add a safe prefix and suffix for correct conditions gluing
|
||||
|
||||
local first_item = items[1]
|
||||
if first_item.text == nil and first_item.divert == nil and first_item.exit == nil then
|
||||
table.insert(items, 1, { text = '' } )
|
||||
end
|
||||
|
||||
local last_item = items[#items]
|
||||
if last_item.text == nil and last_item.divert == nil and last_item.exit == nil then
|
||||
table.insert(items, { text = '' } )
|
||||
elseif last_item.text ~= nil and last_item.divert == nil then
|
||||
last_item.text = last_item.text:gsub('(.-)%s*$', '%1')
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
function constructor:add_choice(level, sticky, label, condition, sentence, divert, tags)
|
||||
local item = {
|
||||
sticky = sticky or nil,
|
||||
condition = condition,
|
||||
label = label,
|
||||
divert = divert,
|
||||
tags = tags
|
||||
}
|
||||
|
||||
if sentence == nil then
|
||||
item.choice = 0
|
||||
else
|
||||
local prefix, divider, suffix = sentence:match('(.*)%[(.*)%](.*)')
|
||||
prefix = prefix or sentence
|
||||
divider = divider or ''
|
||||
suffix = suffix or ''
|
||||
|
||||
local text = (prefix .. suffix):gsub('%s+', ' ')
|
||||
local choice = (prefix .. divider):gsub('%s+', ' '):gsub('^%s*(.-)%s*$', '%1')
|
||||
|
||||
if divert and #text > 0 and text:match('%S+') then
|
||||
text = text .. '<>'
|
||||
else
|
||||
text = text:gsub('^%s*(.-)%s*$', '%1')
|
||||
end
|
||||
|
||||
item.text = constructor.unescape(text)
|
||||
item.choice = constructor.unescape(choice)
|
||||
end
|
||||
|
||||
constructor.add_item(self, level, item)
|
||||
|
||||
if divert == nil then
|
||||
item.node = { }
|
||||
table.insert(self.nodes_chain, item.node)
|
||||
end
|
||||
end
|
||||
|
||||
function constructor:add_item(level, item)
|
||||
local level = (level ~= nil and level > 0) and level or #self.nodes_chain
|
||||
while #self.nodes_chain > level do
|
||||
table.remove(self.nodes_chain)
|
||||
end
|
||||
|
||||
local node = self.nodes_chain[#self.nodes_chain]
|
||||
table.insert(node, item)
|
||||
end
|
||||
|
||||
function constructor:compute_variable(variable, value)
|
||||
local constant = self.book.constants[value]
|
||||
if constant then
|
||||
self.book.variables[variable] = constant
|
||||
return
|
||||
end
|
||||
|
||||
local list_expression = value:match('%(([%s%w%.,_]*)%)')
|
||||
local item_expressions = list_expression and lume.array(list_expression:gmatch('[%w_%.]+')) or { value }
|
||||
local list_variable = list_expression and { } or nil
|
||||
|
||||
for _, item_expression in ipairs(item_expressions) do
|
||||
local list_part, item_part = item_expression:match('([%w_]+)%.([%w_]+)')
|
||||
item_part = item_part or item_expression
|
||||
|
||||
for list_name, list_items in pairs(self.book.lists) do
|
||||
local list_is_valid = list_part == nil or list_part == list_name
|
||||
local item_is_found = lume.find(list_items, item_part)
|
||||
|
||||
if list_is_valid and item_is_found then
|
||||
list_variable = list_variable or { }
|
||||
list_variable[list_name] = list_variable[list_name] or { }
|
||||
list_variable[list_name][item_part] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if list_variable then
|
||||
self.book.variables[variable] = list_variable
|
||||
else
|
||||
self.book.variables[variable] = lume.deserialize(value)
|
||||
end
|
||||
end
|
||||
|
||||
function constructor:compute_variables()
|
||||
for variable, value in pairs(self.variables_to_compute) do
|
||||
constructor.compute_variable(self, variable, value)
|
||||
end
|
||||
end
|
||||
|
||||
function constructor.clear(tree)
|
||||
for knot, node in pairs(tree) do
|
||||
for stitch, node in pairs(node) do
|
||||
constructor.clear_node(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function constructor.clear_node(node)
|
||||
for index, item in ipairs(node) do
|
||||
|
||||
-- Simplify text only items
|
||||
if item.text ~= nil and lume.count(item) == 1 then
|
||||
node[index] = item.text
|
||||
end
|
||||
|
||||
if item.node ~= nil then
|
||||
-- Clear choice nodes
|
||||
if #item.node == 0 then
|
||||
item.node = nil
|
||||
else
|
||||
constructor.clear_node(item.node)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if item.success ~= nil then
|
||||
-- Simplify single condition
|
||||
if type(item.condition) == 'table' and #item.condition == 1 then
|
||||
item.condition = item.condition[1]
|
||||
end
|
||||
|
||||
-- Clear success nodes
|
||||
if item.success[1] ~= nil and item.success[1][1] ~= nil then
|
||||
for index, success_node in ipairs(item.success) do
|
||||
constructor.clear_node(success_node)
|
||||
if #success_node == 1 and type(success_node[1]) == 'string' then
|
||||
item.success[index] = success_node[1]
|
||||
end
|
||||
end
|
||||
|
||||
if #item.success == 1 then
|
||||
item.success = item.success[1]
|
||||
end
|
||||
else
|
||||
constructor.clear_node(item.success)
|
||||
if #item.success == 1 and type(item.success[1]) == 'string' then
|
||||
item.success = item.success[1]
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear failure nodes
|
||||
if item.failure ~= nil then
|
||||
constructor.clear_node(item.failure)
|
||||
if #item.failure == 1 and type(item.failure[1]) == 'string' then
|
||||
item.failure = item.failure[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if item.alts ~= nil then
|
||||
for index, alt_node in ipairs(item.alts) do
|
||||
constructor.clear_node(alt_node)
|
||||
if #alt_node == 1 and type(alt_node[1]) == 'string' then
|
||||
item.alts[index] = alt_node[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return parser
|
||||
1253
lua-scripts/narrator/story.lua
Normal file
1253
lua-scripts/narrator/story.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user