-
Notifications
You must be signed in to change notification settings - Fork 2
/
erogodic.lua
180 lines (154 loc) · 4.61 KB
/
erogodic.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
--[[
erogodic v3.0.0
===============
A library for scripting branching interactive narrative by shru.
https://github.com/oniietzschan/erogodic
License
-------
shru-chan hereby dedicates this source code and associated documentation
(the "App") to the public domain. shru makes this dedication for the
benefit of the Gamers everywhere and to the detriment of trolls and bullies.
Anyone is free to copy, modify, publish, use, sell, distribute, recite in a
spooky voice, or fax the App by any means they desire, so long as they
adhere to one condition:
Please consider buying shru some ice cream. Azuki preferred, but all
flavours except Licorice will be accepted.
In jurisdictions that do not: (a) recognize donation of works to the public
domain; nor (b) consider incitement to be a legally enforcable crime: shru
advocates immediate forceful regime-change.
--]]
local function assertType(obj, expectedType, name)
assert(type(expectedType) == 'string' and type(name) == 'string')
if type(obj) ~= expectedType then
error(name .. ' must be a ' .. expectedType .. ', got: ' .. tostring(obj), 2)
end
end
local PUSHED_SCRIPT = 'PUSHED_SCRIPT'
local Script = {}
local ScriptMetaTable = {__index = Script}
function Script:new(scriptFn)
assertType(scriptFn, 'function', 'script function')
self._attributes = {}
self._selection = nil
self._options = {}
self._onMenu = false
self._scriptStack = {}
self._argsStack = {}
self:_initEnvironment()
self:_pushScript(scriptFn)
return self
end
function Script:_initEnvironment()
local env = setmetatable({}, {__index = _G})
function env.get(attribute)
return self._attributes[attribute]
end
function env.menu(text)
self._onMenu = true
self:_yield({
msg = text,
options = self._options,
})
end
function env.msg(text)
self:_yield({msg = text})
end
function env.option(option)
assertType(option, 'string', 'option')
table.insert(self._options, option)
return option
end
function env.selection(option)
return (option == nil)
and self._selection
or self._selection == option
end
self._env = env
end
function Script:_pushScript(scriptFn, ...)
setfenv(scriptFn, self._env)
local scriptCoroutine = coroutine.create(scriptFn, ...)
self._currentScript = scriptCoroutine
self._currentArgs = {...}
table.insert(self._scriptStack, scriptCoroutine)
table.insert(self._argsStack, self._currentArgs)
end
function Script:_popScript()
table.remove(self._scriptStack)
table.remove(self._currentArgs)
local len = #self._scriptStack
self._currentScript = (len >= 1) and self._scriptStack[len] or nil
self._currentArgs = (len >= 1) and self._argsStack[len] or nil
end
function Script:_yield(node)
for k, v in pairs(self._attributes) do
node[k] = v
end
coroutine.yield(node)
end
function Script:addMacro(name, fn)
assertType(name, 'string', 'macro name')
assertType(fn, 'function', 'macro function')
self._env[name] = function(...)
self:_pushScript(fn, ...)
coroutine.yield(PUSHED_SCRIPT)
end
return self
end
function Script:defineAttributes(attributeNames)
assertType(attributeNames, 'table', 'attributeNames')
for _, attrName in ipairs(attributeNames) do
assertType(attributeNames, 'table', 'attributeNames')
self._env[attrName] = function(val)
self._attributes[attrName] = val
end
end
return self
end
function Script:extendEnvironment(t)
assertType(t, 'table', 'environment table')
for k, v in pairs(t) do
self._env[k] = v
end
return self
end
function Script:select(selection)
for _, option in ipairs(self._options) do
if option == selection then
self._selection = selection
self._options = {}
self._onMenu = false
return self:next()
end
end
error("Selection '" .. selection .. "' was not one of the options.")
end
function Script:next()
if self:hasNext() == false then
error('Script is finished.')
end
if self._onMenu then
return self._currentNode -- Can not skip a question.
end
local isRunning, result = coroutine.resume(self._currentScript, unpack(self._currentArgs))
if result == PUSHED_SCRIPT then
return self:next()
elseif isRunning == false then
error('Error executing script: ' .. tostring(result))
end
self._currentNode = result
if self:hasNext() == false then
self:_popScript()
if self._currentScript ~= nil then
return self:next()
end
end
return self._currentNode
end
function Script:hasNext()
return self._currentScript ~= nil and coroutine.status(self._currentScript) == 'suspended'
end
return function(...)
return setmetatable({}, ScriptMetaTable)
:new(...)
end