Vendor libs/hump, sfxrlua, json4lua, narrator as real files
These were committed as empty submodule gitlinks with no .gitmodules, so the folders were never populated and the game could not load (require of libs/hump/timer, libs/sfxrlua/sfxr, narrator.narrator). Checked out each at its pinned commit and vendored the files, matching the existing bundled-libs convention. Removed the junk libs/libs/ tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e73f63b5d4
commit
03532d29e4
219 changed files with 20254 additions and 252 deletions
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 45a8b03553f2385716c24ec762757e418b2b73c1
|
||||
7
libs/narrator/.gitignore
vendored
Normal file
7
libs/narrator/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# System
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Defold
|
||||
/.internal
|
||||
/build
|
||||
24
libs/narrator/.vscode/launch.json
vendored
Normal file
24
libs/narrator/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug",
|
||||
"type": "lua-local",
|
||||
"request": "launch",
|
||||
"program": {
|
||||
"lua": "lua",
|
||||
"file": "${workspaceFolder}/debug.lua"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Busted",
|
||||
"type": "lua-local",
|
||||
"request": "launch",
|
||||
"program": {
|
||||
"command": "busted"
|
||||
},
|
||||
"args": [
|
||||
"${workspaceFolder}/test/run.lua",
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
81
libs/narrator/.vscode/settings.json
vendored
Normal file
81
libs/narrator/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"Lua.diagnostics.globals": [
|
||||
"unpack",
|
||||
"loadstring",
|
||||
"describe",
|
||||
"it",
|
||||
"msg",
|
||||
"sound",
|
||||
"hash",
|
||||
"vmath",
|
||||
"gui",
|
||||
"socket",
|
||||
"sys",
|
||||
"render",
|
||||
"go",
|
||||
"factory",
|
||||
"resource",
|
||||
"pprint",
|
||||
"timer",
|
||||
"particlefx",
|
||||
"spine",
|
||||
"sprite",
|
||||
"json",
|
||||
"window",
|
||||
"physics"
|
||||
],
|
||||
"Lua.diagnostics.disable": [
|
||||
"trailing-space",
|
||||
"redefined-local",
|
||||
"deprecated",
|
||||
"lowercase-global"
|
||||
],
|
||||
"Lua.completion.callSnippet": "Replace",
|
||||
"Lua.completion.keywordSnippet": "Replace",
|
||||
"Lua.completion.showWord": "Fallback",
|
||||
"Lua.completion.autoRequire": false,
|
||||
"[lua]": {
|
||||
"editor.defaultFormatter": "sumneko.lua"
|
||||
},
|
||||
"glsllint.additionalStageAssociations": {
|
||||
".fp": "frag",
|
||||
".vp": "vert"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.project": "ini",
|
||||
"*.script": "lua",
|
||||
"*.gui_script": "lua",
|
||||
"*.render_script": "lua",
|
||||
"*.editor_script": "lua",
|
||||
"*.fp": "glsl",
|
||||
"*.vp": "glsl",
|
||||
"*.go": "textproto",
|
||||
"*.animationset": "textproto",
|
||||
"*.atlas": "textproto",
|
||||
"*.buffer": "json",
|
||||
"*.camera": "textproto",
|
||||
"*.collection": "textproto",
|
||||
"*.collectionfactory": "textproto",
|
||||
"*.collectionproxy": "textproto",
|
||||
"*.collisionobject": "textproto",
|
||||
"*.display_profiles": "textproto",
|
||||
"*.factory": "textproto",
|
||||
"*.gamepads": "textproto",
|
||||
"*.gui": "textproto",
|
||||
"*.input_binding": "textproto",
|
||||
"*.label": "textproto",
|
||||
"*.material": "textproto",
|
||||
"*.mesh": "textproto",
|
||||
"*.model": "textproto",
|
||||
"*.particlefx": "textproto",
|
||||
"*.render": "textproto",
|
||||
"*.sound": "textproto",
|
||||
"*.spinemodel": "textproto",
|
||||
"*.spinescene": "textproto",
|
||||
"*.sprite": "textproto",
|
||||
"*.texture_profiles": "textproto",
|
||||
"*.tilemap": "textproto",
|
||||
"*.tilesource": "textproto",
|
||||
"*.manifest": "textproto"
|
||||
}
|
||||
}
|
||||
18
libs/narrator/.vscode/tasks.json
vendored
Normal file
18
libs/narrator/.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Busted",
|
||||
"command": "busted",
|
||||
"args": [
|
||||
"${workspaceFolder}/test/run.lua"
|
||||
],
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
21
libs/narrator/LICENSE
Normal file
21
libs/narrator/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Roman Silin
|
||||
|
||||
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.
|
||||
469
libs/narrator/README.md
Normal file
469
libs/narrator/README.md
Normal file
|
|
@ -0,0 +1,469 @@
|
|||

|
||||
|
||||
# Narrator
|
||||
|
||||
[](https://github.com/astrochili/narrator/releases)
|
||||
[](https://github.com/astrochili/narrator/blob/master/LICENSE)
|
||||
[](https://astronachos.com/)
|
||||
[](https://mastodon.gamedev.place/@astronachos)
|
||||
[](https://twitter.com/astronachos)
|
||||
[](https://t.me/astronachos)
|
||||
[](https://buymeacoffee.com/astrochili)
|
||||
|
||||
## Overview
|
||||
|
||||
The [Ink](https://www.inklestudios.com/ink/) language parser and runtime implementation in Lua.
|
||||
|
||||
Ink is a powerful narrative scripting language. You can find more information about how to write Ink scripts [here](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). There is also [Inky](https://github.com/inkle/inky) editor with useful features to test and debug Ink scripts.
|
||||
|
||||
Narrator allows to convert raw Ink scripts to the book (a lua table) and play it as story.
|
||||
|
||||
- 📖 A book is a passive model on the shelf like a game level.
|
||||
- ✨ A story is a runtime state of the book reading like a game process.
|
||||
|
||||
## Quick example
|
||||
|
||||
```lua
|
||||
local narrator = require('narrator.narrator')
|
||||
|
||||
-- Parse a book from the Ink file.
|
||||
local book = narrator.parse_file('stories.game')
|
||||
|
||||
-- Init a story from the book
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
-- Begin the story
|
||||
story:begin()
|
||||
|
||||
while story:can_continue() do
|
||||
|
||||
-- Get current paragraphs to output
|
||||
local paragraphs = story:continue()
|
||||
|
||||
for _, paragraph in ipairs(paragraphs) do
|
||||
local text = paragraph.text
|
||||
|
||||
-- You can handle tags as you like, but we attach them to text here.
|
||||
if paragraph.tags then
|
||||
text = text .. ' #' .. table.concat(paragraph.tags, ' #')
|
||||
end
|
||||
|
||||
-- Output text to the player
|
||||
print(text)
|
||||
end
|
||||
|
||||
-- If there is no choice it seems like the game is over
|
||||
if not story:can_choose() then break end
|
||||
|
||||
-- Get available choices and output them to the player
|
||||
local choices = story:get_choices()
|
||||
for i, choice in ipairs(choices) do
|
||||
print(i .. ') ' .. choice.text)
|
||||
end
|
||||
|
||||
-- Read the choice from the player input
|
||||
local answer = tonumber(io.read())
|
||||
|
||||
-- Send answer to the story to generate new paragraphs
|
||||
story:choose(answer)
|
||||
end
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
- [defold-ink](https://github.com/abadonna/defold-ink) — The Ink language runtime implementation in Lua based on parsing compiled JSON files.
|
||||
|
||||
## Showcase
|
||||
|
||||
- [Cat's Day](https://astronachos.com/catsday/) — A short card game about one furry.
|
||||
- [Rare Pets](https://jetpackcollective.games/rarepets/) — A merge game for mobile about pets that become what they eat.
|
||||
- [Sensual Hunting](https://store.steampowered.com/app/1967470/Sensual_Haunting/) (NSFW) — An adult only game where all the navigation and dialogs made with this library.
|
||||
- [The Secret Laboratory](https://astrochili.itch.io/the-secret-laboratory) — A short card game about the labaratory director.
|
||||
|
||||
## Features
|
||||
|
||||
### Supported
|
||||
|
||||
- [x] Comments: singleline, multiline, todo's
|
||||
- [x] Tags: global tags, knot tags, stitch tags, paragraph tags
|
||||
- [x] Paths and sections: inclusions, knots, stitches, labels
|
||||
- [x] Choices: suppressing and mixing, labels, conditions, sticky and fallback choices, tags
|
||||
- [x] Branching: diversions, glues, gathers, nesting
|
||||
- [x] Tunnels
|
||||
- [x] Alternatives: sequences, cycles, once-only, shuffles, empty steps, nesting
|
||||
- [x] Multiline alternatives: all the same + shuffle options
|
||||
- [x] Conditions: logical operations, string queries, if and else statements, nesting
|
||||
- [x] Multiline conditions: all the same + elseif statements, switches, nesting
|
||||
- [x] Variables: assignments, constants, global variables, temporary variables, visits, lists
|
||||
- [x] Lists: logical operations, multivalued lists, multi-list lists, all the queries, work with numbers
|
||||
- [x] Game queries: all the queries without `TURNS()` and `TURNS_SINCE()`
|
||||
- [x] State: saving and loading
|
||||
- [x] Integration: external functions, variables observing, jumping
|
||||
- [x] Migration: the ability to implement the migration of player's saves after the book update
|
||||
- [x] Internal functions
|
||||
|
||||
### Unsupported
|
||||
|
||||
- [ ] [Threads](https://github.com/astrochili/narrator/issues/22)
|
||||
- [ ] [Divert target as variable type](https://github.com/astrochili/narrator/issues/23)
|
||||
- [ ] [Assigning string evaluations to variables](https://github.com/astrochili/narrator/issues/24)
|
||||
- [ ] [Multiple parallel flows](https://github.com/astrochili/narrator/issues/25)
|
||||
|
||||
Also there is a list of [known limitations](https://github.com/astrochili/narrator/labels/known%20limitation) on the issues page.
|
||||
|
||||
## Installation
|
||||
|
||||
### Common case (Löve, pure Lua, etc.)
|
||||
|
||||
Download the latest [release archive](https://github.com/astrochili/narrator/releases) and require the `narrator` module.
|
||||
|
||||
```lua
|
||||
local narrator = require('narrator.narrator')
|
||||
```
|
||||
|
||||
Narrator requires [lpeg](http://www.inf.puc-rio.br/~roberto/lpeg/) as dependency to parse Ink content. You can install it with [luarocks](https://luarocks.org/).
|
||||
|
||||
```shell
|
||||
$ luarocks install lpeg
|
||||
```
|
||||
|
||||
In fact, you don't need `lpeg` in the release, but you need it locally to parse Ink content and generate lua versions of books to play in your game. Use parsing in development only, prefer already parsed and stored books in production.
|
||||
|
||||
### Defold
|
||||
|
||||
Add links to the zip-archives of the latest versions of [narrator](https://github.com/astrochili/narrator/releases) and [defold-lpeg](https://github.com/astrochili/defold-lpeg/releases) to your Defold project as [dependencies](http://www.defold.com/manuals/libraries/).
|
||||
|
||||
```
|
||||
https://github.com/astrochili/narrator/archive/master.zip
|
||||
https://github.com/astrochili/defold-lpeg/archive/master.zip
|
||||
```
|
||||
|
||||
Then you can require the `narrator` module.
|
||||
|
||||
```lua
|
||||
local narrator = require('narrator.narrator')
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### narrator.parse_file(path, params)
|
||||
|
||||
Parses the Ink file at path with all the inclusions and returns a book instance. Path notations `'stories/game.ink'`, `'stories/game'` and `'stories.game'` are valid.
|
||||
|
||||
You can save a parsed book to the lua file with the same path by passing `{ save = true }` as `params` table. By default, the `params` table is `{ save = false }`.
|
||||
|
||||
```lua
|
||||
-- Parse a Ink file at path 'stories/game.ink'
|
||||
local book = narrator.parse_file('stories.game')
|
||||
|
||||
-- Parse a Ink file at path 'stories/game.ink'
|
||||
-- and save the book at path 'stories/game.lua'
|
||||
local book = narrator.parse_file('stories.game', { save = true })
|
||||
```
|
||||
Reading and saving files required `io` so if you can't work with files by this way use `narrator.parse_content()`.
|
||||
|
||||
### narrator.parse_content(content, inclusions)
|
||||
|
||||
Parses the string with Ink content and returns a book instance. The `inclusions` param is optional and can be used to pass an array of strings with Ink content of inclusions.
|
||||
|
||||
```lua
|
||||
local content = 'Content of a root Ink file'
|
||||
local inclusions = {
|
||||
'Content of an included Ink file',
|
||||
'Content of another included Ink file'
|
||||
}
|
||||
|
||||
-- Parse a string with Ink content
|
||||
local book = narrator.parse_content(content)
|
||||
|
||||
-- Parse a string with Ink content and inclusions
|
||||
local book = narrator.parse_content(content, inclusions)
|
||||
```
|
||||
|
||||
Content parsing is useful when you should manage files by your engine environment and don't want to use `io` module. For example, in Defold, you may want to load ink files as custom resources with [sys.load_resource()](https://defold.com/ref/sys/#sys.load_resource:filename).
|
||||
|
||||
### narrator.init_story(book)
|
||||
|
||||
Inits a story instance from the book. This is aclual to use in production. For example, just load a book with `require()` and pass it to this function.
|
||||
|
||||
```lua
|
||||
-- Require a parsed and saved before book
|
||||
local book = require('stories.game')
|
||||
|
||||
-- Init a story instance
|
||||
local story = narrator.init_story(book)
|
||||
```
|
||||
|
||||
### story:begin()
|
||||
|
||||
Begins the story. Generates the first chunk of paragraphs and choices.
|
||||
|
||||
### story:can_continue()
|
||||
|
||||
Returns a boolean, does the story have paragraphs to output or not.
|
||||
|
||||
```lua
|
||||
while story:can_continue() do
|
||||
-- Get paragraphs?
|
||||
end
|
||||
```
|
||||
|
||||
### story:continue(steps)
|
||||
|
||||
Get the next paragraphs. You can specify the number of paragraphs that you want to pull by the `steps` param.
|
||||
- Pass nothing if you want to get all the currently available paragraphs. `0` also works.
|
||||
- Pass `1` if you want to get one next paragraph without wrapping to array.
|
||||
|
||||
A paragraph is a table like `{ text = 'Hello.', tags = { 'tag1', 'tag2' } }`. Most of the paragraphs do not have tags so `tags` can be `nil`.
|
||||
|
||||
|
||||
```lua
|
||||
-- Get all the currently available paragraphs
|
||||
local paragraphs = story:continue()
|
||||
|
||||
-- Get one next paragraph
|
||||
local paragraph = story:continue(1)
|
||||
```
|
||||
|
||||
### story:can_choose()
|
||||
|
||||
Returns a boolean, does the story have choices to output or not. Also returns `false` if there are available paragraphs to continue.
|
||||
|
||||
```lua
|
||||
if story:can_choose() do
|
||||
-- Get choices?
|
||||
end
|
||||
```
|
||||
|
||||
### story:get_choices()
|
||||
|
||||
Returns an array of available choices. Returns an empty array if there are available paragraphs to continue.
|
||||
|
||||
A choice is a table like `{ text = 'Bye.', tags = { 'tag1', 'tag2' } }`. Most of the choices do not have tags so `tags` can be `nil`.
|
||||
|
||||
Choice tags are not an official feature of Ink, but it's a Narrator feature. These tags also will appear in the answer paragraph as it works in Ink by default. But if you have a completely eaten choice like `'[Answer] #tag'` you will receive tags only in the choice.
|
||||
|
||||
```lua
|
||||
-- Get available choices and output them to the player
|
||||
local choices = story:get_choices()
|
||||
for i, choice in ipairs(choices) do
|
||||
print(i .. ') ' .. choice.text)
|
||||
end
|
||||
```
|
||||
|
||||
### story:choose(index)
|
||||
|
||||
Make a choice to continue the story. Pass the `index` of the choice that you was received with `get_choices()` before. Will do nothing if `can_continue()` returns `false`.
|
||||
|
||||
```lua
|
||||
-- Get the answer from the player in the terminal
|
||||
answer = tonumber(io.read())
|
||||
|
||||
-- Send the answer to the story to generate new paragraphs
|
||||
story:choose(answer)
|
||||
|
||||
-- Get the new paragraphs
|
||||
local new_paragraphs = story:continue()
|
||||
```
|
||||
|
||||
### story:jump_to(path_string)
|
||||
|
||||
Jumps to the path. The `path_string` param is a string like `'knot.stitch.label'`.
|
||||
|
||||
```lua
|
||||
-- Jump to the maze stitch in the adventure knot
|
||||
story:jump_to('adventure.maze')
|
||||
|
||||
-- Get the maze paragraphs
|
||||
local maze_paragraphs = story:continue()
|
||||
```
|
||||
|
||||
### story:get_visits(path_string)
|
||||
|
||||
Returns the number of visits to the path. The `path_string` param is a string like `'knot.stitch.label'`.
|
||||
|
||||
```lua
|
||||
-- Get the number of visits to the maze's red room
|
||||
local red_room_visits = story:get_visits('adventure.maze.red_room')
|
||||
|
||||
-- Get the number of adventures visited.
|
||||
local adventure_visits = story:get_visits('adventure')
|
||||
```
|
||||
|
||||
### story:get_tags(path_string)
|
||||
|
||||
Returns tags for the path. The `path_string` param is a string like `'knot.stitch'`. This function is useful when you want to get tags before continue the story and pull paragraphs. Read more about it [here](https://github.com/inkle/ink/blob/master/Documentation/RunningYourInk.md#knot-tags).
|
||||
|
||||
```lua
|
||||
-- Get tags for the path 'adventure.maze'
|
||||
local mazeTags = story:get_tags('adventure.maze')
|
||||
```
|
||||
|
||||
### story:save_state()
|
||||
|
||||
Raturns a table with the story state that can be saved and restored later. Use it to save the game.
|
||||
|
||||
```lua
|
||||
-- Get the story's state
|
||||
local state = story:save_state()
|
||||
|
||||
-- Save the state to your local storage
|
||||
manager.save(state)
|
||||
```
|
||||
|
||||
### story:load_state(state)
|
||||
|
||||
Restores a story's state from the saved before state. Use it to load the game.
|
||||
|
||||
```lua
|
||||
-- Load the state from your local storage
|
||||
local state = manager.load()
|
||||
|
||||
-- Restore the story's state
|
||||
story:load_state(state)
|
||||
|
||||
```
|
||||
|
||||
### story:observe(variable, observer)
|
||||
|
||||
Assigns an observer function to the variable's changes.
|
||||
|
||||
```lua
|
||||
local function x_did_change(x)
|
||||
print('The x did change! Now it\'s ' .. x)
|
||||
end
|
||||
|
||||
-- Start observing the variable 'x'
|
||||
story:observe('x', x_did_change)
|
||||
```
|
||||
|
||||
### story:bind(func_name, handler)
|
||||
|
||||
Binds a function to external calling from the Ink. The function can returns the value or not.
|
||||
|
||||
```lua
|
||||
local function beep()
|
||||
print('Beep! 😃')
|
||||
end
|
||||
|
||||
local function sum(x, y)
|
||||
return x + y
|
||||
end
|
||||
|
||||
-- Bind the function without params and returned value
|
||||
story:bind('beep', beep)
|
||||
|
||||
-- Bind the function with params and returned value
|
||||
story:bind('sum', sum)
|
||||
```
|
||||
|
||||
### story.global_tags
|
||||
|
||||
An array with book's global tags. Tags are strings of course.
|
||||
|
||||
```lua
|
||||
-- Get the global tags
|
||||
local global_tags = story.global_tags
|
||||
|
||||
-- A hacky way to get the same global tags
|
||||
local global_tags = story:get_tags()
|
||||
```
|
||||
|
||||
### story.constants
|
||||
|
||||
A table with book's constants. Just read them, constants changing is not a good idea.
|
||||
|
||||
```lua
|
||||
-- Get the theme value from the Ink constants
|
||||
local theme = story.constants['theme']
|
||||
```
|
||||
|
||||
### story.variables
|
||||
|
||||
A table with story's variables. You can read or change them by this way.
|
||||
|
||||
```lua
|
||||
-- Get the mood variable value
|
||||
local mood = story.variables['mood']
|
||||
|
||||
-- Set the mood variable value
|
||||
story.variables['mood'] = 'sunny'
|
||||
```
|
||||
|
||||
### story.migrate
|
||||
|
||||
A function that you can specify for migration from old to new versions of your books. This is useful, for example, when you don't want to corrupt player's save after the game update.
|
||||
|
||||
This is the place where you can rename or change variables, visits, update the current path, etc. The default implementation returns the same state without any migration.
|
||||
|
||||
```lua
|
||||
-- Default implementation
|
||||
function(state, old_version, new_version) return state end
|
||||
```
|
||||
|
||||
The `old_version` is the version of the saved state, the `new_version` is the version of the book. You can specify the verson of the book with the constant `'version'` in the Ink content, otherwise it's equal to `0`.
|
||||
|
||||
```lua
|
||||
-- A migration function example
|
||||
local function migrate(state, old_version, new_version)
|
||||
|
||||
-- Check the need for migration
|
||||
if new_version == old_version then
|
||||
return state
|
||||
end
|
||||
|
||||
-- Migration for the second version of the book
|
||||
if new_version == 2 then
|
||||
|
||||
-- Get the old value
|
||||
local old_mood = state.variables['mood']
|
||||
|
||||
-- If it exists then migrate ...
|
||||
if old_mood then
|
||||
-- ... migrate the old number value to the new string value
|
||||
state.variables['mood'] = old_mood < 50 and 'sadly' or 'sunny'
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
-- Assign the migration function before loading a saved game
|
||||
story.migrate = migrate
|
||||
|
||||
-- Load the game
|
||||
story:load_state(saved_state)
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
### Development
|
||||
|
||||
There are some useful extensions and configs for [VSCode](https://code.visualstudio.com/) that I use in development of Narrator.
|
||||
|
||||
- [Local Lua Debugger](https://github.com/tomblind/local-lua-debugger-vscode) by [tomblind](https://github.com/tomblind/).
|
||||
- [Lua Language Server](https://github.com/sumneko/lua-language-server) by [sunmeko](https://github.com/sumneko).
|
||||
- A task named `Busted` runs tests with `tests/run.lua`.
|
||||
- A lunch configuration named `Busted` runs the debugger with `tests/run.lua`.
|
||||
- A lunch configuration named `Debug` runs the debugger with `debug.lua`.
|
||||
|
||||
### Testing
|
||||
|
||||
To run tests you need to install [busted](https://github.com/Olivine-Labs/busted).
|
||||
|
||||
```shell
|
||||
$ luarocks install busted
|
||||
```
|
||||
|
||||
Don't forget also to install `lpeg` as described in [Common case](#common-case-löve-pure-lua-etc) installation section.
|
||||
|
||||
After that you can run tests from the terminal:
|
||||
```shell
|
||||
$ busted test/run.lua
|
||||
```
|
||||
|
||||
## Third Party Libraries
|
||||
|
||||
- [LPeg](http://www.inf.puc-rio.br/~roberto/lpeg/) by [Roberto Ierusalimschy](http://www.inf.puc-rio.br/~roberto/) (MIT Licence).
|
||||
- [classic](https://github.com/rxi/classic) by [rxi](https://github.com/rxi) (MIT Licence).
|
||||
- [lume](https://github.com/rxi/lume) by [rxi](https://github.com/rxi) (MIT Licence).
|
||||
63
libs/narrator/bot.lua
Normal file
63
libs/narrator/bot.lua
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
--
|
||||
-- Bot for story playing
|
||||
|
||||
local bot = { }
|
||||
|
||||
--- Play a story by bot
|
||||
-- @param story Story: a story instance
|
||||
-- @param instructor function: function that will be return the answer index
|
||||
-- @param params.print boolean: print a game log to console or not, false by default
|
||||
-- @return string: a log of the game
|
||||
function bot.play(story, instructor, params)
|
||||
local params = params or { print = false }
|
||||
|
||||
local log = { }
|
||||
local step = 1
|
||||
|
||||
local function output(text)
|
||||
if params.print then print(text) end
|
||||
table.insert(log, text)
|
||||
end
|
||||
|
||||
story:begin()
|
||||
|
||||
while story:can_continue() or story:can_choose() do
|
||||
local paragraphs = story:continue()
|
||||
for _, paragraph in ipairs(paragraphs or { }) do
|
||||
local text = paragraph.text or ''
|
||||
if paragraph.tags then
|
||||
local hashtag = #text > 0 and ' #' or '#'
|
||||
text = text .. hashtag .. table.concat(paragraph.tags, ' #')
|
||||
end
|
||||
output(text)
|
||||
end
|
||||
|
||||
if not story:can_choose() then break end
|
||||
|
||||
local choices = story:get_choices()
|
||||
local answer = instructor(choices, step)
|
||||
step = step + 1
|
||||
|
||||
-- Check for a signal to emergency exit
|
||||
if answer == -1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
output('')
|
||||
for i, choice in ipairs(choices) do
|
||||
local prefix = (i == answer and '>' or i) .. ') '
|
||||
local text = prefix .. choice.text
|
||||
if choice.tags then
|
||||
text = text .. ' #' .. table.concat(choice.tags, ' #')
|
||||
end
|
||||
output(text)
|
||||
end
|
||||
output('')
|
||||
|
||||
story:choose(answer)
|
||||
end
|
||||
|
||||
return table.concat(log, '\n')
|
||||
end
|
||||
|
||||
return bot
|
||||
30
libs/narrator/debug.lua
Normal file
30
libs/narrator/debug.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
--
|
||||
-- Script for debugging in VSCode with
|
||||
-- Local Lua Debugger by Tom Blind (https://github.com/tomblind/local-lua-debugger-vscode)
|
||||
|
||||
-- Dependencies
|
||||
local narrator = require('narrator.narrator')
|
||||
local bot = require('bot')
|
||||
|
||||
-- Loading
|
||||
-- local book = require('stories.debug')
|
||||
-- local book = narrator.parseBook('Hello world!', { '=== one === \n text 1', '=== two === \n text 2' })
|
||||
local book = narrator.parse_file('stories.debug', { save = false })
|
||||
|
||||
local story = narrator.init_story(book)
|
||||
local answers = { }
|
||||
|
||||
-- Choice instructor for a bot
|
||||
local function instructor(choices, step)
|
||||
local answer = answers[step]
|
||||
if answer == nil then
|
||||
math.randomseed(os.time() * 10000000)
|
||||
answer = math.random(1, #choices)
|
||||
end
|
||||
return answer
|
||||
end
|
||||
|
||||
-- Game
|
||||
print('--- Game started ---\n')
|
||||
bot.play(story, instructor, { print = true })
|
||||
print('\n--- Game over ---')
|
||||
19
libs/narrator/example-defold/book.ink
Normal file
19
libs/narrator/example-defold/book.ink
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
- I looked at Monsieur Fogg
|
||||
* ... and I could contain myself no longer.
|
||||
'What is the purpose of our journey, Monsieur?'
|
||||
'A wager,' he replied.
|
||||
* * 'A wager!'[] I returned.
|
||||
He nodded.
|
||||
* * * 'But surely that is foolishness!'
|
||||
* * * 'A most serious matter then!'
|
||||
- - - He nodded again.
|
||||
* * * 'But can we win?'
|
||||
'That is what we will endeavour to find out,' he answered.
|
||||
* * * 'A modest wager, I trust?'
|
||||
'Twenty thousand pounds,' he replied, quite flatly.
|
||||
* * * I asked nothing further of him then[.], and after a final, polite cough, he offered nothing more to me. <>
|
||||
* * 'Ah[.'],' I replied, uncertain what I thought.
|
||||
- - After that, <>
|
||||
* ... but I said nothing[] and <>
|
||||
- we passed the day in silence.
|
||||
- -> END
|
||||
39
libs/narrator/example-defold/examlpe.collection
Normal file
39
libs/narrator/example-defold/examlpe.collection
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: "main"
|
||||
scale_along_z: 0
|
||||
embedded_instances {
|
||||
id: "example"
|
||||
data: "components {\n"
|
||||
" id: \"example\"\n"
|
||||
" component: \"/example-defold/example.gui\"\n"
|
||||
" position {\n"
|
||||
" x: 0.0\n"
|
||||
" y: 0.0\n"
|
||||
" z: 0.0\n"
|
||||
" }\n"
|
||||
" rotation {\n"
|
||||
" x: 0.0\n"
|
||||
" y: 0.0\n"
|
||||
" z: 0.0\n"
|
||||
" w: 1.0\n"
|
||||
" }\n"
|
||||
" property_decls {\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
""
|
||||
position {
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
}
|
||||
rotation {
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
scale3 {
|
||||
x: 1.0
|
||||
y: 1.0
|
||||
z: 1.0
|
||||
}
|
||||
}
|
||||
17
libs/narrator/example-defold/example.font
Normal file
17
libs/narrator/example-defold/example.font
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
font: "/builtins/fonts/vera_mo_bd.ttf"
|
||||
material: "/builtins/fonts/font.material"
|
||||
size: 32
|
||||
antialias: 1
|
||||
alpha: 1.0
|
||||
outline_alpha: 0.0
|
||||
outline_width: 0.0
|
||||
shadow_alpha: 0.0
|
||||
shadow_blur: 0
|
||||
shadow_x: 0.0
|
||||
shadow_y: 0.0
|
||||
extra_characters: ""
|
||||
output_format: TYPE_BITMAP
|
||||
all_chars: false
|
||||
cache_width: 0
|
||||
cache_height: 0
|
||||
render_mode: MODE_SINGLE_LAYER
|
||||
146
libs/narrator/example-defold/example.gui
Normal file
146
libs/narrator/example-defold/example.gui
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
script: "/example-defold/example.gui_script"
|
||||
fonts {
|
||||
name: "example"
|
||||
font: "/example-defold/example.font"
|
||||
}
|
||||
background_color {
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
w: 0.0
|
||||
}
|
||||
nodes {
|
||||
position {
|
||||
x: 320.0
|
||||
y: 340.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
rotation {
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
scale {
|
||||
x: 0.5
|
||||
y: 0.5
|
||||
z: 0.5
|
||||
w: 1.0
|
||||
}
|
||||
size {
|
||||
x: 800.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
color {
|
||||
x: 0.102
|
||||
y: 0.102
|
||||
z: 0.102
|
||||
w: 1.0
|
||||
}
|
||||
type: TYPE_TEXT
|
||||
blend_mode: BLEND_MODE_ALPHA
|
||||
text: "paragraph"
|
||||
font: "example"
|
||||
id: "paragraph"
|
||||
xanchor: XANCHOR_NONE
|
||||
yanchor: YANCHOR_NONE
|
||||
pivot: PIVOT_N
|
||||
outline {
|
||||
x: 1.0
|
||||
y: 1.0
|
||||
z: 1.0
|
||||
w: 1.0
|
||||
}
|
||||
shadow {
|
||||
x: 1.0
|
||||
y: 1.0
|
||||
z: 1.0
|
||||
w: 1.0
|
||||
}
|
||||
adjust_mode: ADJUST_MODE_FIT
|
||||
line_break: true
|
||||
layer: ""
|
||||
inherit_alpha: true
|
||||
alpha: 1.0
|
||||
outline_alpha: 1.0
|
||||
shadow_alpha: 1.0
|
||||
template_node_child: false
|
||||
text_leading: 1.0
|
||||
text_tracking: 0.0
|
||||
custom_type: 0
|
||||
enabled: false
|
||||
visible: true
|
||||
material: ""
|
||||
}
|
||||
nodes {
|
||||
position {
|
||||
x: 320.0
|
||||
y: 300.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
rotation {
|
||||
x: 0.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
scale {
|
||||
x: 0.5
|
||||
y: 0.5
|
||||
z: 0.5
|
||||
w: 1.0
|
||||
}
|
||||
size {
|
||||
x: 800.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
color {
|
||||
x: 0.6
|
||||
y: 0.2
|
||||
z: 0.0
|
||||
w: 1.0
|
||||
}
|
||||
type: TYPE_TEXT
|
||||
blend_mode: BLEND_MODE_ALPHA
|
||||
text: "choice"
|
||||
font: "example"
|
||||
id: "choice"
|
||||
xanchor: XANCHOR_NONE
|
||||
yanchor: YANCHOR_NONE
|
||||
pivot: PIVOT_N
|
||||
outline {
|
||||
x: 1.0
|
||||
y: 1.0
|
||||
z: 1.0
|
||||
w: 1.0
|
||||
}
|
||||
shadow {
|
||||
x: 1.0
|
||||
y: 1.0
|
||||
z: 1.0
|
||||
w: 1.0
|
||||
}
|
||||
adjust_mode: ADJUST_MODE_FIT
|
||||
line_break: true
|
||||
layer: ""
|
||||
inherit_alpha: true
|
||||
alpha: 1.0
|
||||
outline_alpha: 1.0
|
||||
shadow_alpha: 1.0
|
||||
template_node_child: false
|
||||
text_leading: 1.0
|
||||
text_tracking: 0.0
|
||||
custom_type: 0
|
||||
enabled: false
|
||||
visible: true
|
||||
material: ""
|
||||
}
|
||||
material: "/builtins/materials/gui.material"
|
||||
adjust_reference: ADJUST_REFERENCE_PARENT
|
||||
max_nodes: 512
|
||||
167
libs/narrator/example-defold/example.gui_script
Normal file
167
libs/narrator/example-defold/example.gui_script
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
local narrator = require('narrator.narrator')
|
||||
|
||||
---@class Self
|
||||
---@field paragraph_template node
|
||||
---@field paragraph_initial_position vector3
|
||||
---@field paragraph_nodes node[]
|
||||
---@field choice_template node
|
||||
---@field choice_initial_position vector3
|
||||
---@field choice_nodes node[]
|
||||
---@field book Narrator.Book
|
||||
---@field story Narrator.Story
|
||||
|
||||
---@param node node
|
||||
---@param text string
|
||||
---@return integer height
|
||||
local function estimated_height(node, text)
|
||||
local font_name = gui.get_font(node)
|
||||
local font = gui.get_font_resource(font_name)
|
||||
|
||||
local metrics = resource.get_text_metrics(font, text, {
|
||||
width = gui.get_size(node).x,
|
||||
line_break = true
|
||||
})
|
||||
|
||||
return metrics.height
|
||||
end
|
||||
|
||||
---@param self Self
|
||||
local function clear_output(self)
|
||||
for index = #self.paragraph_nodes, 1, -1 do
|
||||
local node = self.paragraph_nodes[index]
|
||||
table.remove(self.paragraph_nodes, index)
|
||||
|
||||
gui.animate(node, 'color.w', 0, gui.EASING_LINEAR, 0.5, 0, function(_, node)
|
||||
gui.delete_node(node)
|
||||
end)
|
||||
end
|
||||
|
||||
for index = #self.choice_nodes, 1, -1 do
|
||||
local node = self.choice_nodes[index]
|
||||
table.remove(self.choice_nodes, index)
|
||||
|
||||
gui.animate(node, 'color.w', 0, gui.EASING_LINEAR, 0.5, 0, function(_, node)
|
||||
gui.delete_node(node)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param self Self
|
||||
local function display_output(self)
|
||||
clear_output(self)
|
||||
|
||||
-- Pull all the paragraphs
|
||||
local paragraphs = self.story:continue()
|
||||
|
||||
-- Get the available choices
|
||||
local choices = self.story:get_choices()
|
||||
|
||||
local paragraph_position = vmath.vector3(self.paragraph_initial_position)
|
||||
local choice_position = vmath.vector3(self.choice_initial_position)
|
||||
|
||||
for index = #paragraphs, 1, -1 do
|
||||
local paragraph = paragraphs[index]
|
||||
local node = gui.clone(self.paragraph_template)
|
||||
gui.set_text(node, paragraph.text)
|
||||
|
||||
local height = estimated_height(node, paragraph.text)
|
||||
if index < #paragraphs then
|
||||
paragraph_position.y = paragraph_position.y + height / 2 + 16
|
||||
end
|
||||
|
||||
gui.set_position(node, paragraph_position)
|
||||
gui.set_enabled(node, true)
|
||||
gui.set_alpha(node, 0)
|
||||
gui.animate(node, 'color.w', 1, go.EASING_LINEAR, 0.5, index / 2)
|
||||
|
||||
table.insert(self.paragraph_nodes, node)
|
||||
end
|
||||
|
||||
for index = 1, #choices do
|
||||
local choice = choices[index]
|
||||
local node = gui.clone(self.choice_template)
|
||||
gui.set_text(node, choice.text)
|
||||
|
||||
local height = estimated_height(node, choice.text)
|
||||
if index > 1 then
|
||||
choice_position.y = choice_position.y - height / 2 - 16
|
||||
end
|
||||
|
||||
gui.set_enabled(node, true)
|
||||
gui.set_position(node, choice_position)
|
||||
|
||||
local size = gui.get_size(node)
|
||||
size.y = height
|
||||
gui.set_size(node, size)
|
||||
|
||||
gui.set_alpha(node, 0)
|
||||
gui.animate(node, 'color.w', 1, go.EASING_LINEAR, 0.5, (#paragraphs + 1) / 2)
|
||||
|
||||
table.insert(self.choice_nodes, node)
|
||||
end
|
||||
end
|
||||
|
||||
---@param self Self
|
||||
function init(self)
|
||||
msg.post('@render:', 'clear_color', { color = vmath.vector4(1, 1, 1, 1)})
|
||||
msg.post('.', 'acquire_input_focus')
|
||||
|
||||
self.paragraph_template = gui.get_node('paragraph')
|
||||
self.paragraph_initial_position = gui.get_position(self.paragraph_template)
|
||||
self.paragraph_nodes = {}
|
||||
|
||||
self.choice_template = gui.get_node('choice')
|
||||
self.choice_initial_position = gui.get_position(self.choice_template)
|
||||
self.choice_nodes = {}
|
||||
|
||||
-- Parse and save a book
|
||||
self.book = narrator.parse_file('example-defold.book', { save = true })
|
||||
|
||||
-- Or load and parse the Ink file from the custom resources
|
||||
-- local content = sys.load_resource('/example-defold/book.ink')
|
||||
-- self.book = narrator.parseBook(content)
|
||||
|
||||
-- Or load a book from the saved lua module
|
||||
-- self.book = require('example-defold.book')
|
||||
|
||||
-- Or parse a book from the string with Ink content
|
||||
-- self.book = narrator.parseBook('Hello world!')
|
||||
|
||||
-- Init a story
|
||||
self.story = narrator.init_story(self.book)
|
||||
|
||||
-- Begin the story
|
||||
self.story:begin()
|
||||
|
||||
display_output(self)
|
||||
end
|
||||
|
||||
---@param self Self
|
||||
---@param action_id hash|string
|
||||
---@param action table
|
||||
function on_input(self, action_id, action)
|
||||
if action_id ~= hash 'touch' or not action.pressed then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.story:can_choose() then
|
||||
-- Begin a new story
|
||||
self.story = narrator.init_story(self.book)
|
||||
self.story:begin()
|
||||
|
||||
display_output(self)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
for index, node in ipairs(self.choice_nodes) do
|
||||
if gui.pick_node(node, action.x, action.y) then
|
||||
-- Make a choice
|
||||
self.story:choose(index)
|
||||
|
||||
display_output(self)
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
60
libs/narrator/game.lua
Normal file
60
libs/narrator/game.lua
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
--
|
||||
-- A simple command-line game example
|
||||
|
||||
-- Dependencies
|
||||
local narrator = require('narrator.narrator')
|
||||
|
||||
-- Parse a book from the Ink file and save as module 'stories.game.lua'
|
||||
local book = narrator.parse_file('stories.game', { save = true })
|
||||
|
||||
-- Init a story from the book
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
-- Start observing the Ink variable 'x'
|
||||
story:observe('x', function(x) print('The x did change! Now it\'s ' .. x) end)
|
||||
|
||||
-- Bind local functions to call from ink as external functions
|
||||
story:bind('beep', function() print('Beep! 😃') end)
|
||||
story:bind('sum', function(x, y) return x + y end)
|
||||
|
||||
-- Begin the story
|
||||
story:begin()
|
||||
|
||||
print('--- Game started ---\n')
|
||||
|
||||
while story:can_continue() do
|
||||
|
||||
-- Get current paragraphs to output
|
||||
local paragraphs = story:continue()
|
||||
|
||||
for _, paragraph in ipairs(paragraphs) do
|
||||
local text = paragraph.text
|
||||
|
||||
-- You can handle tags as you like, but we attach them to text here.
|
||||
if paragraph.tags then
|
||||
text = text .. ' #' .. table.concat(paragraph.tags, ' #')
|
||||
end
|
||||
|
||||
-- Output text to the player
|
||||
print(text)
|
||||
end
|
||||
|
||||
-- If there is no choice, it seems the game is over
|
||||
if not story:can_choose() then break end
|
||||
print('')
|
||||
|
||||
-- Get available choices and output them to the player
|
||||
local choices = story:get_choices()
|
||||
for i, choice in ipairs(choices) do
|
||||
print(i .. ') ' .. choice.text)
|
||||
end
|
||||
|
||||
-- Read the choice from the player input
|
||||
local answer = tonumber(io.read()) or 0
|
||||
print('')
|
||||
|
||||
-- Send an answer to the story to generate new paragraphs
|
||||
story:choose(answer)
|
||||
end
|
||||
|
||||
print('\n--- Game over ---')
|
||||
25
libs/narrator/game.project
Normal file
25
libs/narrator/game.project
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[bootstrap]
|
||||
main_collection = /example-defold/examlpe.collectionc
|
||||
render = /builtins/render/default.renderc
|
||||
|
||||
[script]
|
||||
shared_state = 1
|
||||
|
||||
[display]
|
||||
width = 640
|
||||
height = 640
|
||||
|
||||
[android]
|
||||
input_method = HiddenInputField
|
||||
|
||||
[project]
|
||||
title = Narrator
|
||||
custom_resources = example-defold/book.ink
|
||||
dependencies#0 = https://github.com/astrochili/defold-lpeg/archive/1.0.4.zip
|
||||
|
||||
[library]
|
||||
include_dirs = narrator
|
||||
|
||||
[input]
|
||||
game_binding = /builtins/input/all.input_bindingc
|
||||
|
||||
37
libs/narrator/narrator/annotations.lua
Normal file
37
libs/narrator/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
libs/narrator/narrator/enums.lua
Normal file
32
libs/narrator/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
libs/narrator/narrator/libs/classic.lua
Executable file
68
libs/narrator/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
libs/narrator/narrator/libs/lume.lua
Executable file
780
libs/narrator/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
libs/narrator/narrator/list/mt.lua
Normal file
401
libs/narrator/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
libs/narrator/narrator/narrator.lua
Normal file
150
libs/narrator/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
libs/narrator/narrator/parser.lua
Normal file
789
libs/narrator/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
libs/narrator/narrator/story.lua
Normal file
1253
libs/narrator/narrator/story.lua
Normal file
File diff suppressed because it is too large
Load diff
0
libs/narrator/stories/debug.ink
Normal file
0
libs/narrator/stories/debug.ink
Normal file
19
libs/narrator/stories/game.ink
Normal file
19
libs/narrator/stories/game.ink
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
- I looked at Monsieur Fogg
|
||||
* ... and I could contain myself no longer.
|
||||
'What is the purpose of our journey, Monsieur?'
|
||||
'A wager,' he replied.
|
||||
* * 'A wager!'[] I returned.
|
||||
He nodded.
|
||||
* * * 'But surely that is foolishness!'
|
||||
* * * 'A most serious matter then!'
|
||||
- - - He nodded again.
|
||||
* * * 'But can we win?'
|
||||
'That is what we will endeavour to find out,' he answered.
|
||||
* * * 'A modest wager, I trust?'
|
||||
'Twenty thousand pounds,' he replied, quite flatly.
|
||||
* * * I asked nothing further of him then[.], and after a final, polite cough, he offered nothing more to me. <>
|
||||
* * 'Ah[.'],' I replied, uncertain what I thought.
|
||||
- - After that, <>
|
||||
* ... but I said nothing[] and <>
|
||||
- we passed the day in silence.
|
||||
- -> END
|
||||
1
libs/narrator/stories/game.lua
Normal file
1
libs/narrator/stories/game.lua
Normal file
|
|
@ -0,0 +1 @@
|
|||
return {inclusions={},constants={},version={tree=1,engine=1},tree={_={_={"I looked at Monsieur Fogg",{text="... and I could contain myself no longer.",choice="... and I could contain myself no longer.",node={"'What is the purpose of our journey, Monsieur?'","'A wager,' he replied.",{text="'A wager!' I returned.",choice="'A wager!'",node={"He nodded.",{text="'But surely that is foolishness!'",choice="'But surely that is foolishness!'"},{text="'A most serious matter then!'",choice="'A most serious matter then!'"},"He nodded again.",{text="'But can we win?'",choice="'But can we win?'",node={"'That is what we will endeavour to find out,' he answered."}},{text="'A modest wager, I trust?'",choice="'A modest wager, I trust?'",node={"'Twenty thousand pounds,' he replied, quite flatly."}},{text="I asked nothing further of him then, and after a final, polite cough, he offered nothing more to me. <>",choice="I asked nothing further of him then."}}},{text="'Ah,' I replied, uncertain what I thought.",choice="'Ah.'"},"After that, <>"}},{text="... but I said nothing and <>",choice="... but I said nothing"},"we passed the day in silence.",{divert="END"}}}},lists={},variables={}}
|
||||
78
libs/narrator/test/cases.lua
Normal file
78
libs/narrator/test/cases.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
--
|
||||
-- Test cases
|
||||
|
||||
local runtime = {
|
||||
'continue',
|
||||
'observing',
|
||||
'binding',
|
||||
'set-get',
|
||||
'visits',
|
||||
'tags',
|
||||
'jumping',
|
||||
'save-load'
|
||||
}
|
||||
|
||||
local units = {
|
||||
'inclusions',
|
||||
'comments',
|
||||
'knots',
|
||||
'stitches',
|
||||
|
||||
'text-line',
|
||||
'text-lines',
|
||||
'text-tags',
|
||||
'text-glue',
|
||||
|
||||
'choices-basic',
|
||||
'choices-tags',
|
||||
'choices-conditional',
|
||||
'choices-sticky',
|
||||
'choices-fallback',
|
||||
'choices-tunnel',
|
||||
|
||||
'labels-choices',
|
||||
'labels-nested',
|
||||
|
||||
'branching',
|
||||
'nesting',
|
||||
'gather',
|
||||
'loop',
|
||||
'vars',
|
||||
'constants',
|
||||
'expressions',
|
||||
'queries',
|
||||
|
||||
'conditions-inline',
|
||||
'alts-inline',
|
||||
'conditions-switch',
|
||||
'alts-blocks',
|
||||
|
||||
'lists-basic',
|
||||
'lists-operators',
|
||||
'lists-queries',
|
||||
|
||||
'tunnels',
|
||||
'escape',
|
||||
'functions'
|
||||
}
|
||||
|
||||
local stories = {
|
||||
-- No complex stories at the moment
|
||||
}
|
||||
|
||||
local cases = {
|
||||
runtime = runtime,
|
||||
units = units,
|
||||
stories = stories
|
||||
}
|
||||
|
||||
local folder_separator = package.config:sub(1, 1)
|
||||
for folder_name, folder_cases in pairs(cases) do
|
||||
local items_with_foldes = { }
|
||||
for _, case in ipairs(folder_cases) do
|
||||
table.insert(items_with_foldes, folder_name .. folder_separator .. case)
|
||||
end
|
||||
cases[folder_name] = items_with_foldes
|
||||
end
|
||||
|
||||
return cases
|
||||
252
libs/narrator/test/run.lua
Normal file
252
libs/narrator/test/run.lua
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
--
|
||||
-- Testing with Busted library
|
||||
|
||||
--
|
||||
-- Dependencies
|
||||
|
||||
local import = require
|
||||
import('busted.runner')()
|
||||
|
||||
if os.getenv('LOCAL_LUA_DEBUGGER_VSCODE') == '1' then
|
||||
import('lldebugger').start()
|
||||
end
|
||||
|
||||
local bot = require('bot')
|
||||
local narrator = require('narrator.narrator')
|
||||
local lume = require('narrator.libs.lume')
|
||||
|
||||
--
|
||||
-- Constants
|
||||
|
||||
local folder_separator = package.config:sub(1, 1)
|
||||
local tests_folder = 'test' .. folder_separator
|
||||
|
||||
--- Make path for a .lua file
|
||||
-- @param case string: an runtime test case
|
||||
-- @return string: a .lua path
|
||||
local function lua_path(case)
|
||||
return tests_folder .. case .. '.lua'
|
||||
end
|
||||
|
||||
--- Make path for an .ink file
|
||||
-- @param case string: an Ink test case
|
||||
-- @return string: an .ink path
|
||||
local function ink_path(case)
|
||||
return tests_folder .. case .. '.ink'
|
||||
end
|
||||
|
||||
--- Make path for a .txt file
|
||||
-- @param case string: an Ink test case
|
||||
-- @param answers table: a sequence of answers (numbers)
|
||||
-- @return string: a .txt path
|
||||
local function txt_path(case, answers)
|
||||
local path = tests_folder .. case
|
||||
if answers and #answers > 0 then
|
||||
path = path .. folder_separator .. table.concat(answers, '-')
|
||||
end
|
||||
path = path .. '.txt'
|
||||
return path
|
||||
end
|
||||
|
||||
--- Get all possible sequences and logs of the case
|
||||
-- @param case string: an Ink test case
|
||||
-- @return table: an array of possible games { sequence, log }
|
||||
local function get_possible_results(case)
|
||||
local path = ink_path(case)
|
||||
local book = narrator.parse_file(path)
|
||||
|
||||
local results = { }
|
||||
local sequences = { { } }
|
||||
local seq_index
|
||||
|
||||
local function instructor(choices, step)
|
||||
local cur_seq = sequences[seq_index]
|
||||
local answer = cur_seq[step]
|
||||
|
||||
if not answer then
|
||||
-- Transform a current sequence to branches for each available choice
|
||||
table.remove(sequences, seq_index)
|
||||
|
||||
for index, _ in ipairs(choices) do
|
||||
local new_seq = lume.concat(cur_seq, { index })
|
||||
table.insert(sequences, new_seq)
|
||||
end
|
||||
|
||||
-- Set a stop signal for the bot
|
||||
answer = -1
|
||||
end
|
||||
|
||||
return answer
|
||||
end
|
||||
|
||||
while #sequences > 0 do
|
||||
-- Iterate sequences and play them
|
||||
for index = 1, #sequences do
|
||||
local sequence = sequences[index]
|
||||
seq_index = index
|
||||
|
||||
-- Play the sequence
|
||||
local story = narrator.init_story(book)
|
||||
local log = bot.play(story, instructor)
|
||||
|
||||
-- If the sequence was finished then save the result and mark it as finished
|
||||
if log then
|
||||
local result = { sequence = sequence, log = log }
|
||||
table.insert(results, result)
|
||||
sequences[index] = { is_finished = true }
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove finished sequences
|
||||
for index = #sequences, 1, -1 do
|
||||
local sequence = sequences[index]
|
||||
if sequence.is_finished then
|
||||
table.remove(sequences, index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
--- Create possible results for an Ink test case and save them to txt files
|
||||
-- @param case string: an Ink test case
|
||||
-- @param override boolean: override a txt file if it already exists.
|
||||
local function create_txt_for_ink_case(case, override)
|
||||
local override = override ~= nil and override or false
|
||||
local results = get_possible_results(case)
|
||||
|
||||
for _, result in ipairs(results) do
|
||||
local txt_path = txt_path(case, #results > 1 and result.sequence or nil)
|
||||
local file = io.open(txt_path, 'r')
|
||||
local is_file_exists = file ~= nil
|
||||
if is_file_exists then io.close(file) end
|
||||
|
||||
if not is_file_exists or override then
|
||||
local folder_path = txt_path:match('(.*' .. folder_separator .. ')')
|
||||
local folder = io.open(folder_path, 'r')
|
||||
local is_folder_exists = folder ~= nil
|
||||
if is_folder_exists then
|
||||
io.close(folder)
|
||||
else
|
||||
os.execute('mkdir ' .. folder_path)
|
||||
end
|
||||
|
||||
file = io.open(txt_path, 'w')
|
||||
assert(file, 'Has no access to the file at path \'' .. txt_path .. '\'.')
|
||||
|
||||
file:write(result.log)
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Create possible results for Ink test cases and save them to txt files
|
||||
-- @param cases table: an array of Ink test cases
|
||||
-- @param override bool: override the txt file if it already exists
|
||||
local function create_txt_for_ink_cases(cases, override)
|
||||
for _, case in ipairs(cases) do
|
||||
create_txt_for_ink_case(case, override)
|
||||
end
|
||||
end
|
||||
|
||||
--- Test an Ink case
|
||||
-- @param case string: an Ink test case
|
||||
local function test_ink_case(case)
|
||||
describe('Test an Ink case \'' .. case .. '\'.', function()
|
||||
local path = ink_path(case)
|
||||
local book = narrator.parse_file(path)
|
||||
|
||||
local results = get_possible_results(case)
|
||||
for _, result in ipairs(results) do
|
||||
describe('Sequence is [' .. table.concat(result.sequence, '-') .. '].', function()
|
||||
local txt_path = txt_path(case, #results > 1 and result.sequence or nil)
|
||||
local file = io.open(txt_path, 'r')
|
||||
|
||||
it('Checking results.', function()
|
||||
assert.is_not_nil(file)
|
||||
|
||||
local expected = file:read('*all')
|
||||
file:close()
|
||||
|
||||
assert.are.same(expected, result.log)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Test Ink cases
|
||||
-- @param cases table: an array of test cases
|
||||
local function test_ink_cases(cases)
|
||||
for _, case in ipairs(cases) do
|
||||
test_ink_case(case)
|
||||
end
|
||||
end
|
||||
|
||||
--- Test a runtime case
|
||||
-- @param case string: a runtime test case
|
||||
local function test_lua_case(case)
|
||||
describe('Test a runtime case \'' .. case .. '\'.', function()
|
||||
local lua_path = lua_path(case)
|
||||
loadfile(lua_path)(narrator, describe, it, assert)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Test runtime cases
|
||||
-- @param cases table: an array of runtime test cases
|
||||
local function test_lua_cases(cases)
|
||||
for _, case in ipairs(cases) do
|
||||
test_lua_case(case)
|
||||
end
|
||||
end
|
||||
|
||||
--- Override math.random functions to prevent different results beetween machines and pass test-cases.
|
||||
local function override_random()
|
||||
local test_seed
|
||||
local original_random = math.random
|
||||
|
||||
math.randomseed = function(x)
|
||||
test_seed = x
|
||||
end
|
||||
|
||||
math.random = function(x, y)
|
||||
if test_seed then
|
||||
local result = math.max(x, math.min(test_seed, y))
|
||||
test_seed = nil
|
||||
return result
|
||||
else
|
||||
return original_random(x, y)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Main
|
||||
|
||||
local case = nil
|
||||
local cases = require('test.cases')
|
||||
local override_case_results = false
|
||||
|
||||
override_random()
|
||||
|
||||
if override_case_results then
|
||||
if case then
|
||||
create_txt_for_ink_case(case, true)
|
||||
else
|
||||
create_txt_for_ink_cases(cases.units, true)
|
||||
create_txt_for_ink_cases(cases.stories, true)
|
||||
end
|
||||
end
|
||||
|
||||
if case then
|
||||
if case:find('runtime/', 1, 8) then
|
||||
test_lua_case(case)
|
||||
else
|
||||
test_ink_case(case)
|
||||
end
|
||||
else
|
||||
test_lua_cases(cases.runtime)
|
||||
test_ink_cases(cases.units)
|
||||
test_ink_cases(cases.stories)
|
||||
end
|
||||
42
libs/narrator/test/runtime/binding.lua
Normal file
42
libs/narrator/test/runtime/binding.lua
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
~ beep()
|
||||
{ sum(1, 2) }
|
||||
{ did_solve_puzzle("labirint") }
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
local is_beeped = false
|
||||
local puzzles = { }
|
||||
|
||||
story:bind('beep', function()
|
||||
is_beeped = true
|
||||
end)
|
||||
|
||||
story:bind('sum', function(x, y)
|
||||
return x + y
|
||||
end)
|
||||
|
||||
story:bind('did_solve_puzzle', function(puzzle)
|
||||
puzzles[puzzle] = true
|
||||
end)
|
||||
|
||||
story:begin()
|
||||
|
||||
it('Was a beep?', function()
|
||||
assert.is_true(is_beeped)
|
||||
end)
|
||||
|
||||
it('Sum is equal to 3.', function()
|
||||
local paragraphs = story:continue()
|
||||
assert.equal(#paragraphs, 1)
|
||||
assert.equal('3', paragraphs[1].text)
|
||||
end)
|
||||
|
||||
it('Labirint is sovled.', function()
|
||||
local puzzle_is_solved = puzzles['labirint']
|
||||
assert.is_true(puzzle_is_solved)
|
||||
end)
|
||||
30
libs/narrator/test/runtime/continue.lua
Normal file
30
libs/narrator/test/runtime/continue.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Line 5
|
||||
Line 6
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
story:begin()
|
||||
|
||||
it('Get one paragraph.', function()
|
||||
local paragraph = story:continue(1)
|
||||
assert.equal(paragraph.text, 'Line 1')
|
||||
end)
|
||||
|
||||
it('Get two paragraphs.', function()
|
||||
local paragraphs = story:continue(2)
|
||||
assert.equal(#paragraphs, 2)
|
||||
end)
|
||||
|
||||
it('Get remain paragraphs.', function()
|
||||
local paragraphs = story:continue()
|
||||
assert.equal(#paragraphs, 3)
|
||||
end)
|
||||
39
libs/narrator/test/runtime/jumping.lua
Normal file
39
libs/narrator/test/runtime/jumping.lua
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
A root line
|
||||
=== knot
|
||||
A knot line
|
||||
= stitch
|
||||
A stitch line
|
||||
=== somewhere
|
||||
- (label) A label line
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
story:begin()
|
||||
|
||||
local paragraphs = story:continue()
|
||||
|
||||
it('Jump to label.', function()
|
||||
story:jump_to('somewhere.label')
|
||||
local paragraphs = story:continue()
|
||||
assert.equal(#paragraphs, 1)
|
||||
assert.equal(paragraphs[1].text, 'A label line')
|
||||
end)
|
||||
|
||||
it('Jump to stitch.', function()
|
||||
story:jump_to('knot.stitch')
|
||||
local paragraphs = story:continue()
|
||||
assert.equal(#paragraphs, 1)
|
||||
assert.equal(paragraphs[1].text, 'A stitch line')
|
||||
end)
|
||||
|
||||
it('Jump to knot.', function()
|
||||
story:jump_to('knot')
|
||||
local paragraphs = story:continue()
|
||||
assert.equal(#paragraphs, 1)
|
||||
assert.equal(paragraphs[1].text, 'A knot line')
|
||||
end)
|
||||
21
libs/narrator/test/runtime/observing.lua
Normal file
21
libs/narrator/test/runtime/observing.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
VAR mood = "sadly"
|
||||
~ mood = "sunny"
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
local mood
|
||||
|
||||
story:observe('mood', function(value)
|
||||
mood = value
|
||||
end)
|
||||
|
||||
story:begin()
|
||||
|
||||
it('Sunny mood.', function()
|
||||
assert.equal(mood, 'sunny')
|
||||
end)
|
||||
59
libs/narrator/test/runtime/save-load.lua
Normal file
59
libs/narrator/test/runtime/save-load.lua
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
VAR x = 1
|
||||
* (choice) [Hello]
|
||||
- (hello) Hello world!
|
||||
-> knot
|
||||
=== knot
|
||||
= stitch
|
||||
~ x = 2
|
||||
~ temp y = 3
|
||||
A line 1
|
||||
A line 2
|
||||
A line 3
|
||||
* Just a road to hell -> END
|
||||
* The best road to hell -> END
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
|
||||
local saved_state
|
||||
|
||||
it('Saving', function()
|
||||
local story = narrator.init_story(book)
|
||||
story:begin()
|
||||
story:continue()
|
||||
story:choose(1)
|
||||
story:continue(2)
|
||||
|
||||
saved_state = story:save_state()
|
||||
|
||||
local expected_path = { knot = 'knot', stitch = 'stitch' }
|
||||
assert.are.same(saved_state.path, expected_path)
|
||||
assert.equal(saved_state.variables['x'], 2)
|
||||
assert.equal(saved_state.temp['y'], 3)
|
||||
assert.equal(saved_state.visits._._.hello, 1)
|
||||
assert.equal(#saved_state.output, 2)
|
||||
assert.equal(#saved_state.paragraphs, 2)
|
||||
assert.equal(#saved_state.choices, 2)
|
||||
end)
|
||||
|
||||
it('Loading.', function()
|
||||
local story = narrator.init_story(book)
|
||||
story:begin()
|
||||
story:load_state(saved_state)
|
||||
|
||||
local expected_path = { knot = 'knot', stitch = 'stitch' }
|
||||
assert.are.same(story.current_path, expected_path)
|
||||
assert.equal(story.variables['x'], 2)
|
||||
assert.equal(story.temp['y'], 3)
|
||||
assert.equal(story:get_visits('hello'), 1)
|
||||
assert.equal(#story.output, 2)
|
||||
|
||||
local paragraphs = story:continue()
|
||||
local choices = story:get_choices()
|
||||
|
||||
assert.equal(#paragraphs, 2)
|
||||
assert.equal(#choices, 2)
|
||||
end)
|
||||
30
libs/narrator/test/runtime/set-get.lua
Normal file
30
libs/narrator/test/runtime/set-get.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
VAR x = 1
|
||||
* [Change x to 1]
|
||||
* [Change x to 2]
|
||||
* [Change x to 3]
|
||||
x = { x }
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
story:begin()
|
||||
|
||||
it('The x is equal to 1.', function()
|
||||
local x = story.variables['x']
|
||||
assert.equal(x, 1)
|
||||
end)
|
||||
|
||||
it('The x is changed to 3.', function()
|
||||
local answer = 3
|
||||
story.variables['x'] = answer
|
||||
story:choose(answer)
|
||||
|
||||
local paragraphs = story:continue()
|
||||
assert.equal(#paragraphs, 1)
|
||||
assert.equal(paragraphs[1].text, 'x = 3')
|
||||
end)
|
||||
|
||||
51
libs/narrator/test/runtime/tags.lua
Normal file
51
libs/narrator/test/runtime/tags.lua
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
# global tag 1
|
||||
# global tag 2 # global tag 3
|
||||
Root line -> knot
|
||||
=== knot ===
|
||||
# knot tag
|
||||
A knot line 1 # line 1 tag
|
||||
A knot line 2 # line 2 tag
|
||||
= stitch
|
||||
# stitch tag
|
||||
A stitch line # line 3 tag
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
story:begin()
|
||||
|
||||
local paragraphs = story:continue()
|
||||
|
||||
it('Global tags.', function()
|
||||
local expected = { 'global tag 1', 'global tag 2', 'global tag 3' }
|
||||
assert.are.same(expected, story.global_tags)
|
||||
|
||||
local global_tags = story:get_tags()
|
||||
assert.are.same(expected, global_tags)
|
||||
end)
|
||||
|
||||
it('Knot tags.', function()
|
||||
local expected = { 'knot tag' }
|
||||
local knot_tags = story:get_tags('knot')
|
||||
assert.are.same(expected, knot_tags)
|
||||
end)
|
||||
|
||||
it('Stitch tags.', function()
|
||||
local expected = { 'stitch tag' }
|
||||
local stitch_tags = story:get_tags('knot.stitch')
|
||||
assert.are.same(expected, stitch_tags)
|
||||
end)
|
||||
|
||||
it('Paragraph tags.', function()
|
||||
assert.equal(#paragraphs, 2)
|
||||
|
||||
local expected = { 'global tag 1', 'global tag 2', 'global tag 3', 'knot tag', 'line 1 tag' }
|
||||
assert.are.same(expected, paragraphs[1].tags)
|
||||
|
||||
local expected = { 'line 2 tag' }
|
||||
assert.are.same(expected, paragraphs[2].tags)
|
||||
end)
|
||||
65
libs/narrator/test/runtime/visits.lua
Normal file
65
libs/narrator/test/runtime/visits.lua
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
local narrator, describe, it, assert = ...
|
||||
|
||||
local content = [[
|
||||
A root text
|
||||
+ Go to knot -> knot
|
||||
=== knot
|
||||
+ Go to stitch -> stitch
|
||||
= stitch
|
||||
+ Go to label -> label
|
||||
- (label) Some text
|
||||
+ (choice) Go to root -> _._
|
||||
]]
|
||||
|
||||
local book = narrator.parse_content(content)
|
||||
local story = narrator.init_story(book)
|
||||
|
||||
story:begin()
|
||||
|
||||
local function visits()
|
||||
local visits = {
|
||||
root = story:get_visits(''),
|
||||
knot = story:get_visits('knot'),
|
||||
stitch = story:get_visits('knot.stitch'),
|
||||
label = story:get_visits('knot.stitch.label'),
|
||||
choice = story:get_visits('knot.stitch.choice')
|
||||
}
|
||||
|
||||
return visits
|
||||
end
|
||||
|
||||
local places = { 'root', 'knot', 'stitch', 'label' }
|
||||
|
||||
for cycle = 1, 3 do
|
||||
describe('Visits with cycle ' .. cycle .. '.', function()
|
||||
story:continue()
|
||||
|
||||
for place_index = 1, #places do
|
||||
local place = places[place_index]
|
||||
|
||||
it('Visit the ' .. place .. '.', function()
|
||||
story:continue()
|
||||
|
||||
local place = place
|
||||
local divert = story.choices[1].divert
|
||||
|
||||
local expected_root = cycle
|
||||
local expected_knot = place_index > 1 and cycle or cycle - 1
|
||||
local expected_stitch = place_index > 2 and cycle or cycle - 1
|
||||
local expected_label = place_index > 3 and cycle or cycle - 1
|
||||
local expected_choice = place_index > 4 and cycle or cycle - 1
|
||||
|
||||
local visits = visits()
|
||||
|
||||
assert.equal(visits.root, expected_root)
|
||||
assert.equal(visits.knot, expected_knot)
|
||||
assert.equal(visits.stitch, expected_stitch)
|
||||
assert.equal(visits.label, expected_label)
|
||||
assert.equal(visits.choice, expected_choice)
|
||||
|
||||
story:choose(1)
|
||||
end)
|
||||
end
|
||||
|
||||
end)
|
||||
end
|
||||
60
libs/narrator/test/units/alts-blocks.ink
Normal file
60
libs/narrator/test/units/alts-blocks.ink
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
VAR counter = 1
|
||||
-> casino
|
||||
|
||||
= casino
|
||||
=== { counter }
|
||||
At the table, I drew a card. <>
|
||||
{ stopping:
|
||||
-
|
||||
I entered the casino.
|
||||
- Okay. <>
|
||||
I entered the casino again.
|
||||
- { counter }. <>
|
||||
|
||||
Once more, I went inside.
|
||||
}
|
||||
~ counter++
|
||||
{ counter < 5 : -> casino | -> cycle }
|
||||
|
||||
= cycle
|
||||
=== { counter }
|
||||
{ cycle:
|
||||
- I held my breath.
|
||||
- I waited impatiently.
|
||||
- I paused.
|
||||
}
|
||||
~ counter++
|
||||
{ counter < 9: -> cycle | -> once }
|
||||
|
||||
= once
|
||||
=== { counter }
|
||||
{ once:
|
||||
- Would my luck hold?
|
||||
- Could I win the hand?
|
||||
}
|
||||
~ counter++
|
||||
{ counter < 13 : -> once | -> shuffle }
|
||||
|
||||
= shuffle
|
||||
=== { counter }
|
||||
{ SEED_RANDOM(counter - 13) }
|
||||
At the table, I drew a card. <>
|
||||
{ shuffle:
|
||||
- Ace of Hearts.
|
||||
- King of Spades.
|
||||
- 2 of Diamonds.
|
||||
'You lose this time!' crowed the croupier.
|
||||
}
|
||||
~ counter++
|
||||
{ counter < 16 : -> shuffle | -> nested }
|
||||
|
||||
= nested
|
||||
=== { counter }
|
||||
{ SEED_RANDOM(counter - 16) }
|
||||
{ true:
|
||||
{ shuffle:
|
||||
- Okay. Ace of Hearts again.
|
||||
- Okay. Ace of Hearts?!
|
||||
}
|
||||
}
|
||||
-> END
|
||||
31
libs/narrator/test/units/alts-blocks.txt
Normal file
31
libs/narrator/test/units/alts-blocks.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
=== 1
|
||||
At the table, I drew a card. I entered the casino.
|
||||
=== 2
|
||||
At the table, I drew a card. Okay. I entered the casino again.
|
||||
=== 3
|
||||
At the table, I drew a card. 3. Once more, I went inside.
|
||||
=== 4
|
||||
At the table, I drew a card. 4. Once more, I went inside.
|
||||
=== 5
|
||||
I held my breath.
|
||||
=== 6
|
||||
I waited impatiently.
|
||||
=== 7
|
||||
I paused.
|
||||
=== 8
|
||||
I held my breath.
|
||||
=== 9
|
||||
Would my luck hold?
|
||||
=== 10
|
||||
Could I win the hand?
|
||||
=== 11
|
||||
=== 12
|
||||
=== 13
|
||||
At the table, I drew a card. Ace of Hearts.
|
||||
=== 14
|
||||
At the table, I drew a card. King of Spades.
|
||||
=== 15
|
||||
At the table, I drew a card. 2 of Diamonds.
|
||||
'You lose this time!' crowed the croupier.
|
||||
=== 16
|
||||
Okay. Ace of Hearts again.
|
||||
37
libs/narrator/test/units/alts-inline.ink
Normal file
37
libs/narrator/test/units/alts-inline.ink
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
VAR counter = 0
|
||||
-> coffee
|
||||
|
||||
= coffee
|
||||
{I bought a coffee with my five-pound note.|I bought a second coffee for my friend.|I didn't have enough money to buy any more coffee.}
|
||||
~ counter++
|
||||
{ counter < 4 : -> coffee | -> today }
|
||||
|
||||
= today
|
||||
It was {&Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday} today.
|
||||
~ counter++
|
||||
{ counter < 15 : -> today | -> joke}
|
||||
|
||||
= joke
|
||||
He told me a joke. {!I laughed politely.|I smiled.|I grimaced.|I promised myself to not react again.}
|
||||
~ counter++
|
||||
{ counter < 20 : -> joke | -> coin}
|
||||
|
||||
= coin
|
||||
{ SEED_RANDOM(20 - counter) }
|
||||
I tossed the coin. {~Heads|Tails}.
|
||||
~ counter++
|
||||
{ counter < 22 : -> coin | -> lights}
|
||||
|
||||
= lights
|
||||
I took a step forward. {!||Then the lights went out. -> dark }
|
||||
~ counter++
|
||||
{ counter < 27 : -> lights | -> ratbear}
|
||||
|
||||
= dark
|
||||
So dark...
|
||||
-> ratbear
|
||||
|
||||
= ratbear
|
||||
The Ratbear {&{wastes no time and |}swipes|scratches } {&at you|into your {&leg|arm|cheek}}.
|
||||
~ counter++
|
||||
{ counter < 32 : -> ratbear | -> END }
|
||||
33
libs/narrator/test/units/alts-inline.txt
Normal file
33
libs/narrator/test/units/alts-inline.txt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
I bought a coffee with my five-pound note.
|
||||
I bought a second coffee for my friend.
|
||||
I didn't have enough money to buy any more coffee.
|
||||
I didn't have enough money to buy any more coffee.
|
||||
It was Monday today.
|
||||
It was Tuesday today.
|
||||
It was Wednesday today.
|
||||
It was Thursday today.
|
||||
It was Friday today.
|
||||
It was Saturday today.
|
||||
It was Sunday today.
|
||||
It was Monday today.
|
||||
It was Tuesday today.
|
||||
It was Wednesday today.
|
||||
It was Thursday today.
|
||||
He told me a joke. I laughed politely.
|
||||
He told me a joke. I smiled.
|
||||
He told me a joke. I grimaced.
|
||||
He told me a joke. I promised myself to not react again.
|
||||
He told me a joke.
|
||||
I tossed the coin. Heads.
|
||||
I tossed the coin. Tails.
|
||||
I took a step forward.
|
||||
I took a step forward.
|
||||
I took a step forward. Then the lights went out. So dark...
|
||||
The Ratbear wastes no time and swipes at you.
|
||||
The Ratbear scratches into your leg.
|
||||
The Ratbear swipes at you.
|
||||
The Ratbear scratches into your arm.
|
||||
The Ratbear swipes at you.
|
||||
The Ratbear scratches into your cheek.
|
||||
The Ratbear swipes at you.
|
||||
The Ratbear scratches into your leg.
|
||||
21
libs/narrator/test/units/branching.ink
Normal file
21
libs/narrator/test/units/branching.ink
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-> back_in_london
|
||||
|
||||
=== back_in_london ===
|
||||
|
||||
We arrived into London at 9.45pm exactly.
|
||||
* 'There is not a moment to lose!'[] I declared.
|
||||
-> hurry_outside
|
||||
* 'Monsieur, let us savour this moment!'[] I declared.
|
||||
My master clouted me firmly around the head and dragged me out of the door.
|
||||
-> dragged_outside
|
||||
* [We hurried home] -> hurry_outside
|
||||
|
||||
=== hurry_outside ===
|
||||
We hurried home to Savile Row -> as_fast_as_we_could
|
||||
|
||||
=== dragged_outside ===
|
||||
He insisted that we hurried home to Savile Row
|
||||
-> as_fast_as_we_could
|
||||
|
||||
=== as_fast_as_we_could ===
|
||||
<> as fast as we could.
|
||||
8
libs/narrator/test/units/branching/1.txt
Normal file
8
libs/narrator/test/units/branching/1.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
We arrived into London at 9.45pm exactly.
|
||||
|
||||
>) 'There is not a moment to lose!'
|
||||
2) 'Monsieur, let us savour this moment!'
|
||||
3) We hurried home
|
||||
|
||||
'There is not a moment to lose!' I declared.
|
||||
We hurried home to Savile Row as fast as we could.
|
||||
9
libs/narrator/test/units/branching/2.txt
Normal file
9
libs/narrator/test/units/branching/2.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
We arrived into London at 9.45pm exactly.
|
||||
|
||||
1) 'There is not a moment to lose!'
|
||||
>) 'Monsieur, let us savour this moment!'
|
||||
3) We hurried home
|
||||
|
||||
'Monsieur, let us savour this moment!' I declared.
|
||||
My master clouted me firmly around the head and dragged me out of the door.
|
||||
He insisted that we hurried home to Savile Row as fast as we could.
|
||||
7
libs/narrator/test/units/branching/3.txt
Normal file
7
libs/narrator/test/units/branching/3.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
We arrived into London at 9.45pm exactly.
|
||||
|
||||
1) 'There is not a moment to lose!'
|
||||
2) 'Monsieur, let us savour this moment!'
|
||||
>) We hurried home
|
||||
|
||||
We hurried home to Savile Row as fast as we could.
|
||||
9
libs/narrator/test/units/choices-basic.ink
Normal file
9
libs/narrator/test/units/choices-basic.ink
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"What's that?" my master asked again.
|
||||
* I don't know.
|
||||
Okay...
|
||||
* [Eat answer!]
|
||||
Nice to hear from you!
|
||||
* "I am somewhat tired[."]," I repeated.
|
||||
"Really," he responded. "How deleterious."
|
||||
* "Nothing, Monsieur!"[] I replied.
|
||||
"Very good, then."
|
||||
9
libs/narrator/test/units/choices-basic/1.txt
Normal file
9
libs/narrator/test/units/choices-basic/1.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"What's that?" my master asked again.
|
||||
|
||||
>) I don't know.
|
||||
2) Eat answer!
|
||||
3) "I am somewhat tired."
|
||||
4) "Nothing, Monsieur!"
|
||||
|
||||
I don't know.
|
||||
Okay...
|
||||
8
libs/narrator/test/units/choices-basic/2.txt
Normal file
8
libs/narrator/test/units/choices-basic/2.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"What's that?" my master asked again.
|
||||
|
||||
1) I don't know.
|
||||
>) Eat answer!
|
||||
3) "I am somewhat tired."
|
||||
4) "Nothing, Monsieur!"
|
||||
|
||||
Nice to hear from you!
|
||||
9
libs/narrator/test/units/choices-basic/3.txt
Normal file
9
libs/narrator/test/units/choices-basic/3.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"What's that?" my master asked again.
|
||||
|
||||
1) I don't know.
|
||||
2) Eat answer!
|
||||
>) "I am somewhat tired."
|
||||
4) "Nothing, Monsieur!"
|
||||
|
||||
"I am somewhat tired," I repeated.
|
||||
"Really," he responded. "How deleterious."
|
||||
9
libs/narrator/test/units/choices-basic/4.txt
Normal file
9
libs/narrator/test/units/choices-basic/4.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"What's that?" my master asked again.
|
||||
|
||||
1) I don't know.
|
||||
2) Eat answer!
|
||||
3) "I am somewhat tired."
|
||||
>) "Nothing, Monsieur!"
|
||||
|
||||
"Nothing, Monsieur!" I replied.
|
||||
"Very good, then."
|
||||
17
libs/narrator/test/units/choices-conditional.ink
Normal file
17
libs/narrator/test/units/choices-conditional.ink
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-> door
|
||||
|
||||
=== door ===
|
||||
* { not france.paris } [Go to Paris] -> france.paris
|
||||
* { not england.london } [Go to London] -> england.london
|
||||
* { france || england } [Go home]
|
||||
Sweet home... -> END
|
||||
|
||||
=== france ===
|
||||
= paris
|
||||
Hello Paris!
|
||||
-> door
|
||||
|
||||
=== england ===
|
||||
= london
|
||||
Hello London!
|
||||
-> door
|
||||
14
libs/narrator/test/units/choices-conditional/1-1-1.txt
Normal file
14
libs/narrator/test/units/choices-conditional/1-1-1.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
>) Go to Paris
|
||||
2) Go to London
|
||||
|
||||
Hello Paris!
|
||||
|
||||
>) Go to London
|
||||
2) Go home
|
||||
|
||||
Hello London!
|
||||
|
||||
>) Go home
|
||||
|
||||
Sweet home...
|
||||
10
libs/narrator/test/units/choices-conditional/1-2.txt
Normal file
10
libs/narrator/test/units/choices-conditional/1-2.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
>) Go to Paris
|
||||
2) Go to London
|
||||
|
||||
Hello Paris!
|
||||
|
||||
1) Go to London
|
||||
>) Go home
|
||||
|
||||
Sweet home...
|
||||
14
libs/narrator/test/units/choices-conditional/2-1-1.txt
Normal file
14
libs/narrator/test/units/choices-conditional/2-1-1.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
1) Go to Paris
|
||||
>) Go to London
|
||||
|
||||
Hello London!
|
||||
|
||||
>) Go to Paris
|
||||
2) Go home
|
||||
|
||||
Hello Paris!
|
||||
|
||||
>) Go home
|
||||
|
||||
Sweet home...
|
||||
10
libs/narrator/test/units/choices-conditional/2-2.txt
Normal file
10
libs/narrator/test/units/choices-conditional/2-2.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
1) Go to Paris
|
||||
>) Go to London
|
||||
|
||||
Hello London!
|
||||
|
||||
1) Go to Paris
|
||||
>) Go home
|
||||
|
||||
Sweet home...
|
||||
10
libs/narrator/test/units/choices-fallback.ink
Normal file
10
libs/narrator/test/units/choices-fallback.ink
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-> find_help
|
||||
|
||||
=== find_help ===
|
||||
|
||||
You search desperately for a friendly face in the crowd.
|
||||
* The woman in the hat[?] pushes you roughly aside. -> find_help
|
||||
* The man with the briefcase[?] looks disgusted as you stumble past him. -> find_help
|
||||
* ->
|
||||
But it is too late: you collapse onto the station platform. This is the end.
|
||||
-> END
|
||||
11
libs/narrator/test/units/choices-fallback/1-1.txt
Normal file
11
libs/narrator/test/units/choices-fallback/1-1.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
You search desperately for a friendly face in the crowd.
|
||||
|
||||
>) The woman in the hat?
|
||||
2) The man with the briefcase?
|
||||
|
||||
The woman in the hat pushes you roughly aside. You search desperately for a friendly face in the crowd.
|
||||
|
||||
>) The man with the briefcase?
|
||||
|
||||
The man with the briefcase looks disgusted as you stumble past him. You search desperately for a friendly face in the crowd.
|
||||
But it is too late: you collapse onto the station platform. This is the end.
|
||||
11
libs/narrator/test/units/choices-fallback/2-1.txt
Normal file
11
libs/narrator/test/units/choices-fallback/2-1.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
You search desperately for a friendly face in the crowd.
|
||||
|
||||
1) The woman in the hat?
|
||||
>) The man with the briefcase?
|
||||
|
||||
The man with the briefcase looks disgusted as you stumble past him. You search desperately for a friendly face in the crowd.
|
||||
|
||||
>) The woman in the hat?
|
||||
|
||||
The woman in the hat pushes you roughly aside. You search desperately for a friendly face in the crowd.
|
||||
But it is too late: you collapse onto the station platform. This is the end.
|
||||
13
libs/narrator/test/units/choices-sticky.ink
Normal file
13
libs/narrator/test/units/choices-sticky.ink
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-> homers_couch
|
||||
|
||||
=== homers_couch ===
|
||||
+ [Eat another donut]
|
||||
You eat another donut. -> fine
|
||||
* [Get off the couch]
|
||||
You struggle up off the couch to go and compose epic poetry.
|
||||
-> END
|
||||
|
||||
=== fine ===
|
||||
* Ok, is it fine? -> homers_couch
|
||||
* Ok, it's fine! -> homers_couch
|
||||
* -> END
|
||||
24
libs/narrator/test/units/choices-sticky/1-1-1-1-1.txt
Normal file
24
libs/narrator/test/units/choices-sticky/1-1-1-1-1.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, is it fine?
|
||||
2) Ok, it's fine!
|
||||
|
||||
Ok, is it fine?
|
||||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, it's fine!
|
||||
|
||||
Ok, it's fine!
|
||||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
24
libs/narrator/test/units/choices-sticky/1-1-1-1-2.txt
Normal file
24
libs/narrator/test/units/choices-sticky/1-1-1-1-2.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, is it fine?
|
||||
2) Ok, it's fine!
|
||||
|
||||
Ok, is it fine?
|
||||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, it's fine!
|
||||
|
||||
Ok, it's fine!
|
||||
|
||||
1) Eat another donut
|
||||
>) Get off the couch
|
||||
|
||||
You struggle up off the couch to go and compose epic poetry.
|
||||
15
libs/narrator/test/units/choices-sticky/1-1-2.txt
Normal file
15
libs/narrator/test/units/choices-sticky/1-1-2.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, is it fine?
|
||||
2) Ok, it's fine!
|
||||
|
||||
Ok, is it fine?
|
||||
|
||||
1) Eat another donut
|
||||
>) Get off the couch
|
||||
|
||||
You struggle up off the couch to go and compose epic poetry.
|
||||
24
libs/narrator/test/units/choices-sticky/1-2-1-1-1.txt
Normal file
24
libs/narrator/test/units/choices-sticky/1-2-1-1-1.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
1) Ok, is it fine?
|
||||
>) Ok, it's fine!
|
||||
|
||||
Ok, it's fine!
|
||||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, is it fine?
|
||||
|
||||
Ok, is it fine?
|
||||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
24
libs/narrator/test/units/choices-sticky/1-2-1-1-2.txt
Normal file
24
libs/narrator/test/units/choices-sticky/1-2-1-1-2.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
1) Ok, is it fine?
|
||||
>) Ok, it's fine!
|
||||
|
||||
Ok, it's fine!
|
||||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
>) Ok, is it fine?
|
||||
|
||||
Ok, is it fine?
|
||||
|
||||
1) Eat another donut
|
||||
>) Get off the couch
|
||||
|
||||
You struggle up off the couch to go and compose epic poetry.
|
||||
15
libs/narrator/test/units/choices-sticky/1-2-2.txt
Normal file
15
libs/narrator/test/units/choices-sticky/1-2-2.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
>) Eat another donut
|
||||
2) Get off the couch
|
||||
|
||||
You eat another donut.
|
||||
|
||||
1) Ok, is it fine?
|
||||
>) Ok, it's fine!
|
||||
|
||||
Ok, it's fine!
|
||||
|
||||
1) Eat another donut
|
||||
>) Get off the couch
|
||||
|
||||
You struggle up off the couch to go and compose epic poetry.
|
||||
5
libs/narrator/test/units/choices-sticky/2.txt
Normal file
5
libs/narrator/test/units/choices-sticky/2.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
1) Eat another donut
|
||||
>) Get off the couch
|
||||
|
||||
You struggle up off the couch to go and compose epic poetry.
|
||||
6
libs/narrator/test/units/choices-tags.ink
Normal file
6
libs/narrator/test/units/choices-tags.ink
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Let's try an experemental feature:
|
||||
The Choice Tags!
|
||||
* Are you seriously? #tag1
|
||||
Yeap, absolutely!
|
||||
* [Eat answer!] #tag2
|
||||
Are you hungry?
|
||||
8
libs/narrator/test/units/choices-tags/1.txt
Normal file
8
libs/narrator/test/units/choices-tags/1.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Let's try an experemental feature:
|
||||
The Choice Tags!
|
||||
|
||||
>) Are you seriously? #tag1
|
||||
2) Eat answer! #tag2
|
||||
|
||||
Are you seriously? #tag1
|
||||
Yeap, absolutely!
|
||||
7
libs/narrator/test/units/choices-tags/2.txt
Normal file
7
libs/narrator/test/units/choices-tags/2.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Let's try an experemental feature:
|
||||
The Choice Tags!
|
||||
|
||||
1) Are you seriously? #tag1
|
||||
>) Eat answer! #tag2
|
||||
|
||||
Are you hungry?
|
||||
7
libs/narrator/test/units/choices-tunnel.ink
Normal file
7
libs/narrator/test/units/choices-tunnel.ink
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
* [Master] ->master-> # test
|
||||
Okay...
|
||||
|
||||
=== master
|
||||
"What's that?" my master asked again.
|
||||
* I don't know.
|
||||
->->
|
||||
9
libs/narrator/test/units/choices-tunnel.txt
Normal file
9
libs/narrator/test/units/choices-tunnel.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
>) Master #test
|
||||
|
||||
"What's that?" my master asked again.
|
||||
|
||||
>) I don't know.
|
||||
|
||||
I don't know.
|
||||
Okay...
|
||||
15
libs/narrator/test/units/comments.ink
Normal file
15
libs/narrator/test/units/comments.ink
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"What do you make of this?" she asked.
|
||||
|
||||
// A simple comment
|
||||
|
||||
"I couldn't possibly comment," I replied. // A simple comment after text
|
||||
|
||||
/*
|
||||
... or an unlimited block of text
|
||||
*/
|
||||
|
||||
Before comment ... /* A comment */ ... and after. Must be in one line, but right now it's known limitation.
|
||||
|
||||
TODO: This TODO will not be printed.
|
||||
|
||||
Text before TODO. TODO: This TODO will be printed.
|
||||
5
libs/narrator/test/units/comments.txt
Normal file
5
libs/narrator/test/units/comments.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"What do you make of this?" she asked.
|
||||
"I couldn't possibly comment," I replied.
|
||||
Before comment ...
|
||||
... and after. Must be in one line, but right now it's known limitation.
|
||||
Text before TODO. TODO: This TODO will be printed.
|
||||
15
libs/narrator/test/units/conditions-inline.ink
Normal file
15
libs/narrator/test/units/conditions-inline.ink
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
VAR mood = 10
|
||||
VAR midnight = true
|
||||
|
||||
-> simple
|
||||
|
||||
=== simple
|
||||
Simple condition: { mood > 20 : Hmm... { mood > 40 : Wow! } I was feeling positive enough. } Okay.
|
||||
~ mood += 20
|
||||
{ mood > 50 : -> complex | -> simple }
|
||||
|
||||
=== complex
|
||||
Complex condition with midnight. { midnight : Wow! { midnight : Nice! | Bad! } | { not midnight : Very nice! | Very bad! } This is the end. }
|
||||
{ midnight != true : -> END }
|
||||
~ midnight = false
|
||||
-> complex
|
||||
5
libs/narrator/test/units/conditions-inline.txt
Normal file
5
libs/narrator/test/units/conditions-inline.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Simple condition: Okay.
|
||||
Simple condition: Hmm... I was feeling positive enough. Okay.
|
||||
Simple condition: Hmm... Wow! I was feeling positive enough. Okay.
|
||||
Complex condition with midnight. Wow! Nice!
|
||||
Complex condition with midnight. Very nice! This is the end.
|
||||
78
libs/narrator/test/units/conditions-switch.ink
Normal file
78
libs/narrator/test/units/conditions-switch.ink
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
VAR foo = false
|
||||
VAR x = 0
|
||||
|
||||
-> simple
|
||||
|
||||
== simple
|
||||
{not foo:
|
||||
Hello!
|
||||
}
|
||||
{ foo :
|
||||
True! -> choice
|
||||
- else:
|
||||
False! }
|
||||
~ foo = not foo
|
||||
-> simple
|
||||
|
||||
=== choice
|
||||
|
||||
{ true :
|
||||
* [Answer] -> nested_inline
|
||||
}
|
||||
|
||||
== nested_inline
|
||||
{
|
||||
- foo : { foo: True! | False! }
|
||||
Text here.
|
||||
- else: Badaboom!
|
||||
...again. -> nested_block
|
||||
}
|
||||
~ foo = not foo
|
||||
-> nested_inline
|
||||
|
||||
|
||||
== nested_block
|
||||
I love you. { true:
|
||||
And what about you?
|
||||
{
|
||||
- false:
|
||||
False!
|
||||
- true:
|
||||
* Choice -> tags
|
||||
}
|
||||
...
|
||||
} Something.
|
||||
|
||||
== tags
|
||||
{
|
||||
- not foo:
|
||||
text { true : x = { x } } #success
|
||||
- else:
|
||||
-> not_a_label
|
||||
}
|
||||
~ foo = not foo
|
||||
-> tags
|
||||
|
||||
== not_a_label
|
||||
{
|
||||
- foo: suc...
|
||||
success #success
|
||||
- x < 0: mid...
|
||||
middle
|
||||
text -> END
|
||||
- else: els...
|
||||
(notLabel) else
|
||||
-> switch
|
||||
}
|
||||
~ foo = not foo
|
||||
-> not_a_label
|
||||
|
||||
== switch
|
||||
{ x:
|
||||
- 0: zero
|
||||
- 1: one
|
||||
- 2: two
|
||||
- else: lots -> END
|
||||
}
|
||||
~ x++
|
||||
-> switch
|
||||
23
libs/narrator/test/units/conditions-switch.txt
Normal file
23
libs/narrator/test/units/conditions-switch.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Hello!
|
||||
False!
|
||||
True!
|
||||
|
||||
>) Answer
|
||||
|
||||
True!
|
||||
Text here.
|
||||
Badaboom!
|
||||
...again. I love you.
|
||||
And what about you?
|
||||
|
||||
>) Choice
|
||||
|
||||
Choice text x = 0 #success
|
||||
suc...
|
||||
success #success
|
||||
els...
|
||||
(notLabel) else
|
||||
zero
|
||||
one
|
||||
two
|
||||
lots
|
||||
26
libs/narrator/test/units/constants.ink
Normal file
26
libs/narrator/test/units/constants.ink
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
CONST STRING_EXAMPLE = "This is the string constant"
|
||||
CONST BOOLEAN_EXAMLPE_TRUE = true
|
||||
CONST BOOLEAN_EXAMLPE_FALSE = false
|
||||
|
||||
CONST LOBBY = 1
|
||||
CONST STAIRCASE = 2
|
||||
CONST HALLWAY = 3
|
||||
CONST HELD_BY_AGENT = -1
|
||||
|
||||
VAR secret_agent_location = LOBBY
|
||||
VAR suitcase_location = HALLWAY
|
||||
|
||||
{ STRING_EXAMPLE }. One is { BOOLEAN_EXAMLPE_TRUE }. Zero is { BOOLEAN_EXAMLPE_FALSE }.
|
||||
|
||||
-> report_progress
|
||||
|
||||
=== report_progress ===
|
||||
{ secret_agent_location == suitcase_location:
|
||||
The secret agent grabs the suitcase!
|
||||
~ suitcase_location = HELD_BY_AGENT
|
||||
|
||||
- secret_agent_location < suitcase_location:
|
||||
The secret agent moves forward.
|
||||
~ secret_agent_location++
|
||||
-> report_progress
|
||||
}
|
||||
4
libs/narrator/test/units/constants.txt
Normal file
4
libs/narrator/test/units/constants.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
This is the string constant. One is 1. Zero is 0.
|
||||
The secret agent moves forward.
|
||||
The secret agent moves forward.
|
||||
The secret agent grabs the suitcase!
|
||||
1
libs/narrator/test/units/escape.ink
Normal file
1
libs/narrator/test/units/escape.ink
Normal file
|
|
@ -0,0 +1 @@
|
|||
The \| dark \{grass\} is soft under your feet.
|
||||
1
libs/narrator/test/units/escape.txt
Normal file
1
libs/narrator/test/units/escape.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
The | dark {grass} is soft under your feet.
|
||||
7
libs/narrator/test/units/expressions.ink
Normal file
7
libs/narrator/test/units/expressions.ink
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
VAR friendly_name_of_player = "Katy"
|
||||
VAR age = "32"
|
||||
VAR dalmatians_count = 101
|
||||
|
||||
My friends call me {friendly_name_of_player}. I'm { age } years old.
|
||||
Wow, 2 x 2 = { 2 * 2 }!
|
||||
I have { dalmatians_count / 101 } dalmatian.
|
||||
3
libs/narrator/test/units/expressions.txt
Normal file
3
libs/narrator/test/units/expressions.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
My friends call me Katy. I'm 32 years old.
|
||||
Wow, 2 x 2 = 4!
|
||||
I have 1 dalmatian.
|
||||
23
libs/narrator/test/units/functions.ink
Normal file
23
libs/narrator/test/units/functions.ink
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{foo(): ->continue}
|
||||
something wrong 1
|
||||
|
||||
-(continue)
|
||||
{boo():
|
||||
something wrong 2
|
||||
}
|
||||
|
||||
sum: {test(2, 3)}
|
||||
|
||||
-> END
|
||||
|
||||
=== function test(a, b) ===
|
||||
first param is {a}
|
||||
second param is {b}
|
||||
~return a + b
|
||||
|
||||
=== function foo() ===
|
||||
~return true
|
||||
|
||||
=== function boo() ===
|
||||
~return
|
||||
|
||||
3
libs/narrator/test/units/functions.txt
Normal file
3
libs/narrator/test/units/functions.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
first param is 2
|
||||
second param is 3
|
||||
sum: 5
|
||||
7
libs/narrator/test/units/gather.ink
Normal file
7
libs/narrator/test/units/gather.ink
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
I looked at Monsieur Fogg
|
||||
* ... and I could contain myself no longer.
|
||||
'What is the purpose of our journey, Monsieur?'
|
||||
'A wager,' he replied.
|
||||
-
|
||||
we passed the day in silence.
|
||||
-> END
|
||||
8
libs/narrator/test/units/gather.txt
Normal file
8
libs/narrator/test/units/gather.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
I looked at Monsieur Fogg
|
||||
|
||||
>) ... and I could contain myself no longer.
|
||||
|
||||
... and I could contain myself no longer.
|
||||
'What is the purpose of our journey, Monsieur?'
|
||||
'A wager,' he replied.
|
||||
we passed the day in silence.
|
||||
3
libs/narrator/test/units/inclusions.ink
Normal file
3
libs/narrator/test/units/inclusions.ink
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
INCLUDE text-line.ink
|
||||
|
||||
A line after a line
|
||||
2
libs/narrator/test/units/inclusions.txt
Normal file
2
libs/narrator/test/units/inclusions.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Hello, world! I am a string.
|
||||
A line after a line
|
||||
13
libs/narrator/test/units/knots.ink
Normal file
13
libs/narrator/test/units/knots.ink
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-> back_in_london
|
||||
|
||||
== back_in_london ===
|
||||
|
||||
We arrived into London at 9.45pm exactly.
|
||||
-> hurry_home
|
||||
|
||||
=== hurry_home =
|
||||
We hurried home to Savile Row -> as_fast_as_we_could
|
||||
|
||||
====== as_fast_as_we_could
|
||||
as fast as we could. -> END
|
||||
|
||||
2
libs/narrator/test/units/knots.txt
Normal file
2
libs/narrator/test/units/knots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
We arrived into London at 9.45pm exactly.
|
||||
We hurried home to Savile Row as fast as we could.
|
||||
32
libs/narrator/test/units/labels-choices.ink
Normal file
32
libs/narrator/test/units/labels-choices.ink
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-> meet_guard
|
||||
|
||||
=== meet_guard ===
|
||||
The guard frowns at you.
|
||||
|
||||
* (greet) [Greet him]
|
||||
'Greetings.'
|
||||
* (get_out) 'Get out of my way[.'],' you tell the guard.
|
||||
|
||||
- 'Hmm,' replies the guard.
|
||||
|
||||
* {greet} 'Having a nice day?' // only if you greeted him
|
||||
|
||||
* 'Hmm?'[] you reply.
|
||||
|
||||
* {get_out} [Shove him aside] // only if you threatened him
|
||||
You shove him sharply. He stares in reply, and draws his sword!
|
||||
-> fight_guard // this route diverts out of the weave
|
||||
|
||||
- 'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'
|
||||
|
||||
|
||||
=== fight_guard ===
|
||||
|
||||
-> throw_something
|
||||
|
||||
= throw_something
|
||||
* (rock) [Throw rock at guard] -> throw
|
||||
* (sand) [Throw sand at guard] -> throw
|
||||
|
||||
= throw
|
||||
You hurl {throw_something.rock:a rock|a handful of sand} at the guard.
|
||||
13
libs/narrator/test/units/labels-choices/1-1.txt
Normal file
13
libs/narrator/test/units/labels-choices/1-1.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
The guard frowns at you.
|
||||
|
||||
>) Greet him
|
||||
2) 'Get out of my way.'
|
||||
|
||||
'Greetings.'
|
||||
'Hmm,' replies the guard.
|
||||
|
||||
>) 'Having a nice day?'
|
||||
2) 'Hmm?'
|
||||
|
||||
'Having a nice day?'
|
||||
'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'
|
||||
13
libs/narrator/test/units/labels-choices/1-2.txt
Normal file
13
libs/narrator/test/units/labels-choices/1-2.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
The guard frowns at you.
|
||||
|
||||
>) Greet him
|
||||
2) 'Get out of my way.'
|
||||
|
||||
'Greetings.'
|
||||
'Hmm,' replies the guard.
|
||||
|
||||
1) 'Having a nice day?'
|
||||
>) 'Hmm?'
|
||||
|
||||
'Hmm?' you reply.
|
||||
'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'
|
||||
13
libs/narrator/test/units/labels-choices/2-1.txt
Normal file
13
libs/narrator/test/units/labels-choices/2-1.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
The guard frowns at you.
|
||||
|
||||
1) Greet him
|
||||
>) 'Get out of my way.'
|
||||
|
||||
'Get out of my way,' you tell the guard.
|
||||
'Hmm,' replies the guard.
|
||||
|
||||
>) 'Hmm?'
|
||||
2) Shove him aside
|
||||
|
||||
'Hmm?' you reply.
|
||||
'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'
|
||||
17
libs/narrator/test/units/labels-choices/2-2-1.txt
Normal file
17
libs/narrator/test/units/labels-choices/2-2-1.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
The guard frowns at you.
|
||||
|
||||
1) Greet him
|
||||
>) 'Get out of my way.'
|
||||
|
||||
'Get out of my way,' you tell the guard.
|
||||
'Hmm,' replies the guard.
|
||||
|
||||
1) 'Hmm?'
|
||||
>) Shove him aside
|
||||
|
||||
You shove him sharply. He stares in reply, and draws his sword!
|
||||
|
||||
>) Throw rock at guard
|
||||
2) Throw sand at guard
|
||||
|
||||
You hurl a rock at the guard.
|
||||
17
libs/narrator/test/units/labels-choices/2-2-2.txt
Normal file
17
libs/narrator/test/units/labels-choices/2-2-2.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
The guard frowns at you.
|
||||
|
||||
1) Greet him
|
||||
>) 'Get out of my way.'
|
||||
|
||||
'Get out of my way,' you tell the guard.
|
||||
'Hmm,' replies the guard.
|
||||
|
||||
1) 'Hmm?'
|
||||
>) Shove him aside
|
||||
|
||||
You shove him sharply. He stares in reply, and draws his sword!
|
||||
|
||||
1) Throw rock at guard
|
||||
>) Throw sand at guard
|
||||
|
||||
You hurl a handful of sand at the guard.
|
||||
22
libs/narrator/test/units/labels-nested.ink
Normal file
22
libs/narrator/test/units/labels-nested.ink
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-> dialog_start
|
||||
==dialog_start==
|
||||
|
||||
* [Tell me about the mister]
|
||||
- - (dima)
|
||||
- - His name is Dima.
|
||||
* * {not vika} [Tell me about Vika]
|
||||
-> vika
|
||||
* * [Finish conversation]
|
||||
-> stop_dialog
|
||||
|
||||
* [Tell me about the missis]
|
||||
- - (vika)
|
||||
Her name is Vika.
|
||||
* * {not dima} [Tell me about Dima]
|
||||
-> dima
|
||||
* * [Finish conversation]
|
||||
-> stop_dialog
|
||||
|
||||
==stop_dialog==
|
||||
That's all.
|
||||
-> END
|
||||
14
libs/narrator/test/units/labels-nested/1-1-1.txt
Normal file
14
libs/narrator/test/units/labels-nested/1-1-1.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
>) Tell me about the mister
|
||||
2) Tell me about the missis
|
||||
|
||||
His name is Dima.
|
||||
|
||||
>) Tell me about Vika
|
||||
2) Finish conversation
|
||||
|
||||
Her name is Vika.
|
||||
|
||||
>) Finish conversation
|
||||
|
||||
That's all.
|
||||
10
libs/narrator/test/units/labels-nested/1-2.txt
Normal file
10
libs/narrator/test/units/labels-nested/1-2.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
>) Tell me about the mister
|
||||
2) Tell me about the missis
|
||||
|
||||
His name is Dima.
|
||||
|
||||
1) Tell me about Vika
|
||||
>) Finish conversation
|
||||
|
||||
That's all.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue