Compare commits

...

2 commits

Author SHA1 Message Date
Your Name
5b7ea9ec20 Rebuild level editor sidebar with a fixed virtual-space layout
Replace the fraction-of-window positioning (which overlapped and didn't
scale) with a small layout system: the sidebar is authored in one fixed
virtual coordinate space and rendered at native panel resolution, so
element sizes stay constant relative to the panel at any window size and
fonts scale with it.

- level_editor/layout.lua: sections, scrolling list (with clip + hit-test
  by point), scrollbar. Clip rects are scaled to real canvas pixels.
- Room view split into non-overlapping divs: search/back header, a
  scrollable asset list (replaces the old row-paging + forward buttons),
  a status strip, and a compact bottom control cluster.
- Mouse wheel scrolls the list; clicks map from window coords into the
  virtual space via the same transform used to render.
- Sidebar canvas is panel-sized and blitted 1:1 (nearest filter, never
  averaged) to stay crisp; editorDraw runs under a scale transform.
- Center text-only fields (level name) in their rect.

Also carried in main.lua (share the file, can't split cleanly here):
narrator require path fixed via love.filesystem.setRequirePath; sfxr
static uses waveform=NOISE again after the field rename; low-pass +
lower volume on the static loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 12:24:54 -04:00
Your Name
03532d29e4 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>
2026-08-11 11:37:09 -04:00
223 changed files with 20590 additions and 396 deletions

View file

@ -1,11 +1,11 @@
require "level_editor/object_attributes"
require "level_editor/gui"
require "level_editor/layout"
editorView = "room" --can be "off", "room", "world", "asset"
selectedColor = "red"
local bacSprite = love.graphics.newImage("art/_ui/bac.png")
local forwardSprite = love.graphics.newImage("art/_ui/forward.png")
local folderSprite = love.graphics.newImage("art/_ui/folder.png")
local cancelSprite = love.graphics.newImage("art/_ui/cancel.png")
local confirmSprite = love.graphics.newImage("art/_ui/confirm.png")
@ -14,7 +14,6 @@ local clearSavesSprite = love.graphics.newImage("art/_ui/clear_saves.png")
local selectedFolder = nil
local selectedAsset = nil
local selectedRoom = nil
local selectedRow = 1
local gameAssets = {}
local inputText = ""
local assetProperties = {}
@ -25,7 +24,8 @@ local activeField = nil --this is for whenwe are doing text input things
local uiButtons = {}
local folderButtons = {}
local assetButtons = { {} } --table for rows so we can do stuff
local assetListButtons = {} -- flat list of the selected folder's assets (scrolls)
local listScroll = { scroll = 0 } -- shared vertical scroll for the room list
local editingAssetButtons = {}
local labels = {}
@ -45,6 +45,110 @@ local assetSearchButtons = {}
local worldSearchText = ""
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
-- ── Room-view sidebar layout ────────────────────────────────────────────────
-- Declared up here (not next to editorDraw) so editorMouseHandler, defined
-- earlier in the file, can still capture these as upvalues.
-- Which flat list the room view is currently showing (search > folder > roots).
local function currentRoomList()
if assetSearchText ~= "" then return assetSearchButtons
elseif not selectedFolder then return folderButtons
else return assetListButtons end
end
-- Carve the sidebar into fixed, non-overlapping "divs": a search header pinned
-- to the top, a control cluster pinned to the bottom, a status strip above it,
-- and a flexible list filling whatever is left. Recomputed every frame so it
-- tracks window resizes.
local function roomSections()
local pad = 10
local w = math.max(0, sb.w - pad * 2)
local controlsH = 104
local statusH = 44
local search = { x = pad, y = pad, w = w, h = 34 }
local controls = { x = pad, y = sb.h - pad - controlsH, w = w, h = controlsH }
local status = { x = pad, y = controls.y - statusH, w = w, h = statusH }
local listY = search.y + search.h + 8
local list = { x = pad, y = listY, w = w, h = math.max(0, status.y - 8 - listY) }
return { search = search, listRect = list, status = status, controls = controls }
end
-- Position the bottom control cluster inside its section: level-name field, a
-- row of colour buttons, then a row of action buttons. Runs for both drawing
-- and hit-testing so the two never disagree.
local function layoutRoomControls(c, s)
local vgap = 4 -- vertical space between rows
local hgap = 8 -- horizontal space between buttons in a row
local y = c.y
s.labelY = y
y = y + 15
local field = uiButtons.roomName
if field then field.x = c.x; field.y = y; field.w = c.w; field.h = 22 end
y = y + 22 + vgap
-- Resize a sprite button to a target height in place, so all control sizing
-- lives here instead of being scattered across the creation calls.
local function sizeBtn(name, target)
local b = uiButtons[name]
if b and b.sprite then
b.scale = target / b.sprite:getHeight()
b.w = b.sprite:getWidth() * b.scale
b.h = b.sprite:getHeight() * b.scale
end
return b
end
-- Pack the row centered with a fixed gap, rather than spreading each button
-- across a full-width slot (which left big gaps between small icons).
local function placeRow(names, target)
local btns, total = {}, 0
for _, name in ipairs(names) do
local b = sizeBtn(name, target)
if b then table.insert(btns, b); total = total + b.w end
end
total = total + hgap * math.max(0, #btns - 1)
local x = c.x + (c.w - total) / 2
for _, b in ipairs(btns) do
b.x = x
b.y = y + (target - b.h) / 2
x = x + b.w + hgap
end
y = y + target + vgap
end
placeRow({ "white", "red", "green", "blue" }, 22)
placeRow({ "save", "clearSaves", "world" }, 28)
end
-- Build the full room-view layout (sections + a scroll list + a back/search
-- header) without drawing. Shared by editorDraw and editorMouseHandler.
local function buildRoomLayout()
local s = roomSections()
local showBack = assetSearchText ~= "" or selectedFolder
local sx, sw = s.search.x, s.search.w
if showBack then
s.back = { x = s.search.x, y = s.search.y, w = s.search.h, h = s.search.h }
sx = s.back.x + s.back.w + 6
sw = s.search.w - s.back.w - 6
end
s.searchBox = { x = sx, y = s.search.y, w = sw, h = s.search.h }
s.items = currentRoomList()
s.list = Layout.list(listScroll, s.listRect, 40, #s.items, 2)
layoutRoomControls(s.controls, s)
return s
end
-- Mouse wheel scrolls the room list; clamped by Layout.list next frame.
function editorScrollRoomList(dy)
listScroll.scroll = math.max(0, (listScroll.scroll or 0) - dy * 40)
end
-- ────────────────────────────────────────────────────────────────────────────
local function fuzzyMatches(value, query)
query = (query or ""):lower():gsub("%s+", "")
if query == "" then return true end
@ -66,6 +170,7 @@ end
local function rebuildAssetSearchButtons()
assetSearchButtons = {}
listScroll.scroll = 0
if assetSearchText == "" then return end
local names = {}
for asset in pairs(gameAssets) do
@ -103,12 +208,12 @@ local function rebuildWorldRoomButtons()
for index = first, math.min(first + worldPageSize - 1, #names) do
local roomName = names[index]
local row = index - first
table.insert(worldRoomButtons, { name = roomName, x = 10, y = 70 + row * 68, w = sideBarBox.w - 20, h = 60 })
table.insert(worldRoomButtons, { name = roomName, x = 10, y = 70 + row * 68, w = sb.w - 20, h = 60 })
end
worldControls = {
{ text = worldMode == "place" and "MODE: PLACE ROOMS" or "MODE: OPEN LEVELS", x = 10, y = sideBarBox.h - 194, w = sideBarBox.w - 20, h = 34,
{ text = worldMode == "place" and "MODE: PLACE ROOMS" or "MODE: OPEN LEVELS", x = 10, y = sb.h - 194, w = sb.w - 20, h = 34,
onClic = function() worldMode = worldMode == "place" and "jump" or "place"; rebuildWorldRoomButtons() end },
{ text = "RESEED OLD EXITS", x = 10, y = sideBarBox.h - 152, w = sideBarBox.w - 20, h = 34,
{ text = "RESEED OLD EXITS", x = 10, y = sb.h - 152, w = sb.w - 20, h = 34,
onClic = function()
local now = love.timer.getTime()
if now > worldSeedConfirmationExpires then
@ -121,9 +226,9 @@ local function rebuildWorldRoomButtons()
editorStatus = "Seeded " .. result.placed .. " rooms; " .. result.links .. " legacy links (" .. result.conflicts .. " conflicts kept separate)."
rebuildWorldRoomButtons()
end },
{ text = "< PAGE " .. worldPage .. "/" .. pages .. " >", x = 10, y = sideBarBox.h - 110, w = sideBarBox.w - 20, h = 34,
{ text = "< PAGE " .. worldPage .. "/" .. pages .. " >", x = 10, y = sb.h - 110, w = sb.w - 20, h = 34,
onClic = function(buttonX)
if buttonX < sideBarBox.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end
if buttonX < sb.w / 2 then worldPage = math.max(1, worldPage - 1) else worldPage = math.min(pages, worldPage + 1) end
rebuildWorldRoomButtons()
end },
}
@ -131,8 +236,8 @@ end
local function drawWorldSidebar()
love.graphics.setColor(gColor.white:set())
love.graphics.printf("ROOMS — " .. (worldMode == "place" and "select to place; re-click to clear" or "click a map room to open"), 8, 6, sideBarBox.w - 16, "center")
worldSearchBox.w = sideBarBox.w - 20
love.graphics.printf("ROOMS — " .. (worldMode == "place" and "select to place; re-click to clear" or "click a map room to open"), 8, 6, sb.w - 16, "center")
worldSearchBox.w = sb.w - 20
love.graphics.rectangle("line", worldSearchBox.x, worldSearchBox.y, worldSearchBox.w, worldSearchBox.h)
love.graphics.printf("SEARCH: " .. worldSearchText, worldSearchBox.x + 6, worldSearchBox.y + 7, worldSearchBox.w - 12, "left")
for _, button in ipairs(worldRoomButtons) do
@ -147,6 +252,10 @@ local function drawWorldSidebar()
love.graphics.rectangle("line", control.x, control.y, control.w, control.h)
love.graphics.printf(control.text, control.x, control.y + 9, control.w, "center")
end
-- Pin the assets/world toggle to a fixed spot here; the room view owns its
-- position otherwise, so give the world view a stable one of its own.
uiButtons.world.x = sb.w / 2 - uiButtons.world.w / 2
uiButtons.world.y = sb.h - uiButtons.world.h - 12
uiButtons.world:draw()
end
@ -254,7 +363,7 @@ function _initLevelEditor()
name = "folder",
text = folder,
x = 30,
y = (sideBarBox.h - 50) * (counter / 15),
y = (sb.h - 50) * (counter / 15),
sprite = folderSprite,
onClic = {
left = function(self)
@ -283,9 +392,9 @@ function _initLevelEditor()
end
},
x = sideBarBox.w / 4,
y = sideBarBox.h * (9 / 10)
})
x = sb.w / 4,
y = sb.h * (9 / 10)
}, "save")
_addUIButton(createUIElement{
name = "world",
@ -298,8 +407,8 @@ function _initLevelEditor()
end
},
x = sideBarBox.w * (3 / 4),
y = sideBarBox.h * (9 / 10)
x = sb.w * (3 / 4),
y = sb.h * (9 / 10)
}, "world")
_addUIButton(createUIElement{
@ -308,10 +417,10 @@ function _initLevelEditor()
onClic = {
left = clearRuntimeSaves,
},
x = sideBarBox.w / 2,
y = sideBarBox.h * (9 / 10),
scale = drawScale / 3
})
x = sb.w / 2,
y = sb.h * (9 / 10),
scale = editorScale / 3
}, "clearSaves")
for _, color in ipairs({"white", "red", "green", "blue"}) do
_addUIButton(createUIElement{
@ -327,16 +436,16 @@ function _initLevelEditor()
end
},
x = sideBarBox.w * (_ / 5),
y = sideBarBox.h * (8 / 10)
})
x = sb.w * (_ / 5),
y = sb.h * (8 / 10)
}, color)
end
roomNameField = createUIElement{
name = "roomName",
text = editorRoomName,
x = 25,
y = sideBarBox.h * (7 / 10),
y = sb.h * (7 / 10),
onClic = {
left = beginRoomNameEdit,
}
@ -356,74 +465,29 @@ end
function selectFolder(folder)
selectedFolder = folder
local bacButton = _addUIButton(createUIElement{
name = "bacToFolderView",
sprite = bacSprite,
x = sideBarBox.w / 2,
y = 37.5,
onClic = {
left = function(self)
print(self, self.x)
self = nil
folderView(self)
end,
right = function(self) return end
}
}, "bacToFolderView")
local row = 1
local counter = 1
local limit = 10
listScroll.scroll = 0
assetListButtons = {}
table.sort(hierarchy[selectedFolder])
for _, asset in ipairs(hierarchy[selectedFolder]) do
if counter % limit == 0 then
counter = 1
local forwardButton = createUIElement{
name = "forward",
sprite = forwardSprite,
x = sideBarBox.w / 2,
y = sideBarBox.h - 250,
onClic = {
left = function()
selectedRow = selectedRow + 1
end,
right = function()
end
}
}
table.insert(assetButtons[row], forwardButton)
row = row + 1
table.insert(assetButtons, {})
end
local sprites = {}
for color, location in pairs(gameAssets[asset].sprites) do
sprites[color] = love.graphics.newImage(location)
end
local assetButton = createUIElement{
-- x/y are placeholders; the room-view layout repositions every item
-- each frame from the list section (see buildRoomLayout).
table.insert(assetListButtons, createUIElement{
sprites = sprites,
name = "asset",
text = asset,
x = 30,
y = (sideBarBox.h - 225) * (counter / limit) + 20,
y = 0,
onClic = {
left = function()
print(asset)
selectAsset(asset)
end,
right = function()
editAsset(asset)
end
left = function() selectAsset(asset) end,
right = function() editAsset(asset) end,
}
}
table.insert(assetButtons[row], assetButton)
counter = counter + 1
})
end
end
@ -444,8 +508,8 @@ function editAsset(asset)
local cancelButton = createUIElement{
name = "cancel",
sprite = cancelSprite,
x = sideBarBox.w / 3,
y = sideBarBox.h - 75,
x = sb.w / 3,
y = sb.h - 75,
onClic = {
left = function(self)
assetsView()
@ -457,8 +521,8 @@ function editAsset(asset)
local confirmButton = createUIElement{
name = "confirm",
sprite = confirmSprite,
x = sideBarBox.w * 2 / 3,
y = sideBarBox.h - 75,
x = sb.w * 2 / 3,
y = sb.h - 75,
onClic = {
left = function(self)
saveAsset()
@ -478,12 +542,12 @@ function editAsset(asset)
local assetButton = createUIElement{
name = asset,
sprites = assetSprites,
x = sideBarBox.w / 2 - 18,
x = sb.w / 2 - 18,
y = 80,
}
local classFieldLabel = {
draw = function() love.graphics.printf("CLASS", 0, 125, sideBarBox.w, "center") end
draw = function() love.graphics.printf("CLASS", 0, 125, sb.w, "center") end
}
labels[classFieldLabel] = classFieldLabel
@ -501,7 +565,7 @@ function editAsset(asset)
}
local preloadLabel = {
draw = function() love.graphics.printf("preload", 0, 225, sideBarBox.w, "center") end
draw = function() love.graphics.printf("preload", 0, 225, sb.w, "center") end
}
labels[preloadLabel] = preloadLabel
@ -540,10 +604,9 @@ function _addUIButton(button, name)
end
function folderView()
selectedRow = 1
selectedFolder = nil
assetButtons = {{}}
uiButtons.bacToFolderView = nil
assetListButtons = {}
listScroll.scroll = 0
labels = {}
end
@ -594,39 +657,45 @@ end
function editorMouseHandler(x, y, button)
if x > width then
--we need to adjust for sidebar coords
x = x - width
-- map from real window coords into the sidebar's fixed virtual space
-- (same transform used to scale the sidebar canvas up in love.draw)
x = (x - width) * sb.w / sideBarBox.w
y = y * sb.h / sideBarBox.h
if editorView == "room" then
if buttonContains({ x = 10, y = 5, w = sideBarBox.w - 20, h = 28 }, x, y) then
beginSearch("assetSearch")
local s = buildRoomLayout()
-- back button: drop the search, else leave the folder
if s.back and Layout.hit(s.back, x, y) then
if assetSearchText ~= "" then
assetSearchText = ""
inputText = ""
activeField = nil
rebuildAssetSearchButtons()
elseif selectedFolder then
folderView()
end
return
elseif assetSearchText ~= "" then
for _, uiElement in ipairs(assetSearchButtons) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
return
end
end
elseif not selectedFolder then
for _, uiElement in pairs(folderButtons) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
return
end
end
else
for _, uiElement in pairs(assetButtons[selectedRow]) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
return
end
end
end
for _, uiElement in pairs(uiButtons) do
if uiElement:clicIsInBox(x, y) then
uiElement:onClic(button)
if Layout.hit(s.searchBox, x, y) then
beginSearch("assetSearch")
return
end
-- scrolling list item
local i = s.list.at(x, y)
if i and s.items[i] then
s.items[i]:onClic(button)
return
end
-- control cluster (only the buttons that live there)
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do
local b = uiButtons[name]
if b and b:clicIsInBox(x, y) then
b:onClic(button)
return
end
end
elseif editorView == "asset" then
@ -722,24 +791,43 @@ end
function editorDraw()
if editorView == "room" then
local s = buildRoomLayout()
love.graphics.setColor(gColor.white:set())
love.graphics.rectangle("line", 10, 5, sideBarBox.w - 20, 28)
love.graphics.printf("ASSET SEARCH: " .. assetSearchText, 16, 12, sideBarBox.w - 32, "left")
-- header: optional back button + search box
if s.back then
love.graphics.rectangle("line", s.back.x, s.back.y, s.back.w, s.back.h)
local sc = (s.back.h - 8) / bacSprite:getHeight()
love.graphics.draw(bacSprite, s.back.x + 4, s.back.y + 4, 0, sc, sc)
end
love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h)
love.graphics.printf("SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left")
-- scrolling list, clipped to its section so nothing bleeds into others
Layout.clip(s.listRect)
for i, item in ipairs(s.items) do
if s.list.visible(i) then
item.x, item.y = s.list.pos(i)
item.w = s.listRect.w
item:draw()
end
end
Layout.unclip()
Layout.scrollbar(s.list)
-- control cluster
love.graphics.setColor(gColor.white:set())
love.graphics.printf("LEVEL NAME", 0, sideBarBox.h * (7 / 10) - 24, sideBarBox.w, "center")
for _, button in pairs(uiButtons) do
button:draw()
end
if assetSearchText ~= "" then
for _, asset in ipairs(assetSearchButtons) do asset:draw() end
elseif not selectedFolder then
for _, folder in pairs(folderButtons) do
folder:draw()
end
else
for _, entity in ipairs(assetButtons[selectedRow]) do
entity:draw()
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center")
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "save", "clearSaves", "world" }) do
if uiButtons[name] then uiButtons[name]:draw() end
end
-- status strip between the list and the controls
if editorStatus then
Layout.clip(s.status)
love.graphics.setColor(gColor.white:set())
love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center")
Layout.unclip()
end
elseif editorView == "asset" then
for _, button in pairs(editingAssetButtons) do
@ -752,8 +840,10 @@ function editorDraw()
for _, label in pairs(labels) do
label:draw()
end
if editorStatus then
-- The room view draws its status inside its own strip (see the room branch);
-- the other views keep the simple centered line.
if editorStatus and editorView ~= "room" then
love.graphics.setColor(gColor.white:set())
love.graphics.printf(editorStatus, 10, sideBarBox.h * (6 / 10), sideBarBox.w - 20, "center")
love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center")
end
end

View file

@ -26,7 +26,7 @@ function GUI:initialize(t)
if t.sprite then
print(self.name)
self.scale = t.scale or drawScale * 2 / 3
self.scale = t.scale or editorScale * 2 / 3
self.w = t.sprite:getWidth() * self.scale
self.h = t.sprite:getHeight() * self.scale
self.sprite = t.sprite
@ -41,22 +41,22 @@ function GUI:initialize(t)
h = sprite:getHeight()
end
if w > h then
self.scale = (drawScale * (2 / 3)) / (w / 16)
self.scale = (editorScale * (2 / 3)) / (w / 16)
elseif w < h then
self.scale = (drawScale * (2 / 3)) / (h / 16)
self.scale = (editorScale * (2 / 3)) / (h / 16)
else
self.scale = (drawScale * (2 / 3)) / (w / 16)
self.scale = (editorScale * (2 / 3)) / (w / 16)
end
self.w = sideBarBox.w - (self.x * 2)
self.w = sb.w - (self.x * 2)
self.h = h * self.scale
else
-- this is text
self.x, self.y = t.x, t.y
self.w, self.h = sideBarBox.w - self.x * 2, 50
self.w, self.h = sb.w - self.x * 2, 50
end
if self.name == "folder" then
self.w = sideBarBox.w - self.x * 2
self.w = sb.w - self.x * 2
end
end
@ -106,10 +106,15 @@ function GUI:draw()
if self.text then
love.graphics.setColor(gColor.white:set())
if not self.sprites and not self.sprite then
-- text-only element (e.g. the level-name field): center in its rect
love.graphics.rectangle("line", box.x, box.y, box.w, box.h)
end
local th = love.graphics.getFont():getHeight()
love.graphics.printf(self.text, box.x, box.y + (box.h - th) / 2, box.w, "center")
else
-- label sitting to the right of an icon
love.graphics.print(self.text, box.x + 50, box.y + 15)
end
end
end

63
level_editor/layout.lua Normal file
View file

@ -0,0 +1,63 @@
-- A dead-simple layout helper for the editor sidebar. Everything works in the
-- sidebar's local coordinate space (0,0 = top-left of sideBarCanvas) — the same
-- space editorDraw / editorMouseHandler already use — so section rects double as
-- love.graphics.setScissor rects and as hit boxes. No dependencies, no state of
-- its own beyond the scroll table the caller hands in.
Layout = {}
-- point-in-rect
function Layout.hit(r, x, y)
return x >= r.x and x < r.x + r.w and y >= r.y and y < r.y + r.h
end
-- Clip drawing to a section. setScissor ignores the active graphics transform,
-- so multiply the virtual-space rect by editorRenderScale to hit real canvas
-- pixels. Clamp to non-negative: a too-narrow/short sidebar (e.g. a square
-- window with no room for it) would otherwise hand setScissor a negative size.
function Layout.clip(r)
local s = editorRenderScale or 1
love.graphics.setScissor(r.x * s, r.y * s, math.max(0, r.w * s), math.max(0, r.h * s))
end
function Layout.unclip() love.graphics.setScissor() end
-- A vertical scrolling list confined to rect `r`. `state` is any table the
-- caller keeps between frames to hold `state.scroll`; it is clamped here.
-- Returns helpers to place / cull / hit-test `count` items of height `itemH`.
function Layout.list(state, r, itemH, count, gap)
gap = gap or 6
local step = itemH + gap
local content = count * step
local maxScroll = math.max(0, content - r.h)
state.scroll = math.max(0, math.min(state.scroll or 0, maxScroll))
local scroll = state.scroll
return {
rect = r, itemH = itemH, step = step, count = count, maxScroll = maxScroll, scroll = scroll,
-- top-left corner of item i (1-based), in sidebar space
pos = function(i) return r.x, r.y + (i - 1) * step - scroll end,
-- is item i at all inside the visible window? (cheap cull)
visible = function(i)
local y = r.y + (i - 1) * step - scroll
return y + itemH > r.y and y < r.y + r.h
end,
-- index at a sidebar-space point, or nil if outside the list / no item
at = function(x, y)
if not Layout.hit(r, x, y) then return nil end
local i = math.floor((y - r.y + scroll) / step) + 1
if i >= 1 and i <= count then return i end
return nil
end,
}
end
-- Thin scrollbar thumb on the right edge of a list, drawn only when it scrolls.
function Layout.scrollbar(list)
if list.maxScroll <= 0 then return end
local r = list.rect
local thumb = math.max(16, r.h * (r.h / (r.h + list.maxScroll)))
local ty = r.y + (r.h - thumb) * (list.scroll / list.maxScroll)
local pr, pg, pb, pa = love.graphics.getColor()
love.graphics.setColor(255, 255, 255, 90)
love.graphics.rectangle("fill", r.x + r.w - 4, ty, 3, thumb)
love.graphics.setColor(pr, pg, pb, pa)
end

@ -1 +0,0 @@
Subproject commit 08937cc0ecf72d1a964a8de6cd552c5e136bf0d4

38
libs/hump/.travis.yml Normal file
View file

@ -0,0 +1,38 @@
dist: precise
sudo: false # Use container-based infrastructure
language: python
git:
depth: 4
#Testing with lastest stable version of LUAJIT
env:
- LUA_RUNTIME="luajit"
LUA_VERSION="2.0.5"
#- LUA_RUNTIME="luajit"
# LUA_VERSION="2.1"
before_install:
- pip install hererocks
- hererocks lua_installations/$LUA_RUNTIME$LUA_VERSION/ --luarocks ^ --$LUA_RUNTIME $LUA_VERSION
- export PATH=$PATH:$PWD/lua_installations/$LUA_RUNTIME$LUA_VERSION/bin
before_script:
- luarocks install busted
- busted --version
script:
- busted --output=TAP #default utfTerminal
#Checks if Luajit and luarocks are already installed
#Comment out to reinstall or if changing versions
cache:
directories:
- $PWD/lua_installations
# Can add email notification here
#notifications:
#email:
#recipients:
#- email@here.com
#on_failure: always #default always

48
libs/hump/README.md Normal file
View file

@ -0,0 +1,48 @@
hump - Helper Utilities for Massive Progression
===============================================
**hump** is a small collection of tools for developing games with L&Ouml;VE.
[![Build Status](https://travis-ci.org/vrld/hump.svg?branch=master)](https://travis-ci.org/vrld/hump)
Contents:
------------
* *gamestate.lua*: Easy gamestate management.
* *timer.lua*: Delayed and time-limited function calls and tweening.
* *vector.lua*: 2D vector math.
* *vector-light.lua*: Lightweight 2D vector math (for optimisation purposes - leads to potentially ugly code).
* *class.lua*: Lightweight object orientation (class or prototype based).
* *signal.lua*: Simple Signal/Slot (aka. Observer) implementation.
* *camera.lua*: Move-, zoom- and rotatable camera with camera locking and movement smoothing.
Documentation
=============
You can find the documentation here: [hump.readthedocs.org](http://hump.readthedocs.org)
License
=======
> Copyright (c) 2010-2018 Matthias Richter
>
> 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.
>
> Except as contained in this notice, the name(s) of the above copyright holders
> shall not be used in advertising or otherwise to promote the sale, use or
> other dealings in this Software without prior written authorization.
>
> 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.

216
libs/hump/camera.lua Normal file
View file

@ -0,0 +1,216 @@
--[[
Copyright (c) 2010-2015 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 _PATH = (...):match('^(.*[%./])[^%.%/]+$') or ''
local cos, sin = math.cos, math.sin
local camera = {}
camera.__index = camera
-- Movement interpolators (for camera locking/windowing)
camera.smooth = {}
function camera.smooth.none()
return function(dx,dy) return dx,dy end
end
function camera.smooth.linear(speed)
assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed))
return function(dx,dy, s)
-- normalize direction
local d = math.sqrt(dx*dx+dy*dy)
local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal
if d > 0 then
dx,dy = dx/d, dy/d
end
return dx*dts, dy*dts
end
end
function camera.smooth.damped(stiffness)
assert(type(stiffness) == "number", "Invalid parameter: stiffness = "..tostring(stiffness))
return function(dx,dy, s)
local dts = love.timer.getDelta() * (s or stiffness)
return dx*dts, dy*dts
end
end
local function new(x,y, zoom, rot, smoother)
x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2
zoom = zoom or 1
rot = rot or 0
smoother = smoother or camera.smooth.none() -- for locking, see below
return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera)
end
function camera:lookAt(x,y)
self.x, self.y = x, y
return self
end
function camera:move(dx,dy)
self.x, self.y = self.x + dx, self.y + dy
return self
end
function camera:position()
return self.x, self.y
end
function camera:rotate(phi)
self.rot = self.rot + phi
return self
end
function camera:rotateTo(phi)
self.rot = phi
return self
end
function camera:zoom(mul)
self.scale = self.scale * mul
return self
end
function camera:zoomTo(zoom)
self.scale = zoom
return self
end
function camera:attach(x,y,w,h, noclip)
x,y = x or 0, y or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
self._sx,self._sy,self._sw,self._sh = love.graphics.getScissor()
if not noclip then
love.graphics.setScissor(x,y,w,h)
end
local cx,cy = x+w/2, y+h/2
love.graphics.push()
love.graphics.translate(cx, cy)
love.graphics.scale(self.scale)
love.graphics.rotate(self.rot)
love.graphics.translate(-self.x, -self.y)
end
function camera:detach()
love.graphics.pop()
love.graphics.setScissor(self._sx,self._sy,self._sw,self._sh)
end
function camera:draw(...)
local x,y,w,h,noclip,func
local nargs = select("#", ...)
if nargs == 1 then
func = ...
elseif nargs == 5 then
x,y,w,h,func = ...
elseif nargs == 6 then
x,y,w,h,noclip,func = ...
else
error("Invalid arguments to camera:draw()")
end
self:attach(x,y,w,h,noclip)
func()
self:detach()
end
-- world coordinates to camera coordinates
function camera:cameraCoords(x,y, ox,oy,w,h)
ox, oy = ox or 0, oy or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
-- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center
local c,s = cos(self.rot), sin(self.rot)
x,y = x - self.x, y - self.y
x,y = c*x - s*y, s*x + c*y
return x*self.scale + w/2 + ox, y*self.scale + h/2 + oy
end
-- camera coordinates to world coordinates
function camera:worldCoords(x,y, ox,oy,w,h)
ox, oy = ox or 0, oy or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
-- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y)
local c,s = cos(-self.rot), sin(-self.rot)
x,y = (x - w/2 - ox) / self.scale, (y - h/2 - oy) / self.scale
x,y = c*x - s*y, s*x + c*y
return x+self.x, y+self.y
end
function camera:mousePosition(ox,oy,w,h)
local mx,my = love.mouse.getPosition()
return self:worldCoords(mx,my, ox,oy,w,h)
end
-- camera scrolling utilities
function camera:lockX(x, smoother, ...)
local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...)
self.x = self.x + dx
return self
end
function camera:lockY(y, smoother, ...)
local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...)
self.y = self.y + dy
return self
end
function camera:lockPosition(x,y, smoother, ...)
return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...))
end
function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...)
-- figure out displacement in camera coordinates
x,y = self:cameraCoords(x,y)
local dx, dy = 0,0
if x < x_min then
dx = x - x_min
elseif x > x_max then
dx = x - x_max
end
if y < y_min then
dy = y - y_min
elseif y > y_max then
dy = y - y_max
end
-- transform displacement to movement in world coordinates
local c,s = cos(-self.rot), sin(-self.rot)
dx,dy = (c*dx - s*dy) / self.scale, (s*dx + c*dy) / self.scale
-- move
self:move((smoother or self.smoother)(dx,dy,...))
end
-- the module
return setmetatable({new = new, smooth = camera.smooth},
{__call = function(_, ...) return new(...) end})

98
libs/hump/class.lua Normal file
View file

@ -0,0 +1,98 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 function include_helper(to, from, seen)
if from == nil then
return to
elseif type(from) ~= 'table' then
return from
elseif seen[from] then
return seen[from]
end
seen[from] = to
for k,v in pairs(from) do
k = include_helper({}, k, seen) -- keys might also be tables
if to[k] == nil then
to[k] = include_helper({}, v, seen)
end
end
return to
end
-- deeply copies `other' into `class'. keys in `other' that are already
-- defined in `class' are omitted
local function include(class, other)
return include_helper(class, other, {})
end
-- returns a deep copy of `other'
local function clone(other)
return setmetatable(include({}, other), getmetatable(other))
end
local function new(class)
-- mixins
class = class or {} -- class can be nil
local inc = class.__includes or {}
if getmetatable(inc) then inc = {inc} end
for _, other in ipairs(inc) do
if type(other) == "string" then
other = _G[other]
end
include(class, other)
end
-- class implementation
class.__index = class
class.init = class.init or class[1] or function() end
class.include = class.include or include
class.clone = class.clone or clone
-- constructor call
return setmetatable(class, {__call = function(c, ...)
local o = setmetatable({}, c)
o:init(...)
return o
end})
end
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
if class_commons ~= false and not common then
common = {}
function common.class(name, prototype, parent)
return new{__includes = {prototype, parent}}
end
function common.instance(class, ...)
return class(...)
end
end
-- the module
return setmetatable({new = new, include = include, clone = clone},
{__call = function(_,...) return new(...) end})

192
libs/hump/docs/Makefile Normal file
View file

@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hump.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hump.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/hump"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hump"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

218
libs/hump/docs/_static/graph-tweens.js vendored Normal file
View file

@ -0,0 +1,218 @@
(function() {
"use strict";
// DISCLAIMER: I just started learning d3, so this is certainly not good
// idiomatic d3 code. But hey, it works (kinda).
var tweens = {
'out': function(f) { return function(s) { return 1-f(1-s) } },
'chain': function(f1, f2) { return function(s) { return ((s<.5) ? f1(2*s) : 1+f2(2*s-1)) * .5 } },
'linear': function(s) { return s },
'quad': function(s) { return s*s },
'cubic': function(s) { return s*s*s },
'quart': function(s) { return s*s*s*s },
'quint': function(s) { return s*s*s*s*s },
'sine': function(s) { return 1 - Math.cos(s*Math.PI/2) },
'expo': function(s) { return Math.pow(2, 10*(s-1)) },
'circ': function(s) { return 1 - Math.sqrt(Math.max(0,1-s*s)) },
'back': function(s) { var b = 1.70158; return s*s*((b+1)*s - b) },
'bounce': function(s) {
return Math.min(
7.5625 * Math.pow(s, 2),
7.5625 * Math.pow((s - .545455), 2) + .75,
7.5625 * Math.pow((s - .818182), 2) + .90375,
7.5625 * Math.pow((s - .954546), 2) + .984375)
},
'elastic': function(s) {
return -Math.sin(2/0.3 * Math.PI * (s-1) - Math.asin(1)) * Math.pow(2, 10*(s-1))
},
};
var tweenfunc = tweens.linear;
var width_graph = 320,
width_anim_move = 110,
width_anim_rotate = 110,
width_anim_size = 110,
height = 250;
// "UI"
var graph_ui = d3.select("#tween-graph").append("div")
.attr("id", "tween-graph-ui");
// rest see below
// the graph
var graph = d3.select("#tween-graph").append("svg")
.attr("width", width_graph).attr("height", height);
// background
graph.append("rect")
.attr("width", "100%").attr("height", "100%")
.attr("style", "fill:rgb(240,240,240);stroke-width:1;stroke:rgb(100,100,100);");
var y_zero = height * .78, y_one = height * .22;
graph.append("rect")
.attr("y", y_one)
.attr("width", "100%").attr("height", y_zero - y_one)
.attr("style", "fill:steelblue;fill-opacity:.3;stroke-width:1;stroke:rgba(100,100,100,.7)");
// time arrow
graph.append("defs")
.append("marker")
.attr("id", "triangle")
.attr("viewBox", "0 0 10 10")
.attr("refX", 1).attr("refY", 5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.attr("style", "fill:rgba(0,0,0,.5)")
.append("path").attr("d", "M 0 0 L 10 5 L 0 10 z");
graph.append("line")
.attr("x1", width_graph/2-80)
.attr("x2", width_graph/2+80)
.attr("y1", y_zero + 40).attr("y2", y_zero + 40)
.attr("style", "stroke-width:2;stroke:rgba(0,0,0,.5)")
.attr("marker-end", "url(#triangle)");
graph.append("text")
.text("Time")
.attr("x", width_graph/2).attr("y", y_zero + 55)
.attr("style", "text-anchor:middle;fill:rgba(0,0,0,.5);font-size:15px");
// the actual graph
var curve = d3.svg.line()
.x(function(x) { return x*width_graph; })
.y(function(x) { return tweenfunc(x) * (y_one - y_zero) + y_zero; })
var graph_curve = graph.append("path").attr("d", curve(d3.range(0,1.05,.005)))
.attr("style", "fill:none;stroke-width:2;stroke:seagreen;");
var graph_marker = graph.append("circle")
.attr("r", 5)
.attr("style", "stroke:goldenrod;fill:none;stroke-width:3");
// finally, a label
var graph_label = graph.append("text")
.text("linear")
.attr("x", width_graph/2).attr("y", 20)
.attr("style", "text-anchor:middle;font-weight:bold;font-size:15px;");
// animation examples - moving ball
var anim_move = d3.select("#tween-graph").append("svg")
.attr("width", width_anim_move).attr("height", height);
anim_move.append("rect")
.attr("width", "100%").attr("height", "100%")
.attr("style", "fill:rgb(240,240,240);stroke-width:1;stroke:rgb(100,100,100);");
anim_move.append("rect")
.attr("width", 10).attr("height", (y_zero - y_one))
.attr("x", width_anim_move/2-5).attr("y", y_one)
.attr("style", "fill:black;opacity:.1");
var anim_move_ball = anim_move.append("circle")
.attr("cx", width_anim_move/2).attr("cy", y_one)
.attr("r", 17)
.attr("style", "fill:steelblue;stroke:rgb(90,90,90);stroke-width:5;");
// animation examples - rotating square
var anim_rotate = d3.select("#tween-graph").append("svg")
.attr("width", width_anim_size).attr("height", height);
anim_rotate.append("rect")
.attr("width", "100%").attr("height", "100%")
.attr("style", "fill:rgb(240,240,240);stroke-width:1;stroke:rgb(100,100,100);");
var w = width_anim_size/2;
var anim_rotate_square = anim_rotate.append("rect")
.attr("x", -w/2).attr("y", -w/4)
.attr("width", w).attr("height", w/2)
.attr("style", "fill:steelblue;stroke:rgb(90,90,90);stroke-width:5;");
// animation examples - resizing ellipse
var anim_size = d3.select("#tween-graph").append("svg")
.attr("width", width_anim_size).attr("height", height);
anim_size.append("rect")
.attr("width", "100%").attr("height", "100%")
.attr("style", "fill:rgb(240,240,240);stroke-width:1;stroke:rgb(100,100,100);");
anim_size.append("ellipse")
.attr("cx", width_anim_size/2).attr("cy", height/2)
.attr("rx", 40).attr("ry", 120)
.attr("style", "fill:rgb(150,150,150);stroke:black;stroke-width:2;opacity:.1");
var anim_size_ellipse = anim_size.append("ellipse")
.attr("cx", width_anim_size/2).attr("cy", height/2)
.attr("rx", 40).attr("ry", 40)
.attr("style", "fill:steelblue;stroke:rgb(90,90,90);stroke-width:5;");
// make it move!
var t = 0;
window.setInterval(function() {
t = (t + .025 / 3);
if (t > 1.3) { t = -.3; }
var tt = Math.max(Math.min(t, 1), 0);
var s = tweenfunc(tt)
var yy = s * (y_one - y_zero) + y_zero;
var translate = "translate("+(width_anim_size/2)+" "+(height/2)+")";
var rotate = "rotate(" + (s * 360) + ")";
graph_marker.attr("cx", tt*width_graph).attr("cy", yy);
anim_move_ball.attr("cy", y_one + y_zero - yy);
anim_rotate_square.attr("transform", translate + " " + rotate);
anim_size_ellipse.attr("ry", s * 80 + 40);
}, 25);
// ui continued
graph_ui.append("strong").text("Function: ");
var select_modifier = graph_ui.append("select");
select_modifier.append("option").text("in");
select_modifier.append("option").text("out");
select_modifier.append("option").text("in-out");
select_modifier.append("option").text("out-in");
graph_ui.append("strong").text("-")
var select_func = graph_ui.append("select")
var funcs = [];
for (var k in tweens)
{
if (k != "out" && k != "chain")
{
select_func.append("option").text(k);
funcs.push(k);
}
}
var change_tweenfunc = function()
{
var fname = funcs[select_func.node().selectedIndex];
var mod = select_modifier.node().selectedIndex;
tweenfunc = tweens[fname];
if (mod == 1) // out
tweenfunc = tweens.out(tweenfunc);
else if (mod == 2) // in-out
tweenfunc = tweens.chain(tweenfunc, tweens.out(tweenfunc));
else if (mod == 3) // out-in
tweenfunc = tweens.chain(tweens.out(tweenfunc), tweenfunc);
// update curve
graph_curve.attr("d", curve(d3.range(0,1.05,.005)))
// update label
if (mod != 0)
graph_label.text((['in','out','in-out','out-in'])[mod] + "-" + fname);
else
graph_label.text(fname);
}
select_func.on("change", change_tweenfunc);
select_modifier.on("change", change_tweenfunc);
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
libs/hump/docs/_static/interpolators.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
libs/hump/docs/_static/vector-cross.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

567
libs/hump/docs/camera.rst Normal file
View file

@ -0,0 +1,567 @@
hump.camera
===========
::
Camera = require "hump.camera"
A camera utility for LÖVE. A camera can "look" at a position. It can zoom in
and out and it can rotate it's view. In the background, this is done by
actually moving, scaling and rotating everything in the game world. But don't
worry about that.
**Example**::
function love.load()
camera = Camera(player.pos.x, player.pos.y)
end
function love.update(dt)
local dx,dy = player.x - camera.x, player.y - camera.y
camera:move(dx/2, dy/2)
end
function love.draw()
camera:attach()
-- do your drawing here
camera:detach()
end
List of Functions
-----------------
* :func:`Camera.new(x,y, zoom, rot) <Camera.new>`
* :func:`camera:move(dx,dy) <camera:move>`
* :func:`camera:lookAt(x,y) <camera:lookAt>`
* :func:`camera:position() <camera:position>`
* :func:`camera:rotate(angle) <camera:rotate>`
* :func:`camera:rotateTo(angle) <camera:rotateTo>`
* :func:`camera:zoom(mul) <camera:zoom>`
* :func:`camera:zoomTo(zoom) <camera:zoomTo>`
* :func:`camera:attach() <camera:attach>`
* :func:`camera:detach() <camera:detach>`
* :func:`camera:draw(func) <camera:draw>`
* :func:`camera:worldCoords(x, y) <camera:worldCoords>`
* :func:`camera:cameraCoords(x, y) <camera:cameraCoords>`
* :func:`camera:mousePosition() <camera:mousePosition>`
* :func:`camera:lockX(x, smoother, ...) <camera:lockX>`
* :func:`camera:lockY(y, smoother, ...) <camera:lockY>`
* :func:`camera:lockPosition(x,y, smoother, ...) <camera:lockPosition>`
* :func:`camera:lockWindow(x,y, x_min, x_max, y_min, y_max, smoother, ...) <camera:lockWindow>`
* :func:`Camera.smooth.none() <Camera.smooth.none>`
* :func:`Camera.smooth.linear(speed) <Camera.smooth.linear>`
* :func:`Camera.smooth.damped(stiffness) <Camera.smooth.damped>`
Function Reference
------------------
.. function:: Camera.new(x,y, zoom, rot)
:param numbers x,y: Point for the camera to look at. (optional)
:param number zoom: Camera zoom. (optional)
:param number rot: Camera rotation in radians. (optional)
:returns: A new camera.
Creates a new camera. You can access the camera position using ``camera.x,
camera.y``, the zoom using ``camera.scale`` and the rotation using ``camera.rot``.
The module variable name can be used at a shortcut to ``new()``.
**Example**::
camera = require 'hump.camera'
-- camera looking at (100,100) with zoom 2 and rotated by 45 degrees
cam = camera(100,100, 2, math.pi/2)
.. function:: camera:move(dx,dy)
:param numbers dx,dy: Direction to move the camera.
:returns: The camera.
Move the camera *by* some vector. To set the position, use
:func:`camera:lookAt`.
This function is shortcut to ``camera.x,camera.y = camera.x+dx, camera.y+dy``.
**Examples**::
function love.update(dt)
camera:move(dt * 5, dt * 6)
end
::
function love.update(dt)
camera:move(dt * 5, dt * 6):rotate(dt)
end
.. function:: camera:lookAt(x,y)
:param numbers x,y: Position to look at.
:returns: The camera.
Let the camera look at a point. In other words, it sets the camera position. To
move the camera *by* some amount, use :func:`camera:move`.
This function is shortcut to ``camera.x,camera.y = x, y``.
**Examples**::
function love.update(dt)
camera:lookAt(player.pos:unpack())
end
::
function love.update(dt)
camera:lookAt(player.pos:unpack()):rotate(player.rot)
end
.. function:: camera:position()
:returns: ``x,y`` -- Camera position.
Returns ``camera.x, camera.y``.
**Example**::
-- let the camera fly!
local cam_dx, cam_dy = 0, 0
function love.mousereleased(x,y)
local cx,cy = camera:position()
dx, dy = x-cx, y-cy
end
function love.update(dt)
camera:move(dx * dt, dy * dt)
end
.. function:: camera:rotate(angle)
:param number angle: Rotation angle in radians
:returns: The camera.
Rotate the camera by some angle. To set the angle use :func:`camera:rotateTo`.
This function is shortcut to ``camera.rot = camera.rot + angle``.
**Examples**::
function love.update(dt)
camera:rotate(dt)
end
::
function love.update(dt)
camera:rotate(dt):move(dt,dt)
end
.. function:: camera:rotateTo(angle)
:param number angle: Rotation angle in radians
:returns: The camera.
Set rotation: ``camera.rot = angle``.
**Example**::
camera:rotateTo(math.pi/2)
.. function:: camera:zoom(mul)
:param number mul: Zoom change. Should be > 0.
:returns: The camera.
*Multiply* zoom: ``camera.scale = camera.scale * mul``.
**Examples**::
camera:zoom(2) -- make everything twice as big
::
camera:zoom(0.5) -- ... and back to normal
::
camera:zoom(-1) -- mirror and flip everything upside down
.. function:: camera:zoomTo(zoom)
:param number zoom: New zoom.
:returns: The camera.
Set zoom: ``camera.scale = zoom``.
**Example**::
camera:zoomTo(1) -- reset zoom
.. function:: camera:attach()
Start looking through the camera.
Apply camera transformations, i.e. move, scale and rotate everything until
``camera:detach()`` as if looking through the camera.
**Example**::
function love.draw()
camera:attach()
draw_world()
camera:detach()
draw_hud()
end
.. function:: camera:detach()
Stop looking through the camera.
**Example**::
function love.draw()
camera:attach()
draw_world()
camera:detach()
draw_hud()
end
.. function:: camera:draw(func)
:param function func: Drawing function to be wrapped.
Wrap a function between a ``camera:attach()``/``camera:detach()`` pair.
Equivalent to::
camera:attach()
func()
camera:detach()
**Example**::
function love.draw()
camera:draw(draw_world)
draw_hud()
end
.. function:: camera:worldCoords(x, y)
:param numbers x, y: Point to transform.
:returns: ``x,y`` -- Transformed point.
Because a camera has a point it looks at, a rotation and a zoom factor, it
defines a coordinate system. A point now has two sets of coordinates: One
defines where the point is to be found in the game world, and the other
describes the position on the computer screen. The first set of coordinates is
called world coordinates, the second one camera coordinates. Sometimes it is
needed to convert between the two coordinate systems, for example to get the
position of a mouse click in the game world in a strategy game, or to see if an
object is visible on the screen.
:func:`camera:worldCoords` and :func:`camera:cameraCoords` transform points
between these two coordinate systems.
**Example**::
x,y = camera:worldCoords(love.mouse.getPosition())
selectedUnit:plotPath(x,y)
.. function:: camera:cameraCoords(x, y)
:param numbers x, y: Point to transform.
:returns: ``x,y`` -- Transformed point.
Because a camera has a point it looks at, a rotation and a zoom factor, it
defines a coordinate system. A point now has two sets of coordinates: One
defines where the point is to be found in the game world, and the other
describes the position on the computer screen. The first set of coordinates is
called world coordinates, the second one camera coordinates. Sometimes it is
needed to convert between the two coordinate systems, for example to get the
position of a mouse click in the game world in a strategy game, or to see if an
object is visible on the screen.
:func:`camera:worldCoords` and :func:`camera:cameraCoords` transform points
between these two coordinate systems.
**Example**::
x,y = camera:cameraCoords(player.pos.x, player.pos.y)
love.graphics.line(x, y, love.mouse.getPosition())
.. function:: camera:mousePosition()
:returns: Mouse position in world coordinates.
Shortcut to ``camera:worldCoords(love.mouse.getPosition())``.
**Example**::
x,y = camera:mousePosition()
selectedUnit:plotPath(x,y)
Camera Movement Control
-----------------------
Camera movement is one of these things that go almost unnoticed when done well,
but add a lot to the overall experience.
The article `Scroll Back: The Theory and Practice of Cameras in SideScrollers
<http://gamasutra.com/blogs/ItayKeren/20150511/243083/Scroll_Back_The_Theory_and_Practice_of_Cameras_in_SideScrollers.php>`_
by Itay Keren gives a lot of insight into how to design good camera systems.
**hump.camera** offers functions that help to implement most of the techniques
discussed in the article. The functions :func:`camera:lockX`,
:func:`camera:lockY`, :func:`camera:lockPosition`, and :func:`camera:lockWindow`
move the camera so that the interesting content stays in frame.
Note that the functions must be called every frame::
function love.update()
-- vertical locking
camera:lockX(player.pos.x)
end
All movements are subject to smoothing (see :ref:`Movement Smoothers
<movement-smoothers>`).
You can specify a default movement smoother by assigning the variable
:attr:`camera.smoother`::
cam.smoother = Camera.smooth.linear(100)
.. function:: camera:lockX(x, smoother, ...)
:param number x: X coordinate (in world coordinates) to lock to.
:param function smoother: Movement smoothing override. (optional)
:param mixed ...: Additional parameters to the smoothing function. (optional)
Horizontal camera locking: Keep the camera locked on the defined ``x``-position
(in *world coordinates*). The ``y``-position is not affected.
You can define an off-center locking position by "aiming" the camera left or
right of your actual target. For example, to center the player 20 pixels to the
*left* of the screen, aim 20 pixels to it's *right* (see examples).
**Examples**::
-- lock on player vertically
camera:lockX(player.x)
::
-- ... with linear smoothing at 25 px/s
camera:lockX(player.x, Camera.smooth.linear(25))
::
-- lock player 20px left of center
camera:lockX(player.x + 20)
.. function:: camera:lockY(y, smoother, ...)
:param number y: Y coordinate (in world coordinates) to lock to.
:param function smoother: Movement smoothing override. (optional)
:param mixed ...: Additional parameters to the smoothing function. (optional)
Vertical camera locking: Keep the camera locked on the defined ``y``-position
(in *world coordinates*). The ``x``-position is not affected.
You can define an off-center locking position by "aiming" the camera above or
below your actual target. For example, to center the player 20 pixels *below* the
screen center, aim 20 pixels *above* it (see examples).
**Examples**::
-- lock on player horizontally
camera:lockY(player.y)
::
-- ... with damped smoothing with a stiffness of 10
camera:lockY(player.y, Camera.smooth.damped(10))
::
-- lock player 20px below the screen center
camera:lockY(player.y - 20)
.. function:: camera:lockPosition(x,y, smoother, ...)
:param numbers x,y: Position (in world coordinates) to lock to.
:param function smoother: Movement smoothing override. (optional)
:param mixed ...: Additional parameters to the smoothing function. (optional)
Horizontal and vertical camera locking: Keep the camera locked on the defined
position (in *world coordinates*).
You can define an off-center locking position by "aiming" the camera to the
opposite direction away from your real target.
For example, to center the player 10 pixels to the *left* and 20 pixels *above*
the screen center, aim 10 pixels to the *right* and 20 pixels *below*.
**Examples**::
-- lock on player
camera:lockPosition(player.x, player.y)
::
-- lock 50 pixels into player's aiming direction
camera:lockPosition(player.x - player.aiming.x * 50, player.y - player.aiming.y * 50)
.. function:: camera:lockWindow(x,y, x_min, x_max, y_min, y_max, smoother, ...)
:param numbers x,y: Position (in world coordinates) to lock to.
:param numbers x_min: Upper left X coordinate of the camera window *(in camera coordinates!)*.
:param numbers x_max: Lower right X coordinate of the camera window *(in camera coordinates!)*.
:param numbers y_min: Upper left Y coordinate of the camera window *(in camera coordinates!)*.
:param numbers y_max: Lower right Y coordinate of the camera window *(in camera coordinates!)*.
:param function smoother: Movement smoothing override. (optional)
:param mixed ...: Additional parameters to the smoothing function. (optional)
The most powerful locking method: Lock camera to ``x,y``, but only move the
camera if the position would be out of the screen-rectangle defined by ``x_min``,
``x_max``, ``y_min``, ``y_max``.
.. note::
The locking window is defined in camera coordinates, whereas the position to
lock to is defined in world coordinates!
All of the other locking methods can be implemented by window locking. For
position locking, set ``x_min = x_max`` and ``y_min = y_max``.
Off-center locking can be done by defining the locking window accordingly.
**Examples**::
-- lock on player
camera:lock(player.x, player.y)
.. attribute:: camera.smoother
The default smoothing operator. Must be a ``function`` with the following
prototype::
function customSmoother(dx,dy, ...)
do_stuff()
return new_dx,new_dy
end
where ``dx,dy`` is the offset the camera would move before smoothing and
``new_dx, new_dy`` is the offset the camera should move after smoothing.
.. _movement-smoothers:
Movement Smoothers
^^^^^^^^^^^^^^^^^^
It is not always desirable that the camera instantly locks on a target.
`Platform snapping
<http://gamasutra.com/blogs/ItayKeren/20150511/243083/Scroll_Back_The_Theory_and_Practice_of_Cameras_in_SideScrollers.php#h.rncuomopycy0>`_,
for example, would look terrible if the camera would instantly jump to the
focussed platform.
Smoothly moving the camera to the locked position can also give the illusion of
a camera operator an add to the overall feel of your game.
**hump.camera** allows to smooth the movement by either passing movement
smoother functions to the locking functions or by setting a default smoother
(see :attr:`camera.smoother`).
Smoothing functions must have the following prototype::
function customSmoother(dx,dy, ...)
do_stuff()
return new_dx,new_dy
end
where ``dx,dy`` is the offset the camera would move before smoothing and
``new_dx, new_dy`` is the offset the camera should move after smoothing.
This is a simple "rubber-band" smoother::
function rubber_band(dx,dy)
local dt = love.timer.getDelta()
return dx*dt, dy*dt
end
**hump.camera** defines generators for the most common smoothers:
.. function:: Camera.smooth.none()
:returns: Smoothing function.
Dummy smoother: does not smooth the motion.
**Example**::
cam.smoother = Camera.smooth.none()
.. function:: Camera.smooth.linear(speed)
:param number speed: Smoothing speed.
:returns: Smoothing function.
Smoothly moves the camera towards to snapping goal with constant speed.
**Examples**::
cam.smoother = Camera.smooth.linear(100)
::
-- warning: creates a function every frame!
camera:lockX(player.x, Camera.smooth.linear(25))
.. function:: Camera.smooth.damped(stiffness)
:param number stiffness: Speed of the camera movement.
:returns: Smoothing function.
Smoothly moves the camera towards the goal with a speed proportional to the
distance to the target.
Stiffness defines the speed of the motion: Higher values mean that the camera
moves more quickly.
**Examples**::
cam.smoother = Camera.smooth.damped(10)
::
-- warning: creates a function every frame!
camera:lockPosition(player.x, player.y, Camera.smooth.damped(2))

353
libs/hump/docs/class.rst Normal file
View file

@ -0,0 +1,353 @@
hump.class
==========
::
Class = require "hump.class"
A small, fast class/prototype implementation with multiple inheritance.
Implements `class commons <https://github.com/bartbes/Class-Commons>`_.
**Example**::
Critter = Class{
init = function(self, pos, img)
self.pos = pos
self.img = img
end,
speed = 5
}
function Critter:update(dt, player)
-- see hump.vector
local dir = (player.pos - self.pos):normalize_inplace()
self.pos = self.pos + dir * Critter.speed * dt
end
function Critter:draw()
love.graphics.draw(self.img, self.pos.x, self.pos.y)
end
List of Functions
-----------------
* :func:`Class.new() <Class.new>`
* :func:`class.init(object, ...) <class.init>`
* :func:`Class:include(other) <Class:include>`
* :func:`class:clone() <class:clone>`
Function Reference
------------------
.. function:: Class.new()
Class.new({init = constructor, __includes = parents, ...})
:param function constructor: Class constructor. Can be accessed with ``theclass.init(object, ...)``. (optional)
:param class or table of classes parents: Classes to inherit from. Can either be a single class or a table of classes. (optional)
:param mixed ...: Any other fields or methods common to all instances of this class. (optional)
:returns: The class.
Declare a new class.
``init()`` will receive the new object instance as first argument. Any other
arguments will also be forwarded (see examples), i.e. ``init()`` has the
following signature::
function init(self, ...)
If you do not specify a constructor, an empty constructor will be used instead.
The name of the variable that holds the module can be used as a shortcut to
``new()`` (see example).
**Examples**::
Class = require 'hump.class' -- `Class' is now a shortcut to new()
-- define a class class
Feline = Class{
init = function(self, size, weight)
self.size = size
self.weight = weight
end;
-- define a method
stats = function(self)
return string.format("size: %.02f, weight: %.02f", self.size, self.weight)
end;
}
-- create two objects
garfield = Feline(.7, 45)
felix = Feline(.8, 12)
print("Garfield: " .. garfield:stats(), "Felix: " .. felix:stats())
::
Class = require 'hump.class'
-- same as above, but with 'external' function definitions
Feline = Class{}
function Feline:init(size, weight)
self.size = size
self.weight = weight
end
function Feline:stats()
return string.format("size: %.02f, weight: %.02f", self.size, self.weight)
end
garfield = Feline(.7, 45)
print(Feline, garfield)
::
Class = require 'hump.class'
A = Class{
foo = function() print('foo') end
}
B = Class{
bar = function() print('bar') end
}
-- single inheritance
C = Class{__includes = A}
instance = C()
instance:foo() -- prints 'foo'
instance:bar() -- error: function not defined
-- multiple inheritance
D = Class{__includes = {A,B}}
instance = D()
instance:foo() -- prints 'foo'
instance:bar() -- prints 'bar'
::
-- class attributes are shared across instances
A = Class{ foo = 'foo' } -- foo is a class attribute/static member
one, two, three = A(), A(), A()
print(one.foo, two.foo, three.foo) --> prints 'foo foo foo'
one.foo = 'bar' -- overwrite/specify for instance `one' only
print(one.foo, two.foo, three.foo) --> prints 'bar foo foo'
A.foo = 'baz' -- overwrite for all instances without specification
print(one.foo, two.foo, three.foo) --> prints 'bar baz baz'
.. function:: class.init(object, ...)
:param Object object: The object. Usually ``self``.
:param mixed ...: Arguments to pass to the constructor.
:returns: Whatever the parent class constructor returns.
Calls class constructor of a class on an object.
Derived classes should use this function their constructors to initialize the
parent class(es) portions of the object.
**Example**::
Class = require 'hump.class'
Shape = Class{
init = function(self, area)
self.area = area
end;
__tostring = function(self)
return "area = " .. self.area
end
}
Rectangle = Class{__includes = Shape,
init = function(self, width, height)
Shape.init(self, width * height)
self.width = width
self.height = height
end;
__tostring = function(self)
local strs = {
"width = " .. self.width,
"height = " .. self.height,
Shape.__tostring(self)
}
return table.concat(strs, ", ")
end
}
print( Rectangle(2,4) ) -- prints 'width = 2, height = 4, area = 8'
.. function:: Class:include(other)
:param tables other: Parent classes/mixins.
:returns: The class.
Inherit functions and variables of another class, but only if they are not
already defined. This is done by (deeply) copying the functions and variables
over to the subclass.
.. note::
``class:include()`` doesn't actually care if the arguments supplied are
hump classes. Just any table will work.
.. note::
You can use ``Class.include(a, b)`` to copy any fields from table ``a``
to table ``b`` (see second example).
**Examples**::
Class = require 'hump.class'
Entity = Class{
init = function(self)
GameObjects.register(self)
end
}
Collidable = {
dispatch_collision = function(self, other, dx, dy)
if self.collision_handler[other.type])
return collision_handler[other.type](self, other, dx, dy)
end
return collision_handler["*"](self, other, dx, dy)
end,
collision_handler = {["*"] = function() end},
}
Spaceship = Class{
init = function(self)
self.type = "Spaceship"
-- ...
end
}
-- make Spaceship collidable
Spaceship:include(Collidable)
Spaceship.collision_handler["Spaceship"] = function(self, other, dx, dy)
-- ...
end
::
-- using Class.include()
Class = require 'hump.class'
a = {
foo = 'bar',
bar = {one = 1, two = 2, three = 3},
baz = function() print('baz') end,
}
b = {
foo = 'nothing to see here...'
}
Class.include(b, a) -- copy values from a to b
-- note that neither a nor b are hump classes!
print(a.foo, b.foo) -- prints 'bar nothing to see here...'
b.baz() -- prints 'baz'
b.bar.one = 10 -- changes only values in b
print(a.bar.one, b.bar.one) -- prints '1 10'
.. function:: class:clone()
:returns: A deep copy of the class/table.
Create a clone/deep copy of the class.
.. note::
You can use ``Class.clone(a)`` to create a deep copy of any table (see
second example).
**Examples**::
Class = require 'hump.class'
point = Class{ x = 0, y = 0 }
a = point:clone()
a.x, a.y = 10, 10
print(a.x, a.y) --> prints '10 10'
b = point:clone()
print(b.x, b.y) --> prints '0 0'
c = a:clone()
print(c.x, c.y) --> prints '10 10'
::
-- using Class.clone() to copy tables
Class = require 'hump.class'
a = {
foo = 'bar',
bar = {one = 1, two = 2, three = 3},
baz = function() print('baz') end,
}
b = Class.clone(a)
b.baz() -- prints 'baz'
b.bar.one = 10
print(a.bar.one, b.bar.one) -- prints '1 10'
Caveats
-------
Be careful when using metamethods like ``__add`` or ``__mul``: If a subclass
inherits those methods from a superclass, but does not overwrite them, the
result of the operation may be of the type superclass. Consider the following::
Class = require 'hump.class'
A = Class{init = function(self, x) self.x = x end}
function A:__add(other) return A(self.x + other.x) end
function A:show() print("A:", self.x) end
B = Class{init = function(self, x, y) A.init(self, x) self.y = y end}
function B:show() print("B:", self.x, self.y) end
function B:foo() print("foo") end
B:include(A)
one, two = B(1,2), B(3,4)
result = one + two -- result will be of type A, *not* B!
result:show() -- prints "A: 4"
result:foo() -- error: method does not exist
Note that while you can define the ``__index`` metamethod of the class, this is
not a good idea: It will break the class mechanism. To add a custom ``__index``
metamethod without breaking the class system, you have to use ``rawget()``. But
beware that this won't affect subclasses::
Class = require 'hump.class'
A = Class{}
function A:foo() print('bar') end
function A:__index(key)
print(key)
return rawget(A, key)
end
instance = A()
instance:foo() -- prints foo bar
B = Class{__includes = A}
instance = B()
instance:foo() -- prints only foo

293
libs/hump/docs/conf.py Normal file
View file

@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
#
# hump documentation build configuration file, created by
# sphinx-quickstart on Sat Oct 10 13:10:12 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
import shlex
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.mathjax',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'hump'
copyright = u'2015, Matthias Richter'
author = u'Matthias Richter'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'humpdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'hump.tex', u'hump Documentation',
u'Matthias Richter', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'hump', u'hump Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'hump', u'hump Documentation',
author, 'hump', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
primary_domain = "js"
highlight_language = "lua"
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

View file

@ -0,0 +1,361 @@
hump.gamestate
==============
::
Gamestate = require "hump.gamestate"
A gamestate encapsulates independent data and behaviour in a single table.
A typical game could consist of a menu-state, a level-state and a game-over-state.
**Example**::
local menu = {} -- previously: Gamestate.new()
local game = {}
function menu:draw()
love.graphics.print("Press Enter to continue", 10, 10)
end
function menu:keyreleased(key, code)
if key == 'return' then
Gamestate.switch(game)
end
end
function game:enter()
Entities.clear()
-- setup entities here
end
function game:update(dt)
Entities.update(dt)
end
function game:draw()
Entities.draw()
end
function love.load()
Gamestate.registerEvents()
Gamestate.switch(menu)
end
List of Functions
-----------------
* :func:`Gamestate.new() <Gamestate.new>`
* :func:`Gamestate.switch(to, ...) <Gamestate.switch>`
* :func:`Gamestate.current() <Gamestate.current>`
* :func:`Gamestate.push(to, ...) <Gamestate.push>`
* :func:`Gamestate.pop(...) <Gamestate.pop>`
* :func:`Gamestate.<callback>(...) <Gamestate.<callback>>`
* :func:`Gamestate.registerEvents([callbacks]) <Gamestate.registerEvents>`
.. _callbacks:
Gamestate Callbacks
-------------------
A gamestate can define all callbacks that LÖVE defines. In addition, there are
callbacks for initalizing, entering and leaving a state:
``init()``
Called once, and only once, before entering the state the first time. See
:func:`Gamestate.switch`.
``enter(previous, ...)``
Called every time when entering the state. See :func:`Gamestate.switch`.
``leave()``
Called when leaving a state. See :func:`Gamestate.switch` and :func:`Gamestate.pop`.
``resume()``
Called when re-entering a state by :func:`Gamestate.pop`-ing another state.
``update()``
Update the game state. Called every frame.
``draw()``
Draw on the screen. Called every frame.
``focus()``
Called if the window gets or loses focus.
``keypressed()``
Triggered when a key is pressed.
``keyreleased()``
Triggered when a key is released.
``mousepressed()``
Triggered when a mouse button is pressed.
``mousereleased()``
Triggered when a mouse button is released.
``joystickpressed()``
Triggered when a joystick button is pressed.
``joystickreleased()``
Triggered when a joystick button is released.
``quit()``
Called on quitting the game. Only called on the active gamestate.
When using :func:`Gamestate.registerEvents`, all these callbacks will be called by the
corresponding LÖVE callbacks and receive the same arguments (e.g.
``state:update(dt)`` will be called by ``love.update(dt)``).
**Example**::
menu = {} -- previously: Gamestate.new()
function menu:init()
self.background = love.graphics.newImage('bg.jpg')
Buttons.initialize()
end
function menu:enter(previous) -- runs every time the state is entered
Buttons.setActive(Buttons.start)
end
function menu:update(dt) -- runs every frame
Buttons.update(dt)
end
function menu:draw()
love.graphics.draw(self.background, 0, 0)
Buttons.draw()
end
function menu:keyreleased(key)
if key == 'up' then
Buttons.selectPrevious()
elseif key == 'down' then
Buttons.selectNext()
elseif
Buttons.active:onClick()
end
end
function menu:mousereleased(x,y, mouse_btn)
local button = Buttons.hovered(x,y)
if button then
Button.select(button)
if mouse_btn == 'l' then
button:onClick()
end
end
end
Function Reference
------------------
.. function:: Gamestate.new()
:returns: An empty table.
**Deprecated: Use the table constructor instead (see example)**
Declare a new gamestate (just an empty table). A gamestate can define several
callbacks.
**Example**::
menu = {}
-- deprecated method:
menu = Gamestate.new()
.. function:: Gamestate.switch(to, ...)
:param Gamestate to: Target gamestate.
:param mixed ...: Additional arguments to pass to ``to:enter(current, ...)``.
:returns: The results of ``to:enter(current, ...)``.
Switch to a gamestate, with any additional arguments passed to the new state.
Switching a gamestate will call the ``leave()`` callback on the current
gamestate, replace the current gamestate with ``to``, call the ``init()`` function
if, and only if, the state was not yet inialized and finally call
``enter(old_state, ...)`` on the new gamestate.
.. note::
Processing of callbacks is suspended until ``update()`` is called on the new
gamestate, but the function calling :func:`Gamestate.switch` can still continue - it is
your job to make sure this is handled correctly. See also the examples below.
**Examples**::
Gamestate.switch(game, level_two)
::
-- stop execution of the current state by using return
if player.has_died then
return Gamestate.switch(game, level_two)
end
-- this will not be called when the state is switched
player:update()
.. function:: Gamestate.current()
:returns: The active gamestate.
Returns the currently activated gamestate.
**Example**::
function love.keypressed(key)
if Gamestate.current() ~= menu and key == 'p' then
Gamestate.push(pause)
end
end
.. function:: Gamestate.push(to, ...)
:param Gamestate to: Target gamestate.
:param mixed ...: Additional arguments to pass to ``to:enter(current, ...)``.
:returns: The results of ``to:enter(current, ...)``.
Pushes the ``to`` on top of the state stack, i.e. makes it the active state.
Semantics are the same as ``switch(to, ...)``, except that ``leave()`` is *not*
called on the previously active state.
Useful for pause screens, menus, etc.
.. note::
Processing of callbacks is suspended until ``update()`` is called on the
new gamestate, but the function calling ``GS.push()`` can still continue -
it is your job to make sure this is handled correctly. See also the
example below.
**Example**::
-- pause gamestate
Pause = Gamestate.new()
function Pause:enter(from)
self.from = from -- record previous state
end
function Pause:draw()
local W, H = love.graphics.getWidth(), love.graphics.getHeight()
-- draw previous screen
self.from:draw()
-- overlay with pause message
love.graphics.setColor(0,0,0, 100)
love.graphics.rectangle('fill', 0,0, W,H)
love.graphics.setColor(255,255,255)
love.graphics.printf('PAUSE', 0, H/2, W, 'center')
end
-- [...]
function love.keypressed(key)
if Gamestate.current() ~= menu and key == 'p' then
return Gamestate.push(pause)
end
end
.. function:: Gamestate.pop(...)
:returns: The results of ``new_state:resume(...)``.
Calls ``leave()`` on the current state and then removes it from the stack, making
the state below the current state and calls ``resume(...)`` on the activated state.
Does *not* call ``enter()`` on the activated state.
.. note::
Processing of callbacks is suspended until ``update()`` is called on the
new gamestate, but the function calling ``GS.pop()`` can still continue -
it is your job to make sure this is handled correctly. See also the
example below.
**Example**::
-- extending the example of Gamestate.push() above
function Pause:keypressed(key)
if key == 'p' then
return Gamestate.pop() -- return to previous state
end
end
.. function:: Gamestate.<callback>(...)
:param mixed ...: Arguments to pass to the corresponding function.
:returns: The result of the callback function.
Calls a function on the current gamestate. Can be any function, but is intended
to be one of the :ref:`callbacks`. Mostly useful when not using
:func:`Gamestate.registerEvents`.
**Example**::
function love.draw()
Gamestate.draw() -- <callback> is `draw'
end
function love.update(dt)
Gamestate.update(dt) -- pass dt to currentState:update(dt)
end
function love.keypressed(key, code)
Gamestate.keypressed(key, code) -- pass multiple arguments
end
.. function:: Gamestate.registerEvents([callbacks])
:param table callbacks: Names of the callbacks to register. If omitted,
register all love callbacks (optional).
Overwrite love callbacks to call ``Gamestate.update()``, ``Gamestate.draw()``,
etc. automatically. ``love`` callbacks (e.g. ``love.update()``) are still
invoked as usual.
This is by done by overwriting the love callbacks, e.g.::
local old_update = love.update
function love.update(dt)
old_update(dt)
return Gamestate.current:update(dt)
end
.. note::
Only works when called in love.load() or any other function that is
executed *after* the whole file is loaded.
**Examples**::
function love.load()
Gamestate.registerEvents()
Gamestate.switch(menu)
end
-- love callback will still be invoked
function love.update(dt)
Timer.update(dt)
-- no need for Gamestate.update(dt)
end
::
function love.load()
-- only register draw, update and quit
Gamestate.registerEvents{'draw', 'update', 'quit'}
Gamestate.switch(menu)
end

59
libs/hump/docs/index.rst Normal file
View file

@ -0,0 +1,59 @@
**hump** - Helper Utilities for a Multitude of Problems
=======================================================
**hump** is a set of lightweight helpers for the awesome `LÖVE
<http://love2d.org>`_ game framework.
It will help to get you over the initial hump when starting to build a new
game.
**hump** does nothing that you couldn't do yourself.
But why should you?
You want to write games, not boilerplate!
**hump**'s components are so loosely coupled that every component is
independent of the others.
You can choose what you need and leave the rest behind.
hump won't judge.
**hump** just wants to make you happy.
Read on
-------
.. toctree::
:maxdepth: 2
hump.gamestate <gamestate>
hump.timer <timer>
hump.vector <vector>
hump.vector-light <vector-light>
hump.class <class>
hump.signal <signal>
hump.camera <camera>
license
Get hump
--------
You can view and download the individual modules on github: `vrld/hump
<http://github.com/vrld/hump>`_.
You may also download the whole packed sourcecode either in the `zip
<http://github.com/vrld/hump/zipball/master>`_ or `tar
<http://github.com/vrld/hump/tarball/master>`_ format.
Using `Git <http://git-scm.com>`_, you can clone the project by running:
git clone git://github.com/vrld/hump
Once done, you can check for updates by running
git pull
Indices and tables
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View file

@ -0,0 +1,26 @@
License
=======
Copyright (c) 2011-2018 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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.

197
libs/hump/docs/signal.rst Normal file
View file

@ -0,0 +1,197 @@
hump.signal
===========
::
Signal = require 'hump.signal'
A simple yet effective implementation of `Signals and Slots
<http://en.wikipedia.org/wiki/Signals_and_slots>`_, aka the `Observer pattern
<http://en.wikipedia.org/wiki/Observer_pattern>`_: Functions can be dynamically
bound to signals. When a *signal* is *emitted*, all registered functions will
be invoked. Simple as that.
``hump.signal`` makes things a little more interesting by allowing to emit all
signals that match a `Lua string pattern
<http://www.lua.org/manual/5.1/manual.html#5.4.1>`_.
**Example**::
-- in AI.lua
Signal.register('shoot', function(x,y, dx,dy)
-- for every critter in the path of the bullet:
-- try to avoid being hit
for critter in pairs(critters) do
if critter:intersectsRay(x,y, dx,dy) then
critter:setMoveDirection(-dy, dx)
end
end
end)
-- in sounds.lua
Signal.register('shoot', function()
Sounds.fire_bullet:play()
end)
-- in main.lua
function love.keypressed(key)
if key == ' ' then
local x,y = player.pos:unpack()
local dx,dy = player.direction:unpack()
Signal.emit('shoot', x,y, dx,dy)
end
end
List of Functions
-----------------
* :func:`Signal.new() <Signal.new>`
* :func:`Signal.register(s, f) <Signal.register>`
* :func:`Signal.emit(s, ...) <Signal.emit>`
* :func:`Signal.remove(s, ...) <Signal.remove>`
* :func:`Signal.clear(s) <Signal.clear>`
* :func:`Signal.emitPattern(p, ...) <Signal.emitPattern>`
* :func:`Signal.removePattern(p, ...) <Signal.removePattern>`
* :func:`Signal.clearPattern(p) <Signal.clearPattern>`
Function Reference
------------------
.. function:: Signal.new()
:returns: A new signal registry.
Creates a new signal registry that is independent of the default registry: It
will manage its own list of signals and does not in any way affect the the
global registry. Likewise, the global registry does not affect the instance.
.. note::
If you don't need multiple independent registries, you can use the
global/default registry (see examples).
.. note::
Unlike the default one, signal registry instances use the colon-syntax,
i.e., you need to call ``instance:emit('foo', 23)`` instead of
``Signal.emit('foo', 23)``.
**Example**::
player.signals = Signal.new()
.. function:: Signal.register(s, f)
:param string s: The signal identifier.
:param function f: The function to register.
:returns: A function handle to use in :func:`Signal.remove()`.
Registers a function ``f`` to be called when signal ``s`` is emitted.
**Examples**::
Signal.register('level-complete', function() self.fanfare:play() end)
::
handle = Signal.register('level-load', function(level) level.show_help() end)
::
menu:register('key-left', select_previous_item)
.. function:: Signal.emit(s, ...)
:param string s: The signal identifier.
:param mixed ...: Arguments to pass to the bound functions. (optional)
Calls all functions bound to signal ``s`` with the supplied arguments.
**Examples**::
function love.keypressed(key)
-- using a signal instance
if key == 'left' then menu:emit('key-left') end
end
::
if level.is_finished() then
-- adding arguments
Signal.emit('level-load', level.next_level)
end
.. function:: Signal.remove(s, ...)
:param string s: The signal identifier.
:param functions ...: Functions to unbind from the signal.
Unbinds (removes) functions from signal ``s``.
**Example**::
Signal.remove('level-load', handle)
.. function:: Signal.clear(s)
:param string s: The signal identifier.
Removes all functions from signal ``s``.
**Example**::
Signal.clear('key-left')
.. function:: Signal.emitPattern(p, ...)
:param string p: The signal identifier pattern.
:param mixed ...: Arguments to pass to the bound functions. (optional)
Emits all signals that match a `Lua string pattern
<http://www.lua.org/manual/5.1/manual.html#5.4.1>`_.
**Example**::
-- emit all update signals
Signal.emitPattern('^update%-.*', dt)
.. function:: Signal.removePattern(p, ...)
:param string p: The signal identifier pattern.
:param functions ...: Functions to unbind from the signals.
Removes functions from all signals that match a `Lua string pattern
<http://www.lua.org/manual/5.1/manual.html#5.4.1>`_.
**Example**::
Signal.removePattern('key%-.*', play_click_sound)
.. function:: Signal.clearPattern(p)
:param string p: The signal identifier pattern.
Removes **all** functions from all signals that match a `Lua string pattern
<http://www.lua.org/manual/5.1/manual.html#5.4.1>`_.
**Examples**::
Signal.clearPattern('sound%-.*')
::
player.signals:clearPattern('.*') -- clear all signals

478
libs/hump/docs/timer.rst Normal file
View file

@ -0,0 +1,478 @@
hump.timer
==========
::
Timer = require "hump.timer"
hump.timer offers a simple interface to schedule the execution of functions. It
is possible to run functions *after* and *for* some amount of time. For
example, a timer could be set to move critters every 5 seconds or to make the
player invincible for a short amount of time.
In addition to that, ``hump.timer`` offers various `tweening
<http://en.wikipedia.org/wiki/Inbetweening>`_ functions that make it
easier to produce `juicy games <http://www.youtube.com/watch?v=Fy0aCDmgnxg>`_.
**Example**::
function love.keypressed(key)
if key == ' ' then
Timer.after(1, function() print("Hello, world!") end)
end
end
function love.update(dt)
Timer.update(dt)
end
List of Functions
-----------------
* :func:`Timer.new() <Timer.new>`
* :func:`Timer.after(delay, func) <Timer.after>`
* :func:`Timer.script(func) <Timer.script>`
* :func:`Timer.every(delay, func, count) <Timer.every>`
* :func:`Timer.during(delay, func, after) <Timer.during>`
* :func:`Timer.cancel(handle) <Timer.cancel>`
* :func:`Timer.clear() <Timer.clear>`
* :func:`Timer.update(dt) <Timer.update>`
* :func:`Timer.tween(duration, subject, target, method, after, ...) <Timer.tween>`
Function Reference
------------------
.. function:: Timer.new()
:returns: A timer instance.
Creates a new timer instance that is independent of the global timer: It will
manage it's own list of scheduled functions and does not in any way affect the
the global timer. Likewise, the global timer does not affect timer instances.
.. note::
If you don't need multiple independent schedulers, you can use the
global/default timer (see examples).
.. note::
Unlike the default timer, timer instances use the colon-syntax, i.e.,
you need to call ``instance:after(1, foo)`` instead of ``Timer.after(1,
foo)``.
**Example**::
menuTimer = Timer.new()
.. function:: Timer.after(delay, func)
:param number delay: Number of seconds the function will be delayed.
:param function func: The function to be delayed.
:returns: The timer handle. See also :func:`Timer.cancel`.
Schedule a function. The function will be executed after ``delay`` seconds have
elapsed, given that ``update(dt)`` is called every frame.
.. note::
There is no guarantee that the delay will not be exceeded, it is only
guaranteed that the function will *not* be executed *before* the delay has
passed.
``func`` will receive itself as only parameter. This is useful to implement
periodic behavior (see the example).
**Examples**::
-- grant the player 5 seconds of immortality
player.isInvincible = true
Timer.after(5, function() player.isInvincible = false end)
::
-- print "foo" every second. See also every()
Timer.after(1, function(func) print("foo") Timer.after(1, func) end)
::
--Using a timer instance:
menuTimer:after(1, finishAnimation)
.. function:: Timer.script(func)
:param function func: Script to execute.
Execute a function that can be paused without causing the rest of the program to
be suspended. ``func`` will receive a function - ``wait`` - to do interrupt the
script (but not the whole program) as only argument. The function prototype of
wait is: ``wait(delay)``.
**Examples**::
Timer.script(function(wait)
print("Now")
wait(1)
print("After one second")
wait(1)
print("Bye!")
end)
::
-- useful for splash screens
Timer.script(function(wait)
Timer.tween(0.5, splash.pos, {x = 300}, 'in-out-quad')
wait(5) -- show the splash for 5 seconds
Timer.tween(0.5, slpash.pos, {x = 800}, 'in-out-quad')
end)
::
-- repeat something with a varying delay
Timer.script(function(wait)
while true do
spawn_ship()
wait(1 / (1-production_speed))
end
end)
::
-- jumping with timer.script
self.timers:script(function(wait)
local w = 1/12
self.jumping = true
Timer.tween(w*2, self, {z = -8}, "out-cubic", function()
Timer.tween(w*2, self, {z = 0},"in-cubic")
end)
self.quad = self.quads.jump[1]
wait(w)
self.quad = self.quads.jump[2]
wait(w)
self.quad = self.quads.jump[3]
wait(w)
self.quad = self.quads.jump[4]
wait(w)
self.jumping = false
self.z = 0
end)
.. function:: Timer.every(delay, func, count)
:param number delay: Number of seconds between two consecutive function calls.
:param function func: The function to be called periodically.
:param number count: Number of times the function is to be called (optional).
:returns: The timer handle. See also :func:`Timer.cancel`.
Add a function that will be called ``count`` times every ``delay`` seconds.
If ``count`` is omitted, the function will be called until it returns ``false``
or :func:`Timer.cancel` or :func:`Timer.clear` is called on the timer instance.
**Example**::
-- toggle light on and off every second
Timer.every(1, function() lamp:toggleLight() end)
::
-- launch 5 fighters in quick succession (using a timer instance)
mothership_timer:every(0.3, function() self:launchFighter() end, 5)
::
-- flicker player's image as long as he is invincible
Timer.every(0.1, function()
player:flipImage()
return player.isInvincible
end)
.. function:: Timer.during(delay, func, after)
:param number delay: Number of seconds the func will be called.
:param function func: The function to be called on ``update(dt)``.
:param function after: A function to be called after delay seconds (optional).
:returns: The timer handle. See also :func:`Timer.cancel`.
Run ``func(dt)`` for the next ``delay`` seconds. The function is called every
time ``update(dt)`` is called. Optionally run ``after()`` once ``delay``
seconds have passed.
``after()`` will receive itself as only parameter.
.. note::
You should not add new timers in ``func(dt)``, as this can lead to random
crashes.
**Examples**::
-- play an animation for 5 seconds
Timer.during(5, function(dt) animation:update(dt) end)
::
-- shake the camera for one second
local orig_x, orig_y = camera:position()
Timer.during(1, function()
camera:lookAt(orig_x + math.random(-2,2), orig_y + math.random(-2,2))
end, function()
-- reset camera position
camera:lookAt(orig_x, orig_y)
end)
::
player.isInvincible = true
-- flash player for 3 seconds
local t = 0
player.timer:during(3, function(dt)
t = t + dt
player.visible = (t % .2) < .1
end, function()
-- make sure the player is visible after three seconds
player.visible = true
player.isInvincible = false
end)
.. function:: Timer.cancel(handle)
:param table handle: The function to be canceled.
Prevent a timer from being executed in the future.
**Examples**::
function tick()
print('tick... tock...')
end
handle = Timer.every(1, tick)
-- later
Timer.cancel(handle) -- NOT: Timer.cancel(tick)
::
-- using a timer instance
function tick()
print('tick... tock...')
end
handle = menuTimer:every(1, tick)
-- later
menuTimer:cancel(handle)
.. function:: Timer.clear()
Remove all timed and periodic functions. Functions that have not yet been
executed will discarded.
**Examples**::
Timer.clear()
::
menuTimer:clear()
.. function:: Timer.update(dt)
:param number dt: Time that has passed since the last ``update()``.
Update timers and execute functions if the deadline is reached. Call in
``love.update(dt)``.
**Examples**::
function love.update(dt)
do_stuff()
Timer.update(dt)
end
::
-- using hump.gamestate and a timer instance
function menuState:update(dt)
self.timers:update(dt)
end
.. function:: Timer.tween(duration, subject, target, method, after, ...)
:param number duration: Duration of the tween.
:param table subject: Object to be tweened.
:param table target: Target values.
:param string method: Tweening method, defaults to 'linear' (:ref:`see here
<tweening-methods>`, optional).
:param function after: Function to execute after the tween has finished
(optional).
:param mixed ...: Additional arguments to the *tweening* function.
:returns: A timer handle.
`Tweening <http://en.wikipedia.org/wiki/Inbetweening>`_ (short for
in-betweening) is the process that happens between two defined states. For
example, a tween can be used to gradually fade out a graphic or move a text
message to the center of the screen. For more information why tweening should
be important to you, check out this great talk on `juicy games
<http://www.youtube.com/watch?v=Fy0aCDmgnxg>`_.
``hump.timer`` offers two interfaces for tweening: the low-level
:func:`Timer.during` and the higher level interface :func:`Timer.tween`.
To see which tweening methods hump offers, :ref:`see below <tweening-methods>`.
**Examples**::
function love.load()
color = {0, 0, 0}
Timer.tween(10, color, {255, 255, 255}, 'in-out-quad')
end
function love.update(dt)
Timer.update(dt)
end
function love.draw()
love.graphics.setBackgroundColor(color)
end
::
function love.load()
circle = {rad = 10, pos = {x = 400, y = 300}}
-- multiple tweens can work on the same subject
-- and nested values can be tweened, too
Timer.tween(5, circle, {rad = 50}, 'in-out-quad')
Timer.tween(2, circle, {pos = {y = 550}}, 'out-bounce')
end
function love.update(dt)
Timer.update(dt)
end
function love.draw()
love.graphics.circle('fill', circle.pos.x, circle.pos.y, circle.rad)
end
::
function love.load()
-- repeated tweening
circle = {rad = 10, x = 100, y = 100}
local grow, shrink, move_down, move_up
grow = function()
Timer.tween(1, circle, {rad = 50}, 'in-out-quad', shrink)
end
shrink = function()
Timer.tween(2, circle, {rad = 10}, 'in-out-quad', grow)
end
move_down = function()
Timer.tween(3, circle, {x = 700, y = 500}, 'bounce', move_up)
end
move_up = function()
Timer.tween(5, circle, {x = 200, y = 200}, 'out-elastic', move_down)
end
grow()
move_down()
end
function love.update(dt)
Timer.update(dt)
end
function love.draw()
love.graphics.circle('fill', circle.x, circle.y, circle.rad)
end
.. _tweening-methods:
Tweening methods
----------------
At the core of tweening lie interpolation methods. These methods define how the
output should look depending on how much time has passed. For example, consider
the following tween::
-- now: player.x = 0, player.y = 0
Timer.tween(2, player, {x = 2})
Timer.tween(4, player, {y = 8})
At the beginning of the tweens (no time passed), the interpolation method would
place the player at ``x = 0, y = 0``. After one second, the player should be at
``x = 1, y = 2``, and after two seconds the output is ``x = 2, y = 4``.
The actual duration of and time since starting the tween is not important, only
the fraction of the two. Similarly, the starting value and output are not
important to the interpolation method, since it can be calculated from the
start and end point. Thus an interpolation method can be fully characterized by
a function that takes a number between 0 and 1 and returns a number that
defines the output (usually also between 0 and 1). The interpolation function
must hold that the output is 0 for input 0 and 1 for input 1.
**hump** predefines several commonly used interpolation methods, which are
generalized versions of `Robert Penner's easing
functions <http://www.robertpenner.com/easing/>`_. Those are:
``'linear'``,
``'quad'``,
``'cubic'``,
``'quart'``,
``'quint'``,
``'sine'``,
``'expo'``,
``'circ'``,
``'back'``,
``'bounce'``, and
``'elastic'``.
It's hard to understand how these functions behave by staring at a graph, so
below are some animation examples. You can change the type of the tween by
changing the selections.
.. raw:: html
<div id="tween-graph"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="_static/graph-tweens.js"></script>
Note that while the animations above show tweening of shapes, other attributes
(color, opacity, volume of a sound, ...) can be changed as well.
Custom interpolators
^^^^^^^^^^^^^^^^^^^^
.. warning:
This is a stub
You can add custom interpolation methods by adding them to the `tween` table::
Timer.tween.sqrt = function(t) return math.sqrt(t) end
-- or just Timer.tween.sqrt = math.sqrt
Access the your method like you would the predefined ones. You can even use the
modyfing prefixes::
Timer.tween(5, circle, {radius = 50}, 'in-out-sqrt')
You can also invert and chain functions::
outsqrt = Timer.tween.out(math.sqrt)
inoutsqrt = Timer.tween.chain(math.sqrt, outsqrt)

View file

@ -0,0 +1,492 @@
hump.vector-light
=================
::
vector = require "hump.vector-light"
An table-free version of :doc:`hump.vector <vector>`. Instead of a vector type,
``hump.vector-light`` provides functions that operate on numbers.
.. note::
Using this module instead of :doc:`hump.vector <vector>` may result in
faster code, but does so at the expense of speed of development and code
readability. Unless you are absolutely sure that your code is
significantly slowed down by :doc:`hump.vector <vector>`, I recommend using
it instead.
**Example**::
function player:update(dt)
local dx,dy = 0,0
if love.keyboard.isDown('left') then
dx = -1
elseif love.keyboard.isDown('right') then
dx = 1
end
if love.keyboard.isDown('up') then
dy = -1
elseif love.keyboard.isDown('down') then
dy = 1
end
dx,dy = vector.normalize(dx, dy)
player.velx, player.vely = vector.add(player.velx, player.vely,
vector.mul(dy, dx, dy))
if vector.len(player.velx, player.vely) > player.max_velocity then
player.velx, player.vely = vector.mul(player.max_velocity,
vector.normalize(player.velx, player.vely)
end
player.x = player.x + dt * player.velx
player.y = player.y + dt * player.vely
end
List of Functions
-----------------
* :func:`vector.str(x,y) <vector.str>`
* :func:`vector.fromPolar(angle, radius) <vector.fromPolar>`
* :func:`vector.toPolar(x, y) <vector.toPolar>`
* :func:`vector.randomDirection(len_min, len_max) <vector.randomDirection>`
* :func:`vector.mul(s, x,y) <vector.mul>`
* :func:`vector.div(s, x,y) <vector.div>`
* :func:`vector.idiv(s, x,y) <vector.idiv>`
* :func:`vector.add(x1,y1, x2,y2) <vector.add>`
* :func:`vector.sub(x1,y1, x2,y2) <vector.sub>`
* :func:`vector.permul(x1,y1, x2,y2) <vector.permul>`
* :func:`vector.dot(x1,y1, x2,y2) <vector.dot>`
* :func:`vector.cross(x1,y1, x2,y2) <vector.cross>`
* :func:`vector.vector.det(x1,y1, x2,y2) <vector.vector.det>`
* :func:`vector.eq(x1,y1, x2,y2) <vector.eq>`
* :func:`vector.le(x1,y1, x2,y2) <vector.le>`
* :func:`vector.lt(x1,y1, x2,y2) <vector.lt>`
* :func:`vector.len(x,y) <vector.len>`
* :func:`vector.len2(x,y) <vector.len2>`
* :func:`vector.dist(x1,y1, x2,y2) <vector.dist>`
* :func:`vector.dist2(x1,y1, x2,y2) <vector.dist2>`
* :func:`vector.normalize(x,y) <vector.normalize>`
* :func:`vector.rotate(phi, x,y) <vector.rotate>`
* :func:`vector.perpendicular(x,y) <vector.perpendicular>`
* :func:`vector.project(x,y, u,v) <vector.project>`
* :func:`vector.mirror(x,y, u,v) <vector.mirror>`
* :func:`vector.angleTo(ox,y, u,v) <vector.angleTo>`
* :func:`vector.trim(max_length, x,y) <vector.trim>`
Function Reference
------------------
.. function:: vector.str(x,y)
:param numbers x,y: The vector.
:returns: The string representation.
Produce a human-readable string of the form ``(x,y)``.
Useful for debugging.
**Example**::
print(vector.str(love.mouse.getPosition()))
.. function:: vector.fromPolar(angle, radius)
:param number angle: Angle of the vector in radians.
:param number radius: Length of the vector (optional, default = 1).
:returns: ``x``, ``y``: The vector in cartesian coordinates.
Convert polar coordinates to cartesian coordinates.
The ``angle`` is measured against the vector (1,0), i.e., the x axis.
**Examples**::
x,y = vector.polar(math.pi,10)
.. function:: vector.toPolar(x, y)
:param numbers x,y: A vector.
:returns: ``angle``, ``radius``: The vector in polar coordinates.
Convert the vector to polar coordinates, i.e., the angle and the radius/lenth.
**Example**::
-- complex multiplication
phase1, abs1 = vector.toPolar(re1, im1)
phase2, abs2 = vector.toPolar(re2, im2)
vector.fromPolar(phase1+phase2, abs1*abs2)
.. function:: vector.randomDirection(len_min, len_max)
:param number len_min: Minimum length of the vector (optional, default = 1).
:param number len_max: Maximum length of the vector (optional, default = ``len_min``).
:returns: ``x``, ``y``: A vector pointing in a random direction with a random length between ``len_min`` and ``len_max``.
Sample a vector with random direction and (optional) length.
**Examples**::
x,y = vector.randomDirection() -- length is 1
x,y = vector.randomDirection(1,5) -- length is a random value between 1 and 5
x,y = vector.randomDirection(100) -- length is 100
.. function:: vector.mul(s, x,y)
:param number s: A scalar.
:param numbers x,y: A vector.
:returns: ``x*s, y*s``.
Computes ``x*s,y*s``. The order of arguments is chosen so that it's possible to
chain operations (see example).
**Example**::
velx,vely = vec.mul(dt, vec.add(velx,vely, accx,accy))
.. function:: vector.div(s, x,y)
:param number s: A scalar.
:param numbers x,y: A vector.
:returns: ``x/s, y/s``.
Computes ``x/s,y/s``. The order of arguments is chosen so that it's possible to
chain operations (see example).
**Example**::
x,y = vec.div(self.zoom, vec.sub(x,y, w/2,h/2))
x,y = vec.div(self.zoom, x-w/2, y-h/2)
.. function:: vector.idiv(s, x,y)
:param number s: A scalar.
:param numbers x,y: A vector.
:returns: ``x//s, y//s``.
Computes integer division ``x//s,y//s`` (only Lua 5.3 and up). The order of
arguments is chosen so that it's possible to chain operations (see example).
**Example**::
i,k = vec.idiv(grid.cellsize, x,y)
i,k = vec.idiv(grid.cellsize, love.mouse.getPosition())
.. function:: vector.add(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1+x2, x1+x2``.
Computes the sum \\((x1+x2, y1+y2)\\)`` of two vectors. Meant to be used in
conjunction with other functions like :func:`vector.mul`.
**Example**::
player.x,player.y = vector.add(player.x,player.y, vector.mul(dt, dx,dy))
.. function:: vector.sub(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1-x2, x1-x2``.
Computes the difference \\((x1-x2, y1-y2)\\) of two vectors. Meant to be used in
conjunction with other functions like :func:`vector.mul`.
**Example**::
dx,dy = vector.sub(400,300, love.mouse.getPosition())
.. function:: vector.permul(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1*x2, y1*y2``.
Component-wise multiplication, i.e.: ``x1*x2, y1*y2``.
**Example**::
x,y = vector.permul(x,y, 1,1.5)
.. function:: vector.dot(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1*x2 + y1*y2``.
Computes the `dot product <http://en.wikipedia.org/wiki/Dot_product>`_ of two
vectors: ``x1*x2 + y1*y2``.
**Example**::
cosphi = vector.dot(rx,ry, vx,vy)
.. function:: vector.cross(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1*y2 - y1*x2``.
Computes the `cross product <http://en.wikipedia.org/wiki/Cross_product>`_ of
two vectors: ``x1*y2 - y1*x2``.
**Example**::
parallelogram_area = vector.cross(ax,ay, bx,by)
.. function:: vector.vector.det(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1*y2 - y1*x2``.
Alias to :func:`vector.cross`.
**Example**::
parallelogram_area = vector.det(ax,ay, bx,by)
.. function:: vector.eq(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1 == x2 and y1 == y2``
Test for equality.
**Example**::
if vector.eq(x1,y1, x2,y2) then be.happy() end
.. function:: vector.le(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1 <= x2 and y1 <= y2``.
Test for partial lexicographical order, ``<=``.
**Example**::
if vector.le(x1,y1, x2,y2) then be.happy() end
.. function:: vector.lt(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: ``x1 < x2 or (x1 == x2) and y1 <= y2``.
Test for strict lexicographical order, ``<``.
**Example**::
if vector.lt(x1,y1, x2,y2) then be.happy() end
.. function:: vector.len(x,y)
:param numbers x,y: The vector.
:returns: Length of the vector.
Get length of a vector, i.e. ``math.sqrt(x*x + y*y)``.
**Example**::
distance = vector.len(love.mouse.getPosition())
.. function:: vector.len2(x,y)
:param numbers x,y: The vector.
:returns: Squared length of the vector.
Get squared length of a vector, i.e. ``x*x + y*y``.
**Example**::
-- get closest vertex to a given vector
closest, dsq = vertices[1], vector.len2(px-vertices[1].x, py-vertices[1].y)
for i = 2,#vertices do
local temp = vector.len2(px-vertices[i].x, py-vertices[i].y)
if temp < dsq then
closest, dsq = vertices[i], temp
end
end
.. function:: vector.dist(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: The distance of the points.
Get distance of two points. The same as ``vector.len(x1-x2, y1-y2)``.
**Example**::
-- get closest vertex to a given vector
-- slightly slower than the example using len2()
closest, dist = vertices[1], vector.dist(px,py, vertices[1].x,vertices[1].y)
for i = 2,#vertices do
local temp = vector.dist(px,py, vertices[i].x,vertices[i].y)
if temp < dist then
closest, dist = vertices[i], temp
end
end
.. function:: vector.dist2(x1,y1, x2,y2)
:param numbers x1,y1: First vector.
:param numbers x2,y2: Second vector.
:returns: The squared distance of two points.
Get squared distance of two points. The same as ``vector.len2(x1-x2, y1-y2)``.
**Example**::
-- get closest vertex to a given vector
closest, dsq = vertices[1], vector.dist2(px,py, vertices[1].x,vertices[1].y)
for i = 2,#vertices do
local temp = vector.dist2(px,py, vertices[i].x,vertices[i].y)
if temp < dsq then
closest, dsq = vertices[i], temp
end
end
.. function:: vector.normalize(x,y)
:param numbers x,y: The vector.
:returns: Vector with same direction as the input vector, but length 1.
Get normalized vector, i.e. a vector with the same direction as the input
vector, but with length 1.
**Example**::
dx,dy = vector.normalize(vx,vy)
.. function:: vector.rotate(phi, x,y)
:param number phi: Rotation angle in radians.
:param numbers x,y: The vector.
:returns: The rotated vector
Get a rotated vector.
**Example**::
-- approximate a circle
circle = {}
for i = 1,30 do
local phi = 2 * math.pi * i / 30
circle[i*2-1], circle[i*2] = vector.rotate(phi, 0,1)
end
.. function:: vector.perpendicular(x,y)
:param numbers x,y: The vector.
:returns: A vector perpendicular to the input vector
Quick rotation by 90°. The same (but faster) as ``vector.rotate(math.pi/2, x,y)``.
**Example**::
nx,ny = vector.normalize(vector.perpendicular(bx-ax, by-ay))
.. function:: vector.project(x,y, u,v)
:param numbers x,y: The vector to project.
:param numbers u,v: The vector to project onto.
:returns: The projected vector.
Project vector onto another vector.
**Example**::
vx_p,vy_p = vector.project(vx,vy, ax,ay)
.. function:: vector.mirror(x,y, u,v)
:param numbers x,y: The vector to mirror.
:param numbers u,v: The vector defining the axis.
:returns: The mirrored vector.
Mirrors vector on the axis defined by the other vector.
**Example**::
vx,vy = vector.mirror(vx,vy, surface.x,surface.y)
.. function:: vector.angleTo(ox,y, u,v)
:param numbers x,y: Vector to measure the angle.
:param numbers u,v (optional): Reference vector.
:returns: Angle in radians.
Measures the angle between two vectors. ``u`` and ``v`` default to ``0`` if omitted,
i.e. the function returns the angle to the coordinate system.
**Example**::
lean = vector.angleTo(self.upx, self.upy, 0,1)
if lean > .1 then self:fallOver() end
.. function:: vector.trim(max_length, x,y)
:param number max_length: Maximum allowed length of the vector.
:param numbers x,y: Vector to trim.
:returns: The trimmed vector.
Trim the vector to ``max_length``, i.e. return a vector that points in the same
direction as the source vector, but has a magnitude smaller or equal to
``max_length``.
**Example**::
vel_x, vel_y = vector.trim(299792458,
vector.add(vel_x, vel_y,
vector.mul(mass * dt, force_x, force_y)))

492
libs/hump/docs/vector.rst Normal file
View file

@ -0,0 +1,492 @@
hump.vector
===========
::
vector = require "hump.vector"
A handy 2D vector class providing most of the things you do with vectors.
You can access the individual coordinates by ``vec.x`` and ``vec.y``.
.. note::
The vectors are stored as tables. Most operations create new vectors and
thus new tables, which *may* put the garbage collector under stress.
If you experience slowdowns that are caused by hump.vector, try the
table-less version :doc:`hump.vector-light <vector-light>`.
**Example**::
function player:update(dt)
local delta = vector(0,0)
if love.keyboard.isDown('left') then
delta.x = -1
elseif love.keyboard.isDown('right') then
delta.x = 1
end
if love.keyboard.isDown('up') then
delta.y = -1
elseif love.keyboard.isDown('down') then
delta.y = 1
end
delta:normalizeInplace()
player.velocity = player.velocity + delta * player.acceleration * dt
if player.velocity:len() > player.max_velocity then
player.velocity = player.velocity:normalized() * player.max_velocity
end
player.position = player.position + player.velocity * dt
end
List of Functions
-----------------
* :func:`vector.new(x,y) <vector.new>`
* :func:`vector.fromPolar(angle, radius) <vector.fromPolar>`
* :func:`vector.randomDirection(len_min, len_max) <vector.randomDirection>`
* :func:`vector.isvector(v) <vector.isvector>`
* :func:`vector:clone() <vector:clone>`
* :func:`vector:unpack() <vector:unpack>`
* :func:`vector:permul(other) <vector:permul>`
* :func:`vector:len() <vector:len>`
* :func:`vector:toPolar() <vector:toPolar>`
* :func:`vector:len2() <vector:len2>`
* :func:`vector:dist(other) <vector:dist>`
* :func:`vector:dist2(other) <vector:dist2>`
* :func:`vector:normalized() <vector:normalized>`
* :func:`vector:normalizeInplace() <vector:normalizeInplace>`
* :func:`vector:rotated(angle) <vector:rotated>`
* :func:`vector:rotateInplace(angle) <vector:rotateInplace>`
* :func:`vector:perpendicular() <vector:perpendicular>`
* :func:`vector:projectOn(v) <vector:projectOn>`
* :func:`vector:mirrorOn(v) <vector:mirrorOn>`
* :func:`vector:cross(other) <vector:cross>`
* :func:`vector:angleTo(other) <vector:angleTo>`
* :func:`vector:trimmed(max_length) <vector:trimmed>`
* :func:`vector:trimInplace(max_length) <vector:trimInplace>`
Vector arithmetic
-----------------
**hump** provides vector arithmetic by implement the corresponding metamethods
(``__add``, ``__mul``, etc.). Here are the semantics:
``vector + vector = vector``
Component wise sum: \\((a,b) + (x,y) = (a+x, b+y)\\)
``vector - vector = vector``
Component wise difference: \\((a,b) - (x,y) = (a-x, b-y)\\)
``vector * vector = number``
Dot product: \\((a,b) \\cdot (x,y) = a\\cdot x + b\\cdot y\\)
``number * vector = vector``
Scalar multiplication/scaling: \\((a,b) \\cdot s = (s\\cdot a, s\\cdot b)\\)
``vector * number = vector``
Scalar multiplication/scaling: \\(s \\cdot (x,y) = (s\\cdot x, s\\cdot y)\\)
``vector / number = vector``
Scalar division: \\((a,b) / s = (a/s, b/s)\\).
``vector // number = vector``
Scalar integer division (only Lua 5.3 and up): \\((a,b) // s = (a//s, b//s)\\).
Common relations are also defined:
``a == b``
Same as ``a.x == b.x and a.y == b.y``.
``a <= b``
Same as ``a.x <= b.x and a.y <= b.y``.
``a < b``
Lexicographical order: ``a.x < b.x or (a.x == b.x and a.y < b.y)``.
**Example**::
-- acceleration, player.velocity and player.position are vectors
acceleration = vector(0,-9)
player.velocity = player.velocity + acceleration * dt
player.position = player.position + player.velocity * dt
Function Reference
------------------
.. function:: vector.new(x,y)
:param numbers x,y: Coordinates.
:returns: The vector.
Create a new vector.
**Examples**::
a = vector.new(10,10)
::
-- as a shortcut, you can call the module like a function:
vector = require "hump.vector"
a = vector(10,10)
.. function:: vector.fromPolar(angle, radius)
:param number angle: Angle of the vector in radians.
:param number radius: Length of the vector (optional, default = 1).
:returns: The vector in cartesian coordinates.
Create a new vector from polar coordinates.
The ``angle`` is measured against the vector (1,0), i.e., the x axis.
**Examples**::
a = vector.polar(math.pi,10)
.. function:: vector.randomDirection(len_min, len_max)
:param number len_min: Minimum length of the vector (optional, default = 1).
:param number len_max: Maximum length of the vector (optional, default = ``len_min``).
:returns: A vector pointing in a random direction with a random length between ``len_min`` and ``len_max``.
**Examples**::
rnd = vector.randomDirection() -- length is 1
rnd = vector.randomDirection(100) -- length is 100
rnd = vector.randomDirection(1,5) -- length is a random value between 1 and 5
Sample a vector with random direction and (optional) length.
.. function:: vector.isvector(v)
:param mixed v: The variable to test.
:returns: ``true`` if ``v`` is a vector, ``false`` otherwise.
Test whether a variable is a vector.
**Example**::
if not vector.isvector(v) then
v = vector(v,0)
end
.. function:: vector:clone()
:returns: Copy of the vector.
Copy a vector. Assigning a vector to a variable will create a *reference*, so
when modifying the vector referenced by the new variable would also change the
old one::
a = vector(1,1) -- create vector
b = a -- b references a
c = a:clone() -- c is a copy of a
b.x = 0 -- changes a,b and c
print(a,b,c) -- prints '(1,0), (1,0), (1,1)'
**Example**::
copy = original:clone()
.. function:: vector:unpack()
:returns: The coordinates ``x,y``.
Extract coordinates.
**Examples**::
x,y = pos:unpack()
::
love.graphics.draw(self.image, self.pos:unpack())
.. function:: vector:permul(other)
:param vector other: The second source vector.
:returns: Vector whose components are products of the source vectors.
Multiplies vectors coordinate wise, i.e. ``result = vector(a.x * b.x, a.y *
b.y)``.
Does not change either argument vectors, but creates a new one.
**Example**::
-- scale with different magnitudes
scaled = original:permul(vector(1,1.5))
.. function:: vector:len()
:returns: Length of the vector.
Get length of the vector, i.e. ``math.sqrt(vec.x * vec.x + vec.y * vec.y)``.
**Example**::
distance = (a - b):len()
.. function:: vector:toPolar()
:returns: The vector in polar coordinates (angle, radius).
Convert the vector to polar coordinates, i.e., the angle and the radius/lenth.
**Example**::
-- complex multiplication
p, q = a:toPolar(), b:toPolar()
c = vector(p.x+q.x, p.y*q.y)
.. function:: vector:len2()
:returns: Squared length of the vector.
Get squared length of the vector, i.e. ``vec.x * vec.x + vec.y * vec.y``.
**Example**::
-- get closest vertex to a given vector
closest, dsq = vertices[1], (pos - vertices[1]):len2()
for i = 2,#vertices do
local temp = (pos - vertices[i]):len2()
if temp < dsq then
closest, dsq = vertices[i], temp
end
end
.. function:: vector:dist(other)
:param vector other: Other vector to measure the distance to.
:returns: The distance of the vectors.
Get distance of two vectors. The same as ``(a - b):len()``.
**Example**::
-- get closest vertex to a given vector
-- slightly slower than the example using len2()
closest, dist = vertices[1], pos:dist(vertices[1])
for i = 2,#vertices do
local temp = pos:dist(vertices[i])
if temp < dist then
closest, dist = vertices[i], temp
end
end
.. function:: vector:dist2(other)
:param vector other: Other vector to measure the distance to.
:returns: The squared distance of the vectors.
Get squared distance of two vectors. The same as ``(a - b):len2()``.
**Example**::
-- get closest vertex to a given vector
-- slightly faster than the example using len2()
closest, dsq = vertices[1], pos:dist2(vertices[1])
for i = 2,#vertices do
local temp = pos:dist2(vertices[i])
if temp < dsq then
closest, dsq = vertices[i], temp
end
end
.. function:: vector:normalized()
:returns: Vector with same direction as the input vector, but length 1.
Get normalized vector: a vector with the same direction as the input vector,
but with length 1.
Does not change the input vector, but creates a new vector.
**Example**::
direction = velocity:normalized()
.. function:: vector:normalizeInplace()
:returns: Itself -- the normalized vector
Normalize a vector, i.e. make the vector to have length 1. Great to use on
intermediate results.
.. warning::
This modifies the vector. If in doubt, use :func:`vector:normalized()`.
**Example**::
normal = (b - a):perpendicular():normalizeInplace()
.. function:: vector:rotated(angle)
:param number angle: Rotation angle in radians.
:returns: The rotated vector
Get a vector with same length, but rotated by ``angle``:
.. image:: _static/vector-rotated.png
:alt: Sketch of rotated vector.
Does not change the input vector, but creates a new vector.
**Example**::
-- approximate a circle
circle = {}
for i = 1,30 do
local phi = 2 * math.pi * i / 30
circle[#circle+1] = vector(0,1):rotated(phi)
end
.. function:: vector:rotateInplace(angle)
:param number angle: Rotation angle in radians.
:returns: Itself -- the rotated vector
Rotate a vector in-place. Great to use on intermediate results.
.. warning::
This modifies the vector. If in doubt, use :func:`vector:rotated()`.
**Example**::
-- ongoing rotation
spawner.direction:rotateInplace(dt)
.. function:: vector:perpendicular()
:returns: A vector perpendicular to the input vector
Quick rotation by 90°. Creates a new vector. The same (but faster) as
``vec:rotate(math.pi/2)``:
.. image:: _static/vector-perpendicular.png
:alt: Sketch of two perpendicular vectors
**Example**::
normal = (b - a):perpendicular():normalizeInplace()
.. function:: vector:projectOn(v)
:param vector v: The vector to project on.
:returns: ``vector`` The projected vector.
Project vector onto another vector:
.. image:: _static/vector-projectOn.png
:alt: Sketch of vector projection.
**Example**::
velocity_component = velocity:projectOn(axis)
.. function:: vector:mirrorOn(v)
:param vector v: The vector to mirror on.
:returns: The mirrored vector.
Mirrors vector on the axis defined by the other vector:
.. image: _static/vector-mirrorOn.png
:alt: Sketch of a vector mirrored on another vector
**Example**::
deflected_velocity = ball.velocity:mirrorOn(surface_normal)
.. function:: vector:cross(other)
:param vector other: Vector to compute the cross product with.
:returns: ``number`` Cross product of both vectors.
Get cross product of two vectors. Equals the area of the parallelogram spanned
by both vectors.
**Example**::
parallelogram_area = a:cross(b)
.. function:: vector:angleTo(other)
:param vector other: Vector to measure the angle to (optional).
:returns: Angle in radians.
Measures the angle between two vectors. If ``other`` is omitted it defaults
to the vector ``(0,0)``, i.e. the function returns the angle to the coordinate
system.
**Example**::
lean = self.upvector:angleTo(vector(0,1))
if lean > .1 then self:fallOver() end
.. function:: vector:trimmed(max_length)
:param number max_length: Maximum allowed length of the vector.
:returns: A trimmed vector.
Trim the vector to ``max_length``, i.e. return a vector that points in the same
direction as the source vector, but has a magnitude smaller or equal to
``max_length``.
Does not change the input vector, but creates a new vector.
**Example**::
ship.velocity = ship.force * ship.mass * dt
ship.velocity = ship.velocity:trimmed(299792458)
.. function:: vector:trimInplace(max_length)
:param number max_length: Maximum allowed length of the vector.
:returns: Itself -- the trimmed vector.
Trim the vector to ``max_length``, i.e. return a vector that points in the same
direction as the source vector, but has a magnitude smaller or equal to
``max_length``.
.. warning::
This modifies the vector. If in doubt, use :func:`vector:trimmed()`.
**Example**::
ship.velocity = (ship.velocity + ship.force * ship.mass * dt):trimInplace(299792458)

113
libs/hump/gamestate.lua Normal file
View file

@ -0,0 +1,113 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 function __NULL__() end
-- default gamestate produces error on every callback
local state_init = setmetatable({leave = __NULL__},
{__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end})
local stack = {state_init}
local initialized_states = setmetatable({}, {__mode = "k"})
local state_is_dirty = true
local GS = {}
function GS.new(t) return t or {} end -- constructor - deprecated!
local function change_state(stack_offset, to, ...)
local pre = stack[#stack]
-- initialize only on first call
;(initialized_states[to] or to.init or __NULL__)(to)
initialized_states[to] = __NULL__
stack[#stack+stack_offset] = to
state_is_dirty = true
return (to.enter or __NULL__)(to, pre, ...)
end
function GS.switch(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call switch with colon operator")
;(stack[#stack].leave or __NULL__)(stack[#stack])
return change_state(0, to, ...)
end
function GS.push(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call push with colon operator")
return change_state(1, to, ...)
end
function GS.pop(...)
assert(#stack > 1, "No more states to pop!")
local pre, to = stack[#stack], stack[#stack-1]
stack[#stack] = nil
;(pre.leave or __NULL__)(pre)
state_is_dirty = true
return (to.resume or __NULL__)(to, pre, ...)
end
function GS.current()
return stack[#stack]
end
-- XXX: don't overwrite love.errorhandler by default:
-- this callback is different than the other callbacks
-- (see http://love2d.org/wiki/love.errorhandler)
-- overwriting thi callback can result in random crashes (issue #95)
local all_callbacks = { 'draw', 'update' }
-- fetch event callbacks from love.handlers
for k in pairs(love.handlers) do
all_callbacks[#all_callbacks+1] = k
end
function GS.registerEvents(callbacks)
local registry = {}
callbacks = callbacks or all_callbacks
for _, f in ipairs(callbacks) do
registry[f] = love[f] or __NULL__
love[f] = function(...)
registry[f](...)
return GS[f](...)
end
end
end
-- forward any undefined functions
setmetatable(GS, {__index = function(_, func)
-- call function only if at least one 'update' was called beforehand
-- (see issue #46)
if not state_is_dirty or func == 'update' then
state_is_dirty = false
return function(...)
return (stack[#stack][func] or __NULL__)(stack[#stack], ...)
end
end
return __NULL__
end})
return GS

View file

@ -0,0 +1,33 @@
package = "hump"
version = "0.4-2"
source = {
url = "git://github.com/vrld/hump"
}
description = {
summary = "Lightweight game development utilities",
detailed = [[Collection of independent components that implement common task needed in games:
- Gamestates that can stack on each other (e.g., for menus)
- Timers and Tweens with thread-like scripting support
- Cameras with camera movement control (locking, smooth follow, etc)
- 2D vector math
- Signals and Slots
- Prototype-based OOP helper
]],
homepage = "https://hump.readthedocs.io",
license = "MIT",
}
dependencies = {
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
["hump.camera"] = "camera.lua",
["hump.class"] = "class.lua",
["hump.gamestate"] = "gamestate.lua",
["hump.signal"] = "signal.lua",
["hump.timer"] = "timer.lua",
["hump.vector"] = "vector.lua",
["hump.vector-light"] = "vector-light.lua"
},
}

102
libs/hump/signal.lua Normal file
View file

@ -0,0 +1,102 @@
--[[
Copyright (c) 2012-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 Registry = {}
Registry.__index = function(self, key)
return Registry[key] or (function()
local t = {}
rawset(self, key, t)
return t
end)()
end
function Registry:register(s, f)
self[s][f] = f
return f
end
function Registry:emit(s, ...)
for f in pairs(self[s]) do
f(...)
end
end
function Registry:remove(s, ...)
local f = {...}
for i = 1,select('#', ...) do
self[s][f[i]] = nil
end
end
function Registry:clear(...)
local s = {...}
for i = 1,select('#', ...) do
self[s[i]] = {}
end
end
function Registry:emitPattern(p, ...)
for s in pairs(self) do
if s:match(p) then self:emit(s, ...) end
end
end
function Registry:registerPattern(p, f)
for s in pairs(self) do
if s:match(p) then self:register(s, f) end
end
return f
end
function Registry:removePattern(p, ...)
for s in pairs(self) do
if s:match(p) then self:remove(s, ...) end
end
end
function Registry:clearPattern(p)
for s in pairs(self) do
if s:match(p) then self[s] = {} end
end
end
-- instancing
function Registry.new()
return setmetatable({}, Registry)
end
-- default instance
local default = Registry.new()
-- module forwards calls to default instance
local module = {}
for k in pairs(Registry) do
if k ~= "__index" then
module[k] = function(...) return default[k](default, ...) end
end
end
return setmetatable(module, {__call = Registry.new})

View file

@ -0,0 +1,73 @@
local timer = require 'timer'()
describe('hump.timer', function()
it('runs a function during a specified time', function()
local delta, remaining
timer:during(10, function(...) delta, remaining = ... end)
timer:update(2)
assert.are.equal(delta, 2)
assert.are.equal(8, remaining)
timer:update(5)
assert.are.equal(delta, 5)
assert.are.equal(3, remaining)
timer:update(10)
assert.are.equal(delta, 10)
assert.are.equal(0, remaining)
end)
it('runs a function after a specified time', function()
local finished1 = false
local finished2 = false
timer:after(3, function(...) finished1 = true end)
timer:after(5, function(...) finished2 = true end)
timer:update(4)
assert.are.equal(true, finished1)
assert.are.equal(false, finished2)
timer:update(4)
assert.are.equal(true, finished1)
assert.are.equal(true, finished2)
end)
it('runs a function every so often', function()
local count = 0
timer:every(1, function(...) count = count + 1 end)
timer:update(3)
assert.are.equal(3, count)
timer:update(7)
assert.are.equal(10, count)
end)
it('can script timed events', function()
local state
timer:script(function(wait)
state = 'foo'
wait(1)
state = 'bar'
end)
assert.are.equal('foo', state)
timer:update(0.5)
assert.are.equal('foo', state)
timer:update(1)
assert.are.equal('bar', state)
end)
it('cancels and clears timer functions', function()
pending('to be tested...')
end)
it('tweens', function()
pending('to be tested...')
end)
end)

215
libs/hump/timer.lua Normal file
View file

@ -0,0 +1,215 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 Timer = {}
Timer.__index = Timer
local function _nothing_() end
local function updateTimerHandle(handle, dt)
-- handle: {
-- time = <number>,
-- after = <function>,
-- during = <function>,
-- limit = <number>,
-- count = <number>,
-- }
handle.time = handle.time + dt
handle.during(dt, math.max(handle.limit - handle.time, 0))
while handle.time >= handle.limit and handle.count > 0 do
if handle.after(handle.after) == false then
handle.count = 0
break
end
handle.time = handle.time - handle.limit
handle.count = handle.count - 1
end
end
function Timer:update(dt)
-- timers may create new timers, which leads to undefined behavior
-- in pairs() - so we need to put them in a different table first
local to_update = {}
for handle in pairs(self.functions) do
to_update[handle] = handle
end
for handle in pairs(to_update) do
if self.functions[handle] then
updateTimerHandle(handle, dt)
if handle.count == 0 then
self.functions[handle] = nil
end
end
end
end
function Timer:during(delay, during, after)
local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
self.functions[handle] = true
return handle
end
function Timer:after(delay, func)
return self:during(delay, _nothing_, func)
end
function Timer:every(delay, after, count)
local count = count or math.huge -- exploit below: math.huge - 1 = math.huge
local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
self.functions[handle] = true
return handle
end
function Timer:cancel(handle)
self.functions[handle] = nil
end
function Timer:clear()
self.functions = {}
end
function Timer:script(f)
local co = coroutine.wrap(f)
co(function(t)
self:after(t, co)
coroutine.yield()
end)
end
Timer.tween = setmetatable({
-- helper functions
out = function(f) -- 'rotates' a function
return function(s, ...) return 1 - f(1-s, ...) end
end,
chain = function(f1, f2) -- concatenates two functions
return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end
end,
-- useful tweening functions
linear = function(s) return s end,
quad = function(s) return s*s end,
cubic = function(s) return s*s*s end,
quart = function(s) return s*s*s*s end,
quint = function(s) return s*s*s*s*s end,
sine = function(s) return 1-math.cos(s*math.pi/2) end,
expo = function(s) return 2^(10*(s-1)) end,
circ = function(s) return 1 - math.sqrt(1-s*s) end,
back = function(s,bounciness)
bounciness = bounciness or 1.70158
return s*s*((bounciness+1)*s - bounciness)
end,
bounce = function(s) -- magic numbers ahead
local a,b = 7.5625, 1/2.75
return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375)
end,
elastic = function(s, amp, period)
amp, period = amp and math.max(1, amp) or 1, period or .3
return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1))
end,
}, {
-- register new tween
__call = function(tween, self, len, subject, target, method, after, ...)
-- recursively collects fields that are defined in both subject and target into a flat list
local function tween_collect_payload(subject, target, out)
for k,v in pairs(target) do
local ref = subject[k]
assert(type(v) == type(ref), 'Type mismatch in field "'..k..'".')
if type(v) == 'table' then
tween_collect_payload(ref, v, out)
else
local ok, delta = pcall(function() return (v-ref)*1 end)
assert(ok, 'Field "'..k..'" does not support arithmetic operations')
out[#out+1] = {subject, k, delta}
end
end
return out
end
method = tween[method or 'linear'] -- see __index
local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...}
local last_s = 0
return self:during(len, function(dt)
t = t + dt
local s = method(math.min(1, t/len), unpack(args))
local ds = s - last_s
last_s = s
for _, info in ipairs(payload) do
local ref, key, delta = unpack(info)
ref[key] = ref[key] + delta * ds
end
end, after)
end,
-- fetches function and generated compositions for method `key`
__index = function(tweens, key)
if type(key) == 'function' then return key end
assert(type(key) == 'string', 'Method must be function or string.')
if rawget(tweens, key) then return rawget(tweens, key) end
local function construct(pattern, f)
local method = rawget(tweens, key:match(pattern))
if method then return f(method) end
return nil
end
local out, chain = rawget(tweens,'out'), rawget(tweens,'chain')
return construct('^in%-([^-]+)$', function(...) return ... end)
or construct('^out%-([^-]+)$', out)
or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
or error('Unknown interpolation method: ' .. key)
end})
-- Timer instancing
function Timer.new()
return setmetatable({functions = {}, tween = Timer.tween}, Timer)
end
-- default instance
local default = Timer.new()
-- module forwards calls to default instance
local module = {}
for k in pairs(Timer) do
if k ~= "__index" then
module[k] = function(...) return default[k](default, ...) end
end
end
module.tween = setmetatable({}, {
__index = Timer.tween,
__newindex = function(k,v) Timer.tween[k] = v end,
__call = function(t, ...) return default:tween(...) end,
})
return setmetatable(module, {__call = Timer.new})

186
libs/hump/vector-light.lua Normal file
View file

@ -0,0 +1,186 @@
--[[
Copyright (c) 2012-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2
local function str(x,y)
return "("..tonumber(x)..","..tonumber(y)..")"
end
local function mul(s, x,y)
return s*x, s*y
end
local function div(s, x,y)
return x/s, y/s
end
local function add(x1,y1, x2,y2)
return x1+x2, y1+y2
end
local function sub(x1,y1, x2,y2)
return x1-x2, y1-y2
end
local function permul(x1,y1, x2,y2)
return x1*x2, y1*y2
end
local function dot(x1,y1, x2,y2)
return x1*x2 + y1*y2
end
local function det(x1,y1, x2,y2)
return x1*y2 - y1*x2
end
local function eq(x1,y1, x2,y2)
return x1 == x2 and y1 == y2
end
local function lt(x1,y1, x2,y2)
return x1 < x2 or (x1 == x2 and y1 < y2)
end
local function le(x1,y1, x2,y2)
return x1 <= x2 and y1 <= y2
end
local function len2(x,y)
return x*x + y*y
end
local function len(x,y)
return sqrt(x*x + y*y)
end
local function fromPolar(angle, radius)
radius = radius or 1
return cos(angle)*radius, sin(angle)*radius
end
local function randomDirection(len_min, len_max)
len_min = len_min or 1
len_max = len_max or len_min
assert(len_max > 0, "len_max must be greater than zero")
assert(len_max >= len_min, "len_max must be greater than or equal to len_min")
return fromPolar(math.random()*2*math.pi,
math.random() * (len_max-len_min) + len_min)
end
local function toPolar(x, y)
return atan2(y,x), len(x,y)
end
local function dist2(x1,y1, x2,y2)
return len2(x1-x2, y1-y2)
end
local function dist(x1,y1, x2,y2)
return len(x1-x2, y1-y2)
end
local function normalize(x,y)
local l = len(x,y)
if l > 0 then
return x/l, y/l
end
return x,y
end
local function rotate(phi, x,y)
local c, s = cos(phi), sin(phi)
return c*x - s*y, s*x + c*y
end
local function perpendicular(x,y)
return -y, x
end
local function project(x,y, u,v)
local s = (x*u + y*v) / (u*u + v*v)
return s*u, s*v
end
local function mirror(x,y, u,v)
local s = 2 * (x*u + y*v) / (u*u + v*v)
return s*u - x, s*v - y
end
-- ref.: http://blog.signalsondisplay.com/?p=336
local function trim(maxLen, x, y)
local s = maxLen * maxLen / len2(x, y)
s = s > 1 and 1 or math.sqrt(s)
return x * s, y * s
end
local function angleTo(x,y, u,v)
if u and v then
return atan2(y, x) - atan2(v, u)
end
return atan2(y, x)
end
-- the module
return {
str = str,
fromPolar = fromPolar,
toPolar = toPolar,
randomDirection = randomDirection,
-- arithmetic
mul = mul,
div = div,
idiv = idiv,
add = add,
sub = sub,
permul = permul,
dot = dot,
det = det,
cross = det,
-- relation
eq = eq,
lt = lt,
le = le,
-- misc operations
len2 = len2,
len = len,
dist2 = dist2,
dist = dist,
normalize = normalize,
rotate = rotate,
perpendicular = perpendicular,
project = project,
mirror = mirror,
trim = trim,
angleTo = angleTo,
}

218
libs/hump/vector.lua Normal file
View file

@ -0,0 +1,218 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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 assert = assert
local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2
local vector = {}
vector.__index = vector
local function new(x,y)
return setmetatable({x = x or 0, y = y or 0}, vector)
end
local zero = new(0,0)
local function fromPolar(angle, radius)
radius = radius or 1
return new(cos(angle) * radius, sin(angle) * radius)
end
local function randomDirection(len_min, len_max)
len_min = len_min or 1
len_max = len_max or len_min
assert(len_max > 0, "len_max must be greater than zero")
assert(len_max >= len_min, "len_max must be greater than or equal to len_min")
return fromPolar(math.random() * 2*math.pi,
math.random() * (len_max-len_min) + len_min)
end
local function isvector(v)
return type(v) == 'table' and type(v.x) == 'number' and type(v.y) == 'number'
end
function vector:clone()
return new(self.x, self.y)
end
function vector:unpack()
return self.x, self.y
end
function vector:__tostring()
return "("..tonumber(self.x)..","..tonumber(self.y)..")"
end
function vector.__unm(a)
return new(-a.x, -a.y)
end
function vector.__add(a,b)
assert(isvector(a) and isvector(b), "Add: wrong argument types (<vector> expected)")
return new(a.x+b.x, a.y+b.y)
end
function vector.__sub(a,b)
assert(isvector(a) and isvector(b), "Sub: wrong argument types (<vector> expected)")
return new(a.x-b.x, a.y-b.y)
end
function vector.__mul(a,b)
if type(a) == "number" then
return new(a*b.x, a*b.y)
elseif type(b) == "number" then
return new(b*a.x, b*a.y)
else
assert(isvector(a) and isvector(b), "Mul: wrong argument types (<vector> or <number> expected)")
return a.x*b.x + a.y*b.y
end
end
function vector.__div(a,b)
assert(isvector(a) and type(b) == "number", "wrong argument types (expected <vector> / <number>)")
return new(a.x / b, a.y / b)
end
function vector.__eq(a,b)
return a.x == b.x and a.y == b.y
end
function vector.__lt(a,b)
return a.x < b.x or (a.x == b.x and a.y < b.y)
end
function vector.__le(a,b)
return a.x <= b.x and a.y <= b.y
end
function vector.permul(a,b)
assert(isvector(a) and isvector(b), "permul: wrong argument types (<vector> expected)")
return new(a.x*b.x, a.y*b.y)
end
function vector:toPolar()
return new(atan2(self.x, self.y), self:len())
end
function vector:len2()
return self.x * self.x + self.y * self.y
end
function vector:len()
return sqrt(self.x * self.x + self.y * self.y)
end
function vector.dist(a, b)
assert(isvector(a) and isvector(b), "dist: wrong argument types (<vector> expected)")
local dx = a.x - b.x
local dy = a.y - b.y
return sqrt(dx * dx + dy * dy)
end
function vector.dist2(a, b)
assert(isvector(a) and isvector(b), "dist: wrong argument types (<vector> expected)")
local dx = a.x - b.x
local dy = a.y - b.y
return (dx * dx + dy * dy)
end
function vector:normalizeInplace()
local l = self:len()
if l > 0 then
self.x, self.y = self.x / l, self.y / l
end
return self
end
function vector:normalized()
return self:clone():normalizeInplace()
end
function vector:rotateInplace(phi)
local c, s = cos(phi), sin(phi)
self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y
return self
end
function vector:rotated(phi)
local c, s = cos(phi), sin(phi)
return new(c * self.x - s * self.y, s * self.x + c * self.y)
end
function vector:perpendicular()
return new(-self.y, self.x)
end
function vector:projectOn(v)
assert(isvector(v), "invalid argument: cannot project vector on " .. type(v))
-- (self * v) * v / v:len2()
local s = (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y)
return new(s * v.x, s * v.y)
end
function vector:mirrorOn(v)
assert(isvector(v), "invalid argument: cannot mirror vector on " .. type(v))
-- 2 * self:projectOn(v) - self
local s = 2 * (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y)
return new(s * v.x - self.x, s * v.y - self.y)
end
function vector:cross(v)
assert(isvector(v), "cross: wrong argument types (<vector> expected)")
return self.x * v.y - self.y * v.x
end
-- ref.: http://blog.signalsondisplay.com/?p=336
function vector:trimInplace(maxLen)
local s = maxLen * maxLen / self:len2()
s = (s > 1 and 1) or math.sqrt(s)
self.x, self.y = self.x * s, self.y * s
return self
end
function vector:angleTo(other)
if other then
return atan2(self.y, self.x) - atan2(other.y, other.x)
end
return atan2(self.y, self.x)
end
function vector:trimmed(maxLen)
return self:clone():trimInplace(maxLen)
end
-- the module
return setmetatable({
new = new,
fromPolar = fromPolar,
randomDirection = randomDirection,
isvector = isvector,
zero = zero
}, {
__call = function(_, ...) return new(...) end
})

@ -1 +0,0 @@
Subproject commit a0da807dca77baf07d287631f5ad41a9097fc25c

52
libs/json4lua/README.md Normal file
View file

@ -0,0 +1,52 @@
# json4lua
JSON and JSONRPC for Lua
# Installation #
```
luarocks install --server=http://rocks.moonscript.org/manifests/amrhassan --local json4Lua
```
# JSON Usage #
## Encoding ##
```lua
json = require('json')
print(json.encode({ 1, 2, 'fred', {first='mars',second='venus',third='earth'} }))
```
```json
[1,2,"fred", {"first":"mars","second":"venus","third":"earth"}]
```
## Decoding ##
```lua
json = require("json")
testString = [[ { "one":1 , "two":2, "primes":[2,3,5,7] } ]]
decoded = json.decode(testString)
table.foreach(decoded, print)
print ("Primes are:")
table.foreach(decoded.primes,print)
```
```
one 1
two 2
primes table: 0032B928
Primes are:
1 2
2 3
3 5
4 7
```
# JSONRPC Usage #
```lua
json = require('json')
require("json.rpc")
server = json.rpc.proxy("http://jsolait.net/testj.py")
result, error = server.echo('Test echo!')
print(result)
```
```
Test echo!
```

4
libs/json4lua/doc/INSTALL.txt Executable file
View file

@ -0,0 +1,4 @@
INSTALLATION
============
See INSTALLATION section in doc/index.html

21
libs/json4lua/doc/LICENCE.txt Executable file
View file

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009 Craig Mason-Jones
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.

5
libs/json4lua/doc/README.txt Executable file
View file

@ -0,0 +1,5 @@
===================================================================================================================
== README.txt
===================================================================================================================
Please see doc/index.html

4
libs/json4lua/doc/VERSION.txt Executable file
View file

@ -0,0 +1,4 @@
JSON4Lua and JSONRPC4Lua
Version 1.0.0
4 March 2015
http://github.com/craigmj/json4lua/

View file

@ -0,0 +1,187 @@
<html>
<head>
<title>JSON4Lua and JSONRPC4Lua</title>
<style type="text/css">
body {
font-family: Arial, Helvetica, Geneva;
font-size: 10pt;
}
.copyright {
color: #999999;
font-size: 8pt;
text-align:center;
}
.navBar {
border-left: 1px solid lightgray;
border-right: 1px solid lightgray;
border-bottom: 1px solid lightgray;
margin-top:5px;
margin-bottom: 5px;
}
.navBar h1 {
background-image: url('pics/lunartone.gif');
background-color: #e0c088;
color: white;
font-size: 9pt;
margin: 0px;
padding: 4px;
}
.navBar .navItem a {
color: #999999;
font-size: 8pt;
padding: 2px 2px 2px 10px;
text-decoration:none;
}
.navBar .navItem a:hover {
color: #ff9900;
text-decoration: underline;
}
.titleBar {
border-bottom: 2px dotted gray;
font-size: 20pt;
font-weight: bold;
margin-bottom: 15px;
width: 100%;
}
.workArea {
border: 1px none lightgray;
font-family: Times New Roman;
padding: 8px;
padding-left: 15px;
}
.workArea a {
font-weight: bold;
text-decoration: none;
}
.workArea a:hover {
color: #ff9900;
text-decoration: underline;
}
.workArea .downloadTable {
border: 1px solid lightgray;
padding: 0px;
margin: 0px;
}
.workArea .downloadTable * th {
background-color: #e0c088;
background-image: url('pics/lunartone.gif');
color: white;
}
.workArea .downloadTable * tr {
border: 1px solid lightgray;
}
.workArea .downloadTable * td {
vertical-align: top;
}
.workArea * dt {
font-size: 12pt;
margin-top: 10px;
}
.workArea * .example {
background-color: #ffffcc;
border: 1px dashed lightgray;
padding: 10px;
}
.workArea h1 {
font-size: 16pt;
font-weight: bold;
}
.workArea h2 {
font-size: 14pt;
font-weight: normal;
color: gray;
}
</style>
<body>
<table border="0">
<tr>
<td valign="top">
<img src="pics/json4lua.gif"><p/>
<!-- NAVIGATION BARS -->
<div class="navBar">
<h1>Home Page</h1>
<div class="navItem"><a href="index.html#news">Latest News</a></div>
<div class="navItem"><a href="index.html#introduction">Introduction</a></div>
<div class="navItem"><a href="index.html#licence">Licence</a></div>
<div class="navItem"><a href="index.html#requirements">Requirements</a></div>
<div class="navItem"><a href="index.html#download">Download</a></div>
<div class="navItem"><a href="index.html#installation">Installation</a></div>
<div class="navItem"><a href="index.html#usage">Usage &amp; Reference</a></div>
<div class="navItem"><a href="index.html#roadmap">History &amp; Roadmap</a></div>
</div> <!-- end of navigation bar: This Site -->
<div class="navBar">
<h1>json.luaforge</h1>
<div class="navItem"><a href="http://luaforge.net/projects/json/">Summary</a></div>
<div class="navItem"><a href="http://luaforge.net/forum/?group_id=143">Forums</a></div>
<div class="navItem"><a href="http://luaforge.net/tracker/?group_id=143">Tracker</a></div>
<div class="navItem"><a href="http://luaforge.net/news/?group_id=143">News</a></div>
<!--<div class="navItem"><a href="http://luaforge.net/scm/?group_id=143">CVS</a></div>-->
<div class="navItem"><a href="http://luaforge.net/frs/?group_id=143">Files</a></div>
</div> <!-- end of navigation bar: json.luaforge -->
<div class="navBar">
<h1>Lua</h1>
<div class="navItem"><a href="http://www.lua.org/">Lua</a></div>
<div class="navItem"><a href="http://www.luaforge.net/">Lua Forge</a></div>
<div class="navItem"><a href="http://www.keplerproject.org/cgilua/">CGI Lua</a></div>
<div class="navItem"><a href="http://www.keplerproject.org/compat/">Lua compat-5.1</a></div>
<div class="navItem"><a href="http://www.cs.princeton.edu/~diego/professional/luasocket/">Lua Socket 2.0</a></div>
<div class="navItem"><a href="http://www.keplerproject.org/xavante/">Xavante</a></div>
</div> <!-- end of navigation bar: Lua Resources -->
<div class="navBar">
<h1>JSON</h1>
<div class="navItem"><a href="http://www.json.org/">json.org</a></div>
<div class="navItem"><a href="http://www.json-rpc.org/">json-rpc.org</a></div>
</div> <!-- end of navigation bar: Lua Resources -->
</td>
<!-- MAIN WORK AREA -->
<td valign="top" class="workArea">
<div class="titleBar">Patching CGILua to handle <code>text/plain</code></div>
<!-- cgilua_patch.html -->
JSON RPC (both the JSONRPC4Lua implementation and the <a href="http://www.jsolait.net/">jsolait Javascript</a> implementation) send the http request with a Content-Type of <code>text/plain</code>.<p/>
CGILua 5.0 does not accept <code>text/plain</code> content, and will generate an error of 'Unsupported Media Type: text/plain'.<p/>
This is easily patched in CGILua 5.0 by making the following change to <code>cgilua/post.lua</code>, line 286:<p/>
Change:<pre>
elseif strfind (contenttype, "text/xml") then
</pre>
to
<pre>
elseif strfind (contenttype, "text/xml") or strfind (contenttype, "text/plain") then
</pre>
This makes CGILua handle <code>text/plain</code> as it does <code>text/xml</code>, without parsing the incoming POST data.<p/>
<b>Please note:</b> I have requested the maintainers of CGILua to make this change to CGILua, whereafter this patch will no longer be required.
</td>
</tr>
<tr>
<td colspan="2" class="copyright">&copy; 2005 Craig Mason-Jones</td>
</tr>
</table>
</body>
</html>

394
libs/json4lua/doc/index.html Executable file
View file

@ -0,0 +1,394 @@
<html>
<head>
<title>JSON4Lua and JSONRPC4Lua</title>
<style type="text/css">
body {
font-family: Arial, Helvetica, Geneva;
font-size: 10pt;
}
.copyright {
color: #999999;
font-size: 8pt;
text-align:center;
}
.navBar {
border-left: 1px solid lightgray;
border-right: 1px solid lightgray;
border-bottom: 1px solid lightgray;
margin-top:5px;
margin-bottom: 5px;
}
.navBar h1 {
background-image: url('pics/lunartone.gif');
background-color: #e0c088;
color: white;
font-size: 9pt;
margin: 0px;
padding: 4px;
}
.navBar .navItem a {
color: #999999;
font-size: 8pt;
padding: 2px 2px 2px 10px;
text-decoration:none;
}
.navBar .navItem a:hover {
color: #ff9900;
text-decoration: underline;
}
.titleBar {
border-bottom: 2px dotted gray;
font-size: 20pt;
font-weight: bold;
margin-bottom: 15px;
width: 100%;
}
.workArea {
border: 1px none lightgray;
font-family: Times New Roman;
padding: 8px;
padding-left: 15px;
}
.workArea a {
font-weight: bold;
text-decoration: none;
}
.workArea a:hover {
color: #ff9900;
text-decoration: underline;
}
.workArea .downloadTable {
border: 1px solid lightgray;
padding: 0px;
margin: 0px;
}
.workArea .downloadTable * th {
background-color: #e0c088;
background-image: url('pics/lunartone.gif');
color: white;
}
.workArea .downloadTable * tr {
border: 1px solid lightgray;
}
.workArea .downloadTable * td {
vertical-align: top;
}
.workArea * dt {
font-size: 12pt;
margin-top: 10px;
}
.workArea * .example {
background-color: #ffffcc;
border: 1px dashed lightgray;
padding: 10px;
}
.workArea h1 {
font-size: 16pt;
font-weight: bold;
}
.workArea h2 {
font-size: 14pt;
font-weight: normal;
color: gray;
}
</style>
<body>
<table border="0">
<tr>
<td valign="top">
<img src="pics/json4lua.gif"><p/>
<!-- NAVIGATION BARS -->
<div class="navBar">
<h1>This Page</h1>
<div class="navItem"><a href="#news">Latest News</a></div>
<div class="navItem"><a href="#introduction">Introduction</a></div>
<div class="navItem"><a href="#licence">Licence</a></div>
<div class="navItem"><a href="#requirements">Requirements</a></div>
<div class="navItem"><a href="#download">Download</a></div>
<div class="navItem"><a href="#installation">Installation</a></div>
<div class="navItem"><a href="#usage">Usage &amp; Reference</a>
<div class="navItem" style="margin-left: 4px"><a href="#json_decode">decode</a></div>
<div class="navItem" style="margin-left: 4px"><a href="#json_encode">encode</a></div>
<div class="navItem" style="margin-left: 4px"><a href="#json_null">null</a></div>
<div class="navItem" style="margin-left: 4px"><a href="#json_rpc_call">rpc.call</a></div>
<div class="navItem" style="margin-left: 4px"><a href="#json_rpc_proxy">rpc.proxy</a></div>
<div class="navItem" style="margin-left: 4px"><a href="#json_rpcserver_serve">rpcserver.serve</a></div>
</div>
<div class="navItem"><a href="#roadmap">History &amp; Roadmap</a></div>
</div> <!-- end of navigation bar: This Site -->
<div class="navBar">
<h1>json.luaforge</h1>
<div class="navItem"><a href="http://luaforge.net/projects/json/">Summary</a></div>
<div class="navItem"><a href="http://luaforge.net/forum/?group_id=143">Forums</a></div>
<div class="navItem"><a href="http://luaforge.net/tracker/?group_id=143">Tracker</a></div>
<div class="navItem"><a href="http://luaforge.net/news/?group_id=143">News</a></div>
<!--<div class="navItem"><a href="http://luaforge.net/scm/?group_id=143">CVS</a></div>-->
<div class="navItem"><a href="http://luaforge.net/frs/?group_id=143">Files</a></div>
</div> <!-- end of navigation bar: json.luaforge -->
<div class="navBar">
<h1>Lua</h1>
<div class="navItem"><a href="http://www.lua.org/">Lua</a></div>
<div class="navItem"><a href="http://www.luaforge.net/">Lua Forge</a></div>
<div class="navItem"><a href="http://www.keplerproject.org/cgilua/">CGI Lua</a></div>
<div class="navItem"><a href="http://www.keplerproject.org/compat/">Lua compat-5.1</a></div>
<div class="navItem"><a href="http://www.cs.princeton.edu/~diego/professional/luasocket/">Lua Socket 2.0</a></div>
<div class="navItem"><a href="http://www.keplerproject.org/xavante/">Xavante</a></div>
</div> <!-- end of navigation bar: Lua Resources -->
<div class="navBar">
<h1>JSON</h1>
<div class="navItem"><a href="http://www.json.org/">json.org</a></div>
<div class="navItem"><a href="http://www.json-rpc.org/">json-rpc.org</a></div>
</div> <!-- end of navigation bar: Lua Resources -->
</td>
<!-- MAIN WORK AREA -->
<td valign="top" class="workArea">
<div class="titleBar">JSON4Lua and JSONRPC4Lua</div>
<!-- ***************** LATEST NEWS ***************** -->
<h1><a name="news"></a>Latest News</h1>
<p><small>(2009-08-06)</small> We've changed the JSON4Lua and JSONRPC4Lua licence from the GPL to the MIT licence, like Lua itself.</p>
<p>
The 0.9.20 release fixes a bug in Lua 5.1 operation, introduces a <a href="#json_null"><code>json.null</code></a> value to force null values in JSON encodings, improves performance (over 50% faster on some tests), and permits <code>/* comments */</code> in the JSON string being decoded.
</p>
<!-- ***************** INTRODUCTION ***************** -->
<h1><a name="introduction"></a>Introduction</h1>
JSON4Lua and JSONRPC4Lua implement <a href="http://www.json.org">JSON (JavaScript Object Notation)</a> encoding and decoding and a <a href="http://www.json-rpc.org/">JSON-RPC-over-http</a> client for <a href="http://www.lua.org/">Lua</a>.<p/>
JSON is JavaScript Object Notation, a simple encoding of Javascript-like objects that is ideal for lightweight transmission of relatively weakly-typed data.
A sub-package of JSON4Lua is JSONRPC4Lua, which provides a simple JSON-RPC-over-http client and server (in a CGILua environment) for Lua. Please seen the documentation below for JSONRPC4Lua.
<!-- ***************** LICENCE ***************** -->
<h1><a name="licence"></a>Licence</h1>
JSON4Lua is licensed under the MIT Consortium licence like Lua itself. Please see <a href="LICENCE.txt">LICENCE.txt</a> for details.<p/>
<!-- ***************** REQUIREMENTS ***************** -->
<a name="requirements"></a><h1>Requirements</h1>
JSON4Lua is a pure-Lua module that is Lua 5.0 compatible (if you have <a href="http://www.keplerproject.org/compat/">compat-5.1</a> for Lua 5.0). JSON4Lua also works (perfectly, I hope) under Lua 5.1, which is where I largely use it nowadays. Since Lua is platform independent, so is JSON4Lua.<p/>
The JSON4RPC sub-module requires <a href="http://www.cs.princeton.edu/~diego/professional/luasocket/">Lua Socket 2.0</a>. It uses <code>socket.http</code> for for the RPC over http connection. Socket 2.0 includes <code>ltn12</code>, which is also used by JSON4RPC.<p />
To use <code>json.rpcserver</code> you need a <a href="http://www.keplerproject.org/cgilua/">CGILua</a> enabled webserver. However, a quick <a href="cgilua_patch.html">patch</a> is required in CGILua 5.0 to support JSON-RPC.
<!-- ***************** START OF DOWNLOAD SECTION ***************** -->
<h1><a name="download"></a>Download</h1>
JSON4Lua is hosted on <a href="http://www.luaforge.net">LuaForge</a>.
<table cols="2" class="downloadTable">
<tr><th>Version</th><th>Date</th><th>Notes</th></tr>
<tr><td><a href="http://luaforge.net/frs/?group_id=143">0.9.30</a></td><td>6 August 2009</td><td>
Changed to MIT Licence.
</td></tr>
<tr><td><a href="http://luaforge.net/frs/?group_id=143">0.9.20</a></td><td>4 January 2006</td><td>
Introduction of local Lua functions for private functions (removed _ function prefix). <br />
Fixed Lua 5.1 compatibility issues.<br />
Introduced <code>json.null</code> to have null values in associative arrays.<br />
Performance improvement (more than 50% on some tests) through <code>table.concat</code> rather than <code>..</code> operator.<br/>
<code>json.decode</code> now ignores <code>/* */</code> comments in the JSON string.<br />
</td></tr>
<tr><td><a href="http://luaforge.net/frs/?group_id=143">0.9.10</a></td><td>20 December 2005</td><td>
Fixes bug with array representation when nil / null values occur in the array.<br />
Adds content-type header of <code>text/plain</code> to JSON RPC http requests.<br />
Introduces <code>json.rpcserver</code> module with simple JSON RPC enablement for Lua objects.<br />
Moved the <code>json.lua</code> file into the <code>json</code> directory. Ensure, therefore, that your <code>LUA_PATH</code> contains a module-finding form like <code>LUA_PATH = c:\proj\lua\?\?.lua;?.lua</code>.<br/>
</td></tr>
<tr>
<td><a href="http://luaforge.net/frs/?group_id=143">0.9.01</a></td>
<td>19 December 2005</td>
<td>Minor corrections to documentation.</td>
</tr>
<tr>
<td><a href="http://luaforge.net/frs/?group_id=143">0.9.00</a></td>
<td>19 December 2005</td>
<td>First release</td>
</tr>
</table>
<!-- END OF DOWNLOAD SECTION -->
<!-- ***************** START OF INSTALLATION SECTION ***************** -->
<h1><a name="installation"></a>Installation</h1>
As of version 0.9.10, all the JSON4Lua files are contained in the <code>json</code> subdirectory in the distribution zip.<p />
Simply copy the <code>json</code> subdirectory so that it is in your Lua path.<p />
Ensure that your <code>LUA_PATH</code> variable permits module resolution of the form <code>?/?.lua</code>.<p />
<b>Example</b>
<div class="example">
</div>
<h2>Using Windows</h2>
Under Windows, set your Lua path as (my Lua installation is in <code>c:\proj\lua\</code>): <p/>
<code>
set LUA_PATH=c:\proj\lua\?.lua;c:\proj\lua\?\?.lua;?.lua
</code><p/>
For compat-5.1.lua to start when Lua starts (if you're using Lua 5.0), you also need:<p/>
<code>
set LUA_INIT=@c:\proj\lua\compat-5.1.lua
</code><p />
You probably also want to set your library path:<p/>
<code>set LUA_CPATH=c:\proj\lua\lib\?.dll;?.dll</code>
<!------------------------ USAGE AND REFERENCE -------------------->
<h1><a name="usage"></a>Usage &amp; Reference</h1>
The following functions in <b>JSON4Lua</b> and <b>JSONRPC4Lua</b> are of interest:<p />
<dl>
<dt><a name="json_encode"></a><code>string <b>json.encode</b>( lua_object )</code></dt>
<dd>Returns the Lua object JSON encoded into a string.<p/>
<b>Example</b>
<div class="example">
<code>
json = require("json")<br/>
print (json.encode( { 1, 2, 'fred', {first='mars',second='venus',third='earth'} } ))
</code><p/>
prints<p/><code> [1,2,"fred", {"first":"mars","second":"venus","third","earth"}]</code>
</div>
</dd>
<dt><a name="json_decode"></a><code>lua_object <b>json.decode</b>( json_string )</code></dt>
<dd>Decodes the JSON encoded data structure, and returns a Lua object with the appropriate data.<p/>
<b>Example</b>
<div class="example">
<code>
json = require("json")<br/>
testString = [[ { "one":1 , "two":2, "primes":[2,3,5,7] } ]]<br/>
o = json.decode(testString)<br/>
table.foreach(o,print)<br />
print ("Primes are:")<br />
table.foreach(o.primes,print)
</code><p/>
prints:<p/>
<pre>
one 1
two 2
primes table: 0032B928
Primes are:
1 2
2 3
3 5
4 7</pre>
</div></dd>
<dt><a name="json_null"></a><code><b>json.null</b></code></dt>
<dd>Returns a unique value that will be encoded as a <code>null</code> in a JSON encoding.
<p>This is necessary in one situation. In Lua, if a key in a table has a <code>nil</code> value, the key is simply discarded (since any non-existent key has a nil value). The encoding of arrays has been built to manage this nil-values in arrays, but associative arrays provide a problem. Consider:
<div class="example"><pre>t = { user="test", password=nil }</pre></div>
Since Lua simply discards the <code>password</code> key, JSON4Lua encodes this as the JSON string
<div class="example"><pre>{"user":"test"}</pre></div>
If, for some reason, your JSON RPC Server requires a defined <code>null</code> value, use the following code:
<div class="example"><pre>t = { user="test", password=json.null }</pre></div>
This will now correctly encode to:
<div class="example"><pre>{"user":"test","password":null}</pre></div>
Incidentally, <code>json.null</code> is simply a function that returns itself, so that you can use either <code>json.null</code> or <code>json.null()</code> as you fancy.
</dd>
<dt><a name="json_rpc_call"></a><code>result, error <b>json.rpc.call</b> ( url, method, ...)</code></dt>
<dd>Calls the named method on the given url with the arg parameters. Returns the result and the error. If <code>error</code> is <code>nil</code>, no error occurred.<p/>
<b>Example</b>
<div class="example">
<pre>require ("json.rpc")
result, error = json.rpc.call("http://jsolait.net/testj.py","echo","Test echo!")
print(result)</pre>
<p />prints<p />
<pre>Test echo!</pre>
</div>
</dd>
<dt><a name="json_rpc_proxy"></a><code>proxyServer = <b>json.rpc.proxy</b> (url)</code></dt>
<dd>Creates a proxy server object on which JSON-RPC calls can be made. Each call will return the <code>result, error</code>. If <code>error</code> is <code>nil</code>, no error occurred.<p/>
<b>Example</b>
<div class="example">
<pre>
require ("json.rpc")
server = json.rpc.proxy("http://jsolait.net/testj.py")
result, error = server.echo('Test echo!')
print(result)</pre>
<p />prints<p />
<pre>Test echo!</pre>
</div>
</dd>
<dt><a name="json_rpcserver_serve"></a><code><b>json.rpcserver.serve</b>(object[, packReturn])</code></dt>
<dd>
Handles an incoming CGILua request as a JSON RPC request and serves the request from
the given object.
<p />The optional <code>packReturn</code> parameter, if set <code>true</code>, will, if the requested
method returns more than one value, pack these returned values into an array. If only a single value
is returned, it is not packed into an array. If <code>packReturn</code> is <code>false</code> (or not set), only the first
return value from the requested method will be returned. This is necessitated since the JSON protocol does not permit a method call to return more than a single value.
<p/>
<code>serve</code> returns nothing.<p/>
<b>Example</b>
<div class="example">
<pre>
--
-- jsonrpc.lua
-- Installed in a CGILua webserver environment (with necessary CGI Lua 5.0 patch)
--
require ('json.rpcserver')
-- The Lua class that is to serve JSON RPC requests
local myServer = {
echo = function (msg) return msg end,
average = function(...)
local total=0
local count=0
for i=1, table.getn(arg) do
total = total + arg[i]
count = count + 1
end
return { average= total/count, sum = total, n=count }
end
}
json.rpcserver.serve(myServer)
</pre>
An example of using this JSON RPC server from a Lua file:
<pre>
require ('json.rpc')
local server = json.rpc.proxy('http://www.myserver.com/jsonrpc.lua')
table.foreach(server.average(10,15,23), print)
</pre>
Prints:
<pre>
average 16
sum 48
n 3
</pre>
</div>
</dd>
</dl>
<!-- ***************** HISTORY AND ROADMAP ***************** -->
<h1><a name="roadmap"></a>History &amp; Roadmap</h1>
The downloads sections details the versions and their related histories. I will wait for Lua 5.1 to be final (expected late January) before making the JSON4Lua module beta. If no serious bugs or objections are encountered, I will make the module 1.0 on 1 April 2006 (to coincide with April Fool's day!)
</td>
</tr>
<tr>
<td colspan="2" class="copyright">&copy; 2005, 2006 Craig Mason-Jones</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

View file

@ -0,0 +1,23 @@
--[[
JSON4Lua example script.
Demonstrates the simple functionality of the json module.
]]--
json = require('json')
-- Object to JSON encode
test = {
one='first',two='second',three={2,3,5}
}
jsonTest = json.encode(test)
print('JSON encoded test is: ' .. jsonTest)
-- Now JSON decode the json string
result = json.decode(jsonTest)
print ("The decoded table result:")
table.foreach(result,print)
print ("The decoded table result.three")
table.foreach(result.three, print)

View file

@ -0,0 +1,21 @@
--
-- jsonrpc.lua
-- Installed in a CGILua webserver environment (with necessary CGI Lua 5.0 patch)
--
require ('json.rpcserver')
-- The Lua class that is to serve JSON RPC requests
local myServer = {
echo = function (msg) return msg end,
average = function(...)
local total=0
local count=0
for i=1, table.getn(arg) do
total = total + arg[i]
count = count + 1
end
return { average= total/count, sum = total, n=count }
end
}
json.rpcserver.serve(myServer)

223
libs/json4lua/examples/tests.lua Executable file
View file

@ -0,0 +1,223 @@
--[[
Some basic tests for JSON4Lua.
]]--
--- Compares two tables for being data-identical.
function compareData(a,b)
if (type(a)=='string' or type(a)=='number' or type(a)=='boolean' or type(a)=='nil') then return a==b end
-- After basic data types, we're only interested in tables
if (type(a)~='table') then return true end
-- Check that a has everything b has
for k,v in pairs(b) do
if (not compareData( a[k], v ) ) then return false end
end
for k,v in pairs(a) do
if (not compareData( v, b[k] ) ) then return false end
end
return true
end
---
-- Checks that our compareData function works properly
function testCompareData()
s = "name"
r = "name"
assert(compareData(s,r))
assert(not compareData('fred',s))
assert(not compareData(nil, s))
assert(not compareData("123",123))
assert(not compareData(false, nil))
assert(compareData(true, true))
assert(compareData({1,2,3},{1,2,3}))
assert(compareData({'one',2,'three'},{'one',2,'three'}))
assert(not compareData({'one',2,4},{4,2,'one'}))
assert(compareData({one='ichi',two='nichi',three='san'}, {three='san',two='nichi',one='ichi'}))
s = { one={1,2,3}, two={one='hitotsu',two='futatsu',three='mitsu'} }
assert(compareData(s,s))
t = { one={1,2,3}, two={one='een',two='twee',three='drie'} }
assert(not compareData(s,t))
end
testCompareData()
--
--
-- Performs some perfunctory tests on JSON module
function testJSON4Lua()
json = require('json')
if nil then
-- Test encodeString
s = [["\"
]]
r = json._encodeString(s)
assert(r=='\\"\\\\\\"\\n')
s = [["""\\\"]]
r = json._encodeString(s)
assert(r==[[\"\"\"\\\\\\\"]])
end
-- Test encode for basic strings (complicated strings)
s = [[Hello, Lua!]]
r = json.encode(s)
assert(r=='"Hello, Lua!"')
s = [["\"
]]
r = json.encode(s)
assert(r=='\"\\"\\\\\\"\\n\"')
s = [["""\\\"]]
r = json.encode(s)
assert(r==[["\"\"\"\\\\\\\""]])
-- Test encode for numeric values
s = 23
r = json.encode(s)
assert(r=='23')
s=48.123
r = json.encode(s)
assert(r=='48.123')
-- Test encode for boolean values
assert(json.encode(true)=='true')
assert(json.encode(false)=='false')
assert(json.encode(nil)=='null')
-- Test encode for arrays
s = {1,2,3}
r = json.encode(s)
assert(r=="[1,2,3]")
s = {9,9,9}
r = json.encode(s)
assert(r=="[9,9,9]")
-- Complex array test
s = { 2, 'joe', false, nil, 'hi' }
r = json.encode(s)
assert(r=='[2,"joe",false,null,"hi"]')
-- Test encode for tables
s = {Name='Craig',email='craig@lateral.co.za',age=35}
r = json.encode(s)
-- NB: This test can fail because of order: need to test further once
-- decoding is supported.
-- assert(r==[[{"age":35,"Name":"Craig","email":"craig@lateral.co.za"}]])
-- Test encoding tables with numeric (string) indexes
s = {}
s['1']='One'
r = json.encode(s)
-- print("r = ", r)
assert(r=='{"1":"One"}')
s['2']= {One='Uno'}
r = json.encode(s)
assert(compareData(json.decode(r), s))
-- Test decode_scanWhitespace
if nil then
s = " \n \r \t "
e = json._decode_scanWhitespace(s,1)
assert(e==string.len(s)+1)
s = " \n\r\t4"
assert(json._decode_scanWhitespace(s,1)==5)
-- Test decode_scanString
s = [["Test"]]
r,e = json._decode_scanString(s,1)
assert(r=='Test' and e==7)
s = [["This\nis a \"test"]]
r = json._decode_scanString(s,1)
assert(r=="This\nis a \"test")
s = [["Test\u00A7\\"]]
r,e = json._decode_scanString(s,1)
assert(r=="Test\xC2\xA7\\" and e==9)
-- Test decode_scanNumber
s = [[354]]
r,e = json._decode_scanNumber(s,1)
assert(r==354 and e==4)
s = [[ 4565.23 AND OTHER THINGS ]]
r,e = json._decode_scanNumber(s,2)
assert(r==4565.23 and e==9)
s = [[ -23.22 and ]]
r,e = json._decode_scanNumber(s,2)
assert(r==-23.22 and e==8)
-- Test decode_scanConstant
s = "true"
r,e = json._decode_scanConstant(s,1)
assert(r==true and e==5)
s = " false "
r,e = json._decode_scanConstant(s,3)
assert(r==false and e==8)
s = "1null6"
r,e = json._decode_scanConstant(s,2)
assert(r==nil and e==6)
-- Test decode_scanArray
s = "[1,2,3]"
r,e = json._decode_scanArray(s,1)
assert(compareData(r,{1,2,3}))
s = [[[ 1 , 3 ,5 , "Fred" , true, false, null, -23 ] ]]
r,e = json._decode_scanArray(s,1)
assert(compareData(r, {1,3,5,'Fred',true,false,nil,-23} ) )
s = "[3,5,null,7,9]"
r,e = json._decode_scanArray(s,1)
assert(compareData(r, {3,5,nil,7,9}))
s = "[3,5,null,7,9,null,null]"
r,e = json._decode_scanArray(s,1)
assert(compareData(r, {3,5,nil,7,9,nil,nil}))
end
s = [["Test\u00A7\\\""]]
r,e = json.decode(s)
assert(r=="Test\xC2\xA7\\\"", r)
-- Test decode_scanObject
s = [[ {"one":1, "two":2, "three":"three", "four":true} ]]
r,e = json.decode(s)
-- for x,y in pairs(r) do
-- print(x,y)
-- end
assert(compareData(r,{one=1,two=2,three='three',four=true}))
s = [[ { "one" : { "first":1,"second":2,"third":3}, "two":2, "three":false } ]]
r,e = json.decode(s)
assert(compareData(r, {one={first=1,second=2,third=3},two=2,three=false}))
s = [[ { "primes" : [2,3,5,7,9], "user":{"name":"craig","age":35,"programs_lua":true},
"lua_is_great":true } ]]
r,e = json.decode(s)
assert(compareData(r, {primes={2,3,5,7,9},user={name='craig',age=35,programs_lua=true},lua_is_great=true}))
-- Test json.null management
t = { 1,2,json.null,4 }
assert( json.encode(t)=="[1,2,null,4]" )
t = {x=json.null }
r = json.encode(t)
assert( json.encode(t) == '{"x":null}' )
-- Test comment decoding
s = [[ /* A comment
that spans
a few lines
*/
"test"
]]
r,e = json.decode(s)
assert(r=='test',"Comment decoding failed")
-- Per error reported by M.Hund, with incorrect decoding of string-numbered tables
s = {}
subt = {a="a",b="b",c="c"}
s['1'] = subt
s['2'] = subt
s['3'] = subt
r = json.decode('{"1":{"a":"a","b":"b","c":"c"},"2":{"a":"a","b":"b","c":"c"},"3":{"a":"a","b":"b","c":"c"}}')
assert(compareData(s, r))
end
testJSON4Lua()
print("JSON4Lua tests completed successfully")

View file

@ -0,0 +1,46 @@
--[[
Some Time Trails for the JSON4Lua package
]]--
require('json')
require('os')
require('table')
local t1 = os.clock()
local jstr
local v
for i=1,100 do
local t = {}
for j=1,500 do
table.insert(t,j)
end
for j=1,500 do
table.insert(t,"VALUE")
end
jstr = json.encode(t)
v = json.decode(jstr)
--print(json.encode(t))
end
for i = 1,100 do
local t = {}
for j=1,500 do
local m= math.mod(j,3)
if (m==0) then
t['a'..j] = true
elseif m==1 then
t['a'..j] = json.null
else
t['a'..j] = j
end
end
jstr = json.encode(t)
v = json.decode(jstr)
end
print (jstr)
--print(type(t1))
local t2 = os.clock()
print ("Elapsed time=" .. os.difftime(t2,t1) .. "s")

442
libs/json4lua/json/json.lua Executable file
View file

@ -0,0 +1,442 @@
-----------------------------------------------------------------------------
-- JSON4Lua: JSON encoding / decoding support for the Lua language.
-- json Module.
-- Author: Craig Mason-Jones
-- Homepage: http://github.com/craigmj/json4lua/
-- Version: 1.0.1
-- This module is released under the MIT License (MIT).
-- Please see LICENCE.txt for details.
--
-- USAGE:
-- This module exposes two functions:
-- json.encode(o)
-- Returns the table / string / boolean / number / nil / json.null value as a JSON-encoded string.
-- json.decode(json_string)
-- Returns a Lua object populated with the data encoded in the JSON string json_string.
--
-- REQUIREMENTS:
-- compat-5.1 if using Lua 5.0
--
-- CHANGELOG
-- 1.0.1 Introduced set_plugin_info call to register plugin details for Wireshark. Per
-- suggestion by T. Lerman.
-- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix).
-- Fixed Lua 5.1 compatibility issues.
-- Introduced json.null to have null values in associative arrays.
-- json.encode() performance improvement (more than 50%) through table.concat rather than ..
-- Introduced decode ability to ignore /**/ comments in the JSON string.
-- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays.
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Imports and dependencies
-----------------------------------------------------------------------------
local math = require('math')
local string = require("string")
local table = require("table")
-----------------------------------------------------------------------------
-- Module declaration
-----------------------------------------------------------------------------
local json = {} -- Public namespace
local json_private = {} -- Private namespace
-- Public constants
json.EMPTY_ARRAY={}
json.EMPTY_OBJECT={}
-- Public functions
-- Private functions
local decode_scanArray
local decode_scanComment
local decode_scanConstant
local decode_scanNumber
local decode_scanObject
local decode_scanString
local decode_scanWhitespace
local encodeString
local isArray
local isEncodable
-----------------------------------------------------------------------------
-- PUBLIC FUNCTIONS
-----------------------------------------------------------------------------
--- Encodes an arbitrary Lua object / variable.
-- @param v The Lua object / variable to be JSON encoded.
-- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode)
function json.encode (v)
-- Handle nil values
if v==nil then
return "null"
end
local vtype = type(v)
-- Handle strings
if vtype=='string' then
return '"' .. json_private.encodeString(v) .. '"' -- Need to handle encoding in string
end
-- Handle booleans
if vtype=='number' or vtype=='boolean' then
return tostring(v)
end
-- Handle tables
if vtype=='table' then
local rval = {}
-- Consider arrays separately
local bArray, maxCount = isArray(v)
if bArray then
for i = 1,maxCount do
table.insert(rval, json.encode(v[i]))
end
else -- An object, not an array
for i,j in pairs(v) do
if isEncodable(i) and isEncodable(j) then
table.insert(rval, '"' .. json_private.encodeString(i) .. '":' .. json.encode(j))
end
end
end
if bArray then
return '[' .. table.concat(rval,',') ..']'
else
return '{' .. table.concat(rval,',') .. '}'
end
end
-- Handle null values
if vtype=='function' and v==json.null then
return 'null'
end
assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. tostring(v))
end
--- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
-- @param s The string to scan.
-- @param [startPos] Optional starting position where the JSON string is located. Defaults to 1.
-- @param Lua object, number The object that was scanned, as a Lua table / string / number / boolean or nil,
-- and the position of the first character after
-- the scanned JSON object.
function json.decode(s, startPos)
startPos = startPos and startPos or 1
startPos = decode_scanWhitespace(s,startPos)
assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']')
local curChar = string.sub(s,startPos,startPos)
-- Object
if curChar=='{' then
return decode_scanObject(s,startPos)
end
-- Array
if curChar=='[' then
return decode_scanArray(s,startPos)
end
-- Number
if string.find("+-0123456789.e", curChar, 1, true) then
return decode_scanNumber(s,startPos)
end
-- String
if curChar==[["]] or curChar==[[']] then
return decode_scanString(s,startPos)
end
if string.sub(s,startPos,startPos+1)=='/*' then
return json.decode(s, decode_scanComment(s,startPos))
end
-- Otherwise, it must be a constant
return decode_scanConstant(s,startPos)
end
--- The null function allows one to specify a null value in an associative array (which is otherwise
-- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null }
function json.null()
return json.null -- so json.null() will also return null ;-)
end
-----------------------------------------------------------------------------
-- Internal, PRIVATE functions.
-- Following a Python-like convention, I have prefixed all these 'PRIVATE'
-- functions with an underscore.
-----------------------------------------------------------------------------
--- Scans an array from JSON into a Lua object
-- startPos begins at the start of the array.
-- Returns the array and the next starting position
-- @param s The string being scanned.
-- @param startPos The starting position for the scan.
-- @return table, int The scanned array as a table, and the position of the next character to scan.
function decode_scanArray(s,startPos)
local array = {} -- The return value
local stringLen = string.len(s)
assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s )
startPos = startPos + 1
-- Infinite loop for array elements
local index = 1
repeat
startPos = decode_scanWhitespace(s,startPos)
assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.')
local curChar = string.sub(s,startPos,startPos)
if (curChar==']') then
return array, startPos+1
end
if (curChar==',') then
startPos = decode_scanWhitespace(s,startPos+1)
end
assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.')
object, startPos = json.decode(s,startPos)
array[index] = object
index = index + 1
until false
end
--- Scans a comment and discards the comment.
-- Returns the position of the next character following the comment.
-- @param string s The JSON string to scan.
-- @param int startPos The starting position of the comment
function decode_scanComment(s, startPos)
assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos)
local endPos = string.find(s,'*/',startPos+2)
assert(endPos~=nil, "Unterminated comment in string at " .. startPos)
return endPos+2
end
--- Scans for given constants: true, false or null
-- Returns the appropriate Lua type, and the position of the next character to read.
-- @param s The string being scanned.
-- @param startPos The position in the string at which to start scanning.
-- @return object, int The object (true, false or nil) and the position at which the next character should be
-- scanned.
function decode_scanConstant(s, startPos)
local consts = { ["true"] = true, ["false"] = false, ["null"] = nil }
local constNames = {"true","false","null"}
for i,k in pairs(constNames) do
if string.sub(s,startPos, startPos + string.len(k) -1 )==k then
return consts[k], startPos + string.len(k)
end
end
assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos)
end
--- Scans a number from the JSON encoded string.
-- (in fact, also is able to scan numeric +- eqns, which is not
-- in the JSON spec.)
-- Returns the number, and the position of the next character
-- after the number.
-- @param s The string being scanned.
-- @param startPos The position at which to start scanning.
-- @return number, int The extracted number and the position of the next character to scan.
function decode_scanNumber(s,startPos)
local endPos = startPos+1
local stringLen = string.len(s)
local acceptableChars = "+-0123456789.e"
while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true)
and endPos<=stringLen
) do
endPos = endPos + 1
end
local stringValue = 'return ' .. string.sub(s,startPos, endPos-1)
local stringEval = load(stringValue)
assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos)
return stringEval(), endPos
end
--- Scans a JSON object into a Lua object.
-- startPos begins at the start of the object.
-- Returns the object and the next starting position.
-- @param s The string being scanned.
-- @param startPos The starting position of the scan.
-- @return table, int The scanned object as a table and the position of the next character to scan.
function decode_scanObject(s,startPos)
local object = {}
local stringLen = string.len(s)
local key, value
assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s)
startPos = startPos + 1
repeat
startPos = decode_scanWhitespace(s,startPos)
assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.')
local curChar = string.sub(s,startPos,startPos)
if (curChar=='}') then
return object,startPos+1
end
if (curChar==',') then
startPos = decode_scanWhitespace(s,startPos+1)
end
assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.')
-- Scan the key
key, startPos = json.decode(s,startPos)
assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key)
startPos = decode_scanWhitespace(s,startPos)
assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key)
assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos)
startPos = decode_scanWhitespace(s,startPos+1)
assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key)
value, startPos = json.decode(s,startPos)
object[key]=value
until false -- infinite loop while key-value pairs are found
end
-- START SoniEx2
-- Initialize some things used by decode_scanString
-- You know, for efficiency
local escapeSequences = {
["\\t"] = "\t",
["\\f"] = "\f",
["\\r"] = "\r",
["\\n"] = "\n",
["\\b"] = "\b"
}
setmetatable(escapeSequences, {__index = function(t,k)
-- skip "\" aka strip escape
return string.sub(k,2)
end})
-- END SoniEx2
--- Scans a JSON string from the opening inverted comma or single quote to the
-- end of the string.
-- Returns the string extracted as a Lua string,
-- and the position of the next non-string character
-- (after the closing inverted comma or single quote).
-- @param s The string being scanned.
-- @param startPos The starting position of the scan.
-- @return string, int The extracted string as a Lua string, and the next character to parse.
function decode_scanString(s,startPos)
assert(startPos, 'decode_scanString(..) called without start position')
local startChar = string.sub(s,startPos,startPos)
-- START SoniEx2
-- PS: I don't think single quotes are valid JSON
assert(startChar == [["]] or startChar == [[']],'decode_scanString called for a non-string')
--assert(startPos, "String decoding failed: missing closing " .. startChar .. " for string at position " .. oldStart)
local t = {}
local i,j = startPos,startPos
while string.find(s, startChar, j+1) ~= j+1 do
local oldj = j
i,j = string.find(s, "\\.", j+1)
local x,y = string.find(s, startChar, oldj+1)
if not i or x < i then
i,j = x,y-1
end
table.insert(t, string.sub(s, oldj+1, i-1))
if string.sub(s, i, j) == "\\u" then
local a = string.sub(s,j+1,j+4)
j = j + 4
local n = tonumber(a, 16)
assert(n, "String decoding failed: bad Unicode escape " .. a .. " at position " .. i .. " : " .. j)
-- math.floor(x/2^y) == lazy right shift
-- a % 2^b == bitwise_and(a, (2^b)-1)
-- 64 = 2^6
-- 4096 = 2^12 (or 2^6 * 2^6)
local x
if n < 0x80 then
x = string.char(n % 0x80)
elseif n < 0x800 then
-- [110x xxxx] [10xx xxxx]
x = string.char(0xC0 + (math.floor(n/64) % 0x20), 0x80 + (n % 0x40))
else
-- [1110 xxxx] [10xx xxxx] [10xx xxxx]
x = string.char(0xE0 + (math.floor(n/4096) % 0x10), 0x80 + (math.floor(n/64) % 0x40), 0x80 + (n % 0x40))
end
table.insert(t, x)
else
table.insert(t, escapeSequences[string.sub(s, i, j)])
end
end
table.insert(t,string.sub(j, j+1))
assert(string.find(s, startChar, j+1), "String decoding failed: missing closing " .. startChar .. " at position " .. j .. "(for string at position " .. startPos .. ")")
return table.concat(t,""), j+2
-- END SoniEx2
end
--- Scans a JSON string skipping all whitespace from the current start position.
-- Returns the position of the first non-whitespace character, or nil if the whole end of string is reached.
-- @param s The string being scanned
-- @param startPos The starting position where we should begin removing whitespace.
-- @return int The first position where non-whitespace was encountered, or string.len(s)+1 if the end of string
-- was reached.
function decode_scanWhitespace(s,startPos)
local whitespace=" \n\r\t"
local stringLen = string.len(s)
while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do
startPos = startPos + 1
end
return startPos
end
--- Encodes a string to be JSON-compatible.
-- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-)
-- @param s The string to return as a JSON encoded (i.e. backquoted string)
-- @return The string appropriately escaped.
local escapeList = {
['"'] = '\\"',
['\\'] = '\\\\',
['/'] = '\\/',
['\b'] = '\\b',
['\f'] = '\\f',
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t'
}
function json_private.encodeString(s)
local s = tostring(s)
return s:gsub(".", function(c) return escapeList[c] end) -- SoniEx2: 5.0 compat
end
-- Determines whether the given Lua type is an array or a table / dictionary.
-- We consider any table an array if it has indexes 1..n for its n items, and no
-- other data in the table.
-- I think this method is currently a little 'flaky', but can't think of a good way around it yet...
-- @param t The table to evaluate as an array
-- @return boolean, number True if the table can be represented as an array, false otherwise. If true,
-- the second returned value is the maximum
-- number of indexed elements in the array.
function isArray(t)
-- Next we count all the elements, ensuring that any non-indexed elements are not-encodable
-- (with the possible exception of 'n')
if (t == json.EMPTY_ARRAY) then return true, 0 end
if (t == json.EMPTY_OBJECT) then return false end
local maxIndex = 0
for k,v in pairs(t) do
if (type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair
if (not isEncodable(v)) then return false end -- All array elements must be encodable
maxIndex = math.max(maxIndex,k)
else
if (k=='n') then
if v ~= (t.n or #t) then return false end -- False if n does not hold the number of elements
else -- Else of (k=='n')
if isEncodable(v) then return false end
end -- End of (k~='n')
end -- End of k,v not an indexed pair
end -- End of loop across all pairs
return true, maxIndex
end
--- Determines whether the given Lua object / table / variable can be JSON encoded. The only
-- types that are JSON encodable are: string, boolean, number, nil, table and json.null.
-- In this implementation, all other types are ignored.
-- @param o The object to examine.
-- @return boolean True if the object should be JSON encoded, false if it should be ignored.
function isEncodable(o)
local t = type(o)
return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or
(t=='function' and o==json.null)
end
-----------------------------------------------------------------------------
-- Display version information for Wireshark
-----------------------------------------------------------------------------
if set_plugin_info~=nil then
local plugin_info = {
version = "1.0.1",
author = "Craig Mason-Jones",
repository = "https://github.com/craigmj/json4lua"
}
set_plugin_info(plugin_info)
end
return json

107
libs/json4lua/json/rpc.lua Executable file
View file

@ -0,0 +1,107 @@
-----------------------------------------------------------------------------
-- JSONRPC4Lua: JSON RPC client calls over http for the Lua language.
-- json.rpc Module.
-- Author: Craig Mason-Jones
-- Homepage: http://github.com/craigmj/json4lua/
-- Version: 1.0.0
-- This module is released under the MIT License (MIT).
-- Please see LICENCE.txt for details.
--
-- USAGE:
-- This module exposes two functions:
-- proxy( 'url')
-- Returns a proxy object for calling the JSON RPC Service at the given url.
-- call ( 'url', 'method', ...)
-- Calls the JSON RPC server at the given url, invokes the appropriate method, and
-- passes the remaining parameters. Returns the result and the error. If the result is nil, an error
-- should be there (or the system returned a null). If an error is there, the result should be nil.
--
-- REQUIREMENTS:
-- Lua socket 2.0 (http://www.cs.princeton.edu/~diego/professional/luasocket/)
-- json (The JSON4Lua package with which it is bundled)
-- compat-5.1 if using Lua 5.0.
-----------------------------------------------------------------------------
local json = require('json')
json.rpc = {} -- Module public namespace
-----------------------------------------------------------------------------
-- Imports and dependencies
-----------------------------------------------------------------------------
local json = require('json')
local http = require("socket.http")
-----------------------------------------------------------------------------
-- PUBLIC functions
-----------------------------------------------------------------------------
--- Creates an RPC Proxy object for the given Url of a JSON-RPC server.
-- @param url The URL for the JSON RPC Server.
-- @return Object on which JSON-RPC remote methods can be called.
-- EXAMPLE Usage:
-- local jsolait = json.rpc.proxy('http://jsolait.net/testj.py')
-- print(jsolait.echo('This is a test of the echo method!'))
-- print(jsolait.args2String('first','second','third'))
-- table.foreachi( jsolait.args2Array(5,4,3,2,1), print)
function json.rpc.proxy(url)
local serverProxy = {}
local proxyMeta = {
__index = function(self, key)
return function(...)
return json.rpc.call(url, key, ...)
end
end
}
setmetatable(serverProxy, proxyMeta)
return serverProxy
end
--- Calls a JSON RPC method on a remote server.
-- Returns a boolean true if the call succeeded, false otherwise.
-- On success, the second returned parameter is the decoded
-- JSON object from the server.
-- On http failure, returns nil and an error message.
-- On success, returns the result and nil.
-- @param url The url of the JSON RPC server.
-- @param method The method being called.
-- @param ... Parameters to pass to the method.
-- @return result, error The JSON RPC result and error. One or the other should be nil. If both
-- are nil, this means that the result of the RPC call was nil.
-- EXAMPLE Usage:
-- print(json.rpc.call('http://jsolait.net/testj.py','echo','This string will be returned'))
function json.rpc.call(url, method, ...)
local JSONRequestArray = {
id=tostring(math.random()),
["method"]=method,
["jsonrpc"]="2.0",
params = ...
}
local httpResponse, result , code
local jsonRequest = json.encode(JSONRequestArray)
-- We use the sophisticated http.request form (with ltn12 sources and sinks) so that
-- we can set the content-type to text/plain. While this shouldn't strictly-speaking be true,
-- it seems a good idea (Xavante won't work w/out a content-type header, although a patch
-- is needed to Xavante to make it work with text/plain)
local ltn12 = require('ltn12')
local resultChunks = {}
httpResponse, code = http.request(
{ ['url'] = url,
sink = ltn12.sink.table(resultChunks),
method = 'POST',
headers = { ['content-type']='application/json-rpc', ['content-length']=string.len(jsonRequest) },
source = ltn12.source.string(jsonRequest)
}
)
httpResponse = table.concat(resultChunks)
-- Check the http response code
if (code~=200) then
return nil, "HTTP ERROR: " .. code
end
-- And decode the httpResponse and check the JSON RPC result code
result = json.decode( httpResponse )
if result.result then
return result.result, nil
else
return nil, result.error
end
end

View file

@ -0,0 +1,78 @@
-----------------------------------------------------------------------------
-- JSONRPC4Lua: JSON RPC server for exposing Lua objects as JSON RPC callable
-- objects via http.
-- json.rpcserver Module.
-- Author: Craig Mason-Jones
-- Homepage: http://github.com/craigmj/json4lua/
-- Version: 1.0.0
-- This module is released under the MIT License (MIT).
-- Please see LICENCE.txt for details.
--
-- USAGE:
-- This module exposes one function:
-- server(luaClass, packReturn)
-- Manages incoming JSON RPC request forwarding the method call to the given
-- object. If packReturn is true, multiple return values are packed into an
-- array on return.
--
-- IMPORTANT NOTES:
-- 1. This version ought really not be 0.9.10, since this particular part of the
-- JSONRPC4Lua package is very first-draft. However, the JSON4Lua package with which
-- it comes is quite solid, so there you have it :-)
-- 2. This has only been tested with Xavante webserver, with which it works
-- if you patch CGILua to accept 'text/plain' content type. See doc\cgilua_patch.html
-- for details.
----------------------------------------------------------------------------
module ('json.rpcserver')
---
-- Implements a JSON RPC Server wrapping for luaClass, exposing each of luaClass's
-- methods as JSON RPC callable methods.
-- @param luaClass The JSON RPC class to expose.
-- @param packReturn If true, the server will automatically wrap any
-- multiple-value returns into an array. Single returns remain single returns. If
-- false, when a function returns multiple values, only the first of these values will
-- be returned.
--
function serve(luaClass, packReturn)
cgilua.contentheader('text','plain')
require('cgilua')
require ('json')
local postData = ""
if not cgilua.servervariable('CONTENT_LENGTH') then
cgilua.put("Please access JSON Request using HTTP POST Request")
return 0
else
postData = cgi[1] -- SAPI.Request.getpostdata() --[[{ "id":1, "method":"echo","params":["Hi there"]}]] --
end
-- @TODO Catch an error condition on decoding the data
local jsonRequest = json.decode(postData)
local jsonResponse = {}
jsonResponse.id = jsonRequest.id
local method = luaClass[ jsonRequest.method ]
if not method then
jsonResponse.error = 'Method ' .. jsonRequest.method .. ' does not exist at this server.'
else
local callResult = { pcall( method, unpack( jsonRequest.params ) ) }
if callResult[1] then -- Function call successfull
table.remove(callResult,1)
if packReturn and table.getn(callResult)>1 then
jsonResponse.result = callResult
else
jsonResponse.result = unpack(callResult) -- NB: Does not support multiple argument returns
end
else
jsonResponse.error = callResult[2]
end
end
-- Output the result
-- TODO: How to be sure that the result and error tags are there even when they are nil in Lua?
-- Can force them by hand... ?
cgilua.contentheader('text','plain')
cgilua.put( json.encode( jsonResponse ) )
end

View file

@ -0,0 +1,32 @@
package="JSON4Lua"
version="1.0.0"
source = {
url = "git://github.com/craigmj/json4lua.git",
tag = "1.0.0"
}
description = {
summary = "JSON4Lua and JSONRPC4Lua implement JSON (JavaScript Object Notation) encoding and decoding and a JSON-RPC-over-http client for Lua.",
detailed = [[
JSON4Lua and JSONRPC4Lua implement JSON (JavaScript Object Notation)
encoding and decoding and a JSON-RPC-over-http client for Lua.
JSON is JavaScript Object Notation, a simple encoding of
Javascript-like objects that is ideal for lightweight transmission
of relatively weakly-typed data. A sub-package of JSON4Lua is
JSONRPC4Lua, which provides a simple JSON-RPC-over-http client and server
(in a CGILua environment) for Lua.
]],
homepage = "http://github.com/craigmj/json4lua/",
license = "GPL"
}
dependencies = {
"lua >= 5.2",
"luasocket",
}
build = {
type = "builtin",
modules = {
["json"] = "json/json.lua",
["json.rpc"] = "json/rpc.lua"
}
}

View file

@ -1,63 +0,0 @@
--- Tserial v1.51d, a simple table serializer which turns tables into Lua script
-- @author Taehl (SelfMadeSpirit@gmail.com)
Tserial = {}
TSerial = Tserial -- for backwards-compatibility
--- Serializes a table into a string, in form of Lua script.
-- @param t table to be serialized (may not contain any circular reference)
-- @param drop if true, unserializable types will be silently dropped instead of raising errors
-- if drop is a function, it will be called to serialize unsupported types
-- if drop is a table, it will be used as a serialization table (where {[value] = serial})
-- @param indent if true, output "human readable" mode with newlines and indentation (for debug)
-- @return string recreating given table
function Tserial.pack(t, drop, indent)
assert(type(t) == "table", "Can only Tserial.pack tables.")
local s, empty, indent = "{"..(indent and "\n" or ""), true, indent and math.max(type(indent)=="number" and indent or 0,0)
local function proc(k,v, omitKey) -- encode a key/value pair
empty = nil -- helps ensure empty tables return as "{}"
local tk, tv, skip = type(k), type(v)
if type(drop)=="table" and drop[k] then k = "["..drop[k].."]"
elseif tk == "boolean" then k = k and "[true]" or "[false]"
elseif tk == "string" then
local f = string.format("%q",k)
if f ~= '"'..k..'"' or string.find(k, " ") then k = '['..f..']' end
elseif tk == "number" then k = "["..k.."]"
elseif tk == "table" then k = "["..Tserial.pack(k, drop, indent and indent+1).."]"
elseif type(drop) == "function" then k = "["..string.format("%q",drop(k)).."]"
elseif drop then skip = true
else error("Attempted to Tserial.pack a table with an invalid key: "..tostring(k))
end
if type(drop)=="table" and drop[v] then v = drop[v]
elseif tv == "boolean" then v = v and "true" or "false"
elseif tv == "string" then v = string.format("%q", v)
elseif tv == "number" then -- no change needed
elseif tv == "table" then v = Tserial.pack(v, drop, indent and indent+1)
elseif type(drop) == "function" then v = string.format("%q",drop(v))
elseif drop then skip = true
else error("Attempted to Tserial.pack a table with an invalid value: "..tostring(v))
end
if not skip then return string.rep("\t",indent or 0)..(omitKey and "" or k.."=")..v..","..(indent and "\n" or "") end
return ""
end
local l, did=-1,{} repeat l=l+1 until t[l+1]==nil -- #t "can" lie!
for i=1,l do s = s..proc(i, t[i], true) did[i]=true end -- use ordered values when possible for better string
for k, v in pairs(t) do if not did[k] then s = s..proc(k, v) end end
if not empty then s = string.sub(s,1,string.len(s)-1) end
if indent then s = string.sub(s,1,string.len(s)-1).."\n" end
return s..string.rep("\t",(indent or 1)-1).."}"
end
--- Loads a table into memory from a string (like those output by Tserial.pack)
-- @param s a string of Lua defining a table, such as "{2,4,8,ex='ample'}"
-- @param safe if true, all extraneous parts of the string will be removed, leaving only a table (prevents running anomalous code when unpacking untrusted strings). Will also cause malformed tables to quietly return nil and an error message, instead of throwing an error (so your program can't be crashed with a bad string)
-- @return a table recreated from the given string.
function Tserial.unpack(s, safe)
if safe then s = string.match(s, "(%b{})") end
assert(type(s) == "string", "Can only Tserial.unpack strings.")
local f, result = loadstring("Tserial.table="..s)
if not safe then assert(f,result) elseif not f then return nil, result end
result = f()
local t = Tserial.table
Tserial.table = nil
return t, result
end

@ -1 +0,0 @@
Subproject commit 08937cc0ecf72d1a964a8de6cd552c5e136bf0d4

@ -1 +0,0 @@
Subproject commit a0da807dca77baf07d287631f5ad41a9097fc25c

View file

@ -1,182 +0,0 @@
local middleclass = {
_VERSION = 'middleclass v3.0.1',
_DESCRIPTION = 'Object Orientation for Lua',
_URL = 'https://github.com/kikito/middleclass',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2011 Enrique García Cota
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 function _setClassDictionariesMetatables(aClass)
local dict = aClass.__instanceDict
dict.__index = dict
local super = aClass.super
if super then
local superStatic = super.static
setmetatable(dict, super.__instanceDict)
setmetatable(aClass.static, { __index = function(_,k) return dict[k] or superStatic[k] end })
else
setmetatable(aClass.static, { __index = function(_,k) return dict[k] end })
end
end
local function _setClassMetatable(aClass)
setmetatable(aClass, {
__tostring = function() return "class " .. aClass.name end,
__index = aClass.static,
__newindex = aClass.__instanceDict,
__call = function(self, ...) return self:new(...) end
})
end
local function _createClass(name, super)
local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict={} }
aClass.subclasses = setmetatable({}, {__mode = "k"})
_setClassDictionariesMetatables(aClass)
_setClassMetatable(aClass)
return aClass
end
local function _createLookupMetamethod(aClass, name)
return function(...)
local method = aClass.super[name]
assert( type(method)=='function', tostring(aClass) .. " doesn't implement metamethod '" .. name .. "'" )
return method(...)
end
end
local function _setClassMetamethods(aClass)
for _,m in ipairs(aClass.__metamethods) do
aClass[m]= _createLookupMetamethod(aClass, m)
end
end
local function _setDefaultInitializeMethod(aClass, super)
aClass.initialize = function(instance, ...)
return super.initialize(instance, ...)
end
end
local function _includeMixin(aClass, mixin)
assert(type(mixin)=='table', "mixin must be a table")
for name,method in pairs(mixin) do
if name ~= "included" and name ~= "static" then aClass[name] = method end
end
if mixin.static then
for name,method in pairs(mixin.static) do
aClass.static[name] = method
end
end
if type(mixin.included)=="function" then mixin:included(aClass) end
aClass.__mixins[mixin] = true
end
local Object = _createClass("Object", nil)
Object.static.__metamethods = { '__add', '__call', '__concat', '__div', '__ipairs', '__le',
'__len', '__lt', '__mod', '__mul', '__pairs', '__pow', '__sub',
'__tostring', '__unm'}
function Object.static:allocate()
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict)
end
function Object.static:new(...)
local instance = self:allocate()
instance:initialize(...)
return instance
end
function Object.static:subclass(name)
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
_setClassMetamethods(subclass)
_setDefaultInitializeMethod(subclass, self)
self.subclasses[subclass] = true
self:subclassed(subclass)
return subclass
end
function Object.static:subclassed(other) end
function Object.static:isSubclassOf(other)
return type(other) == 'table' and
type(self) == 'table' and
type(self.super) == 'table' and
( self.super == other or
type(self.super.isSubclassOf) == 'function' and
self.super:isSubclassOf(other)
)
end
function Object.static:include( ... )
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end
return self
end
function Object.static:includes(mixin)
return type(mixin) == 'table' and
type(self) == 'table' and
type(self.__mixins) == 'table' and
( self.__mixins[mixin] or
type(self.super) == 'table' and
type(self.super.includes) == 'function' and
self.super:includes(mixin)
)
end
function Object:initialize() end
function Object:__tostring() return "instance of " .. tostring(self.class) end
function Object:isInstanceOf(aClass)
return type(self) == 'table' and
type(self.class) == 'table' and
type(aClass) == 'table' and
( aClass == self.class or
type(aClass.isSubclassOf) == 'function' and
self.class:isSubclassOf(aClass)
)
end
function middleclass.class(name, super, ...)
super = super or Object
return super:subclass(name, ...)
end
middleclass.Object = Object
setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end })
return middleclass

@ -1 +0,0 @@
Subproject commit 949429d4618aee8b019899af101bd5f793d07a64

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

7
libs/narrator/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# System
.DS_Store
Thumbs.db
# Defold
/.internal
/build

24
libs/narrator/.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,469 @@
![logo](https://user-images.githubusercontent.com/4752473/85455900-141f8f80-b5a7-11ea-8cd7-b441d662b361.png)
# Narrator
[![Release](https://img.shields.io/github/v/release/astrochili/narrator.svg?include_prereleases=&sort=semver&color=blue)](https://github.com/astrochili/narrator/releases)
[![License](https://img.shields.io/badge/License-MIT-blue)](https://github.com/astrochili/narrator/blob/master/LICENSE)
[![Website](https://img.shields.io/badge/website-gray.svg?&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxNiIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDE4IDE2Ij48Y2lyY2xlIGN4PSIzLjY2IiBjeT0iMTQuNzUiIHI9IjEuMjUiIGZpbGw9InVybCgjYSkiLz48Y2lyY2xlIGN4PSI4LjY2IiBjeT0iMTQuNzUiIHI9IjEuMjUiIGZpbGw9InVybCgjYikiLz48Y2lyY2xlIGN4PSIxMy42NSIgY3k9IjE0Ljc1IiByPSIxLjI1IiBmaWxsPSJ1cmwoI2MpIi8+PHBhdGggZmlsbD0idXJsKCNkKSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNNy42MyAxLjQ4Yy41LS43IDEuNTUtLjcgMi4wNSAwbDYuMjIgOC44MWMuNTguODMtLjAxIDEuOTctMS4wMyAxLjk3SDIuNDRhMS4yNSAxLjI1IDAgMCAxLTEuMDItMS45N2w2LjIxLTguODFaIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiLz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIyLjQxIiB4Mj0iMi40MSIgeTE9IjEzLjUiIHkyPSIxNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRDhENDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGOTU0MUYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjcuNDEiIHgyPSI3LjQxIiB5MT0iMTMuNSIgeTI9IjE2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iI0ZEOEQ0MiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y5NTQxRiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJjIiB4MT0iMTIuNCIgeDI9IjEyLjQiIHkxPSIxMy41IiB5Mj0iMTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjRkQ4RDQyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRjk1NDFGIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImQiIHgxPSIuMDMiIHgyPSIuMDMiIHkxPSIuMDMiIHkyPSIxMi4yNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRkU2NUUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRkM4MzAiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4=)](https://astronachos.com/)
[![Mastodon](https://img.shields.io/badge/mastodon-gray?&logo=mastodon)](https://mastodon.gamedev.place/@astronachos)
[![Twitter](https://img.shields.io/badge/twitter-gray?&logo=twitter)](https://twitter.com/astronachos)
[![Telegram](https://img.shields.io/badge/telegram-gray?&logo=telegram)](https://t.me/astronachos)
[![Buy me a coffee](https://img.shields.io/badge/buy_me_a_coffee-gray?&logo=buy%20me%20a%20coffee)](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
View 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
View 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 ---')

View 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

View 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
}
}

View 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

View 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

View 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
View 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 ---')

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load diff

View file

View 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

View 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={}}

View 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
View 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

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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

View 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

View 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.

View 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 }

View 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.

View 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.

Some files were not shown because too many files have changed in this diff Show more