More work on Forge.
This commit is contained in:
parent
3f69551719
commit
2819449810
70 changed files with 12700 additions and 1067 deletions
96
CHANGELOG
96
CHANGELOG
|
|
@ -7,6 +7,22 @@ SINGE 3.00
|
|||
API Changes
|
||||
-----------
|
||||
|
||||
- spriteFlip mirrors a sprite left to right, top to bottom, or both, for
|
||||
every draw of it, its frames included, so art that faces one way is
|
||||
enough for a character that walks both ways.
|
||||
|
||||
- soundPlay takes a volume as its third argument, and
|
||||
soundSetChannelVolume changes a playing channel's own volume, both under
|
||||
the master from soundSetVolume: a quiet footstep and a loud shot can
|
||||
share one master. The master used to be applied through the mixer's
|
||||
tag gain, which overwrote the distance fade of positioned channels
|
||||
whenever it changed; it is applied per track now.
|
||||
|
||||
- materialSetOccluder makes a material write depth and no colour: an
|
||||
invisible shape that hides what walks behind it while what was drawn
|
||||
behind it -- a painted room, the disc -- shows through, which is how a
|
||||
3D character walks behind a pillar that is only in the picture.
|
||||
|
||||
- --gamedir names the directory holding the games, the .game files and
|
||||
the Singe support folder, so the executable can live anywhere. It
|
||||
becomes the working directory; relative names count from it. The
|
||||
|
|
@ -1510,6 +1526,13 @@ Fixes
|
|||
read as pixels. The menu's backdrop stops rewriting its grid colours
|
||||
every frame once the grid is lit.
|
||||
|
||||
- navAgentNew placed an agent from the node's world position as of the
|
||||
last drawn frame, so an agent made before the first frame -- or in a
|
||||
game that never draws the scene, which is what a 2D room walking a
|
||||
navigation mesh is -- started at the origin, off the mesh, and never
|
||||
moved. The transforms are refreshed first now, as navAddNode already
|
||||
did.
|
||||
|
||||
Net.singe verified a connection against Linux's CA list by path, so
|
||||
without a pin every HTTPS request failed on Windows, macOS and the
|
||||
handhelds. The engine now extracts Mozilla's CA list as
|
||||
|
|
@ -1521,10 +1544,79 @@ Fixes
|
|||
or scriptExecute is reported like any other bad API argument -- the
|
||||
script line and the call named -- rather than as a bare exit message.
|
||||
|
||||
- Forge's description format is new (FORGE.md section 4): kinds with
|
||||
instances, rooms that remember themselves, rules triggered by events
|
||||
(collisions between kinds, hits, deaths, timers, disc frames) as well as
|
||||
every frame, vars and states on every instance, expressions where a
|
||||
number goes, any-of groups, timers, sound, a HUD document bound to vars,
|
||||
actions that take time run as coroutines, and hitbox tracks keyed to
|
||||
disc frames or time. A shoot-em-up and a light gun game over video
|
||||
prove it (testScripts/author, scenes 58 and 59). The editor gained
|
||||
kinds, rooms, event pickers, and a timeline that scrubs the disc's video
|
||||
under the cursor (scene 60). Nothing of the first slice's format loads;
|
||||
it was never released.
|
||||
|
||||
- Forge builds 3D games (FORGE.md section 5): a scene3d layer, model,
|
||||
mesh, light, billboard, and text looks, the character controller moving
|
||||
relative to a camera that is fixed, follows, orbits, sits in first
|
||||
person, or rides a rail of points with stops, enemies that seek, an
|
||||
animator playing a clip per state, targets with zones on named bones so
|
||||
a gun's ray says which part it hit, ragdolls on death, bodies and
|
||||
triggers. A rail shooter in the manner of House of the Dead and a 3D
|
||||
platformer prove it (scenes 61 and 62), and the editor shows a 3D room
|
||||
as the scene itself under an orbiting camera (scene 63).
|
||||
|
||||
- Forge builds adventure games (FORGE.md section 6): rooms that remember
|
||||
themselves, walk areas baked to a navigation mesh in a painted room or
|
||||
a modelled one, hotspots, a verb line and a text parser with the most
|
||||
specific rule winning, an inventory, dialogue trees, cut-scenes across
|
||||
rooms with fades, depth sorting and scaling, and whole-game saves. The
|
||||
same rules run a two-room adventure in 2D and in 3D (scenes 64 and 65).
|
||||
|
||||
- Forge builds racing games and tower defences (FORGE.md section 7):
|
||||
vehicles on the engine's vehicle physics with an opponent that drives a
|
||||
track on its own, walkers that follow a target or patrol a lane, turrets
|
||||
that fire at what is in range, instances placed by a click on the floor,
|
||||
thrust, and hinge, ball, and slider joints (scenes 66 and 67).
|
||||
|
||||
- Forge builds branching video games and carries the arcade plumbing
|
||||
(FORGE.md section 8): windows on the disc that want a move, with the
|
||||
disc sent to a success or a failure clip, the score bezel mirroring the
|
||||
game's vars, coins, continues, a results card with the best score kept,
|
||||
and scores sent to the master service's board (scene 68).
|
||||
|
||||
- Forge's editor gained a search in every picker, a file picker for any
|
||||
field that names a file, polygon drawing for walk areas and hotspot
|
||||
outlines, a dialogue outline panel, and play-from-this-room (FORGE.md
|
||||
section 9, scene 69).
|
||||
|
||||
- Forge's long tail (FORGE.md section 10): sprite sheets stepped by
|
||||
state, tile maps from a sheet, MIDI notes as events, water that bodies
|
||||
float in, boats under their propellers, and soft bodies (scenes 70 and
|
||||
71). Every kind of game in FORGE.md's catalogue now has its vocabulary.
|
||||
|
||||
- Forge's sprites turn (a var, a spin behaviour, or a projectile aiming
|
||||
along its flight), a particles look streams from an instance, the
|
||||
checker names misspelt fields, and 2D particle bursts are drawn at all
|
||||
(they never had been).
|
||||
|
||||
- Forge builds painted rooms in 3D with occluders, and its 3D viewport
|
||||
gained axis gizmos with a turn handle; entities carry a rotation, and
|
||||
a sprite faces left through a frame range of its own (FORGE.md section
|
||||
12, scenes 70, 79, and 80).
|
||||
|
||||
- Forge's catalogue is written out (FORGE.md section 11): a first-person
|
||||
shooter, breakout, a maze, third-person action, a space shooter, a
|
||||
rhythm game, and bowling join the samples (scenes 72 to 78), and what
|
||||
they found is fixed: a gun's ray passes through what is not a target,
|
||||
events raised while the first room is built reach the rules, and a
|
||||
rule on a key or a room can run once per instance of a kind.
|
||||
|
||||
- Forge has its own manual, docs/Forge.adoc, built to Forge.html and
|
||||
Forge.pdf beside the Singe manual; the engine's manual keeps a pointer.
|
||||
The build packs Forge.game itself, with the manual inside, and Forge's
|
||||
first run copies Forge.pdf out to its data directory and says where.
|
||||
The build packs Forge.game itself, with the manual and four samples
|
||||
that need no art inside, and Forge's first run copies Forge.pdf out to
|
||||
its data directory and says where.
|
||||
R redoes what U undid, forty steps deep.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -298,11 +298,22 @@ singeEmbedLua(thirdparty/bump.lua/bump.lua "")
|
|||
|
||||
# Forge's manual, rendered the same way. It is not embedded: the superbuild packs it into
|
||||
# Forge.game (cmake/Superbuild.cmake), which is the one thing that ships Forge.
|
||||
# Its vocabulary tables are generated from the manifest in Author.singe, so the manual cannot
|
||||
# drift from it; the generated file is kept in the tree for a host without a Lua interpreter.
|
||||
find_program(LUA_INTERPRETER NAMES lua5.4 lua)
|
||||
if(LUA_INTERPRETER)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
|
||||
COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_SOURCE_DIR} ${LUA_INTERPRETER} util/forgeVocabulary.lua > ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
|
||||
DEPENDS assets/Forge/Author.singe util/forgeVocabulary.lua
|
||||
COMMENT "Writing the Forge vocabulary"
|
||||
)
|
||||
endif()
|
||||
add_custom_command(
|
||||
OUTPUT ${MANUAL_DIR}/Forge.pdf
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
|
||||
COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Forge.adoc -o ${MANUAL_DIR}/Forge.pdf
|
||||
DEPENDS docs/Forge.adoc
|
||||
DEPENDS docs/Forge.adoc docs/ForgeVocabulary.adoc
|
||||
COMMENT "Rendering Forge.pdf"
|
||||
VERBATIM
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -23,19 +23,19 @@
|
|||
|
||||
|
||||
|
||||
-- Turns a game description into an ordinary Singe game (PLAN section 58).
|
||||
-- The compiler: a game description to a Singe game (FORGE.md).
|
||||
--
|
||||
-- The output is Lua a person can read, keep, and edit by hand; the description is kept beside it
|
||||
-- so the editor can open it again. Compiling rather than interpreting costs nothing at build time
|
||||
-- and saves walking a rule table every frame on a Pi or a handheld -- and it means there is no wall
|
||||
-- between what the tools make and what someone writes themselves.
|
||||
-- A description is data -- kinds, rooms, and rules -- and what comes out is a Lua script that
|
||||
-- calls the runtime in Author.singe. The rules become real functions: a condition is a Lua
|
||||
-- expression, an action a statement, and a rule with an action that takes time a coroutine. The
|
||||
-- description travels with the game so it can be opened again; nothing is lost in either
|
||||
-- direction.
|
||||
--
|
||||
-- This is written in Lua rather than in util/ as a Python script because the editor is itself a
|
||||
-- Singe game: it has to be able to compile what it is editing without leaving the engine. It
|
||||
-- lives with Forge rather than in Singe/ because only building needs it -- a finished game is
|
||||
-- already Lua. Author.singe, the runtime that game loads, travels with the game: a copy is
|
||||
-- written beside it when it is built, so a game handed to someone who has never installed Forge
|
||||
-- still runs. The compiler loads it too, for the vocabulary it emits against.
|
||||
-- Written in Lua rather than as a util/ script because the editor is itself a Singe game and has to
|
||||
-- compile what it is editing without leaving the engine. It lives with Forge rather than in Singe/
|
||||
-- because only building needs it; Author.singe, the runtime a built game loads, travels with the
|
||||
-- game -- a copy is written beside it when it is built -- so a game handed to someone who has never
|
||||
-- installed Forge still runs.
|
||||
|
||||
-- Where the runtime is taken from when a game is built. Forge carries it; a game gets a copy.
|
||||
AUTHOR_RUNTIME = "Forge/Author.singe"
|
||||
|
|
@ -46,209 +46,477 @@ dofile(AUTHOR_RUNTIME)
|
|||
local INDENT = "\t"
|
||||
|
||||
|
||||
-- A Lua literal for a value that came out of a description. Numbers and booleans go through as
|
||||
-- they are; everything else is quoted, because an author typing a name must not be able to end
|
||||
-- the string and start writing code. AUTHOR.actions.lua is the one deliberate way in.
|
||||
local function literal(value)
|
||||
if type(value) == "number" or type(value) == "boolean" then
|
||||
-- ===== Expressions ============================================================================
|
||||
--
|
||||
-- A small, safe language where a number goes: numbers, strings, arithmetic, comparison, and, or,
|
||||
-- not, self.var, other.var, event.field, an entity's field by name, the game's vars by name, and a
|
||||
-- few functions. Anything else is a compile error naming the expression, and the lua action is
|
||||
-- the way past it.
|
||||
|
||||
local FUNCTIONS = {
|
||||
count = "authorCount",
|
||||
random = "authorRandom",
|
||||
distance = "authorDistance",
|
||||
has = "authorHas",
|
||||
abs = "math.abs",
|
||||
min = "math.min",
|
||||
max = "math.max",
|
||||
floor = "math.floor",
|
||||
time = "authorTime",
|
||||
frame = "discGetFrame"
|
||||
}
|
||||
|
||||
local NAMES = {
|
||||
self = "self",
|
||||
other = "other",
|
||||
event = "event",
|
||||
time = "authorTime()",
|
||||
frame = "discGetFrame()"
|
||||
}
|
||||
|
||||
local OPERATORS = {
|
||||
["=="] = "==", ["~="] = "~=", ["!="] = "~=", ["<="] = "<=", [">="] = ">=", ["<"] = "<", [">"] = ">",
|
||||
["+"] = "+", ["-"] = "-", ["*"] = "*", ["/"] = "/", ["%"] = "%", ["^"] = "^", [".."] = ".."
|
||||
}
|
||||
|
||||
|
||||
local function tokenize(text)
|
||||
local tokens = {}
|
||||
local at = 1
|
||||
|
||||
while at <= #text do
|
||||
local c = text:sub(at, at)
|
||||
|
||||
if c:match("%s") then
|
||||
at = at + 1
|
||||
elseif c:match("[%d%.]") and text:sub(at):match("^%d") or text:sub(at):match("^%.%d") then
|
||||
local number = text:sub(at):match("^%d*%.?%d+[eE][-+]?%d+") or text:sub(at):match("^%d*%.?%d+") or text:sub(at):match("^%d+")
|
||||
|
||||
tokens[#tokens + 1] = { kind = "number", value = number }
|
||||
at = at + #number
|
||||
elseif c:match("[%a_]") then
|
||||
local name = text:sub(at):match("^[%a_][%w_]*")
|
||||
|
||||
tokens[#tokens + 1] = { kind = "name", value = name }
|
||||
at = at + #name
|
||||
elseif (c == '"') or (c == "'") then
|
||||
-- A backslash escapes the next character, so a line can hold its own quote marks.
|
||||
local value = {}
|
||||
local scan = at + 1
|
||||
|
||||
while (scan <= #text) and (text:sub(scan, scan) ~= c) do
|
||||
local ch = text:sub(scan, scan)
|
||||
|
||||
if ch == "\\" then
|
||||
local escaped = text:sub(scan + 1, scan + 1)
|
||||
|
||||
value[#value + 1] = (escaped == "n") and "\n" or escaped
|
||||
scan = scan + 2
|
||||
else
|
||||
value[#value + 1] = ch
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
if scan > #text then
|
||||
error("unterminated string in: " .. text)
|
||||
end
|
||||
tokens[#tokens + 1] = { kind = "string", value = table.concat(value) }
|
||||
at = scan + 1
|
||||
else
|
||||
local two = text:sub(at, at + 1)
|
||||
|
||||
if OPERATORS[two] then
|
||||
tokens[#tokens + 1] = { kind = "op", value = two }
|
||||
at = at + 2
|
||||
elseif OPERATORS[c] or c == "(" or c == ")" or c == "," or c == "." then
|
||||
tokens[#tokens + 1] = { kind = "op", value = c }
|
||||
at = at + 1
|
||||
else
|
||||
error("cannot read '" .. c .. "' in: " .. text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return tokens
|
||||
end
|
||||
|
||||
|
||||
-- Recursive descent, lowest precedence first. Every level returns Lua source.
|
||||
local function parser(text)
|
||||
local tokens = tokenize(text)
|
||||
local at = 1
|
||||
local p = {}
|
||||
|
||||
local function peek(kind, value)
|
||||
local token = tokens[at]
|
||||
|
||||
return token and (token.kind == kind) and ((value == nil) or (token.value == value))
|
||||
end
|
||||
|
||||
local function take()
|
||||
at = at + 1
|
||||
return tokens[at - 1]
|
||||
end
|
||||
|
||||
local function expect(kind, value)
|
||||
if not peek(kind, value) then
|
||||
error("expected " .. (value or kind) .. " in: " .. text)
|
||||
end
|
||||
return take()
|
||||
end
|
||||
|
||||
function p.primary()
|
||||
local token = take()
|
||||
|
||||
if token == nil then
|
||||
error("expression ends early: " .. text)
|
||||
end
|
||||
if token.kind == "number" then
|
||||
return token.value
|
||||
elseif token.kind == "string" then
|
||||
return string.format("%q", token.value)
|
||||
elseif token.kind == "op" and token.value == "(" then
|
||||
local inner = p.expression()
|
||||
|
||||
expect("op", ")")
|
||||
return "(" .. inner .. ")"
|
||||
elseif token.kind == "op" and token.value == "-" then
|
||||
return "(-" .. p.unary() .. ")"
|
||||
elseif token.kind == "name" then
|
||||
local name = token.value
|
||||
|
||||
if name == "true" or name == "false" or name == "nil" then
|
||||
return name
|
||||
end
|
||||
if name == "not" then
|
||||
return "(not " .. p.unary() .. ")"
|
||||
end
|
||||
if peek("op", "(") then
|
||||
local target = FUNCTIONS[name]
|
||||
local args = {}
|
||||
|
||||
if target == nil then
|
||||
error("no function called " .. name .. " in: " .. text)
|
||||
end
|
||||
take()
|
||||
if not peek("op", ")") then
|
||||
args[#args + 1] = p.expression()
|
||||
while peek("op", ",") do
|
||||
take()
|
||||
args[#args + 1] = p.expression()
|
||||
end
|
||||
end
|
||||
expect("op", ")")
|
||||
return target .. "(" .. table.concat(args, ", ") .. ")"
|
||||
end
|
||||
if peek("op", ".") then
|
||||
take()
|
||||
local field = expect("name").value
|
||||
|
||||
if name == "event" then
|
||||
return "event[" .. string.format("%q", field) .. "]"
|
||||
elseif NAMES[name] and (name == "self" or name == "other") then
|
||||
return "authorField(" .. name .. ", " .. string.format("%q", field) .. ")"
|
||||
end
|
||||
return "authorField(authorEntity(" .. string.format("%q", name) .. "), " .. string.format("%q", field) .. ")"
|
||||
end
|
||||
if NAMES[name] then
|
||||
return NAMES[name]
|
||||
end
|
||||
return "AUTHOR_VARS[" .. string.format("%q", name) .. "]"
|
||||
end
|
||||
error("unexpected '" .. tostring(token.value) .. "' in: " .. text)
|
||||
end
|
||||
|
||||
function p.unary()
|
||||
return p.primary()
|
||||
end
|
||||
|
||||
function p.power()
|
||||
local left = p.unary()
|
||||
|
||||
while peek("op", "^") do
|
||||
take()
|
||||
left = "(" .. left .. " ^ " .. p.unary() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.product()
|
||||
local left = p.power()
|
||||
|
||||
while peek("op", "*") or peek("op", "/") or peek("op", "%") do
|
||||
local op = take().value
|
||||
|
||||
left = "(" .. left .. " " .. op .. " " .. p.power() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.sum()
|
||||
local left = p.product()
|
||||
|
||||
while peek("op", "+") or peek("op", "-") or peek("op", "..") do
|
||||
local op = take().value
|
||||
|
||||
left = "(" .. left .. " " .. op .. " " .. p.product() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.comparison()
|
||||
local left = p.sum()
|
||||
|
||||
while peek("op") and OPERATORS[tokens[at].value] and tokens[at].value:match("[=<>~!]") do
|
||||
local op = OPERATORS[take().value]
|
||||
|
||||
left = "(" .. left .. " " .. op .. " " .. p.sum() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.conjunction()
|
||||
local left = p.comparison()
|
||||
|
||||
while peek("name", "and") do
|
||||
take()
|
||||
left = "(" .. left .. " and " .. p.comparison() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.expression()
|
||||
local left = p.conjunction()
|
||||
|
||||
while peek("name", "or") do
|
||||
take()
|
||||
left = "(" .. left .. " or " .. p.conjunction() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
local out = p.expression()
|
||||
|
||||
if at <= #tokens then
|
||||
error("unexpected '" .. tostring(tokens[at].value) .. "' in: " .. text)
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
-- An expression as Lua. A number or a boolean given as a value is itself.
|
||||
function authorExpression(value)
|
||||
if type(value) == "number" then
|
||||
return tostring(value)
|
||||
elseif type(value) == "boolean" then
|
||||
return tostring(value)
|
||||
elseif value == nil then
|
||||
return "nil"
|
||||
end
|
||||
|
||||
return parser(tostring(value))
|
||||
end
|
||||
|
||||
|
||||
-- ===== Parameters =============================================================================
|
||||
--
|
||||
-- A parameter's value as a Lua fragment, by the type the manifest declares for it.
|
||||
|
||||
local function fragment(kind, value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
if kind == "number" or kind == "expression" then
|
||||
return authorExpression(value)
|
||||
elseif kind == "boolean" then
|
||||
return value and "true" or "false"
|
||||
elseif kind == "entity" then
|
||||
if value == "self" or value == "other" then
|
||||
return value
|
||||
end
|
||||
return "authorEntity(" .. string.format("%q", value) .. ")"
|
||||
elseif kind == "scancode" then
|
||||
return "SCANCODE." .. value .. ".value"
|
||||
elseif kind == "switch" then
|
||||
return value
|
||||
elseif kind == "lua" then
|
||||
return value
|
||||
end
|
||||
|
||||
return string.format("%q", tostring(value))
|
||||
end
|
||||
|
||||
|
||||
-- The table a description carries for an entity's look or behaviour, written back out as source.
|
||||
local function tableSource(t)
|
||||
local parts = {}
|
||||
local keys = {}
|
||||
local function fragments(entry, item)
|
||||
local out = {}
|
||||
|
||||
for key in pairs(t) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
for _, key in ipairs(keys) do
|
||||
parts[#parts + 1] = string.format("%s = %s", key, literal(t[key]))
|
||||
for key, kind in pairs(entry.params or {}) do
|
||||
out[key] = fragment(kind, item[key])
|
||||
end
|
||||
|
||||
return "{ " .. table.concat(parts, ", ") .. " }"
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
-- One condition or action, through the manifest entry that owns it. An unknown name is a fault in
|
||||
-- the description rather than in the generated game, so it is reported here and not emitted.
|
||||
-- ===== Rules ==================================================================================
|
||||
|
||||
local function emitOne(set, item, what)
|
||||
local entry = set[item[1]]
|
||||
local name = item[1]
|
||||
local entry = set[name]
|
||||
|
||||
if entry == nil then
|
||||
debugPrint(string.format("Author: no %s called '%s'", what, tostring(item[1])))
|
||||
debugPrint("Author: no " .. what .. " called '" .. tostring(name) .. "'; skipped")
|
||||
return nil
|
||||
end
|
||||
|
||||
return entry.emit(item)
|
||||
return entry.emit(fragments(entry, item))
|
||||
end
|
||||
|
||||
|
||||
local function emitConditions(rule)
|
||||
local tests = {}
|
||||
-- A condition list is an AND; an entry named any is an OR of what it holds, and an entry named
|
||||
-- all an AND, nesting as deep as the author likes.
|
||||
local function emitConditions(list)
|
||||
local parts = {}
|
||||
|
||||
for _, item in ipairs(rule.when or {}) do
|
||||
for _, item in ipairs(list or {}) do
|
||||
local test = emitOne(AUTHOR.conditions, item, "condition")
|
||||
|
||||
if test ~= nil then
|
||||
tests[#tests + 1] = "(" .. test .. ")"
|
||||
parts[#parts + 1] = test
|
||||
end
|
||||
end
|
||||
if #tests == 0 then
|
||||
if list and list.any then
|
||||
local inner = {}
|
||||
|
||||
for _, item in ipairs(list.any) do
|
||||
if item[1] then
|
||||
local test = emitOne(AUTHOR.conditions, item, "condition")
|
||||
|
||||
if test then
|
||||
inner[#inner + 1] = test
|
||||
end
|
||||
else
|
||||
inner[#inner + 1] = emitConditions(item)
|
||||
end
|
||||
end
|
||||
if #inner > 0 then
|
||||
parts[#parts + 1] = "(" .. table.concat(inner, " or ") .. ")"
|
||||
end
|
||||
end
|
||||
if list and list.all then
|
||||
parts[#parts + 1] = emitConditions(list.all)
|
||||
end
|
||||
if #parts == 0 then
|
||||
return "true"
|
||||
end
|
||||
|
||||
-- Every condition on a rule has to hold. A rule needing "either" is two rules, which reads
|
||||
-- better in a list than a nested group does.
|
||||
return table.concat(tests, " and ")
|
||||
return table.concat(parts, " and ")
|
||||
end
|
||||
|
||||
|
||||
local function ruleWaits(rule)
|
||||
for _, item in ipairs(rule.act or {}) do
|
||||
local entry = AUTHOR.actions[item[1]]
|
||||
|
||||
if entry and entry.waits then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function emitRule(out, rule, index)
|
||||
out[#out + 1] = string.format("%s-- %s", INDENT, rule.note or ("rule " .. index))
|
||||
out[#out + 1] = string.format("%sif %s then", INDENT, emitConditions(rule))
|
||||
local event = AUTHOR.events[rule.on or "frame"]
|
||||
local filter = {}
|
||||
|
||||
if event == nil then
|
||||
debugPrint("Author: no event called '" .. tostring(rule.on) .. "'; rule " .. index .. " skipped")
|
||||
return
|
||||
end
|
||||
for key, kind in pairs(event.filter) do
|
||||
if rule[key] ~= nil then
|
||||
filter[#filter + 1] = key .. " = " .. fragment(kind, rule[key])
|
||||
end
|
||||
end
|
||||
out[#out + 1] = INDENT .. "{"
|
||||
out[#out + 1] = INDENT .. INDENT .. string.format("note = %q, on = %q,", rule.note or ("rule " .. index), rule.on or "frame")
|
||||
out[#out + 1] = INDENT .. INDENT .. "filter = { " .. table.concat(filter, ", ") .. " },"
|
||||
out[#out + 1] = INDENT .. INDENT .. string.format("each = %s, room = %s, waits = %s, controls = %s, interrupt = %s, guarded = %s,",
|
||||
rule.each and string.format("%q", rule.each) or "nil",
|
||||
rule.room and string.format("%q", rule.room) or "nil",
|
||||
tostring(ruleWaits(rule)),
|
||||
tostring(rule.controls ~= false),
|
||||
tostring(rule.interrupt == true),
|
||||
tostring((rule.when ~= nil) and ((#rule.when > 0) or (rule.when.any ~= nil) or (rule.when.all ~= nil))))
|
||||
out[#out + 1] = INDENT .. INDENT .. "run = function(self, other, event)"
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "if " .. emitConditions(rule.when) .. " then"
|
||||
for _, item in ipairs(rule.act or {}) do
|
||||
local statement = emitOne(AUTHOR.actions, item, "action")
|
||||
|
||||
if statement ~= nil then
|
||||
out[#out + 1] = INDENT .. INDENT .. statement
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. INDENT .. statement
|
||||
end
|
||||
end
|
||||
out[#out + 1] = INDENT .. "end"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. INDENT .. "return true"
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "end"
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "return false"
|
||||
out[#out + 1] = INDENT .. INDENT .. "end"
|
||||
out[#out + 1] = INDENT .. "},"
|
||||
end
|
||||
|
||||
|
||||
-- The whole game, as source. Returns a string; the caller decides where it goes.
|
||||
--
|
||||
-- Every built game loads the runtime sitting beside it. There is no other copy to load: nothing
|
||||
-- of Forge ships inside Singe, so Author.singe travels with the game that needs it. Framework is
|
||||
-- the engine's, as it is for every game ever written for Singe.
|
||||
function authorCompile(game)
|
||||
local out = {}
|
||||
-- A dialogue: its start and its nodes; a choice's when is an expression and its act a list of
|
||||
-- actions, compiled as a rule's are, with self and other absent.
|
||||
local function emitDialogue(out, name, dialogue)
|
||||
out[#out + 1] = INDENT .. string.format("[%q] = { start = %q, nodes = {", name, dialogue.start or "start")
|
||||
for nodeName, node in pairs(dialogue.nodes or {}) do
|
||||
out[#out + 1] = INDENT .. INDENT .. string.format("[%q] = { name = %q, who = %s, text = %s, seconds = %s, next = %s, choices = {",
|
||||
nodeName, nodeName,
|
||||
node.who and string.format("%q", node.who) or "nil",
|
||||
node.text and string.format("%q", node.text) or "nil",
|
||||
node.seconds and tostring(node.seconds) or "nil",
|
||||
node.next and string.format("%q", node.next) or "nil")
|
||||
for _, choice in ipairs(node.choices or {}) do
|
||||
local parts = { string.format("text = %q", choice.text or "..."), "next = " .. (choice.next and string.format("%q", choice.next) or "nil"), "once = " .. tostring(choice.once == true) }
|
||||
|
||||
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
|
||||
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
||||
out[#out + 1] = "--"
|
||||
out[#out + 1] = "-- This is an ordinary Singe game and can be edited by hand. Doing so and then"
|
||||
out[#out + 1] = "-- recompiling the description will overwrite it, so keep one or the other."
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = 'dofile("Singe/Framework.singe")'
|
||||
-- The game finds its own directory rather than trusting DIR. Framework sets DIR from the
|
||||
-- script the *engine was launched with*, which is this file only when the game is played
|
||||
-- directly; a game reached by dofile -- a test, a launcher, a menu that previews it -- would
|
||||
-- otherwise look for its runtime beside the caller and not find it. debug.getinfo names the
|
||||
-- chunk actually running, either way.
|
||||
out[#out + 1] = 'local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\\\])") or "")'
|
||||
out[#out + 1] = 'dofile(here .. "Author.singe")'
|
||||
out[#out + 1] = ""
|
||||
|
||||
out[#out + 1] = "authorBegin({"
|
||||
for _, layer in ipairs(game.layers or {}) do
|
||||
out[#out + 1] = INDENT .. tableSource(layer) .. ","
|
||||
end
|
||||
out[#out + 1] = "})"
|
||||
out[#out + 1] = ""
|
||||
|
||||
for _, entity in ipairs(game.entities or {}) do
|
||||
local parts = {}
|
||||
|
||||
parts[#parts + 1] = string.format("id = %q", entity.id)
|
||||
parts[#parts + 1] = string.format("x = %s, y = %s", entity.x or 0, entity.y or 0)
|
||||
parts[#parts + 1] = "look = " .. tableSource(entity.look)
|
||||
if entity.behaviours ~= nil and #entity.behaviours > 0 then
|
||||
local made = {}
|
||||
|
||||
for _, b in ipairs(entity.behaviours) do
|
||||
made[#made + 1] = tableSource(b)
|
||||
if choice.when then
|
||||
parts[#parts + 1] = "when = function() local self, other, event = nil, nil, {} return " .. emitConditions(type(choice.when) == "table" and choice.when or { { "test", expr = choice.when } }) .. " end"
|
||||
end
|
||||
parts[#parts + 1] = "behaviours = { " .. table.concat(made, ", ") .. " }"
|
||||
if choice.act then
|
||||
local body = {}
|
||||
|
||||
for _, item in ipairs(choice.act) do
|
||||
local statement = emitOne(AUTHOR.actions, item, "action")
|
||||
|
||||
if statement then
|
||||
body[#body + 1] = statement
|
||||
end
|
||||
end
|
||||
parts[#parts + 1] = "run = function() local self, other, event = nil, nil, {} " .. table.concat(body, " ") .. " end"
|
||||
end
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "{ " .. table.concat(parts, ", ") .. " },"
|
||||
end
|
||||
out[#out + 1] = "authorMake({ " .. table.concat(parts, ", ") .. " })"
|
||||
out[#out + 1] = INDENT .. INDENT .. "} },"
|
||||
end
|
||||
out[#out + 1] = ""
|
||||
|
||||
out[#out + 1] = "-- The rules, in the order they were written. Every one is tested every frame."
|
||||
out[#out + 1] = "local function rules()"
|
||||
for index, rule in ipairs(game.rules or {}) do
|
||||
emitRule(out, rule, index)
|
||||
end
|
||||
out[#out + 1] = "end"
|
||||
out[#out + 1] = ""
|
||||
|
||||
out[#out + 1] = "onKeyPressed = authorKeyDown"
|
||||
out[#out + 1] = "onKeyReleased = authorKeyUp"
|
||||
out[#out + 1] = "onInputPressed = authorSwitchDown"
|
||||
out[#out + 1] = "onInputReleased = authorSwitchUp"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "function onOverlayUpdate()"
|
||||
out[#out + 1] = INDENT .. "authorFrame(rules)"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = INDENT .. "return OVERLAY_UPDATED"
|
||||
out[#out + 1] = "end"
|
||||
out[#out + 1] = ""
|
||||
|
||||
return table.concat(out, "\n")
|
||||
end
|
||||
|
||||
|
||||
-- Compiles a description file to a game beside it. Returns the path written.
|
||||
-- Copies a file through the VFS, so the source may be inside a database and the destination is an
|
||||
-- ordinary file on disc. Used to put the runtime beside a released game.
|
||||
function authorCopy(fromPath, toPath)
|
||||
local input = io.open(fromPath, "rb")
|
||||
local output
|
||||
|
||||
if input == nil then
|
||||
debugPrint("Author: cannot read " .. tostring(fromPath))
|
||||
return false
|
||||
end
|
||||
output = io.open(toPath, "wb")
|
||||
if output == nil then
|
||||
input:close()
|
||||
debugPrint("Author: cannot write " .. tostring(toPath))
|
||||
return false
|
||||
end
|
||||
output:write(input:read("a"))
|
||||
input:close()
|
||||
output:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- The directory part of a path, with its separator, or "" for a bare name.
|
||||
local function directoryOf(path)
|
||||
return (string.match(path, "^(.*[/\\])") or "")
|
||||
end
|
||||
|
||||
|
||||
-- Compiles a description and puts the runtime beside the result, because that is what the result
|
||||
-- loads. Building without copying it would produce a game that cannot start.
|
||||
function authorBuild(descriptionFile, outputFile)
|
||||
local chunk = assert(loadfile(descriptionFile))
|
||||
local game = chunk()
|
||||
local file
|
||||
|
||||
game.source = descriptionFile
|
||||
file = assert(io.open(outputFile, "w"))
|
||||
file:write(authorCompile(game))
|
||||
file:close()
|
||||
authorCopy(AUTHOR_RUNTIME, directoryOf(outputFile) .. "Author.singe")
|
||||
|
||||
return outputFile
|
||||
out[#out + 1] = INDENT .. "} },"
|
||||
end
|
||||
|
||||
|
||||
-- ===== Reading and writing a description ======================================================
|
||||
--
|
||||
-- The editor produces descriptions, so the format has to survive a round trip: load, change
|
||||
-- nothing, save, and the result must compile to the same game. scene54 asserts exactly that.
|
||||
-- nothing, save, and the result must compile to the same game.
|
||||
|
||||
local function literal(value)
|
||||
if type(value) == "string" then
|
||||
return string.format("%q", value)
|
||||
end
|
||||
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
|
||||
local function indentOf(depth)
|
||||
|
|
@ -256,10 +524,10 @@ local function indentOf(depth)
|
|||
end
|
||||
|
||||
|
||||
-- Whether a table holds only leaves, in which case it is written on one line. A rule reads far
|
||||
-- better as { "keyHeld", key = "LEFT" } than as six lines, and it is what an author typed.
|
||||
-- Whether a table holds only leaves, in which case it is written on one line. A condition reads
|
||||
-- far better as { "keyHeld", key = "LEFT" } than as six lines, and it is what an author typed.
|
||||
local function isLeaf(t)
|
||||
for key, value in pairs(t) do
|
||||
for _, value in pairs(t) do
|
||||
if type(value) == "table" then
|
||||
return false
|
||||
end
|
||||
|
|
@ -277,9 +545,6 @@ local function valueSource(value, depth)
|
|||
local parts = {}
|
||||
local keys = {}
|
||||
local count = 0
|
||||
local sep
|
||||
local open
|
||||
local close
|
||||
|
||||
if type(value) ~= "table" then
|
||||
return literal(value)
|
||||
|
|
@ -296,7 +561,12 @@ local function valueSource(value, depth)
|
|||
end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
for _, key in ipairs(keys) do
|
||||
parts[#parts + 1] = string.format("%s = %s", key, valueSource(value[key], depth + 1))
|
||||
local name = tostring(key)
|
||||
|
||||
if not name:match("^[%a_][%w_]*$") then
|
||||
name = "[" .. literal(key) .. "]"
|
||||
end
|
||||
parts[#parts + 1] = string.format("%s = %s", name, valueSource(value[key], depth + 1))
|
||||
end
|
||||
if #parts == 0 then
|
||||
return "{}"
|
||||
|
|
@ -304,11 +574,8 @@ local function valueSource(value, depth)
|
|||
if isLeaf(value) then
|
||||
return "{ " .. table.concat(parts, ", ") .. " }"
|
||||
end
|
||||
sep = ",\n" .. indentOf(depth + 1)
|
||||
open = "{\n" .. indentOf(depth + 1)
|
||||
close = "\n" .. indentOf(depth) .. "}"
|
||||
|
||||
return open .. table.concat(parts, sep) .. close
|
||||
return "{\n" .. indentOf(depth + 1) .. table.concat(parts, ",\n" .. indentOf(depth + 1)) .. "\n" .. indentOf(depth) .. "}"
|
||||
end
|
||||
|
||||
|
||||
|
|
@ -341,3 +608,215 @@ function authorSave(game, path)
|
|||
|
||||
return path
|
||||
end
|
||||
|
||||
|
||||
function authorCopy(fromPath, toPath)
|
||||
local input = io.open(fromPath, "rb")
|
||||
local output
|
||||
|
||||
if input == nil then
|
||||
return false
|
||||
end
|
||||
output = io.open(toPath, "wb")
|
||||
if output == nil then
|
||||
input:close()
|
||||
return false
|
||||
end
|
||||
output:write(input:read("a"))
|
||||
input:close()
|
||||
output:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- ===== Checking ===============================================================================
|
||||
--
|
||||
-- What the compiler can tell an author before the game runs: a kind that is not declared, a
|
||||
-- condition the manifest does not have, an expression that does not parse. Returns a list of
|
||||
-- messages, empty when the description is sound.
|
||||
|
||||
function authorCheck(game)
|
||||
local problems = {}
|
||||
local kinds = game.kinds or {}
|
||||
|
||||
local function problem(text)
|
||||
problems[#problems + 1] = text
|
||||
end
|
||||
|
||||
local function checkItems(set, list, what, where)
|
||||
for _, item in ipairs(list or {}) do
|
||||
local entry = set[item[1]]
|
||||
|
||||
if entry == nil then
|
||||
problem(where .. ": no " .. what .. " called '" .. tostring(item[1]) .. "'")
|
||||
else
|
||||
for key, kind in pairs(entry.params) do
|
||||
if (kind == "number" or kind == "expression") and (type(item[key]) == "string") then
|
||||
local ok, err = pcall(parser, item[key])
|
||||
|
||||
if not ok then
|
||||
problem(where .. ": " .. tostring(err))
|
||||
end
|
||||
elseif (kind == "kind") and (item[key] ~= nil) and (kinds[item[key]] == nil) then
|
||||
problem(where .. ": no kind called '" .. tostring(item[key]) .. "'")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A field the manifest does not declare is a misspelling nine times in ten, and it is
|
||||
-- otherwise silently ignored.
|
||||
local function checkFields(t, declared, skip, where)
|
||||
for key in pairs(t or {}) do
|
||||
if (type(key) == "string") and (declared[key] == nil) and not skip[key] then
|
||||
problem(where .. ": '" .. key .. "' is not something it takes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for name, kind in pairs(kinds) do
|
||||
local look = kind.look and AUTHOR.looks[kind.look.kind] or nil
|
||||
|
||||
if kind.look and (look == nil) then
|
||||
problem("kind " .. name .. ": no look called '" .. tostring(kind.look.kind) .. "'")
|
||||
elseif look then
|
||||
checkFields(kind.look, look.params, { kind = true }, "kind " .. name .. "'s look")
|
||||
end
|
||||
for _, b in ipairs(kind.behaviours or {}) do
|
||||
local behaviour = AUTHOR.behaviours[b.kind]
|
||||
|
||||
if behaviour == nil then
|
||||
problem("kind " .. name .. ": no behaviour called '" .. tostring(b.kind) .. "'")
|
||||
else
|
||||
checkFields(b, behaviour.params, { kind = true }, "kind " .. name .. "'s " .. b.kind)
|
||||
end
|
||||
end
|
||||
checkFields(kind, { look = true, vars = true, behaviours = true }, {}, "kind " .. name)
|
||||
end
|
||||
if #(game.rooms or {}) == 0 then
|
||||
problem("the game has no rooms")
|
||||
end
|
||||
for _, room in ipairs(game.rooms or {}) do
|
||||
for _, entry in ipairs(room.entities or {}) do
|
||||
if kinds[entry.kind] == nil then
|
||||
problem("room " .. tostring(room.name) .. ": no kind called '" .. tostring(entry.kind) .. "'")
|
||||
end
|
||||
end
|
||||
end
|
||||
for index, rule in ipairs(game.rules or {}) do
|
||||
local where = "rule " .. index .. " (" .. tostring(rule.note or "") .. ")"
|
||||
|
||||
if rule.on and (AUTHOR.events[rule.on] == nil) then
|
||||
problem(where .. ": no event called '" .. tostring(rule.on) .. "'")
|
||||
elseif AUTHOR.events[rule.on or "frame"] then
|
||||
checkFields(rule, AUTHOR.events[rule.on or "frame"].filter, { note = true, on = true, each = true, room = true, when = true, act = true, controls = true, interrupt = true }, where)
|
||||
end
|
||||
if rule.each and (kinds[rule.each] == nil) then
|
||||
problem(where .. ": no kind called '" .. tostring(rule.each) .. "'")
|
||||
end
|
||||
checkItems(AUTHOR.conditions, rule.when, "condition", where)
|
||||
checkItems(AUTHOR.conditions, rule.when and rule.when.any, "condition", where)
|
||||
checkItems(AUTHOR.actions, rule.act, "action", where)
|
||||
end
|
||||
|
||||
return problems
|
||||
end
|
||||
|
||||
|
||||
-- ===== The whole game =========================================================================
|
||||
|
||||
-- The whole game, as source. Returns a string; the caller decides where it goes.
|
||||
--
|
||||
-- Every built game loads the runtime sitting beside it. There is no other copy to load: nothing
|
||||
-- of Forge ships inside Singe, so Author.singe travels with the game that needs it. Framework is
|
||||
-- the engine's, as it is for every game ever written for Singe.
|
||||
function authorCompile(game)
|
||||
local out = {}
|
||||
local data = {}
|
||||
|
||||
for key, value in pairs(game) do
|
||||
if (key ~= "rules") and (key ~= "source") and (key ~= "dialogues") then
|
||||
data[key] = value
|
||||
end
|
||||
end
|
||||
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
|
||||
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
||||
out[#out + 1] = "--"
|
||||
out[#out + 1] = "-- This is an ordinary Singe game and can be edited by hand. Doing so and then"
|
||||
out[#out + 1] = "-- recompiling the description will overwrite it, so keep one or the other."
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = 'dofile("Singe/Framework.singe")'
|
||||
-- The game finds its own directory rather than trusting DIR. Framework sets DIR from the
|
||||
-- script the *engine was launched with*, which is this file only when the game is played
|
||||
-- directly; a game reached by dofile -- a test, a launcher, a menu that previews it -- would
|
||||
-- otherwise look for its runtime beside the caller and not find it. debug.getinfo names the
|
||||
-- chunk actually running, either way.
|
||||
out[#out + 1] = 'local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\\\])") or "")'
|
||||
out[#out + 1] = 'dofile(here .. "Author.singe")'
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "-- The description, as the runtime wants it: everything but the rules, which follow as code."
|
||||
out[#out + 1] = "authorBegin(" .. valueSource(data, 0) .. ")"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "-- The rules. Each is a trigger, its filter, and a function that tests the conditions and"
|
||||
out[#out + 1] = "-- does the actions; one with an action that waits runs as a coroutine."
|
||||
out[#out + 1] = "authorRules({"
|
||||
for index, rule in ipairs(game.rules or {}) do
|
||||
emitRule(out, rule, index)
|
||||
end
|
||||
out[#out + 1] = "})"
|
||||
out[#out + 1] = ""
|
||||
if game.dialogues then
|
||||
out[#out + 1] = "-- The dialogues: nodes of a line and choices, each choice's condition and actions as code."
|
||||
out[#out + 1] = "authorDialogues({"
|
||||
for name, dialogue in pairs(game.dialogues) do
|
||||
emitDialogue(out, name, dialogue)
|
||||
end
|
||||
out[#out + 1] = "})"
|
||||
out[#out + 1] = ""
|
||||
end
|
||||
out[#out + 1] = "onKeyPressed = authorKeyDown"
|
||||
out[#out + 1] = "onKeyReleased = authorKeyUp"
|
||||
out[#out + 1] = "onInputPressed = authorSwitchDown"
|
||||
out[#out + 1] = "onInputReleased = authorSwitchUp"
|
||||
out[#out + 1] = "onMouseMoved = authorMouseMoved"
|
||||
out[#out + 1] = "onCollision = authorCollision"
|
||||
out[#out + 1] = "onTrigger = authorTrigger"
|
||||
out[#out + 1] = "onNavArrived = authorNavArrived"
|
||||
out[#out + 1] = "onMidiMessage = authorMidi"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "function onOverlayUpdate()"
|
||||
out[#out + 1] = INDENT .. "authorFrame()"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = INDENT .. "return OVERLAY_UPDATED"
|
||||
out[#out + 1] = "end"
|
||||
out[#out + 1] = ""
|
||||
|
||||
return table.concat(out, "\n")
|
||||
end
|
||||
|
||||
|
||||
local function directoryOf(path)
|
||||
return (string.match(path, "^(.*[/\\])") or "")
|
||||
end
|
||||
|
||||
|
||||
-- Compiles a description file to a game beside it, with the runtime beside that, because that is
|
||||
-- what the result loads. Returns the path written.
|
||||
function authorBuild(descriptionFile, outputFile)
|
||||
local chunk = assert(loadfile(descriptionFile))
|
||||
local game = chunk()
|
||||
local file
|
||||
|
||||
game.source = descriptionFile
|
||||
for _, problem in ipairs(authorCheck(game)) do
|
||||
debugPrint("Author: " .. problem)
|
||||
end
|
||||
file = assert(io.open(outputFile, "w"))
|
||||
file:write(authorCompile(game))
|
||||
file:close()
|
||||
authorCopy(AUTHOR_RUNTIME, directoryOf(outputFile) .. "Author.singe")
|
||||
|
||||
return outputFile
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
.row { margin: 2px 8px; padding: 4px 6px; background-color: #242433ff; }
|
||||
.row:hover { background-color: #32324aff; }
|
||||
.row.selected { background-color: #c03c96ff; color: #ffffff; }
|
||||
.thumb { display: inline-block; vertical-align: middle; margin-right: 6px; width: 24px; text-align: center; color: #ffcf4a; }
|
||||
.thumb.dim { color: #6a6a80; }
|
||||
.thumb.swatch { height: 16px; width: 16px; margin-left: 4px; margin-right: 10px; }
|
||||
.part { margin: 1px 8px 1px 18px; padding: 2px 5px; background-color: #1b1b28ff; color: #aeb4c8; font-size: 12px; }
|
||||
.part.here { background-color: #4a3a6aff; color: #ffffff; }
|
||||
#detail { margin: 14px 8px; padding: 6px; background-color: #10101aff; color: #b9bfd4; }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,7 @@ for doc in "$src" "$forge"; do
|
|||
exit 1
|
||||
fi
|
||||
done
|
||||
for tool in asciidoctor asciidoctor-pdf; do
|
||||
for tool in asciidoctor asciidoctor-pdf lua5.4; do
|
||||
if ! command -v "$tool" > /dev/null; then
|
||||
echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
|
||||
exit 1
|
||||
|
|
@ -25,6 +25,8 @@ version=$(sed -n 's/^project(singe2 VERSION \([0-9.]*\).*/\1/p' "$here/CMakeList
|
|||
|
||||
out=$here/.builddir
|
||||
mkdir -p "$out"
|
||||
# The Forge vocabulary tables come from the manifest itself, so the manual cannot drift from it.
|
||||
lua5.4 "$here/util/forgeVocabulary.lua" > "$here/docs/ForgeVocabulary.adoc"
|
||||
asciidoctor -a revnumber="$version" "$src" -o "$out/Manual.html"
|
||||
asciidoctor-pdf -a revnumber="$version" "$src" -o "$out/Manual.pdf"
|
||||
asciidoctor -a revnumber="$version" "$forge" -o "$out/Forge.html"
|
||||
|
|
|
|||
|
|
@ -471,9 +471,12 @@ if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux"))
|
|||
COMMAND ${CMAKE_COMMAND} -E rm -rf ${forgePack} ${forgeStage}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets/Forge ${forgeStage}/Forge
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
|
||||
# The samples that need no art beyond what the engine ships, so the chooser has something to
|
||||
# open on a fresh install. They come from the test set, which is the one copy of them.
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.game ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.game ${CMAKE_SOURCE_DIR}/testScripts/author/qte.game ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.game ${forgeStage}/Forge/
|
||||
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
|
||||
COMMAND ${CMAKE_COMMAND} -E chdir ${forgeStage} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${forgeStage}/Forge ${forgePack}
|
||||
DEPENDS singe ${forgeSources} ${forgeManual}
|
||||
DEPENDS singe ${forgeSources} ${forgeManual} ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.game ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.game ${CMAKE_SOURCE_DIR}/testScripts/author/qte.game ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.game
|
||||
COMMENT "Packing Forge into .builddir/Forge.game"
|
||||
VERBATIM
|
||||
)
|
||||
|
|
|
|||
654
docs/Forge.adoc
654
docs/Forge.adoc
|
|
@ -18,61 +18,70 @@ Forge is Singe's authoring tool: a way to make a game by describing it --
|
|||
placing things, attaching behaviours, writing rules -- and having ordinary
|
||||
Singe Lua written for you. It is itself a Singe game and is distributed on
|
||||
its own, beside the engine rather than inside it. This document covers the
|
||||
description format, the vocabulary, the compiler and the editor. The engine
|
||||
description format, the vocabulary, the compiler, and the editor. The engine
|
||||
calls the generated code makes (`playerNew`, `collidePointRect`,
|
||||
`onKeyPressed`, `scriptPush` and the rest) are documented in the Singe Manual.
|
||||
`onKeyPressed`, `scriptPush`, and the rest) are documented in the Singe Manual.
|
||||
|
||||
== Describing a Game Instead of Writing One
|
||||
|
||||
A game can be written as a *description* -- a table of layers, entities and
|
||||
rules -- and compiled into an ordinary Singe game.
|
||||
`Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime
|
||||
the result calls. **No part of Forge ships with Singe** -- not the editor, not
|
||||
the compiler, not the runtime -- so a game built with it carries its own copy of
|
||||
that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules
|
||||
become real Lua `if` statements, so a description costs nothing per frame on a
|
||||
Raspberry Pi, and the game it produces can be opened, read and edited by hand
|
||||
like any other.
|
||||
A game can be written as a *description* -- a table of kinds, rooms, and rules
|
||||
-- and compiled into an ordinary Singe game. `Forge/AuthorCompile.singe` does
|
||||
the compiling and `Author.singe` is the runtime the result calls. **No part of
|
||||
Forge ships with Singe** -- not the editor, not the compiler, not the runtime --
|
||||
so a game built with it carries its own copy of that runtime and stands entirely
|
||||
on its own. Nothing is interpreted at run time: the rules become real Lua
|
||||
functions, so a description costs nothing per frame on a Raspberry Pi, and the
|
||||
game it produces can be opened, read, and edited by hand like any other.
|
||||
|
||||
There is no notion of genre anywhere in it. A game declares which of the
|
||||
There is no notion of genre anywhere in it. A game declares which of the
|
||||
engine's own layers it uses, and that is the only difference between a light
|
||||
gun game, a platformer and a quick-time event over video.
|
||||
gun game, a platformer, and a quick-time event over video.
|
||||
|
||||
=== The three nouns
|
||||
=== The nouns
|
||||
|
||||
*Layers* are what the game draws through: `world2d` (physics in the XY plane,
|
||||
drawn into the overlay), `overlay` (flat drawing over everything) and `disc`
|
||||
(the video the game is played over). A game lists the ones it wants.
|
||||
*Layers* are what the game draws through: `world2d` (physics in the XY plane,
|
||||
drawn into the overlay), `scene3d` (the 3D scene, with a sun, a sky, and fog;
|
||||
positions are world units), `overlay` (flat drawing), `disc` (the video the
|
||||
game is played over), `hud` (an RmlUi document bound to the game's vars), and
|
||||
`music`. A game lists the ones it wants.
|
||||
|
||||
*Entities* are things on a layer. Each has a position, a `look` (`box`,
|
||||
`sprite` or `text`) and any number of behaviours. Every entity is a node,
|
||||
whether or not the 3D scene is drawing, which is what lets a 2D game built this
|
||||
way run on a machine with no GPU.
|
||||
*Kinds* are what things are: a `look` (`box`, `sprite`, `text`, or `none` in
|
||||
2D; `model`, `mesh`, `light`, `billboard`, or `text3d` in the scene), any
|
||||
number of `behaviours`, and `vars` -- the state every instance starts with.
|
||||
Everything placed or spawned is an instance of a kind.
|
||||
|
||||
*Behaviours* are bundles over engine calls that already exist: `platformer` is
|
||||
the character controller (`playerNew` and friends), `solid` is a
|
||||
static body, `drift` moves at a constant velocity. Attaching one is a line in
|
||||
the description rather than code.
|
||||
*Rooms* are where things are. A room lists its `entities` -- a kind, an id, a
|
||||
position, and any vars that differ -- and its `tracks`. A room keeps its state
|
||||
when it is left unless it says `reset = true`; vars declared on the game live
|
||||
across rooms.
|
||||
|
||||
*Rules* are conditions and actions. Every rule is tested every frame, in the
|
||||
order written, and all of a rule's conditions must hold for its actions to run.
|
||||
*Rules* are a trigger, conditions, and actions. `on` names the event -- every
|
||||
frame, a collision between two kinds, a shot landing, a death, a timer, a disc
|
||||
frame passing -- and `each` scopes the rule to every instance of a kind, with
|
||||
`self` bound. All of a rule's conditions must hold for its actions to run; an
|
||||
`any` group inside them is an OR.
|
||||
|
||||
.A description, in full
|
||||
[source,lua]
|
||||
----
|
||||
return {
|
||||
title = "One rule",
|
||||
title = "One rule",
|
||||
layers = { { kind = "world2d", gravity = 1500 } },
|
||||
entities = {
|
||||
{ id = "ground", x = 360, y = 440,
|
||||
look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
|
||||
behaviours = { { kind = "solid" } } },
|
||||
{ id = "hero", x = 120, y = 380,
|
||||
look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
|
||||
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
|
||||
vars = { score = 0 },
|
||||
kinds = {
|
||||
ground = { look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
|
||||
behaviours = { { kind = "solid" } } },
|
||||
hero = { look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
|
||||
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
|
||||
},
|
||||
rules = {
|
||||
rooms = {
|
||||
{ name = "start",
|
||||
entities = { { kind = "ground", id = "ground", x = 360, y = 440 },
|
||||
{ kind = "hero", id = "hero", x = 120, y = 380 } } }
|
||||
},
|
||||
rules = {
|
||||
{ note = "Run right",
|
||||
on = "frame",
|
||||
when = { { "keyHeld", key = "RIGHT" } },
|
||||
act = { { "run", entity = "hero", direction = 1 } } }
|
||||
}
|
||||
|
|
@ -87,56 +96,375 @@ dofile("Forge/AuthorCompile.singe")
|
|||
dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
|
||||
----
|
||||
|
||||
`testScripts/author/platformer.game` and `testScripts/author/qte.game` are
|
||||
worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile
|
||||
and play them.
|
||||
`testScripts/author/` holds the worked examples -- a platformer, a QTE over
|
||||
video, a shoot-em-up, a light gun game over video, a rail shooter, a 3D
|
||||
platformer, an adventure in 2D and in 3D, a racing game, a tower defence,
|
||||
branching video, a first-person shooter, breakout, a maze, third-person
|
||||
action, a space shooter, a rhythm game, and bowling, with two more for the
|
||||
sprite, tile, MIDI, water, and soft-body pieces -- and
|
||||
`testScripts/scene52.singe` onward compile and play them.
|
||||
|
||||
=== The vocabulary, and adding to it
|
||||
=== Instances, vars, and states
|
||||
|
||||
Conditions and actions are not built into the compiler. Each is an entry in
|
||||
the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind
|
||||
of game is a set of entries rather than a new release. The conditions today
|
||||
are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`,
|
||||
`touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`,
|
||||
`moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`.
|
||||
A kind's `vars` are what each instance starts with: `vars = { health = 3,
|
||||
ammo = 6 }`. An entity in a room may override them (`vars = { health = 5 }`),
|
||||
and rules read and change them: `self.health` in an expression, `setVar` and
|
||||
`addVar` as actions. `state` is a var every instance has, `"idle"` to begin
|
||||
with; `setState` changes it and `inState` tests it.
|
||||
|
||||
`once` deserves a word. Rules run every frame, so anything that should happen
|
||||
a single time -- a door opening, a score awarded -- needs it:
|
||||
Vars declared on the game -- `vars = { score = 0, lives = 3 }` -- belong to
|
||||
no instance and are read by name: `score`, `lives`. `setVar` and `addVar`
|
||||
with no entity change them, and `addScore` is `addVar` on `score`.
|
||||
|
||||
Instances are made when a room is entered, by a `spawner` or `shooter`
|
||||
behaviour, or by the `spawn` action, and go with `destroy`, with a `health`
|
||||
that reaches zero, or with a `projectile` that leaves the picture. An
|
||||
instance made at run time has an id of its own (`raider#7`); rules reach it as
|
||||
`self` or `other`, or through `each`.
|
||||
|
||||
=== Events
|
||||
|
||||
A rule's `on` says what triggers it:
|
||||
|
||||
[cols="1,3"]
|
||||
|===
|
||||
| `frame` | every frame, the default
|
||||
| `pressed`, `released` | a key or a switch, once per press; `key` or `switch` narrows it
|
||||
| `collision` | an instance of kind `a` began touching one of kind `b`; `self` is `a`, `other` is `b`
|
||||
| `hit`, `miss` | a gun's shot landed on `self` (`other` is the gun), or landed on nothing; `event.offscreen` says whether a miss left the picture
|
||||
| `death`, `spawn` | `self`'s health reached zero; `self` was just made
|
||||
| `timer` | a timer on `self` went off; `event.name` says which
|
||||
| `frameReached` | the disc passed `frame`
|
||||
| `enter`, `leave` | an instance of kind `b` entered or left a trigger of kind `a`
|
||||
| `arrived` | a seeking instance reached its target
|
||||
| `stopped`, `railEnd` | a rail camera reached a stop (`event.name`), or the end of its track
|
||||
| `animationDone` | a clip that does not loop ended on `self`
|
||||
| `roomStart`, `roomEnd`, `gameOver` | the game's own moments
|
||||
|===
|
||||
|
||||
The event's parameters are written on the rule itself (`on = "collision",
|
||||
a = "shot", b = "raider"`). `each = "raider"` runs a frame rule once per live
|
||||
raider, and narrows an event rule to that kind; `room = "hall"` keeps a rule
|
||||
to one room.
|
||||
|
||||
=== Expressions
|
||||
|
||||
Anywhere a number goes, an expression may go instead, as a string: numbers,
|
||||
strings, `+ - * / % ^ ..`, comparisons, `and`, `or`, `not`, and the names
|
||||
below. Nothing else is allowed -- an expression is checked when the game is
|
||||
built -- and the `lua` action is the way past it.
|
||||
|
||||
[cols="1,2"]
|
||||
|===
|
||||
| `self.health`, `other.x`, `event.frame` | the instance the rule is about, the other one, the event's own fields
|
||||
| `hero.x` | a placed entity's field or var, by id
|
||||
| `score`, `lives` | the game's vars
|
||||
| `time`, `frame` | seconds since the game began; the disc frame
|
||||
| `count("raider")` | live instances of a kind
|
||||
| `random(a, b)`, `distance(a, b)`, `has("key")` | a number in a range; between two instances; whether the inventory holds an item
|
||||
| `abs`, `min`, `max`, `floor` | arithmetic
|
||||
|===
|
||||
|
||||
A `test` condition takes one: `{ "test", expr = "count('raider') == 0" }`.
|
||||
|
||||
=== Sequences
|
||||
|
||||
Some actions take time. `say` shows a line and waits for it; `wait` waits.
|
||||
A rule with one of them runs as a coroutine, its actions in order, each waiting
|
||||
its turn -- a cut-scene is the same shape as `addScore`. While it runs the
|
||||
same rule does not start again for the same instance unless the rule says
|
||||
`interrupt = true`, and `controls = false` on the rule takes the keys away for
|
||||
its duration.
|
||||
|
||||
=== Tracks
|
||||
|
||||
A track is keys by disc frame (`key = "frame"`) or by time (`key = "time"`),
|
||||
and a room may hold any number. A track of `boxes` is a hitbox that moves
|
||||
with the picture:
|
||||
|
||||
[source,lua]
|
||||
----
|
||||
{ when = { { "touching", entity = "hero", other = "prize" },
|
||||
{ "once", tag = "prize" } },
|
||||
act = { { "addScore", amount = 100 },
|
||||
{ "show", entity = "prize", visible = false } } }
|
||||
tracks = {
|
||||
{ name = "bandit", key = "frame",
|
||||
boxes = { { at = 120, x = 100, y = 200, w = 60, h = 100 },
|
||||
{ at = 220, x = 400, y = 200, w = 60, h = 100 } } }
|
||||
}
|
||||
----
|
||||
|
||||
Between keys the box is interpolated; outside them there is none. A kind with
|
||||
`{ kind = "hitbox", track = "bandit" }` is hit where the track says, which is
|
||||
how a light gun game over video is 2,750 rectangles no longer.
|
||||
|
||||
A track of `points` is a rail for a camera: positions by time, each looking
|
||||
at a point (`look = { x, y, z }`) or at an entity by id, and any point may be
|
||||
a `stop` that holds the camera there until a `pathNext` action sends it on --
|
||||
which is what House of the Dead does at every doorway.
|
||||
|
||||
[source,lua]
|
||||
----
|
||||
{ name = "rail", key = "time",
|
||||
points = { { at = 0, x = 0, y = 1.6, z = 0, look = { 0, 1.2, -10 } },
|
||||
{ at = 4, x = 0, y = 1.6, z = -6, look = { 0, 1.2, -16 }, stop = "hall" } } }
|
||||
----
|
||||
|
||||
=== Games in the scene
|
||||
|
||||
A game with a `scene3d` layer places its kinds in world units and draws them
|
||||
through the engine's renderer. A `model` look is a glTF file with a scale and
|
||||
a starting clip; `mesh` is a box, sphere, cylinder, or plane with a colour;
|
||||
`light` and `billboard` are what they say. The 2D looks still draw on the
|
||||
overlay over the scene, which is how a score readout works in both.
|
||||
|
||||
The behaviours that live there: `character` is the engine's controller
|
||||
moving by the `keys` vars relative to the camera; `camera` draws the scene
|
||||
from the instance -- `fixed`, `follow` (an offset from a target), `orbit`
|
||||
and `first` (the mouse looks), or `rail`; `seek` walks toward a target and
|
||||
raises `arrived`; `animator` plays a clip per state and raises
|
||||
`animationDone`; `target` gives a `gun` something to hit, with `zones` on
|
||||
named bones so a shot says which part it landed on (`hitPart`); `health`
|
||||
with `ragdoll` lets a model fall limp on death; `solid`, `body`, and
|
||||
`trigger` are the physics. A gun in the scene casts a ray through the
|
||||
pointer, so the same `hit` and `miss` rules serve a light gun over video and
|
||||
a rail shooter.
|
||||
|
||||
`testScripts/author/railshooter.game` is House of the Dead in a table: a
|
||||
rail with two stops, zombies that spawn there, walk to the camera, and bite
|
||||
on a timer unless they are killed, headshots through a zone on the head bone,
|
||||
and the rail moving on when the stop is clear.
|
||||
|
||||
=== Adventures: rooms, walking, hotspots, verbs, and talk
|
||||
|
||||
A point-and-click adventure -- Sierra's or LucasArts' -- is rooms that
|
||||
remember themselves, a character that walks where the player clicks, hotspots
|
||||
a verb is used on, an inventory, dialogue, and cut-scenes. All of it is the
|
||||
same nouns.
|
||||
|
||||
*Walking.* A 2D room declares `walk = { { x, y, x, y, ... }, ... }`, its
|
||||
walkable floor as polygons in overlay coordinates; a 3D room declares
|
||||
`navFrom = { "floor", "ledge" }`, the entities whose meshes are its floor.
|
||||
Either is baked into a navigation mesh when the room is entered, and a kind
|
||||
with a `walker` behaviour walks it: `walkTo` sends it to a point (and waits),
|
||||
`walkToPointer` to where the player clicked, `walkToHotspot` to a hotspot's
|
||||
walk point; it raises `arrived`, and its state is `walk` or `idle` as it
|
||||
goes. A painted room may say `depthSort = true`, so what is lower on the
|
||||
picture is drawn over what is higher, and `scaleBy = { { y = 300, scale = 0.6
|
||||
}, { y = 460, scale = 1 } }`, so a character further up the picture is drawn
|
||||
smaller; looks with `anchor = "feet"` stand on their position rather than
|
||||
being centred on it, which is what a walking character wants.
|
||||
|
||||
*3D over a painting.* A room in the scene may be a picture on a flat `mesh`
|
||||
(its `texture`, `unlit`) with 3D characters in front of it. Whatever stands
|
||||
in the picture -- a pillar, a doorway, a table -- that a character has to walk
|
||||
behind is a `mesh` look with `occluder = true`: an invisible box that hides
|
||||
what is behind it while the picture shows through it. The editor's 3D
|
||||
viewport keeps occluders visible so they can be placed; the game hides them.
|
||||
`testScripts/author/painted.game` and scene 79 show a character crossing a
|
||||
painted room behind such a pillar.
|
||||
|
||||
*Hotspots.* A kind with `{ kind = "hotspot", name = "door", walkX = 600,
|
||||
walkY = 330 }` is something a verb can be used on: its look's box, or a
|
||||
`polygon` of x,y pairs, in 2D; a box the size of its look, met by the ray
|
||||
through the pointer, in 3D. The name is what the sentence line shows.
|
||||
|
||||
*Verbs.* A game with `verbs = { "walk", "look", "take", "use", "open", "talk"
|
||||
}` builds a sentence line from the current verb, the hotspot under the
|
||||
pointer, and the item in hand (`sentence`, `hover`, `verb`, and `item` are
|
||||
game vars, so a text look or a HUD element can show them). A click on a
|
||||
hotspot raises `verb` with `verb`, `target`, and `item`, and **the most
|
||||
specific rule wins**: `verb = "use", item = "key", target = "door"` beats
|
||||
`verb = "use", target = "door"` beats `verb = "use"` beats a rule that names
|
||||
nothing, which is the "That doesn't work" every adventure needs. A rule with
|
||||
conditions ranks above one without among the same parameters, so "open the
|
||||
door while the guard objects" and "open the door" can share their parameters
|
||||
and differ in a `test`. `setVerb`, `nextVerb`, and `useItem` choose; the
|
||||
first verb in the list is what a click on the floor means, and the `pressed`
|
||||
event carries the pointer's `x` and `y` for a `walkToPointer` rule.
|
||||
|
||||
*The parser.* A `parser` layer takes typing at a prompt: `words = { look =
|
||||
{ "look", "examine", "l" }, door = { "door", "gate" } }` says what the game
|
||||
knows, and a typed line raises `said` with its `verb`, `noun`, and `second`
|
||||
as the words table knows them -- or `event.unknown`, the first word it did
|
||||
not -- with the same most-specific-wins rule. A game may offer both a verb
|
||||
line and a parser; the rules do not care which was used.
|
||||
|
||||
*Inventory.* `give` and `take` keep a list in the `inventory` var;
|
||||
`has("key")` tests it in an expression and `has` as a condition.
|
||||
|
||||
*Dialogue.* The game's `dialogues` are trees:
|
||||
|
||||
[source,lua]
|
||||
----
|
||||
dialogues = {
|
||||
guard = {
|
||||
start = "hello",
|
||||
nodes = {
|
||||
hello = { who = "Guard", text = "Nobody passes.",
|
||||
choices = { { text = "Why not?", next = "why" }, { text = "Fine." } } },
|
||||
why = { who = "Guard", text = "The door is locked.",
|
||||
choices = { { text = "I have a key.", when = 'has("key")',
|
||||
act = { { "setVar", name = "allowed", value = true } }, next = "ok" },
|
||||
{ text = "I see." } } },
|
||||
ok = { who = "Guard", text = "Then go ahead." }
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
`talk` runs one and waits for it: each node's line is said, its choices
|
||||
(those whose `when` holds, and not those already chosen once with `once`) are
|
||||
offered and picked with the number keys or a click, the choice's `act` runs,
|
||||
and `next` names the node after. With a HUD, the choices go in the element
|
||||
called `choices`.
|
||||
|
||||
*Cut-scenes and rooms.* A rule with `controls = false` takes the keys away
|
||||
while it runs; `fade` (out, then back with `out = false`) and `goTo` change
|
||||
the picture and the room, and `goTo` with `at = "arrival"` puts the carried
|
||||
entity where the entity of that id stands in the new room, so the same rule
|
||||
serves a painted room and a modelled one. Sequences survive a room change.
|
||||
|
||||
*Saving.* `saveGame` keeps the whole game -- the room, every visited room's
|
||||
instances and their vars, the game's vars, the inventory -- in a numbered
|
||||
slot, and `loadGame` brings it back. A save taken while a sequence runs
|
||||
records the world as it stands; the sequence does not resume. `die` is the
|
||||
Sierra death: a line, a fade, and the game over from the start.
|
||||
|
||||
`testScripts/author/adventure2d.game` is the two-room adventure in a painted
|
||||
room, and `adventure3d.game` is the same game in a modelled room from a fixed
|
||||
camera -- it takes the 2D description's rules, dialogue, verbs, and parser as
|
||||
they are and supplies only kinds and rooms. Scenes 64 and 65 play them.
|
||||
|
||||
=== Vehicles, crowds, turrets, and physics toys
|
||||
|
||||
`vehicle` puts a kind on the engine's vehicle physics -- a car, a motorcycle,
|
||||
a tank, or a boat -- with wheels hung at the corners its look's size gives,
|
||||
driven by the `keys` vars (`dy` throttle, up is forward; `dx` steering), and
|
||||
`self.speed` says how fast. `racer` drives a vehicle round a track of
|
||||
`points` on its own, steering toward the next and easing off in bends, which
|
||||
is every opponent in a racing game. `camera` in `follow` mode with a `lag`
|
||||
is what sits behind the car.
|
||||
|
||||
`walker` with `follow = "hero"` keeps an instance after an entity (or the
|
||||
camera) across the navigation mesh, asking again every so often -- a crowd of
|
||||
enemies in a shooter, a companion. `patrol` walks a list of points in turn,
|
||||
looping or stopping, and raises `patrolEnd` at the last: a creep's lane, a
|
||||
guard's round. `turret` fires at the nearest instance of a kind within range,
|
||||
no faster than its rate, spawning a projectile aimed at it (or doing the
|
||||
damage itself), and `spawnAtPointer` makes an instance where the player
|
||||
clicked -- on the floor the ray meets, in 3D -- which is how turrets are
|
||||
placed.
|
||||
|
||||
`body` is a thing physics moves (mass, bounce, friction); `thrust` pushes a
|
||||
body along its nose by `dy` and turns it by `dx` (a hovercraft, a ship);
|
||||
`joint` hangs a body from another's, or from the world, by a hinge, a ball,
|
||||
or a slider through a point along an axis; and `push` shoves a body, or a
|
||||
ragdoll's bone.
|
||||
|
||||
`testScripts/author/racing.game` (a car, a rival on a track, checkpoint
|
||||
triggers, a hinged bar, a hovercraft) and `towerdefence.game` (creeps in
|
||||
waves on a lane, turrets placed by clicks, gold and lives) are the worked
|
||||
examples; scenes 66 and 67 play them.
|
||||
|
||||
=== Arcade plumbing: branching video, lives, credits, the board
|
||||
|
||||
A `branches` track is Dragon's Lair: each branch is a window of disc frames,
|
||||
the `move` (a key) or `switch` it wants, and the frames the disc goes to on
|
||||
`success` and on `fail`; a kind with `{ kind = "branching", track = "moves" }`
|
||||
watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and
|
||||
`branchMissed`. Lives, credits, and continues are vars and rules -- `credit`
|
||||
on `SWITCH_COIN1`, a `restart` on `SWITCH_START1` when `credits > 0` -- and a
|
||||
`bezel` layer shows `score`, `lives`, and `credits` (and a second player's
|
||||
`score2` and `lives2` with `twin`) on the arcade panel around the picture.
|
||||
|
||||
`gameOver` ends the rules, keeps the best score across runs, and shows the
|
||||
results card; `submitScore` sends the score to the master service's board
|
||||
for the game, queued until it can go. `testScripts/author/fmv.game` is the
|
||||
worked example and scene 68 plays it: one window answered, three missed, the
|
||||
lives spent, the score sent, a coin, and a continue.
|
||||
|
||||
=== The long tail: sprite sheets, tiles, MIDI, water, soft bodies
|
||||
|
||||
`distance(a, b)` takes instances or ids (`distance(self, "hero")`) and
|
||||
measures through the scene in 3D. A rule with `each` on an event that has no
|
||||
instance of its own -- a key, a room, the disc -- runs once per instance of
|
||||
the kind, as a frame rule does, which is how "a swing of the sword hits every
|
||||
enemy in reach" is one rule. A `solid` with `moving = true` is a body that
|
||||
follows wherever the instance is moved: a paddle, a lift.
|
||||
|
||||
A `sprite` look turns with the var `angle` (degrees, clockwise), which an
|
||||
entity's `rz` starts and which rules, a `spin` behaviour, or a `projectile`
|
||||
with `aim` change -- a turning image is a whole one, not a sheet. `spin`
|
||||
turns a 3D instance about Y the same way. A `particles` look is a steady
|
||||
stream from the instance -- a fire, smoke, a thruster, rain -- with a colour,
|
||||
a rate, a size, a speed, and a life, and the `emit` action is a burst from any
|
||||
instance.
|
||||
|
||||
A `sprite` look with `frames` is a sheet of that many columns, and a `frames`
|
||||
behaviour steps it: `states = "idle=1-1, walk=2-5"` gives each state its run
|
||||
of frames, played at `fps`, so a walker's walk cycle follows the state the
|
||||
`walker` (or a rule) sets. Art need only face one way: the look's `faces`
|
||||
says which (`right` unless said), and while the var `facing` -- which the
|
||||
`walker` and the `mover` set as they go -- is the other way, the image is
|
||||
mirrored. A sheet drawn facing both ways names the other way's frames for
|
||||
the state with `Left` or `Right` on the end (`walkLeft=6-9`), and those are
|
||||
used instead of the mirror; `faces = "none"` never mirrors, for a top-down
|
||||
sprite that turns rather than flips. A sheet entity with an `rz` (or a var
|
||||
`angle`) draws its frame turned. A `grid` look draws a map of tiles from a sheet --
|
||||
`columns` tiles wide, each `tile` pixels square -- from rows of tile numbers
|
||||
(`map = "1 2 1; 2 1 2"`, `0` for nothing), which is a maze or a level in a
|
||||
string. A game that opened a MIDI port hears `midi` events with
|
||||
`event.pitch` and `event.velocity` for every note struck.
|
||||
|
||||
In the scene, a mesh look with `water` is filled with water: its top is the
|
||||
surface, and a `body` with `buoyancy` floats in it; a `vehicle` of type
|
||||
`boat` with `thrust` is pushed by its propeller while that sits under the
|
||||
surface; and `soft` makes a mesh look a cloth or an inflated body.
|
||||
`testScripts/author/tail.game` and `pool.game` are the worked examples;
|
||||
scenes 70 and 71 play them.
|
||||
|
||||
=== The way out
|
||||
|
||||
The `lua` action takes a line of Lua and emits it as it stands. It is there on
|
||||
purpose: when a rule needs something the vocabulary cannot say, that rule drops
|
||||
to Lua and the rest of the game is unaffected. A description is a convenience,
|
||||
The `lua` action takes a line of Lua and emits it as it stands. It is there on
|
||||
purpose: when a rule needs something the vocabulary cannot say, that rule drops
|
||||
to Lua and the rest of the game is unaffected. A description is a convenience,
|
||||
not a cage, and the compiled output is a normal game you can stop describing
|
||||
and start editing whenever it suits you.
|
||||
|
||||
=== The editor
|
||||
== The Vocabulary
|
||||
|
||||
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
||||
Conditions, actions, behaviours, looks, layers, and events are not built into
|
||||
the compiler. Each is an entry in the `AUTHOR` table in `Author.singe`
|
||||
declaring its parameters and the Lua it emits, so a new kind of game is a set
|
||||
of entries rather than a new release. The tables below are generated from
|
||||
that manifest.
|
||||
|
||||
The parameter types: `number` (a number or an expression), `expression`,
|
||||
`string`, `boolean`, `entity` (`"self"`, `"other"`, or an entity's id),
|
||||
`kind`, `scancode` (a key name from `SCANCODE`), `switch` (a `SWITCH_*` name),
|
||||
`file`, `state`, `track`, `room`, and `lua`. A behaviour's parameters are
|
||||
plain values in the description; key and switch names among them are resolved
|
||||
when the game runs.
|
||||
|
||||
include::ForgeVocabulary.adoc[]
|
||||
|
||||
== The Editor
|
||||
|
||||
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
||||
its own directory beside the games, appears in the menu like one, and packs to
|
||||
`Forge.game` with `--pack`; the build does that itself (the `forge` target),
|
||||
with this manual inside.
|
||||
Nothing in it is a preview: the canvas is the same overlay at the same
|
||||
`Forge.game`; the build does that itself (the `forge` target), with this manual
|
||||
inside. Nothing in it is a preview: the canvas is the same overlay at the same
|
||||
coordinates the game will be played in, so what is placed is what is seen.
|
||||
|
||||
Started from the menu it opens on a chooser: the descriptions in its data
|
||||
directory, any dropped into the `Forge` directory itself (those are opened as a
|
||||
copy, since inside a `.game` they are read only), and *New game*, which writes
|
||||
Started from the menu it opens on a chooser: the descriptions in its data
|
||||
directory, the samples inside `Forge.game` and any dropped into the `Forge`
|
||||
directory itself (those are opened as a copy, since inside a `.game` they are
|
||||
read only), and *New game*, which writes
|
||||
a starter -- ground, a hero that runs and jumps, a score readout -- and opens
|
||||
it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into
|
||||
that data directory, and the chooser says where it is. `Esc` in the editor closes the description (twice, when it has unsaved
|
||||
changes) and `Esc` on the chooser leaves Forge. `P` plays: the description is
|
||||
saved, compiled beside a copy of the runtime, and handed to the engine with
|
||||
`scriptPush`; when the game ends Forge comes back on the same file.
|
||||
it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into
|
||||
that data directory, and the chooser says where it is. `Esc` in the editor
|
||||
closes the description (twice, when it has unsaved changes) and `Esc` on the
|
||||
chooser leaves Forge. `P` plays: the description is saved, compiled beside a
|
||||
copy of the runtime, and handed to the engine with `scriptPush`; when the game
|
||||
ends Forge comes back on the same file.
|
||||
|
||||
A script can drive the editor instead, which is how the test scenes do it:
|
||||
|
||||
|
|
@ -155,99 +483,153 @@ function onOverlayUpdate()
|
|||
end
|
||||
----
|
||||
|
||||
The entity list and the details are an RmlUi document; the canvas beside them is
|
||||
drawn into the overlay and picked with `collidePointRect`.
|
||||
The two compose because the engine offers a button to the GUI first and passes
|
||||
on what it did not use, while pointer motion is never consumed at all -- so
|
||||
point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse
|
||||
callbacks and clicks on the panels will not reach the canvas.
|
||||
The panel is an RmlUi document; the canvas beside it is drawn into the overlay
|
||||
and picked with `collidePointRect`. The two compose because the engine offers
|
||||
a button to the GUI first and passes on what it did not use, while pointer
|
||||
motion is never consumed at all -- so point `forgePress`, `forgeDrag`, and
|
||||
`forgeRelease` at the mouse callbacks and clicks on the panel will not reach
|
||||
the canvas. The panel slides: drag the tab on its outer edge, or use the
|
||||
arrow keys, so an entity that lives underneath it is never permanently out of
|
||||
reach.
|
||||
|
||||
`forgeSave()` writes the description back, `forgeBuild(path)`
|
||||
compiles what is on screen (and puts the runtime beside it, so the result
|
||||
plays), and `forgeMove(index, x, y)` moves an entity without a pointer, which
|
||||
is how `testScripts/scene54.singe` drives it.
|
||||
=== The four panels
|
||||
|
||||
=== Editing the entities
|
||||
`Tab` cycles the panel through the room's *entities*, the *kinds*, the
|
||||
*rules*, the room's *tracks*, and the *dialogues*. Whatever is selected is edited the same way:
|
||||
`Enter` walks its fields and each is typed; `Enter` again moves to the next,
|
||||
`Esc` puts the value back. A typed number comes back a number, because `100`
|
||||
and `"100"` compile to different source.
|
||||
|
||||
`A` adds a box in the middle of the canvas, `D` duplicates the selected entity
|
||||
a little to one side, and `Delete` removes it. `Enter` walks the entity's
|
||||
fields the same way it walks a rule's parameters: `id`, `x`, `y`, the look's
|
||||
`kind`, then whatever that look takes, then `behaviours` -- typed as a list of
|
||||
kinds, `platformer, solid` -- and then each behaviour's own parameters as
|
||||
`platformer.speed` and so on. The fields come from the `AUTHOR` manifest, so a
|
||||
new look or behaviour is editable the moment it is declared. Renaming an
|
||||
entity renames it in every rule that talks about it. A `sprite` look is drawn
|
||||
with its image once the file name is right, and as a box until then.
|
||||
*Entities*: `A` adds one of the selected kind in the middle of the canvas,
|
||||
`D` duplicates the selected one, `Delete` removes it, and dragging moves it.
|
||||
Its fields are `id`, `kind`, `x`, `y`, `z`, `scale` -- and in a 3D room
|
||||
`rx`, `ry`, `rz`, its turn about each axis in degrees -- and each var its kind
|
||||
declares, overridable here. A scale scales the look, the body, and how big
|
||||
the instance counts as for touching. Renaming an entity renames it in every rule that talks about
|
||||
it. `PgUp` and `PgDn` move between rooms; `N` adds a room. With nothing
|
||||
selected, `Enter` edits the room itself: its `name`, `reset`, `depthSort`,
|
||||
`walk` areas (polygons of x,y pairs, separated by semicolons, drawn on the
|
||||
canvas), `navFrom`, and `scaleBy` (as `300:0.6, 460:1`).
|
||||
|
||||
From a script, `forgeEntityAdd(x, y)`, `forgeEntityDuplicate()`,
|
||||
`forgeEntityDelete()` and `forgeEntitySet(entity, field, value)` do the same.
|
||||
In every picker, typing narrows the list to the names that contain what was
|
||||
typed, and `Backspace` widens it again. A field that names a file -- a look's
|
||||
image or model, a sound, a HUD document -- opens a picker of the files under
|
||||
the game's directory instead of a prompt; `Esc` on it leaves the field to be
|
||||
typed.
|
||||
|
||||
=== Editing the rules
|
||||
*Kinds*: `A` adds one, `D` duplicates, `Delete` removes one no room still
|
||||
places. Its fields are `name`, `look` and what that look takes (`look.w`,
|
||||
`look.file`, ...), `vars` typed as `health=3, ammo=6`, `behaviours` typed as a
|
||||
list of names (`keys, mover, shooter`), and each behaviour's own parameters as
|
||||
`mover.speed` and so on. The fields come from the manifest, so a new look or
|
||||
behaviour is editable the moment it is declared; renaming a kind renames it in
|
||||
every entity and rule.
|
||||
|
||||
The panel shows either the entities or the event sheet; `Tab` swaps them. In
|
||||
the rules, the selected rule opens in place and its conditions and actions are
|
||||
listed under it, because a rule only means anything whole -- a `when` without a
|
||||
`then` tells you nothing.
|
||||
In a 2D room, `V` draws a polygon corner by corner on the canvas: with the
|
||||
selected entity's kind carrying a `hotspot` behaviour it becomes that hotspot's
|
||||
outline, otherwise it is added to the room's walk areas. `V` or `Enter` closes
|
||||
it, `Esc` drops it.
|
||||
|
||||
Point `onKeyPressed` at `forgeKey` and the whole editor
|
||||
works without a pointer, which is how the bundled menu has always been driven
|
||||
and what a cabinet wants:
|
||||
*Rules*: the selected rule opens in place with its conditions and actions
|
||||
under it, because a rule only means anything whole. `N` adds a rule, `E` picks
|
||||
what it is on (the event's own parameters then appear as fields on the rule),
|
||||
`C` adds a condition, `O` a condition in the rule's `any` group, `T` an action
|
||||
-- each picked from the vocabulary with its help beside it -- and `[` and `]`
|
||||
move the rule up and down the sheet, since rules run in order. `Delete`
|
||||
removes the selected condition or action, or the rule itself.
|
||||
|
||||
*Tracks*: the room's tracks and their keys. The canvas shows the frame of the
|
||||
disc layer's video under the cursor and every track's box there; `,` and `.`
|
||||
step the cursor a frame, `[` and `]` leap ten. `N` adds a track, `K` a key at
|
||||
the cursor -- taking the box interpolated there, or the nearest key's -- and a
|
||||
key is moved by dragging its box on the canvas or by typing. `Delete` removes
|
||||
the selected key, or the track. In a 3D room a new track is a rail of points,
|
||||
drawn through the scene with its stops marked, and `K` puts a point where the
|
||||
editor camera stands, looking where it looks: stand where the player should
|
||||
and press it.
|
||||
|
||||
*Dialogues*: an outline of every dialogue, the selected one's nodes under it,
|
||||
the selected node's choices under that, and a choice's actions under that.
|
||||
`A` adds at the level selected -- a dialogue, a node in it, a choice in the
|
||||
node -- `T` adds an action to the selected choice from the vocabulary, and
|
||||
`Delete` removes what is selected. A dialogue's `name` and `start`, a node's
|
||||
`name`, `who`, `text`, `seconds`, and `next`, and a choice's `text`, `when`,
|
||||
`next`, and `once` are typed like everything else.
|
||||
|
||||
`P` plays from the room on screen: the built game begins in it, with the
|
||||
other rooms following in order.
|
||||
|
||||
On the 2D canvas the selected entity carries the yellow turn handle alone:
|
||||
an arm the way its `rz` points, past its box, dragged round the entity to
|
||||
turn it, and a sheet entity shows its first frame turned the same way. In
|
||||
the kinds panel each kind's picture stands beside its name -- the sprite
|
||||
itself (one frame of a sheet), `Aa` for a text, or a swatch of the box's
|
||||
colour -- so a kind can be told from its neighbours without opening it.
|
||||
`testScripts/scene82.singe` drives both.
|
||||
|
||||
=== The 3D viewport
|
||||
|
||||
A room in a game with a `scene3d` layer is shown as the scene itself, built
|
||||
from the kinds' looks exactly as the runtime builds them, under an editor
|
||||
camera that orbits whatever is selected: `J` and `L` orbit, `Y` and `H` tilt,
|
||||
`I` and `K` close in and back off. Nothing steps -- no behaviours, no physics
|
||||
-- so it is the description that is on screen, not a running game. An entity
|
||||
is picked by where it projects and dragged across the floor it stands on, and
|
||||
the selected one carries a gizmo: three arms along the world axes, red for
|
||||
X, green for Y, blue for Z, each ending in a handle that drags the entity
|
||||
along that axis alone -- the pointer's travel along the arm, as a fraction of
|
||||
the arm, is the distance moved -- and a fourth, yellow, the way the entity
|
||||
faces, whose handle dragged round the entity turns it about Y. `testScripts/scene63.singe` and `scene80`
|
||||
drive it.
|
||||
|
||||
=== Keys
|
||||
|
||||
[cols="1,4"]
|
||||
|===
|
||||
| `Tab` | entities or rules
|
||||
| Up, Down | move through the list, and through the parts of the open rule
|
||||
| `Tab` | entities, kinds, rules, tracks
|
||||
| Up, Down | move through the list, and through the parts of the open rule or track
|
||||
| Left, Right | slide the panel
|
||||
| `PgUp`, `PgDn` | the previous or next room
|
||||
| `Enter` | type a value for whatever is selected; again for its next value
|
||||
| `Esc` | put the value back
|
||||
| `A`, `D`, `Delete` | add, duplicate, delete the selected entity
|
||||
| `N` | a new rule
|
||||
| `C`, `T` | add a condition, an action -- picked from the vocabulary, with its help beside it
|
||||
| `[`, `]` | move the rule up or down the sheet
|
||||
| `Delete` | delete the selected rule, or the selected condition or action
|
||||
| `Esc` | put the value back; otherwise close the description
|
||||
| `A`, `D`, `Delete` | add, duplicate, delete an entity or a kind
|
||||
| `N` | a new rule, a new track, or (in the entities) a new room
|
||||
| `E` | pick what the rule is on
|
||||
| `C`, `O`, `T` | add a condition, an any-of condition, an action
|
||||
| `[`, `]` | move the rule up or down; in the tracks, leap the cursor
|
||||
| `,`, `.` | step the cursor
|
||||
| `V` | draw a walk area, or the selected hotspot's outline, corner by corner
|
||||
| `K` | a key at the cursor; in a 3D room, a rail point where the camera stands
|
||||
| `J`, `L`, `Y`, `H`, `I`, `K` | in a 3D room: orbit, tilt, and zoom the editor camera
|
||||
| `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history
|
||||
| `S`, `B`, `P` | save, build, play
|
||||
|===
|
||||
|
||||
With the rule itself selected rather than one of its parts, `Enter` edits its
|
||||
note.
|
||||
From a script the same work is `forgeEntityAdd`, `forgeEntityDuplicate`,
|
||||
`forgeEntityDelete`, `forgeEntitySet`, `forgeKindAdd`, `forgeKindSet`,
|
||||
`forgeRuleNew`, `forgeRuleOn`, `forgeRuleAdd("when"|"any"|"act", name)`,
|
||||
`forgePartSet`, `forgePartDelete`, `forgeRuleMove`, `forgeTrackNew`,
|
||||
`forgeKeyAdd`, `forgeCursorTo`, `forgeUndo`, and `forgeRedo`.
|
||||
|
||||
A typed number comes back a number rather than a string, because `100` and
|
||||
`"100"` compile to different source and a description that changed shape when a
|
||||
value was retyped would stop round-tripping.
|
||||
|
||||
From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act",
|
||||
name)`, `forgePartSet(key, value)`, `forgePartDelete()`, `forgeRuleMove(by)`,
|
||||
`forgeUndo()` and `forgeRedo()` do the same work. Adding a condition selects it, so its
|
||||
parameters can be set at once.
|
||||
|
||||
The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs
|
||||
changing when a condition or an action is added: it offers whatever is
|
||||
declared. A new parameter gets a sensible starting value for its type, so a
|
||||
rule compiles the moment it is made rather than only once every field is
|
||||
filled in.
|
||||
|
||||
A description survives the round trip: loading one, saving it and loading it
|
||||
again compiles to the same game, byte for byte. The editor depends on that and
|
||||
the test asserts it.
|
||||
A description survives the round trip: loading one, saving it, and loading it
|
||||
again compiles to the same game, byte for byte. The editor depends on that
|
||||
and `testScripts/scene54.singe` asserts it.
|
||||
|
||||
=== Releasing a game
|
||||
|
||||
`forgeExport(folder, name)` writes everything a finished game needs into a
|
||||
directory of its own: the compiled script, a `games.dat` so the menu lists it,
|
||||
directory of its own: the compiled script, a `games.dat` so the menu lists it,
|
||||
the description it was built from so it can be opened again, and **a copy of
|
||||
the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
|
||||
the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
|
||||
like any other, and it runs on a machine that has never had Forge on it.
|
||||
|
||||
Every built game finds its own directory to load that runtime from, using
|
||||
`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
|
||||
`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
|
||||
engine was *launched* with, so a game reached by `dofile` -- a test, a
|
||||
launcher, a preview -- would otherwise look beside the caller.
|
||||
|
||||
The panel slides. Drag the tab on its outer edge and it moves across the
|
||||
window, so an entity that lives underneath it -- a score readout at `12, 12`
|
||||
does -- is never permanently out of reach. `forgePanelTo(x)` moves it from
|
||||
a script, and `forgeOverPanel(x)` says whether a point is currently
|
||||
covered.
|
||||
|
||||
[#migrating]
|
||||
`forgeBuild(path)` compiles what is on screen (and puts the runtime beside it,
|
||||
so the result plays); what `authorCheck` has to say about the description --
|
||||
a kind that is not declared, an expression that does not parse, a field a
|
||||
look, a behaviour, or an event does not take (a misspelling, nine times in
|
||||
ten) -- goes to the console and the panel.
|
||||
|
|
|
|||
687
docs/ForgeVocabulary.adoc
Normal file
687
docs/ForgeVocabulary.adoc
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
// Generated by util/forgeVocabulary.lua from assets/Forge/Author.singe. Do not edit.
|
||||
|
||||
=== Layers
|
||||
|
||||
[cols="1,3,2"]
|
||||
|===
|
||||
| Name | What it does | Parameters
|
||||
|
||||
| `bezel`
|
||||
| The arcade score panel around the picture, showing the vars score (and score2), lives (and lives2), and credits as they change.
|
||||
| `twin` (boolean)
|
||||
|
||||
| `disc`
|
||||
| The video the game is played over. The file is what the editor scrubs; games.dat names it when the game runs.
|
||||
| `file` (file), `start` (number)
|
||||
|
||||
| `hud`
|
||||
| An RmlUi document over everything. bind maps element ids to game vars, refreshed as they change.
|
||||
| `bind` (table), `document` (file)
|
||||
|
||||
| `music`
|
||||
| A track that plays from the start, looping.
|
||||
| `file` (file), `volume` (number)
|
||||
|
||||
| `overlay`
|
||||
| Flat drawing over everything: sprites, scores, prompts.
|
||||
| --
|
||||
|
||||
| `parser`
|
||||
| A text parser, the Sierra way: a prompt the player types at, and words the game knows. What is typed raises the said event.
|
||||
| `ignore` (table), `prompt` (string), `words` (table)
|
||||
|
||||
| `scene3d`
|
||||
| The 3D scene: physics in three axes, a sun, a sky, fog. Positions are world units.
|
||||
| `ambient` (table), `exposure` (number), `fog` (table), `fov` (number), `gravity` (number), `sky` (file), `sun` (table)
|
||||
|
||||
| `world2d`
|
||||
| A flat world with gravity: physics in the XY plane, drawn into the overlay.
|
||||
| `gravity` (number)
|
||||
|
||||
|===
|
||||
|
||||
=== Looks
|
||||
|
||||
[cols="1,3,2"]
|
||||
|===
|
||||
| Name | What it does | Parameters
|
||||
|
||||
| `billboard`
|
||||
| A sprite in the scene, always facing the camera: a tree, a puff, a health bar.
|
||||
| `d` (number), `file` (file), `h` (number), `height` (number), `w` (number)
|
||||
|
||||
| `box`
|
||||
| A filled rectangle, centred on the instance -- or standing on it, with anchor feet.
|
||||
| `anchor` (string), `b` (number), `g` (number), `h` (number), `r` (number), `w` (number)
|
||||
|
||||
| `grid`
|
||||
| A map of tiles from a sheet, its top left at the instance: columns is the sheet's width in tiles, tile the tile's size, map the rows of tile numbers (1 is the first; 0 is empty), rows separated by semicolons.
|
||||
| `columns` (number), `file` (file), `map` (string), `tile` (number)
|
||||
|
||||
| `light`
|
||||
| A point or spot light, or a sun, on the instance.
|
||||
| `b` (number), `g` (number), `intensity` (number), `r` (number), `range` (number), `shadow` (boolean), `type` (string)
|
||||
|
||||
| `mesh`
|
||||
| A box, sphere, cylinder, or plane with a colour or a texture. Floors, walls, crates, targets. An occluder is not seen but hides what is behind it: a pillar that is only in the painting.
|
||||
| `b` (number), `d` (number), `g` (number), `h` (number), `metallic` (number), `occluder` (boolean), `r` (number), `roughness` (number), `shape` (string), `texture` (file), `unlit` (boolean), `w` (number)
|
||||
|
||||
| `model`
|
||||
| A glTF model under the instance, scaled and turned, playing a clip. w, h, d say how big it counts as.
|
||||
| `clip` (string), `d` (number), `file` (file), `h` (number), `rx` (number), `ry` (number), `rz` (number), `scale` (number), `w` (number)
|
||||
|
||||
| `none`
|
||||
| Nothing drawn: a trigger, a spawner, a hitbox over video, a camera.
|
||||
| `d` (number), `h` (number), `w` (number)
|
||||
|
||||
| `particles`
|
||||
| A steady stream of particles from the instance: a fire, smoke, a thruster, rain. Colour, size, speed, and life as an emitter takes them.
|
||||
| `b` (number), `g` (number), `gravity` (number), `life` (number), `r` (number), `rate` (number), `size` (number), `speed` (number), `spread` (number)
|
||||
|
||||
| `sprite`
|
||||
| An image, centred on the instance -- or standing on it, with anchor feet. frames splits a sheet into that many columns; the var frame picks one. The var angle turns it, starting from the entity's rz. faces says which way the art looks (right unless said, or none): while the var facing is the other way the image is mirrored, so art drawn one way walks both.
|
||||
| `anchor` (string), `faces` (string), `file` (file), `frames` (number)
|
||||
|
||||
| `text`
|
||||
| A line of text, its top left at the instance. The var text is what it says.
|
||||
| `b` (number), `g` (number), `r` (number), `text` (string)
|
||||
|
||||
| `text3d`
|
||||
| A line of text standing in the scene. The var text is what it says.
|
||||
| `height` (number), `text` (string)
|
||||
|
||||
|===
|
||||
|
||||
=== Behaviours
|
||||
|
||||
[cols="1,3,2"]
|
||||
|===
|
||||
| Name | What it does | Parameters
|
||||
|
||||
| `animator`
|
||||
| A clip per state: idle, walk, attack, hit, dead, or any state a rule sets. A clip that does not loop raises animationDone when it ends.
|
||||
| `attack` (string), `dead` (string), `fade` (number), `hit` (string), `idle` (string), `loop` (string), `walk` (string)
|
||||
|
||||
| `body`
|
||||
| A thing physics moves: a crate, a ball, debris. Mass, bounce, friction, and buoyancy as Jolt takes them.
|
||||
| `bounce` (number), `buoyancy` (number), `friction` (number), `mass` (number), `shape` (string)
|
||||
|
||||
| `branching`
|
||||
| Dragon's Lair: a track of branches, each a disc frame window, the move it wants, and where the disc goes on success and on failure. Raises branchTaken and branchMissed.
|
||||
| `track` (track)
|
||||
|
||||
| `camera`
|
||||
| The scene is drawn from this instance. fixed looks at a point; follow keeps an offset from a target; first sits on a target's eyes and the mouse looks; orbit circles a target; rail rides a track of points with stops.
|
||||
| `eye` (number), `lag` (number), `lookX` (number), `lookY` (number), `lookZ` (number), `mode` (string), `target` (string), `track` (track), `x` (number), `y` (number), `z` (number)
|
||||
|
||||
| `character`
|
||||
| Walks, falls, and jumps in the scene: the engine's character controller. Moves by the vars dx and dy, relative to the camera.
|
||||
| `height` (number), `jump` (number), `radius` (number), `speed` (number)
|
||||
|
||||
| `drift`
|
||||
| Moves steadily, for a cloud, a platform, or a target.
|
||||
| `vx` (number), `vy` (number)
|
||||
|
||||
| `frames`
|
||||
| Steps a sprite look's frame: a range of frames per state, as "idle=1-1, walk=2-5", at a rate a second. A sheet drawn facing both ways names the other way's frames stateLeft or stateRight (walkLeft), used while the var facing is that way instead of mirroring the look.
|
||||
| `fps` (number), `states` (string)
|
||||
|
||||
| `gun`
|
||||
| The pointer as a gun for one player: a press of the trigger raises hit on what is under it, or miss. Ammo counts down; a shot off the screen reloads. With aim centre it fires from the middle of the picture -- a first-person gun.
|
||||
| `aim` (string), `ammo` (number), `player` (number), `reload` (string), `trigger` (switch)
|
||||
|
||||
| `health`
|
||||
| The var health; the damage action lowers it, and at zero the instance raises death and is destroyed unless keep is set. With ragdoll, a model falls limp instead and goes after linger seconds.
|
||||
| `keep` (boolean), `linger` (number), `max` (number), `ragdoll` (boolean)
|
||||
|
||||
| `hitbox`
|
||||
| The instance's hit shape comes from a track of boxes keyed by disc frame or time; between keys it is interpolated, outside them there is none.
|
||||
| `track` (track)
|
||||
|
||||
| `hotspot`
|
||||
| Something a verb can be used on: a name for the sentence line, a place to walk to first, and a polygon (x,y pairs) when its look's box will not do.
|
||||
| `name` (string), `polygon` (table), `walkX` (number), `walkY` (number), `walkZ` (number)
|
||||
|
||||
| `joint`
|
||||
| Joins this body to another's -- or to the world, with no other -- by a hinge, a ball, or a slider through a world point along an axis.
|
||||
| `ax` (number), `ay` (number), `az` (number), `dx` (number), `dy` (number), `dz` (number), `other` (string), `type` (string)
|
||||
|
||||
| `keys`
|
||||
| Reads a player's keys into the vars dx and dy (-1 to 1) and fire, so rules and movers read intent rather than hardware.
|
||||
| `down` (scancode), `fire` (scancode), `left` (scancode), `player` (number), `right` (scancode), `up` (scancode)
|
||||
|
||||
| `mover`
|
||||
| Moves by the vars dx and dy at a speed, kept on the screen when clamp is set. Pair it with keys.
|
||||
| `clamp` (boolean), `speed` (number)
|
||||
|
||||
| `patrol`
|
||||
| Walks a list of points in turn -- a creep's lane, a guard's round -- looping or stopping, and raises patrolEnd at the last.
|
||||
| `loop` (boolean), `points` (table)
|
||||
|
||||
| `platformer`
|
||||
| Runs, falls, and jumps: the engine's character controller in 2D. Rules say which way with run and jump.
|
||||
| `jump` (number), `speed` (number)
|
||||
|
||||
| `projectile`
|
||||
| Flies at a velocity and is dropped past the edge (or far away, in 3D) or after life seconds. With aim, a sprite turns to point the way it flies.
|
||||
| `aim` (boolean), `life` (number), `vx` (number), `vy` (number), `vz` (number)
|
||||
|
||||
| `racer`
|
||||
| Drives a vehicle round a track of points on its own: steering toward the next, throttling by how straight the road is.
|
||||
| `reach` (number), `throttle` (number), `track` (track)
|
||||
|
||||
| `seek`
|
||||
| Moves straight toward a target -- an entity's id, or "camera" -- and raises arrived within stopAt of it.
|
||||
| `fly` (boolean), `speed` (number), `stopAt` (number), `target` (string)
|
||||
|
||||
| `shooter`
|
||||
| Spawns a kind from an offset while the var fire is set (or always, with auto), no faster than the rate.
|
||||
| `auto` (boolean), `offsetX` (number), `offsetY` (number), `rate` (number), `spawns` (kind)
|
||||
|
||||
| `soft`
|
||||
| A mesh look made soft: a cloth, or a body inflated to a pressure.
|
||||
| `mass` (number), `pressure` (number), `stiffness` (number), `type` (string)
|
||||
|
||||
| `solid`
|
||||
| Immovable ground or a wall, in a world2d or a scene3d room. With moving, a body that follows wherever the instance is moved -- a paddle, a lift.
|
||||
| `moving` (boolean)
|
||||
|
||||
| `sound`
|
||||
| A clip per event: spawn, hit, death, or any event the instance sees. Values are file names. volume is the clips' own, 0 to 100, under the game's.
|
||||
| `death` (file), `hit` (file), `pressed` (file), `spawn` (file), `volume` (number)
|
||||
|
||||
| `spawner`
|
||||
| Makes instances of a kind every so many seconds at its own position or at one of its points, up to max alive at once and total in all.
|
||||
| `every` (number), `max` (number), `pick` (string), `points` (table), `spawns` (kind), `total` (number)
|
||||
|
||||
| `spin`
|
||||
| Turns steadily: the var angle in 2D (a sprite turns with it), the node about Y in 3D, in degrees a second.
|
||||
| `rate` (number)
|
||||
|
||||
| `target`
|
||||
| Something a gun can hit. In 2D its look's box; in 3D a body the size of its look, plus zones -- boxes on named bones, so a shot says which part it hit.
|
||||
| `zones` (table)
|
||||
|
||||
| `thrust`
|
||||
| A body pushed along its nose by the var dy and turned by dx: a hovercraft, a ship, a plane without lift.
|
||||
| `force` (number), `turn` (number)
|
||||
|
||||
| `timer`
|
||||
| Raises the timer event with its name after some seconds, or every so many. start false waits for timerStart.
|
||||
| `after` (number), `every` (number), `name` (string), `start` (boolean)
|
||||
|
||||
| `trigger`
|
||||
| A volume that reports what enters and leaves it, as the enter and leave events. A checkpoint, a doorway, a prize.
|
||||
| --
|
||||
|
||||
| `turret`
|
||||
| Fires at the nearest instance of a kind within range, no faster than the rate: spawns a projectile aimed at it, or, with no projectile, does the damage itself.
|
||||
| `damage` (number), `range` (number), `rate` (number), `spawns` (kind), `speed` (number), `targets` (kind)
|
||||
|
||||
| `vehicle`
|
||||
| A car, motorcycle, tank, or boat on the engine's vehicle physics, driven by the vars dy (throttle, up is forward) and dx (steering) from keys. Wheels hang at the four corners the sizes say; a boat needs none.
|
||||
| `mass` (number), `radius` (number), `suspension` (number), `thrust` (number), `torque` (number), `type` (string), `wheelX` (number), `wheelZ` (number), `width` (number)
|
||||
|
||||
| `walker`
|
||||
| Walks the room's walk areas (2D) or its floors (3D) on the navigation mesh: walkTo sends it, a click on the floor can, follow keeps it after an entity (or the camera), and it raises arrived. Its state is walk or idle as it goes.
|
||||
| `every` (number), `follow` (string), `radius` (number), `speed` (number), `stopAt` (number)
|
||||
|
||||
| `water`
|
||||
| Fills a mesh look with water: its top is the surface, and bodies inside float or sink by their buoyancy.
|
||||
| `density` (number), `drag` (number)
|
||||
|
||||
|===
|
||||
|
||||
=== Events
|
||||
|
||||
[cols="1,3,2"]
|
||||
|===
|
||||
| Name | What it does | Parameters
|
||||
|
||||
| `animationDone`
|
||||
| A clip that does not loop ended on self; event.state says which state it was for.
|
||||
| `kind` (kind), `state` (state)
|
||||
|
||||
| `arrived`
|
||||
| A seeking instance reached its target.
|
||||
| `kind` (kind)
|
||||
|
||||
| `branchMissed`
|
||||
| The window closed without the move and the disc went to the fail frame.
|
||||
| --
|
||||
|
||||
| `branchOpen`
|
||||
| A branch's window opened; event.move says what it wants.
|
||||
| --
|
||||
|
||||
| `branchTaken`
|
||||
| The move was made in time and the disc went to the success frame.
|
||||
| --
|
||||
|
||||
| `collision`
|
||||
| An instance of kind a began touching one of kind b (self is a, other is b).
|
||||
| `a` (kind), `b` (kind)
|
||||
|
||||
| `death`
|
||||
| self's health reached zero.
|
||||
| `kind` (kind)
|
||||
|
||||
| `enter`
|
||||
| An instance of kind b entered a trigger of kind a (self is the trigger, other what entered).
|
||||
| `a` (kind), `b` (kind)
|
||||
|
||||
| `frame`
|
||||
| Every frame.
|
||||
| --
|
||||
|
||||
| `frameReached`
|
||||
| The disc passed a frame.
|
||||
| `frame` (number)
|
||||
|
||||
| `gameOver`
|
||||
| The game ended.
|
||||
| --
|
||||
|
||||
| `hit`
|
||||
| A gun shot landed on self; other is the gun.
|
||||
| `kind` (kind), `player` (number)
|
||||
|
||||
| `leave`
|
||||
| An instance of kind b left a trigger of kind a.
|
||||
| `a` (kind), `b` (kind)
|
||||
|
||||
| `midi`
|
||||
| A MIDI note came in on the port the game opened; event.pitch and event.velocity say which and how hard. (pitch, since note is what a rule's comment is called.)
|
||||
| `pitch` (number)
|
||||
|
||||
| `miss`
|
||||
| A gun shot hit nothing; self is the gun. event.offscreen says whether it left the screen.
|
||||
| `player` (number)
|
||||
|
||||
| `patrolEnd`
|
||||
| A patrolling instance reached the last of its points.
|
||||
| `kind` (kind)
|
||||
|
||||
| `pressed`
|
||||
| A key or a switch went down.
|
||||
| `key` (scancode), `player` (number), `switch` (switch)
|
||||
|
||||
| `railEnd`
|
||||
| A rail camera reached the end of its track.
|
||||
| --
|
||||
|
||||
| `released`
|
||||
| A key or a switch came up.
|
||||
| `key` (scancode), `player` (number), `switch` (switch)
|
||||
|
||||
| `roomEnd`
|
||||
| A room is about to be left.
|
||||
| `room` (room)
|
||||
|
||||
| `roomStart`
|
||||
| A room has been entered.
|
||||
| `room` (room)
|
||||
|
||||
| `said`
|
||||
| A line was typed at the parser: its verb, noun, and second noun as the words table knows them, or unknown for a word it does not. The most specific rule wins.
|
||||
| `noun` (string), `second` (string), `verb` (string)
|
||||
|
||||
| `spawn`
|
||||
| self has just been made.
|
||||
| `kind` (kind)
|
||||
|
||||
| `stopped`
|
||||
| A rail camera reached a stop; event.name says which. pathNext moves it on.
|
||||
| `name` (string)
|
||||
|
||||
| `timer`
|
||||
| A timer on self went off; event.name says which.
|
||||
| `kind` (kind), `name` (string)
|
||||
|
||||
| `verb`
|
||||
| A verb was used on a hotspot (self), with an item or not: "use key on door". The most specific rule wins.
|
||||
| `item` (string), `target` (string), `verb` (string)
|
||||
|
||||
|===
|
||||
|
||||
=== Conditions
|
||||
|
||||
[cols="1,3,2"]
|
||||
|===
|
||||
| Name | What it does | Parameters
|
||||
|
||||
| `below`
|
||||
| An instance has fallen past a line -- a pit, or the bottom of the screen.
|
||||
| `entity` (entity), `y` (number)
|
||||
|
||||
| `chance`
|
||||
| True with a probability, 0 to 1.
|
||||
| `p` (number)
|
||||
|
||||
| `discBetween`
|
||||
| The disc is between two frames -- the window a QTE is answered in.
|
||||
| `from` (number), `to` (number)
|
||||
|
||||
| `every`
|
||||
| True once every so many seconds, for a rule that fires steadily.
|
||||
| `seconds` (number), `tag` (string)
|
||||
|
||||
| `has`
|
||||
| The inventory holds an item.
|
||||
| `item` (string)
|
||||
|
||||
| `hitPart`
|
||||
| The shot landed on a named zone of the target: head, weakspot.
|
||||
| `part` (string)
|
||||
|
||||
| `hover`
|
||||
| A player's pointer is over the named hotspot.
|
||||
| `player` (number), `target` (string)
|
||||
|
||||
| `inState`
|
||||
| An instance's state is this.
|
||||
| `entity` (entity), `state` (state)
|
||||
|
||||
| `keyHeld`
|
||||
| A key is down.
|
||||
| `key` (scancode)
|
||||
|
||||
| `keyPressed`
|
||||
| A key went down this frame.
|
||||
| `key` (scancode)
|
||||
|
||||
| `onGround`
|
||||
| A platformer has ground under it.
|
||||
| `entity` (entity)
|
||||
|
||||
| `onScreen`
|
||||
| An instance's box is at least partly on the overlay.
|
||||
| `entity` (entity)
|
||||
|
||||
| `once`
|
||||
| Only the first time this rule would run -- per instance and per room unless scope is "game".
|
||||
| `scope` (string), `tag` (string)
|
||||
|
||||
| `pointerIn`
|
||||
| A player's pointer is over an instance.
|
||||
| `entity` (entity), `player` (number)
|
||||
|
||||
| `pointerOffscreen`
|
||||
| A player's pointer is off the picture, which is how a light gun reloads.
|
||||
| `player` (number)
|
||||
|
||||
| `switchHeld`
|
||||
| A pad, gun, or mouse switch is down.
|
||||
| `switch` (switch)
|
||||
|
||||
| `test`
|
||||
| An expression is true: self.health <= 0, count("zombie") == 0, score > 1000.
|
||||
| `expr` (expression)
|
||||
|
||||
| `timeBetween`
|
||||
| The game is between two moments, in seconds.
|
||||
| `from` (number), `to` (number)
|
||||
|
||||
| `touching`
|
||||
| Two instances overlap.
|
||||
| `entity` (entity), `other` (entity)
|
||||
|
||||
| `waiting`
|
||||
| A rail camera is held at a stop.
|
||||
| `entity` (entity)
|
||||
|
||||
|===
|
||||
|
||||
=== Actions
|
||||
|
||||
[cols="1,3,2,1"]
|
||||
|===
|
||||
| Name | What it does | Parameters | Waits
|
||||
|
||||
| `addScore`
|
||||
| Add to the score.
|
||||
| `amount` (number)
|
||||
|
|
||||
|
||||
| `addVar`
|
||||
| Add to a var on an instance, or on the game with no entity.
|
||||
| `amount` (number), `entity` (entity), `name` (string)
|
||||
|
|
||||
|
||||
| `cameraCut`
|
||||
| Draw the scene from another camera instance.
|
||||
| `entity` (entity)
|
||||
|
|
||||
|
||||
| `credit`
|
||||
| A coin: one more credit.
|
||||
| --
|
||||
|
|
||||
|
||||
| `damage`
|
||||
| Take from an instance's health; at zero it dies.
|
||||
| `amount` (number), `entity` (entity)
|
||||
|
|
||||
|
||||
| `destroy`
|
||||
| Remove an instance.
|
||||
| `entity` (entity)
|
||||
|
|
||||
|
||||
| `die`
|
||||
| The Sierra death: a message, then the game starts over.
|
||||
| `text` (expression)
|
||||
| yes
|
||||
|
||||
| `discTo`
|
||||
| Send the disc to a frame -- the branch a QTE takes.
|
||||
| `frame` (number)
|
||||
|
|
||||
|
||||
| `emit`
|
||||
| A burst of particles at an instance: an explosion, sparks, a puff.
|
||||
| `b` (number), `count` (number), `entity` (entity), `g` (number), `r` (number), `speed` (number)
|
||||
|
|
||||
|
||||
| `face`
|
||||
| Turn a walker to face left or right, or toward an instance.
|
||||
| `direction` (string), `entity` (entity), `target` (entity)
|
||||
|
|
||||
|
||||
| `fade`
|
||||
| Fade the picture to black, or back (with out false), and wait.
|
||||
| `out` (boolean), `seconds` (number)
|
||||
| yes
|
||||
|
||||
| `flash`
|
||||
| Flash the screen a colour.
|
||||
| `b` (number), `g` (number), `r` (number), `seconds` (number)
|
||||
|
|
||||
|
||||
| `gameOver`
|
||||
| End the game: rules stop, the gameOver event fires, restart begins again.
|
||||
| --
|
||||
|
|
||||
|
||||
| `give`
|
||||
| Put an item in the inventory.
|
||||
| `item` (string)
|
||||
|
|
||||
|
||||
| `goTo`
|
||||
| Go to a room, taking the named entity along and placing it at a point there -- or where the entity called at stands in that room.
|
||||
| `at` (string), `entity` (entity), `room` (room), `x` (number), `y` (number), `z` (number)
|
||||
|
|
||||
|
||||
| `jump`
|
||||
| Ask a platformer to jump.
|
||||
| `entity` (entity)
|
||||
|
|
||||
|
||||
| `loadGame`
|
||||
| Bring a slot back.
|
||||
| `slot` (number)
|
||||
|
|
||||
|
||||
| `lookAt`
|
||||
| Turn an instance toward another.
|
||||
| `entity` (entity), `target` (entity)
|
||||
|
|
||||
|
||||
| `lua`
|
||||
| Anything the vocabulary cannot say. The way out, and it is meant to be used.
|
||||
| `code` (lua)
|
||||
|
|
||||
|
||||
| `moveTo`
|
||||
| Put an instance somewhere.
|
||||
| `entity` (entity), `x` (number), `y` (number), `z` (number)
|
||||
|
|
||||
|
||||
| `nextVerb`
|
||||
| Cycle to the next verb in the game's list.
|
||||
| --
|
||||
|
|
||||
|
||||
| `pathNext`
|
||||
| Send a rail camera on from its stop.
|
||||
| `entity` (entity)
|
||||
|
|
||||
|
||||
| `playAnimation`
|
||||
| Play a model's clip, once or looping.
|
||||
| `clip` (string), `entity` (entity), `loop` (boolean)
|
||||
|
|
||||
|
||||
| `playMusic`
|
||||
| Play a track, looping.
|
||||
| `file` (file)
|
||||
|
|
||||
|
||||
| `playSound`
|
||||
| Play a clip, at a volume of its own (0 to 100) under the game's.
|
||||
| `file` (file), `volume` (number)
|
||||
|
|
||||
|
||||
| `push`
|
||||
| Shove a body, or a ragdoll's bone, along a direction.
|
||||
| `bone` (string), `entity` (entity), `x` (number), `y` (number), `z` (number)
|
||||
|
|
||||
|
||||
| `reload`
|
||||
| Fill a gun's ammo.
|
||||
| `entity` (entity)
|
||||
|
|
||||
|
||||
| `restart`
|
||||
| Begin the game again from its first room.
|
||||
| --
|
||||
|
|
||||
|
||||
| `run`
|
||||
| Drive a platformer left (-1) or right (1) this frame.
|
||||
| `direction` (number), `entity` (entity)
|
||||
|
|
||||
|
||||
| `saveGame`
|
||||
| Keep the whole game -- room, positions, vars, inventory -- in a numbered slot.
|
||||
| `slot` (number)
|
||||
|
|
||||
|
||||
| `say`
|
||||
| Show a line for a while, and wait for it. With a hud it goes in the element called say; otherwise it is drawn.
|
||||
| `seconds` (number), `text` (expression)
|
||||
| yes
|
||||
|
||||
| `setState`
|
||||
| Change an instance's state.
|
||||
| `entity` (entity), `state` (state)
|
||||
|
|
||||
|
||||
| `setText`
|
||||
| Change what a text instance says.
|
||||
| `entity` (entity), `text` (expression)
|
||||
|
|
||||
|
||||
| `setVar`
|
||||
| Set a var on an instance, or on the game with no entity.
|
||||
| `entity` (entity), `name` (string), `value` (expression)
|
||||
|
|
||||
|
||||
| `setVerb`
|
||||
| Choose the verb the next click uses.
|
||||
| `verb` (string)
|
||||
|
|
||||
|
||||
| `shake`
|
||||
| Shake the camera for a moment.
|
||||
| `amount` (number), `seconds` (number)
|
||||
|
|
||||
|
||||
| `show`
|
||||
| Show or hide an instance.
|
||||
| `entity` (entity), `visible` (boolean)
|
||||
|
|
||||
|
||||
| `spawn`
|
||||
| Make an instance of a kind at a point.
|
||||
| `kind` (kind), `x` (number), `y` (number), `z` (number)
|
||||
|
|
||||
|
||||
| `spawnAtPointer`
|
||||
| Make an instance of a kind where the player's pointer is: on the overlay in 2D, on the floor the ray meets in 3D.
|
||||
| `kind` (kind), `player` (number)
|
||||
|
|
||||
|
||||
| `stopMusic`
|
||||
| Stop the track that is playing.
|
||||
| --
|
||||
|
|
||||
|
||||
| `submitScore`
|
||||
| Send the score to the master service's board for this game, queued until it can go.
|
||||
| `board` (string)
|
||||
|
|
||||
|
||||
| `take`
|
||||
| Take an item out of the inventory.
|
||||
| `item` (string)
|
||||
|
|
||||
|
||||
| `talk`
|
||||
| Run a dialogue from the game's dialogues, and wait for it to end.
|
||||
| `dialogue` (string)
|
||||
| yes
|
||||
|
||||
| `timerStart`
|
||||
| Start a named timer on an instance.
|
||||
| `after` (number), `entity` (entity), `name` (string)
|
||||
|
|
||||
|
||||
| `useItem`
|
||||
| Choose an inventory item for the next click: "use key on ...".
|
||||
| `item` (string)
|
||||
|
|
||||
|
||||
| `wait`
|
||||
| Wait some seconds before the next action.
|
||||
| `seconds` (number)
|
||||
| yes
|
||||
|
||||
| `walkTo`
|
||||
| Send a walker to a point, and wait until it gets there.
|
||||
| `entity` (entity), `x` (number), `y` (number), `z` (number)
|
||||
| yes
|
||||
|
||||
| `walkToHotspot`
|
||||
| Send a walker to a hotspot's walk point (the one the verb was used on, with self), and wait.
|
||||
| `entity` (entity), `target` (entity)
|
||||
| yes
|
||||
|
||||
| `walkToPointer`
|
||||
| Send a walker where the player's pointer is, and wait.
|
||||
| `entity` (entity), `player` (number)
|
||||
| yes
|
||||
|
||||
|===
|
||||
|
||||
109
docs/Manual.adoc
109
docs/Manual.adoc
|
|
@ -8591,6 +8591,37 @@ nodeSetPosition(wall, 0, 2, -3)
|
|||
nodeSetRotation(wall, 90, 0, 0)
|
||||
----
|
||||
|
||||
[#materialsetoccluder]
|
||||
==== materialSetOccluder
|
||||
|
||||
[source,text]
|
||||
----
|
||||
materialSetOccluder(material, occluder)
|
||||
----
|
||||
|
||||
Makes the material write depth and no colour. Whatever the mesh covers is
|
||||
hidden as if the mesh were solid, but the mesh itself is not seen: what was
|
||||
drawn behind it -- a painted backdrop, the disc -- shows through. It is the
|
||||
stand-in for something that is in a picture but not in the scene: a pillar in
|
||||
a painted room that a 3D character has to walk behind, a doorway, a table.
|
||||
An occluder casts no shadow, since the painting already has one, and takes
|
||||
no light. `false` (the default) makes the material ordinary again.
|
||||
|
||||
*Since:* 3.00.
|
||||
*See also:* <<materialsetblend,materialSetBlend>>, <<materialsetunlit,materialSetUnlit>>
|
||||
|
||||
.Example
|
||||
[source,lua]
|
||||
----
|
||||
-- A pillar that is in the painting: an invisible box where it stands, so
|
||||
-- the hero disappears behind it and reappears on the other side.
|
||||
hidden = materialNew()
|
||||
materialSetOccluder(hidden, true)
|
||||
pillar = nodeNew()
|
||||
nodeSetMesh(pillar, meshBox(0.8, 4, 0.8), hidden)
|
||||
nodeSetPosition(pillar, 2, 2, -3)
|
||||
----
|
||||
|
||||
[#materialsetocclusionmap]
|
||||
==== materialSetOcclusionMap
|
||||
|
||||
|
|
@ -15244,19 +15275,21 @@ end
|
|||
----
|
||||
channel = soundPlay(id)
|
||||
channel = soundPlay(id, loops)
|
||||
channel = soundPlay(id, loops, volume)
|
||||
----
|
||||
|
||||
Starts a loaded clip on the first free channel and returns that channel's number. A channel counts as free when it is neither playing nor paused. The new channel starts at the master volume, centered, with no scene position and the default range, whatever the channel's previous use set. When all sixteen channels are busy nothing plays and `-1` (`SOUND_ERROR_INVALID`) comes back; a game that fires many overlapping sounds should check for it. A handle that is not loaded aborts the script.
|
||||
Starts a loaded clip on the first free channel and returns that channel's number. A channel counts as free when it is neither playing nor paused. The new channel starts at the master volume (or at `volume` under it), centered, with no scene position and the default range, whatever the channel's previous use set. When all sixteen channels are busy nothing plays and `-1` (`SOUND_ERROR_INVALID`) comes back; a game that fires many overlapping sounds should check for it. A handle that is not loaded aborts the script.
|
||||
|
||||
*Parameters:*
|
||||
|
||||
* `id` -- sound handle from `soundLoad`.
|
||||
* `loops` -- `0` (the default) plays the clip once, a positive count repeats it that many more times, `-1` loops it until it is stopped. Anything below `-1` aborts the script.
|
||||
* `volume` -- the channel's own volume, `0` through `63`, under the master; the default is `63`, the clip as loud as the master allows. Outside that range aborts the script. `soundSetChannelVolume` changes it while the clip plays.
|
||||
|
||||
*Returns:* integer channel number, `0` to `15`, or `-1` when no channel is free.
|
||||
|
||||
*Since:* 1.x
|
||||
*See also:* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetnode,soundSetNode>>
|
||||
*Since:* 1.x; `volume` since 3.00.
|
||||
*See also:* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetchannelvolume,soundSetChannelVolume>>, <<soundsetnode,soundSetNode>>
|
||||
|
||||
.Example
|
||||
[source,lua]
|
||||
|
|
@ -15302,6 +15335,36 @@ function onSoundCompleted(channel)
|
|||
end
|
||||
----
|
||||
|
||||
[#soundsetchannelvolume]
|
||||
==== soundSetChannelVolume
|
||||
|
||||
[source,text]
|
||||
----
|
||||
soundSetChannelVolume(channel, volume)
|
||||
----
|
||||
|
||||
Sets one channel's own volume, on top of the master from `soundSetVolume`: the channel plays at the master times this, times its distance fade when it is in the scene. Every channel starts at full when `soundPlay` takes it, unless `soundPlay` was given a volume, so a quiet footstep and a loud shot can share one master. Values outside `0` to `63` abort the script, and so does a channel outside `0` to `15`.
|
||||
|
||||
*Parameters:*
|
||||
|
||||
* `channel` -- channel number from `soundPlay`.
|
||||
* `volume` -- integer, `0` (silent) through `63` (as loud as the master).
|
||||
|
||||
*Since:* 3.00
|
||||
*See also:* <<soundplay,soundPlay>>, <<soundsetvolume,soundSetVolume>>
|
||||
|
||||
.Example
|
||||
[source,lua]
|
||||
----
|
||||
-- An engine that gets louder with the throttle: one looping clip, its channel's volume
|
||||
-- following the pedal every frame.
|
||||
engine = soundPlay(engineClip, -1, 20)
|
||||
|
||||
function onOverlayUpdate()
|
||||
soundSetChannelVolume(engine, math.floor(20 + throttle * 43))
|
||||
end
|
||||
----
|
||||
|
||||
[#soundsetlistener]
|
||||
==== soundSetListener
|
||||
|
||||
|
|
@ -15442,14 +15505,14 @@ soundSetRange(siren, 10, 200)
|
|||
soundSetVolume(volume)
|
||||
----
|
||||
|
||||
Sets the master effects volume and applies it at once to every channel, playing or paused. Positioned channels keep their distance fade on top of it. Values outside `0` to `63` abort the script.
|
||||
Sets the master effects volume and applies it at once to every channel, playing or paused. Each channel's own volume from `soundSetChannelVolume` and, for positioned channels, the distance fade stay on top of it. Values outside `0` to `63` abort the script.
|
||||
|
||||
*Parameters:*
|
||||
|
||||
* `volume` -- integer, `0` (silent) through `63` (loudest).
|
||||
|
||||
*Since:* 1.16
|
||||
*See also:* <<soundgetvolume,soundGetVolume>>
|
||||
*See also:* <<soundgetvolume,soundGetVolume>>, <<soundsetchannelvolume,soundSetChannelVolume>>
|
||||
|
||||
.Example
|
||||
[source,lua]
|
||||
|
|
@ -15592,7 +15655,7 @@ spriteDrawFrame(id, x, y, frame, scale)
|
|||
spriteDrawFrame(id, x, y, frame, scaleX, scaleY)
|
||||
----
|
||||
|
||||
Hypseus extension. Blits one frame of a sprite sheet (or of any animation) onto the overlay with `(x, y)` as its top-left corner, without changing which frame the sprite is parked on or whether it is playing. Frames are numbered from `1`, as Hypseus counts them; a frame outside `1` to the frame count draws frame `1`. A still image draws itself and ignores `frame`. The sprite's own rotation and scale from `spriteRotate` and `spriteScale` are ignored: the first form draws the frame at its natural size, the others multiply the frame's width by `scaleX` (or `scale`) and its height by `scaleY` (or `scale`) with nearest-neighbor sampling. Call it from `onOverlayUpdate`.
|
||||
Hypseus extension. Blits one frame of a sprite sheet (or of any animation) onto the overlay with `(x, y)` as its top-left corner, without changing which frame the sprite is parked on or whether it is playing. Frames are numbered from `1`, as Hypseus counts them; a frame outside `1` to the frame count draws frame `1`. A still image draws itself and ignores `frame`. A mirror set by `spriteFlip` applies. The sprite's own rotation and scale from `spriteRotate` and `spriteScale` are ignored: the first form draws the frame at its natural size, the others multiply the frame's width by `scaleX` (or `scale`) and its height by `scaleY` (or `scale`) with nearest-neighbor sampling. Call it from `onOverlayUpdate`.
|
||||
|
||||
*Parameters:*
|
||||
|
||||
|
|
@ -15695,6 +15758,40 @@ spriteRotateFrame(needle, heading, 1)
|
|||
spriteDrawRotatedFrame(needle, 320, 240)
|
||||
----
|
||||
|
||||
[#spriteflip]
|
||||
==== spriteFlip
|
||||
|
||||
[source,text]
|
||||
----
|
||||
spriteFlip(id, horizontal, vertical)
|
||||
----
|
||||
|
||||
Mirrors the sprite left to right, top to bottom, or both, until it is called again with `false, false`. The mirror applies to every draw of the sprite: `spriteDraw`, each frame from `spriteDrawFrame`, and the turned frame from `spriteDrawRotatedFrame`. It is applied after the rotation and scale from `spriteRotate` and `spriteScale`, so a turned sprite is mirrored as it appears on screen, and a turned frame is simply mirrored the other way rather than turned again. The mirrored image is rebuilt only when the flags change; a frame drawn by `spriteDrawFrame` is mirrored once and kept until another frame is asked for. `spriteDrawGrid` ignores the mirror, since its rectangle is cut from the unmirrored image.
|
||||
|
||||
A character that walks both ways needs only art that faces one way: flip the sprite when it turns round.
|
||||
|
||||
*Parameters:*
|
||||
|
||||
* `id` -- sprite handle.
|
||||
* `horizontal` -- `true` mirrors left to right.
|
||||
* `vertical` -- `true` mirrors top to bottom.
|
||||
|
||||
*Returns:* nothing.
|
||||
|
||||
*Since:* 3.00
|
||||
*See also:* <<spriterotate,spriteRotate>>, <<spritedrawframe,spriteDrawFrame>>, <<spritedrawrotatedframe,spriteDrawRotatedFrame>>
|
||||
|
||||
.Example
|
||||
[source,lua]
|
||||
----
|
||||
-- The hero's sheet faces right; facing left is the same frames, mirrored.
|
||||
function onOverlayUpdate()
|
||||
overlayClear()
|
||||
spriteFlip(hero, heroVx < 0, false)
|
||||
spriteDrawFrame(hero, heroX, heroY, walkFrame)
|
||||
end
|
||||
----
|
||||
|
||||
[#spriteframeheight]
|
||||
==== spriteFrameHeight
|
||||
|
||||
|
|
|
|||
|
|
@ -1555,7 +1555,7 @@ static bool _runTool(const ConfigT *conf) {
|
|||
// know so that later versions may add them.
|
||||
static void _showApiVersion(void) {
|
||||
// VERSION_STRING is "vX.YY" for people; a machine wants the number on its own.
|
||||
printf("singe version=%s protocol=%d\n", VERSION_STRING + 1, API_VERSION_PROTOCOL);
|
||||
printf("singe version=%s protocol=%d\n", &VERSION_STRING[1], API_VERSION_PROTOCOL);
|
||||
fflush(stdout);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -563,6 +563,9 @@ int32_t navAgentNew(int32_t nav, int32_t node, float radius, float height, float
|
|||
}
|
||||
radius = SDL_max(radius, MIN_AGENT_SIZE);
|
||||
height = SDL_max(height, MIN_AGENT_SIZE);
|
||||
// A node placed this frame has no world matrix until the scene walks them, and a game that
|
||||
// never draws the scene (a 2D room walking a mesh) never walks them at all.
|
||||
sceneUpdateTransforms();
|
||||
position = nodeGetWorldPosition(node);
|
||||
if (_nearestPoly(record, position, start) == 0) {
|
||||
start[0] = position.x;
|
||||
|
|
|
|||
25
src/scene.c
25
src/scene.c
|
|
@ -63,10 +63,11 @@
|
|||
#define SH_SAMPLES_ACROSS 128 // Equirect columns sampled for the harmonics
|
||||
#define HALF_BYTES 2
|
||||
#define CUBE_CHANNELS 4
|
||||
#define PIPELINE_COUNT 8 // skinned x blend x double sided
|
||||
#define PIPELINE_COUNT 16 // skinned x blend x double sided x occluder
|
||||
#define PIPELINE_SKINNED 1
|
||||
#define PIPELINE_BLEND 2
|
||||
#define PIPELINE_TWO_SIDED 4
|
||||
#define PIPELINE_OCCLUDER 8 // Writes depth and no colour: a stand-in for something painted
|
||||
#define SHADOW_PIPELINES 8 // skinned x double sided x cutout
|
||||
#define SHADOW_PIPELINE_SKINNED 1
|
||||
#define SHADOW_PIPELINE_TWO_SIDED 2
|
||||
|
|
@ -245,6 +246,7 @@ typedef struct MaterialS {
|
|||
bool unlit;
|
||||
bool doubleSided;
|
||||
bool blend;
|
||||
bool occluder; // Depth only: hides what is behind it and shows what is behind itself
|
||||
bool textureBorrowed; // The base texture belongs to a sprite node: never released here
|
||||
bool used;
|
||||
} MaterialT;
|
||||
|
|
@ -1218,6 +1220,12 @@ static bool _createPipeline(int32_t sampleSet, int32_t variant) {
|
|||
if (variant & PIPELINE_BLEND) {
|
||||
_alphaBlendState(&colour, false);
|
||||
}
|
||||
if (variant & PIPELINE_OCCLUDER) {
|
||||
// The shape goes into the depth buffer and nowhere else, so a painted backdrop stays in
|
||||
// view where it stands while whatever walks behind it is hidden.
|
||||
colour.blend_state.enable_color_write_mask = true;
|
||||
colour.blend_state.color_write_mask = 0;
|
||||
}
|
||||
info.vertex_shader = (variant & PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic;
|
||||
info.fragment_shader = _scene.fragment;
|
||||
info.vertex_input_state.vertex_buffer_descriptions = &buffer;
|
||||
|
|
@ -1811,7 +1819,8 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i
|
|||
continue;
|
||||
}
|
||||
if (shadowPass) {
|
||||
if (!node->shadowCaster && !_scene.depthPrepass) {
|
||||
// An occluder stands for something already painted, shadows included.
|
||||
if ((!node->shadowCaster && !_scene.depthPrepass) || material->occluder) {
|
||||
continue;
|
||||
}
|
||||
// A bulb inside a closed mesh sees only its back faces; they must still cast.
|
||||
|
|
@ -3021,6 +3030,9 @@ static int32_t _pipelineVariant(int32_t node) {
|
|||
if (material->doubleSided) {
|
||||
variant |= PIPELINE_TWO_SIDED;
|
||||
}
|
||||
if (material->occluder) {
|
||||
variant |= PIPELINE_OCCLUDER;
|
||||
}
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
|
|
@ -4113,6 +4125,15 @@ bool materialSetMetallic(int32_t material, float metallic) {
|
|||
}
|
||||
|
||||
|
||||
bool materialSetOccluder(int32_t material, bool occluder) {
|
||||
if (!materialValid(material)) {
|
||||
return false;
|
||||
}
|
||||
_scene.materials[material].occluder = occluder;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool materialSetRoughness(int32_t material, float roughness) {
|
||||
if (!materialValid(material)) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ bool materialSetGui(int32_t material, int32_t gui); // A GUI's texture
|
|||
bool materialSetMap(int32_t material, MaterialMapE map, const Ktx2ImageT *image, float strength); // A transcoded KTX2 image (NULL clears); strength for normal and occlusion
|
||||
bool materialSetMapSurface(int32_t material, MaterialMapE map, SDL_Surface *image, float strength); // The same from a surface (NULL clears)
|
||||
bool materialSetMetallic(int32_t material, float metallic);
|
||||
bool materialSetOccluder(int32_t material, bool occluder); // Depth only: hides what is behind it, shows the backdrop through it
|
||||
bool materialSetRoughness(int32_t material, float roughness);
|
||||
bool materialSetTiling(int32_t material, float u, float v);
|
||||
bool materialSetUnlit(int32_t material, bool unlit);
|
||||
|
|
|
|||
169
src/singe.c
169
src/singe.c
|
|
@ -427,6 +427,9 @@ typedef struct SpriteS {
|
|||
SDL_Surface *originalSurface; // Owned unless it points into animation->frames
|
||||
SDL_Surface *surface; // What gets drawn: originalSurface, or a transformed copy
|
||||
SDL_Surface *rotatedFrame; // spriteRotateFrame's turned copy of one sheet frame, drawn by spriteDrawRotatedFrame
|
||||
SDL_Surface *flippedFrame; // spriteFlip's mirrored copy of the sheet frame spriteDrawFrame last drew
|
||||
int32_t flippedIndex; // Which frame that is
|
||||
SDL_FlipMode flip; // How spriteFlip mirrors every draw
|
||||
bool surfaceOwned; // True when surface is a transformed copy to free
|
||||
int32_t frameWidth; // Unscaled size of one frame (the image, for a still)
|
||||
int32_t frameHeight;
|
||||
|
|
@ -480,7 +483,7 @@ typedef struct EffectS {
|
|||
float farOff; // ... silent beyond this
|
||||
float relative[3]; // The last position handed to the mixer, listener space
|
||||
float gain; // The last distance gain
|
||||
float baseGain; // The effects volume the track gain was last built from
|
||||
float volume; // The channel's own volume, 0 to 1, on top of the master
|
||||
bool positioned;
|
||||
} EffectT;
|
||||
|
||||
|
|
@ -785,6 +788,7 @@ static void _drawPauseIndicator(const SDL_FRect *target);
|
|||
static void _drawRing(const SDL_FRect *outer, const SDL_FRect *inner, const SDL_Color *color);
|
||||
static void _drawSindenBorder(void);
|
||||
static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY);
|
||||
static void _effectApplyGain(int32_t channel);
|
||||
static float _effectGain(const EffectT *effect, float distance);
|
||||
static void _effectReset(int32_t channel);
|
||||
static void _effectStopped(void *userdata, MIX_Track *track);
|
||||
|
|
@ -941,11 +945,13 @@ static SoundT *_soundLoadFromIO(lua_State *L, const char *method, SDL_IOStr
|
|||
static int32_t _soundQueueDrain(int32_t *finished);
|
||||
static void _spriteAdoptAnimation(SpriteT *sprite);
|
||||
static void _spriteDestroy(SpriteT *sprite);
|
||||
static SDL_Surface *_spriteFlippedFrame(SpriteT *sprite, SDL_Surface *source, int32_t frame);
|
||||
static void _spriteFreeSurface(SpriteT *sprite);
|
||||
static SpriteT *_spriteLoadFromIO(lua_State *L, const char *method, SDL_IOStream *io, int32_t width, int32_t height);
|
||||
static SpriteT *_spriteNew(lua_State *L, const char *method);
|
||||
static void _spriteRebuildSurface(SpriteT *sprite);
|
||||
static void _spriteRegister(SpriteT *sprite);
|
||||
static SDL_Surface *_spriteTransform(SpriteT *sprite, SDL_Surface *source);
|
||||
static const char *_startAudioSuffix(void);
|
||||
static void _startControllers(void);
|
||||
static void _startLuaContext(lua_State *L);
|
||||
|
|
@ -1142,6 +1148,7 @@ static int32_t apiMaterialSetGui(lua_State *L);
|
|||
static int32_t apiMaterialSetMetallic(lua_State *L);
|
||||
static int32_t apiMaterialSetMetallicRoughnessMap(lua_State *L);
|
||||
static int32_t apiMaterialSetNormalMap(lua_State *L);
|
||||
static int32_t apiMaterialSetOccluder(lua_State *L);
|
||||
static int32_t apiMaterialSetOcclusionMap(lua_State *L);
|
||||
static int32_t apiMaterialSetRoughness(lua_State *L);
|
||||
static int32_t apiMaterialSetTexture(lua_State *L);
|
||||
|
|
@ -1371,6 +1378,7 @@ static int32_t apiSoundLoadData(lua_State *L);
|
|||
static int32_t apiSoundPause(lua_State *L);
|
||||
static int32_t apiSoundPlay(lua_State *L);
|
||||
static int32_t apiSoundResume(lua_State *L);
|
||||
static int32_t apiSoundSetChannelVolume(lua_State *L);
|
||||
static int32_t apiSoundSetListener(lua_State *L);
|
||||
static int32_t apiSoundSetNode(lua_State *L);
|
||||
static int32_t apiSoundSetPan(lua_State *L);
|
||||
|
|
@ -1384,6 +1392,7 @@ static int32_t apiSpriteDraw(lua_State *L);
|
|||
static int32_t apiSpriteDrawFrame(lua_State *L);
|
||||
static int32_t apiSpriteDrawGrid(lua_State *L);
|
||||
static int32_t apiSpriteDrawRotatedFrame(lua_State *L);
|
||||
static int32_t apiSpriteFlip(lua_State *L);
|
||||
static int32_t apiSpriteFrameHeight(lua_State *L);
|
||||
static int32_t apiSpriteFrameWidth(lua_State *L);
|
||||
static int32_t apiSpriteGetFrame(lua_State *L);
|
||||
|
|
@ -3108,15 +3117,27 @@ static float _effectGain(const EffectT *effect, float distance) {
|
|||
|
||||
|
||||
// A channel about to play afresh: no position, no pan, the default range.
|
||||
// What a channel's track plays at: the master effects volume, the channel's own volume, and its
|
||||
// distance gain while it is in the scene. Every gain change goes through here so there is one
|
||||
// answer to what a channel is worth, and the master can move without losing the rest.
|
||||
static void _effectApplyGain(int32_t channel) {
|
||||
EffectT *effect = &_effects[channel];
|
||||
|
||||
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME) * effect->volume * (effect->positioned ? effect->gain : 1.0f));
|
||||
}
|
||||
|
||||
|
||||
static void _effectReset(int32_t channel) {
|
||||
EffectT *effect = &_effects[channel];
|
||||
|
||||
memset(effect, 0, sizeof(*effect));
|
||||
effect->node = LISTENER_CAMERA;
|
||||
effect->node = LISTENER_CAMERA;
|
||||
effect->nearBy = SOUND_DEFAULT_NEAR;
|
||||
effect->farOff = SOUND_DEFAULT_FAR;
|
||||
effect->gain = 1.0f;
|
||||
effect->gain = 1.0f;
|
||||
effect->volume = 1.0f;
|
||||
MIX_SetTrack3DPosition(_effectTracks[channel], NULL);
|
||||
_effectApplyGain(channel);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -5909,6 +5930,7 @@ static void _registerApi(lua_State *L) {
|
|||
lua_register(L, "materialSetMetallic", apiMaterialSetMetallic); // 3.00
|
||||
lua_register(L, "materialSetMetallicRoughnessMap", apiMaterialSetMetallicRoughnessMap); // 3.00
|
||||
lua_register(L, "materialSetNormalMap", apiMaterialSetNormalMap); // 3.00
|
||||
lua_register(L, "materialSetOccluder", apiMaterialSetOccluder); // 3.00
|
||||
lua_register(L, "materialSetOcclusionMap", apiMaterialSetOcclusionMap); // 3.00
|
||||
lua_register(L, "materialSetRoughness", apiMaterialSetRoughness); // 3.00
|
||||
lua_register(L, "materialSetTexture", apiMaterialSetTexture); // 3.00
|
||||
|
|
@ -6141,6 +6163,7 @@ static void _registerApi(lua_State *L) {
|
|||
lua_register(L, "soundPause", apiSoundPause); // 1.16 RDG
|
||||
lua_register(L, "soundPlay", apiSoundPlay); // 1.xx
|
||||
lua_register(L, "soundResume", apiSoundResume); // 1.16 RDG
|
||||
lua_register(L, "soundSetChannelVolume", apiSoundSetChannelVolume); // 3.00
|
||||
lua_register(L, "soundSetListener", apiSoundSetListener); // 3.00
|
||||
lua_register(L, "soundSetNode", apiSoundSetNode); // 3.00
|
||||
lua_register(L, "soundSetPan", apiSoundSetPan); // 3.00
|
||||
|
|
@ -6155,6 +6178,7 @@ static void _registerApi(lua_State *L) {
|
|||
lua_register(L, "spriteDrawFrame", apiSpriteDrawFrame); // Hypseus
|
||||
lua_register(L, "spriteDrawGrid", apiSpriteDrawGrid); // Hypseus
|
||||
lua_register(L, "spriteDrawRotatedFrame", apiSpriteDrawRotatedFrame); // Hypseus Handle first since 3.00.
|
||||
lua_register(L, "spriteFlip", apiSpriteFlip); // 3.00
|
||||
lua_register(L, "spriteFrameHeight", apiSpriteFrameHeight); // Hypseus
|
||||
lua_register(L, "spriteFrameWidth", apiSpriteFrameWidth); // Hypseus
|
||||
lua_register(L, "spriteGetFrame", apiSpriteGetFrame); // 2.10
|
||||
|
|
@ -6352,7 +6376,9 @@ static void _resetScriptState(void) {
|
|||
}
|
||||
_soundQueueDrain(finished);
|
||||
_global.effectsVolume = _defaultVolume(AUDIO_MAX_VOLUME);
|
||||
MIX_SetTagGain(videoGetMixer(), EFFECT_TAG, _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME));
|
||||
for (x = 0; x < EFFECT_TRACKS; x++) {
|
||||
_effectApplyGain(x);
|
||||
}
|
||||
// Music starts as loud as the sound effects, on its own scale and with its own gain, so
|
||||
// musicSetVolume and soundSetVolume never move each other.
|
||||
_global.musicVolume = _defaultVolume(MUSIC_MAX_VOLUME);
|
||||
|
|
@ -7239,6 +7265,7 @@ static void _spriteDestroy(SpriteT *sprite) {
|
|||
HASH_DEL(_global.spriteList, sprite);
|
||||
_spriteFreeSurface(sprite);
|
||||
SDL_DestroySurface(sprite->rotatedFrame);
|
||||
SDL_DestroySurface(sprite->flippedFrame);
|
||||
if (sprite->animation != NULL) {
|
||||
// Frames belong to the animation.
|
||||
IMG_FreeAnimation(sprite->animation);
|
||||
|
|
@ -7249,6 +7276,23 @@ static void _spriteDestroy(SpriteT *sprite) {
|
|||
}
|
||||
|
||||
|
||||
// The mirrored copy of one sheet frame that spriteDrawFrame draws while the sprite is flipped, kept
|
||||
// from one draw to the next so a sprite standing on a frame is not mirrored again every frame.
|
||||
static SDL_Surface *_spriteFlippedFrame(SpriteT *sprite, SDL_Surface *source, int32_t frame) {
|
||||
if ((sprite->flippedFrame == NULL) || (sprite->flippedIndex != frame)) {
|
||||
SDL_DestroySurface(sprite->flippedFrame);
|
||||
sprite->flippedFrame = SDL_DuplicateSurface(source);
|
||||
if (sprite->flippedFrame == NULL) {
|
||||
utilDie("Unable to mirror frame %d of sprite %d.", frame, sprite->id);
|
||||
}
|
||||
SDL_FlipSurface(sprite->flippedFrame, sprite->flip);
|
||||
sprite->flippedIndex = frame;
|
||||
}
|
||||
|
||||
return sprite->flippedFrame;
|
||||
}
|
||||
|
||||
|
||||
// Releases the drawn surface if it is a transformed copy. Animation frames are never freed here.
|
||||
static void _spriteFreeSurface(SpriteT *sprite) {
|
||||
if (sprite->surfaceOwned) {
|
||||
|
|
@ -7324,11 +7368,11 @@ static SpriteT *_spriteNew(lua_State *L, const char *method) {
|
|||
// Rebuilds the drawn surface after a frame, angle, scale, or quality change.
|
||||
static void _spriteRebuildSurface(SpriteT *sprite) {
|
||||
_spriteFreeSurface(sprite);
|
||||
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0)) {
|
||||
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0) && (sprite->flip == SDL_FLIP_NONE)) {
|
||||
// Untransformed sprites draw straight from the original.
|
||||
sprite->surface = sprite->originalSurface;
|
||||
} else {
|
||||
sprite->surface = rotoZoomSurface(sprite->originalSurface, -sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth != 0);
|
||||
sprite->surface = _spriteTransform(sprite, sprite->originalSurface);
|
||||
if (sprite->surface == NULL) {
|
||||
utilDie("Unable to transform sprite %d.", sprite->id);
|
||||
}
|
||||
|
|
@ -7350,6 +7394,25 @@ static void _spriteRegister(SpriteT *sprite) {
|
|||
}
|
||||
|
||||
|
||||
// A fresh copy of source turned by the sprite's angle, scaled by its scale, and then mirrored as
|
||||
// its flip says, or NULL when the copy could not be made. Mirroring after turning is what lets
|
||||
// spriteFlip change the mirror on a turned frame without turning it again.
|
||||
static SDL_Surface *_spriteTransform(SpriteT *sprite, SDL_Surface *source) {
|
||||
SDL_Surface *copy = NULL;
|
||||
|
||||
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0)) {
|
||||
copy = SDL_DuplicateSurface(source);
|
||||
} else {
|
||||
copy = rotoZoomSurface(source, -sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth != 0);
|
||||
}
|
||||
if ((copy != NULL) && (sprite->flip != SDL_FLIP_NONE)) {
|
||||
SDL_FlipSurface(copy, sprite->flip);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
// The disc audio suffix a run and a reload start from: --altaudio's, or the game's own audio.
|
||||
static const char *_startAudioSuffix(void) {
|
||||
return (_global.conf->audioSuffix != NULL) ? _global.conf->audioSuffix : "";
|
||||
|
|
@ -8082,7 +8145,6 @@ static void _updateSounds(void) {
|
|||
Vec3T relative;
|
||||
MIX_Point3D point;
|
||||
EffectT *effect = NULL;
|
||||
float baseGain = _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME);
|
||||
float distance = 0.0f;
|
||||
float gain = 0.0f;
|
||||
bool useNode = false;
|
||||
|
|
@ -8103,7 +8165,7 @@ static void _updateSounds(void) {
|
|||
if (!nodeValid(effect->node)) {
|
||||
effect->positioned = false;
|
||||
MIX_SetTrack3DPosition(_effectTracks[x], NULL);
|
||||
MIX_SetTrackGain(_effectTracks[x], baseGain);
|
||||
_effectApplyGain(x);
|
||||
continue;
|
||||
}
|
||||
world = nodeGetWorldPosition(effect->node);
|
||||
|
|
@ -8117,11 +8179,10 @@ static void _updateSounds(void) {
|
|||
}
|
||||
gain = _effectGain(effect, distance);
|
||||
// The mixer only hears about changes.
|
||||
if ((gain == effect->gain) && (baseGain == effect->baseGain) && (relative.x == effect->relative[0]) && (relative.y == effect->relative[1]) && (relative.z == effect->relative[2])) {
|
||||
if ((gain == effect->gain) && (relative.x == effect->relative[0]) && (relative.y == effect->relative[1]) && (relative.z == effect->relative[2])) {
|
||||
continue;
|
||||
}
|
||||
effect->gain = gain;
|
||||
effect->baseGain = baseGain;
|
||||
effect->relative[0] = relative.x;
|
||||
effect->relative[1] = relative.y;
|
||||
effect->relative[2] = relative.z;
|
||||
|
|
@ -8129,7 +8190,7 @@ static void _updateSounds(void) {
|
|||
point.y = relative.y;
|
||||
point.z = relative.z;
|
||||
MIX_SetTrack3DPosition(_effectTracks[x], &point);
|
||||
MIX_SetTrackGain(_effectTracks[x], baseGain * gain);
|
||||
_effectApplyGain(x);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10404,6 +10465,14 @@ static int32_t apiMaterialSetNormalMap(lua_State *L) {
|
|||
}
|
||||
|
||||
|
||||
// materialSetOccluder(material, bool)
|
||||
static int32_t apiMaterialSetOccluder(lua_State *L) {
|
||||
_argCheck(L, "materialSetOccluder", 2, 2);
|
||||
materialSetOccluder(_argMaterial(L, "materialSetOccluder", 1), _argBoolean(L, "materialSetOccluder", 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// materialSetOcclusionMap(material[, image[, strength]]) occlusion in R; nil clears; strength defaults to 1.
|
||||
static int32_t apiMaterialSetOcclusionMap(lua_State *L) {
|
||||
return _materialSetMap(L, "materialSetOcclusionMap", MAP_OCCLUSION, true);
|
||||
|
|
@ -13727,9 +13796,10 @@ static int32_t apiSoundPlay(lua_State *L) {
|
|||
SoundT *sound = NULL;
|
||||
int32_t channel = SOUND_CHANNEL_NONE;
|
||||
int32_t loops = 0;
|
||||
int32_t volume = AUDIO_MAX_VOLUME;
|
||||
SDL_PropertiesID options;
|
||||
|
||||
_argCheck(L, "soundPlay", 1, 2);
|
||||
_argCheck(L, "soundPlay", 1, 3);
|
||||
sound = _argSound(L, "soundPlay", 1);
|
||||
if (lua_gettop(L) >= 2) {
|
||||
loops = _argInteger(L, "soundPlay", 2);
|
||||
|
|
@ -13737,11 +13807,18 @@ static int32_t apiSoundPlay(lua_State *L) {
|
|||
_luaDie(L, "soundPlay", "Loops must be -1 (forever), 0 (once) or a repeat count: %d", loops);
|
||||
}
|
||||
}
|
||||
if (lua_gettop(L) >= 3) {
|
||||
volume = _argInteger(L, "soundPlay", 3);
|
||||
if ((volume < 0) || (volume > AUDIO_MAX_VOLUME)) {
|
||||
_luaDie(L, "soundPlay", "Invalid sound volume value: %d", volume);
|
||||
}
|
||||
}
|
||||
channel = _effectTrackFree();
|
||||
if (channel >= 0) {
|
||||
_effectReset(channel);
|
||||
_effects[channel].volume = _mixerGain(volume, AUDIO_MAX_VOLUME);
|
||||
_effectApplyGain(channel);
|
||||
MIX_SetTrackAudio(_effectTracks[channel], sound->audio);
|
||||
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME));
|
||||
options = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loops);
|
||||
if (!MIX_PlayTrack(_effectTracks[channel], options)) {
|
||||
|
|
@ -13750,7 +13827,7 @@ static int32_t apiSoundPlay(lua_State *L) {
|
|||
}
|
||||
SDL_DestroyProperties(options);
|
||||
}
|
||||
_luaTrace(L, "soundPlay", "%d %d loops %d", sound->id, channel, loops);
|
||||
_luaTrace(L, "soundPlay", "%d %d loops %d volume %d", sound->id, channel, loops, volume);
|
||||
lua_pushinteger(L, channel);
|
||||
|
||||
return 1;
|
||||
|
|
@ -13773,6 +13850,26 @@ static int32_t apiSoundResume(lua_State *L) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
// soundSetChannelVolume(channel, volume): one channel's own volume, 0 to AUDIO_MAX_VOLUME, under
|
||||
// the master. soundPlay starts every channel at full.
|
||||
static int32_t apiSoundSetChannelVolume(lua_State *L) {
|
||||
int32_t channel = 0;
|
||||
int32_t volume = 0;
|
||||
|
||||
_argCheck(L, "soundSetChannelVolume", 2, 2);
|
||||
channel = _argChannel(L, "soundSetChannelVolume", 1);
|
||||
volume = _argInteger(L, "soundSetChannelVolume", 2);
|
||||
if ((volume < 0) || (volume > AUDIO_MAX_VOLUME)) {
|
||||
_luaDie(L, "soundSetChannelVolume", "Invalid sound volume value: %d", volume);
|
||||
}
|
||||
_effects[channel].volume = _mixerGain(volume, AUDIO_MAX_VOLUME);
|
||||
_effectApplyGain(channel);
|
||||
_luaTrace(L, "soundSetChannelVolume", "%d %d", channel, volume);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// soundSetListener([node]): positioned sounds are heard from this node; none means the scene camera
|
||||
static int32_t apiSoundSetListener(lua_State *L) {
|
||||
_argCheck(L, "soundSetListener", 0, 1);
|
||||
|
|
@ -13797,7 +13894,7 @@ static int32_t apiSoundSetNode(lua_State *L) {
|
|||
effect->node = LISTENER_CAMERA;
|
||||
effect->positioned = false;
|
||||
MIX_SetTrack3DPosition(_effectTracks[channel], NULL);
|
||||
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME));
|
||||
_effectApplyGain(channel);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -13818,7 +13915,7 @@ static int32_t apiSoundSetPan(lua_State *L) {
|
|||
gains.left = SDL_min(1.0f, 1.0f - pan);
|
||||
gains.right = SDL_min(1.0f, 1.0f + pan);
|
||||
MIX_SetTrackStereo(_effectTracks[channel], &gains);
|
||||
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME));
|
||||
_effectApplyGain(channel);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -13862,6 +13959,7 @@ static int32_t apiSoundSetRange(lua_State *L) {
|
|||
// soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel.
|
||||
static int32_t apiSoundSetVolume(lua_State *L) {
|
||||
int32_t volume = 0;
|
||||
int32_t x = 0;
|
||||
|
||||
_argCheck(L, "soundSetVolume", 1, 1);
|
||||
volume = _argInteger(L, "soundSetVolume", 1);
|
||||
|
|
@ -13869,7 +13967,9 @@ static int32_t apiSoundSetVolume(lua_State *L) {
|
|||
_luaDie(L, "soundSetVolume", "Invalid sound volume value: %d", volume);
|
||||
}
|
||||
_global.effectsVolume = volume;
|
||||
MIX_SetTagGain(videoGetMixer(), EFFECT_TAG, _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME));
|
||||
for (x = 0; x < EFFECT_TRACKS; x++) {
|
||||
_effectApplyGain(x);
|
||||
}
|
||||
_luaTrace(L, "soundSetVolume", "%d", _global.effectsVolume);
|
||||
|
||||
return 0;
|
||||
|
|
@ -14022,6 +14122,9 @@ static int32_t apiSpriteDrawFrame(lua_State *L) {
|
|||
}
|
||||
source = sprite->animation->frames[frame - 1];
|
||||
}
|
||||
if (sprite->flip != SDL_FLIP_NONE) {
|
||||
source = _spriteFlippedFrame(sprite, source, (sprite->animation != NULL) ? frame : 1);
|
||||
}
|
||||
dest.w = source->w;
|
||||
dest.h = source->h;
|
||||
if (n == 4) {
|
||||
|
|
@ -14110,6 +14213,36 @@ static int32_t apiSpriteDrawRotatedFrame(lua_State *L) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
// spriteFlip(id, horizontal, vertical) Mirrors the sprite: every draw of it, of its frames, and of
|
||||
// its turned frame, until it is called again with false, false. The mirror is applied after the
|
||||
// turn, so a turned frame is just mirrored the other way rather than turned again.
|
||||
static int32_t apiSpriteFlip(lua_State *L) {
|
||||
SpriteT *sprite = NULL;
|
||||
SDL_FlipMode flip = SDL_FLIP_NONE;
|
||||
|
||||
_argCheck(L, "spriteFlip", 3, 3);
|
||||
sprite = _argSprite(L, "spriteFlip", 1);
|
||||
if (_argBoolean(L, "spriteFlip", 2)) {
|
||||
flip |= SDL_FLIP_HORIZONTAL;
|
||||
}
|
||||
if (_argBoolean(L, "spriteFlip", 3)) {
|
||||
flip |= SDL_FLIP_VERTICAL;
|
||||
}
|
||||
if (flip != sprite->flip) {
|
||||
if (sprite->rotatedFrame != NULL) {
|
||||
SDL_FlipSurface(sprite->rotatedFrame, sprite->flip ^ flip);
|
||||
}
|
||||
SDL_DestroySurface(sprite->flippedFrame);
|
||||
sprite->flippedFrame = NULL;
|
||||
sprite->flip = flip;
|
||||
_spriteRebuildSurface(sprite);
|
||||
}
|
||||
_luaTrace(L, "spriteFlip", "%d %d", sprite->id, flip);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// height = spriteFrameHeight(id) Hypseus extension. One frame's height before scaling and rotation; the image's, for a still.
|
||||
static int32_t apiSpriteFrameHeight(lua_State *L) {
|
||||
SpriteT *sprite = NULL;
|
||||
|
|
@ -14462,7 +14595,7 @@ static int32_t apiSpriteRotateFrame(lua_State *L) {
|
|||
source = sprite->animation->frames[frame - 1];
|
||||
}
|
||||
SDL_DestroySurface(sprite->rotatedFrame);
|
||||
sprite->rotatedFrame = rotoZoomSurface(source, -sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth != 0);
|
||||
sprite->rotatedFrame = _spriteTransform(sprite, source);
|
||||
if (sprite->rotatedFrame == NULL) {
|
||||
_luaDie(L, "spriteRotateFrame", "Unable to turn frame %d of sprite %d.", frame, sprite->id);
|
||||
}
|
||||
|
|
|
|||
140
testScripts/author/adventure2d.game
Normal file
140
testScripts/author/adventure2d.game
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
-- Genre one of milestone 3: a point-and-click adventure in a painted room, Sierra and LucasArts
|
||||
-- in one table. Walk areas that become a navigation mesh, hotspots, a verb line, a text parser
|
||||
-- as well, an inventory, a dialogue with a choice that changes the world, a cut-scene, two rooms
|
||||
-- that remember themselves, and a save. The "painting" is a box, since no art is needed.
|
||||
return {
|
||||
title = "Author test: adventure",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "overlay" },
|
||||
{ kind = "parser", prompt = "> ",
|
||||
words = { look = { "look", "examine", "l" }, take = { "take", "get", "pick" }, use = { "use" }, open = { "open" }, talk = { "talk", "speak" },
|
||||
key = { "key" }, door = { "door", "gate" }, guard = { "guard", "man" } },
|
||||
ignore = { "the", "a", "an", "at", "to", "with", "on", "up" } } },
|
||||
vars = { score = 0, allowed = false },
|
||||
verbs = { "walk", "look", "take", "use", "open", "talk" },
|
||||
|
||||
kinds = {
|
||||
backdrop = { look = { kind = "box", w = 720, h = 480, r = 40, g = 45, b = 60 } },
|
||||
floor = { look = { kind = "box", w = 720, h = 200, r = 70, g = 60, b = 50, anchor = "feet" } },
|
||||
hero = { look = { kind = "box", w = 30, h = 70, r = 230, g = 90, b = 170, anchor = "feet" },
|
||||
behaviours = { { kind = "walker", speed = 160 } } },
|
||||
door = { look = { kind = "box", w = 60, h = 120, r = 90, g = 60, b = 40, anchor = "feet" },
|
||||
behaviours = { { kind = "hotspot", name = "door", walkX = 600, walkY = 330 } } },
|
||||
key = { look = { kind = "box", w = 16, h = 8, r = 255, g = 220, b = 60 },
|
||||
behaviours = { { kind = "hotspot", name = "key", walkX = 200, walkY = 400 } } },
|
||||
guard = { look = { kind = "box", w = 34, h = 80, r = 80, g = 120, b = 200, anchor = "feet" },
|
||||
behaviours = { { kind = "hotspot", name = "guard", walkX = 480, walkY = 360 } } },
|
||||
pillar = { look = { kind = "box", w = 40, h = 200, r = 60, g = 60, b = 80, anchor = "feet" } },
|
||||
marker = { look = { kind = "none" } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "hall",
|
||||
depthSort = true,
|
||||
scaleBy = { { y = 300, scale = 0.6 }, { y = 460, scale = 1.0 } },
|
||||
walk = { { 120, 300, 640, 300, 700, 460, 60, 460 } },
|
||||
entities = {
|
||||
{ kind = "backdrop", id = "backdrop", x = 360, y = 240, z = 0 },
|
||||
{ kind = "floor", id = "floor", x = 360, y = 480, z = 0 },
|
||||
{ kind = "door", id = "door", x = 600, y = 300, z = 0 },
|
||||
{ kind = "guard", id = "guard", x = 540, y = 340, z = 0 },
|
||||
{ kind = "pillar", id = "pillar", x = 360, y = 420, z = 0 },
|
||||
{ kind = "key", id = "key", x = 200, y = 380, z = 0 },
|
||||
{ kind = "hero", id = "hero", x = 150, y = 440, z = 0 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} },
|
||||
{ name = "yard",
|
||||
depthSort = true,
|
||||
walk = { { 40, 320, 680, 320, 680, 460, 40, 460 } },
|
||||
entities = {
|
||||
{ kind = "backdrop", id = "backdrop", x = 360, y = 240, z = 0 },
|
||||
{ kind = "door", id = "door", x = 100, y = 300, z = 0 },
|
||||
{ kind = "marker", id = "arrival", x = 160, y = 420, z = 0 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
dialogues = {
|
||||
guard = {
|
||||
start = "hello",
|
||||
nodes = {
|
||||
hello = { who = "Guard", text = "Nobody passes.",
|
||||
choices = { { text = "Why not?", next = "why" },
|
||||
{ text = "Fine." } } },
|
||||
why = { who = "Guard", text = "The door is locked, and I have no key.",
|
||||
choices = { { text = "I have a key.", when = 'has("key")', act = { { "setVar", name = "allowed", value = true }, { "addScore", amount = 1 } }, next = "ok" },
|
||||
{ text = "I see." } } },
|
||||
ok = { who = "Guard", text = "Then go ahead." }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A click on the floor walks there",
|
||||
on = "pressed", switch = "SWITCH_BUTTON3", interrupt = true,
|
||||
act = { { "walkToPointer", entity = "hero", player = 1 } } },
|
||||
|
||||
{ note = "Look at anything",
|
||||
on = "verb", verb = "look",
|
||||
act = { { "say", text = '"It is a " .. event.target .. "."', seconds = 1 } } },
|
||||
|
||||
{ note = "Take the key",
|
||||
on = "verb", verb = "take", target = "key",
|
||||
act = { { "walkToHotspot", entity = "hero", target = "self" },
|
||||
{ "give", item = "key" },
|
||||
{ "addScore", amount = 1 },
|
||||
{ "destroy", entity = "self" } } },
|
||||
|
||||
{ note = "Talk to the guard",
|
||||
on = "verb", verb = "talk", target = "guard",
|
||||
act = { { "walkToHotspot", entity = "hero", target = "self" },
|
||||
{ "face", entity = "hero", target = "self" },
|
||||
{ "talk", dialogue = "guard" } } },
|
||||
|
||||
{ note = "Open the door while the guard objects",
|
||||
on = "verb", verb = "open", target = "door",
|
||||
when = { { "test", expr = "not allowed" } },
|
||||
act = { { "say", text = '"The guard shakes his head."', seconds = 1 } } },
|
||||
|
||||
{ note = "Open the door: the cut-scene",
|
||||
on = "verb", verb = "open", target = "door", room = "hall", controls = false,
|
||||
when = { { "test", expr = "allowed" } },
|
||||
act = { { "walkToHotspot", entity = "hero", target = "self" },
|
||||
{ "say", text = '"The door swings open."', seconds = 1 },
|
||||
{ "fade", seconds = 0.5, out = true },
|
||||
{ "goTo", room = "yard", entity = "hero", at = "arrival" },
|
||||
{ "fade", seconds = 0.5, out = false },
|
||||
{ "addScore", amount = 1 },
|
||||
{ "setVar", name = "outside", value = true } } },
|
||||
|
||||
{ note = "Use the key on the door",
|
||||
on = "verb", verb = "use", item = "key", target = "door",
|
||||
act = { { "say", text = '"It is not that kind of lock."', seconds = 1 } } },
|
||||
|
||||
{ note = "Anything else",
|
||||
on = "verb",
|
||||
act = { { "say", text = '"That does not work."', seconds = 1 } } },
|
||||
|
||||
{ note = "Typed: look at the door",
|
||||
on = "said", verb = "look", noun = "door",
|
||||
act = { { "say", text = '"A heavy door."', seconds = 1 } } },
|
||||
|
||||
{ note = "Typed: a word the game does not know",
|
||||
on = "said",
|
||||
when = { { "test", expr = "event.unknown != nil" } },
|
||||
act = { { "say", text = '"I do not know the word \\"" .. event.unknown .. "\\"."', seconds = 1 } } },
|
||||
|
||||
{ note = "Typed: anything else",
|
||||
on = "said",
|
||||
act = { { "say", text = '"You cannot do that here."', seconds = 1 } } },
|
||||
|
||||
{ note = "The verb keys",
|
||||
on = "pressed", key = "L",
|
||||
act = { { "setVerb", verb = "look" } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '(sentence or "") .. " score " .. score' } } }
|
||||
}
|
||||
}
|
||||
53
testScripts/author/adventure3d.game
Normal file
53
testScripts/author/adventure3d.game
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
-- Genre two of milestone 3: the same adventure in a modelled room from a fixed camera -- Grim
|
||||
-- Fandango's shape. The rules, the dialogue, the verbs, and the parser are the 2D game's own,
|
||||
-- taken from its description; only the kinds and the rooms are new. That the rules do not
|
||||
-- change between a painting and a scene is the point.
|
||||
local base = dofile("testScripts/author/adventure2d.game")
|
||||
|
||||
return {
|
||||
title = "Author test: adventure in 3D",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 80, g = 80, b = 100 }, fov = 50 }, { kind = "overlay" }, base.layers[2] },
|
||||
vars = { score = 0, allowed = false },
|
||||
verbs = base.verbs,
|
||||
dialogues = base.dialogues,
|
||||
rules = base.rules,
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 16, h = 0.4, d = 12, r = 80, g = 70, b = 60 }, behaviours = { { kind = "solid" } } },
|
||||
hero = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.008, w = 0.6, h = 0.8, d = 0.6 },
|
||||
behaviours = { { kind = "walker", speed = 2.5, radius = 0.3 }, { kind = "animator", idle = "Survey", walk = "Walk" } } },
|
||||
door = { look = { kind = "mesh", shape = "box", w = 1.2, h = 2.4, d = 0.2, r = 90, g = 60, b = 40 },
|
||||
behaviours = { { kind = "hotspot", name = "door", walkX = 5, walkY = 0, walkZ = -3 } } },
|
||||
key = { look = { kind = "mesh", shape = "sphere", w = 0.3, r = 255, g = 220, b = 60 },
|
||||
behaviours = { { kind = "hotspot", name = "key", walkX = -3, walkY = 0, walkZ = 1 } } },
|
||||
guard = { look = { kind = "mesh", shape = "cylinder", w = 0.7, h = 1.8, r = 80, g = 120, b = 200 },
|
||||
behaviours = { { kind = "hotspot", name = "guard", walkX = 3.2, walkY = 0, walkZ = -1.5 } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "fixed", lookX = 0, lookY = 0.5, lookZ = -1 } } },
|
||||
marker = { look = { kind = "none" } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "hall",
|
||||
navFrom = { "floor" },
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.2, z = -1 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 6, z = 8 },
|
||||
{ kind = "door", id = "door", x = 5, y = 1.2, z = -4.2 },
|
||||
{ kind = "guard", id = "guard", x = 4, y = 0.9, z = -3 },
|
||||
{ kind = "key", id = "key", x = -3, y = 0.15, z = 0 },
|
||||
{ kind = "hero", id = "hero", x = -4, y = 0, z = 3 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} },
|
||||
{ name = "yard",
|
||||
navFrom = { "floor" },
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.2, z = -1 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 6, z = 8 },
|
||||
{ kind = "door", id = "door", x = -5, y = 1.2, z = -4.2 },
|
||||
{ kind = "marker", id = "arrival", x = -4, y = 0, z = -2 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
}
|
||||
}
|
||||
55
testScripts/author/bowling.game
Normal file
55
testScripts/author/bowling.game
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
-- Sports with a ball, from the catalogue: a ball on a body shoved down a lane by a charged
|
||||
-- press, pins that are bodies, a gutter that is a trigger, and pins counted by how far they fell.
|
||||
return {
|
||||
title = "Author test: bowling",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 100, g = 100, b = 110 }, fov = 50 }, { kind = "overlay" } },
|
||||
vars = { score = 0, down = 0, thrown = false },
|
||||
|
||||
kinds = {
|
||||
lane = { look = { kind = "mesh", shape = "box", w = 3, h = 0.3, d = 24, r = 180, g = 150, b = 100 }, behaviours = { { kind = "solid" } } },
|
||||
ball = { look = { kind = "mesh", shape = "sphere", w = 0.44, r = 40, g = 40, b = 60 }, behaviours = { { kind = "body", shape = "sphere", mass = 7, friction = 0.2, bounce = 0.1 } } },
|
||||
pin = { look = { kind = "mesh", shape = "cylinder", w = 0.24, h = 0.8, r = 240, g = 240, b = 240 }, vars = { counted = false },
|
||||
behaviours = { { kind = "body", mass = 1.5, friction = 0.5 } } },
|
||||
gutter = { look = { kind = "none", w = 3, h = 1, d = 2 }, behaviours = { { kind = "trigger" } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "follow", target = "ball", x = 0, y = 2.5, z = 5, lookY = 0.2, lag = 0.3 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "alley",
|
||||
entities = {
|
||||
{ kind = "lane", id = "lane", x = 0, y = -0.15, z = -8 },
|
||||
{ kind = "ball", id = "ball", x = 0, y = 0.3, z = 2 },
|
||||
{ kind = "pin", id = "pin1", x = 0, y = 0.4, z = -16 },
|
||||
{ kind = "pin", id = "pin2", x = -0.4, y = 0.4, z = -16.7 },
|
||||
{ kind = "pin", id = "pin3", x = 0.4, y = 0.4, z = -16.7 },
|
||||
{ kind = "pin", id = "pin4", x = -0.8, y = 0.4, z = -17.4 },
|
||||
{ kind = "pin", id = "pin5", x = 0, y = 0.4, z = -17.4 },
|
||||
{ kind = "pin", id = "pin6", x = 0.8, y = 0.4, z = -17.4 },
|
||||
{ kind = "gutter", id = "gutter", x = 0, y = 0, z = -21 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 2.5, z = 7 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "The throw",
|
||||
on = "pressed", key = "SPACE",
|
||||
when = { { "test", expr = "not thrown" } },
|
||||
act = { { "setVar", name = "thrown", value = true }, { "push", entity = "ball", x = 0, y = 0, z = -60 } } },
|
||||
|
||||
{ note = "A pin has fallen when it leans past the point of no return",
|
||||
on = "frame", each = "pin",
|
||||
when = { { "test", expr = "not self.counted and self.y < 0.25" } },
|
||||
act = { { "setVar", entity = "self", name = "counted", value = true }, { "addVar", name = "down", amount = 1 }, { "addScore", amount = 10 } } },
|
||||
|
||||
{ note = "The ball reaches the end",
|
||||
on = "enter", a = "gutter", b = "ball",
|
||||
act = { { "setVar", name = "done", value = true } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"pins down " .. down .. " score " .. score' } } }
|
||||
}
|
||||
}
|
||||
55
testScripts/author/breakout.game
Normal file
55
testScripts/author/breakout.game
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
-- Arcade physics, from the catalogue: a ball that bounces, bricks that break, a paddle the keys
|
||||
-- move, and a life lost when the ball is missed. world2d with no gravity.
|
||||
return {
|
||||
title = "Author test: breakout",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "world2d", gravity = 0 } },
|
||||
vars = { score = 0, lives = 3, broken = 0 },
|
||||
|
||||
kinds = {
|
||||
wall = { look = { kind = "box", w = 20, h = 480, r = 90, g = 90, b = 110 }, behaviours = { { kind = "solid" } } },
|
||||
roof = { look = { kind = "box", w = 720, h = 20, r = 90, g = 90, b = 110 }, behaviours = { { kind = "solid" } } },
|
||||
paddle = { look = { kind = "box", w = 100, h = 14, r = 230, g = 230, b = 240 },
|
||||
behaviours = { { kind = "keys", player = 1, left = "LEFT", right = "RIGHT" }, { kind = "mover", speed = 400, clamp = true }, { kind = "solid", moving = true } } },
|
||||
ball = { look = { kind = "box", w = 14, h = 14, r = 255, g = 220, b = 80 },
|
||||
behaviours = { { kind = "body", shape = "sphere", mass = 1, bounce = 1, friction = 0 } } },
|
||||
brick = { look = { kind = "box", w = 60, h = 20, r = 220, g = 90, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "court",
|
||||
entities = {
|
||||
{ kind = "wall", id = "wallL", x = 10, y = 240 },
|
||||
{ kind = "wall", id = "wallR", x = 710, y = 240 },
|
||||
{ kind = "roof", id = "roof", x = 360, y = 10 },
|
||||
{ kind = "brick", id = "brick1", x = 200, y = 100 },
|
||||
{ kind = "brick", id = "brick2", x = 280, y = 100 },
|
||||
{ kind = "brick", id = "brick3", x = 360, y = 100 },
|
||||
{ kind = "brick", id = "brick4", x = 440, y = 100 },
|
||||
{ kind = "brick", id = "brick5", x = 520, y = 100 },
|
||||
{ kind = "paddle", id = "paddle", x = 360, y = 440 },
|
||||
{ kind = "ball", id = "ball", x = 360, y = 380 },
|
||||
{ kind = "readout", id = "readout", x = 30, y = 30 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "Serve",
|
||||
on = "roomStart",
|
||||
act = { { "push", entity = "ball", x = 120, y = -380 } } },
|
||||
|
||||
{ note = "A brick breaks",
|
||||
on = "collision", a = "ball", b = "brick",
|
||||
act = { { "destroy", entity = "other" }, { "addScore", amount = 10 }, { "addVar", name = "broken", amount = 1 } } },
|
||||
|
||||
{ note = "The ball is missed",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "ball.y > 500" } },
|
||||
act = { { "addVar", name = "lives", amount = -1 }, { "moveTo", entity = "ball", x = 360, y = 380 }, { "push", entity = "ball", x = -100, y = -380 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " lives " .. lives' } } }
|
||||
}
|
||||
}
|
||||
74
testScripts/author/fmv.game
Normal file
74
testScripts/author/fmv.game
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
-- Genre one of milestone 5: branching full-motion video, Dragon's Lair in a table. Windows on
|
||||
-- the disc that want a move, the disc sent to a success or a failure clip, lives, credits and a
|
||||
-- continue, the arcade score panel, and the score sent to the board at the end. The menu video
|
||||
-- stands in for the disc.
|
||||
return {
|
||||
title = "Author test: branching video",
|
||||
layers = { { kind = "disc", file = "Singe/menuBackground.mkv", start = 100 }, { kind = "overlay" }, { kind = "bezel" } },
|
||||
vars = { score = 0, lives = 3, credits = 0, taken = 0, missed = 0 },
|
||||
|
||||
kinds = {
|
||||
dirk = { look = { kind = "none" }, behaviours = { { kind = "branching", track = "moves" } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } },
|
||||
prompt = { look = { kind = "text", text = "", r = 255, g = 220, b = 80 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "cavern",
|
||||
entities = {
|
||||
{ kind = "dirk", id = "dirk", x = 0, y = 0 },
|
||||
{ kind = "prompt", id = "prompt", x = 250, y = 60 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
},
|
||||
tracks = {
|
||||
{ name = "moves", key = "frame",
|
||||
branches = { { from = 120, to = 160, move = "RIGHT", success = 200, fail = 340 },
|
||||
{ from = 220, to = 260, move = "UP", success = 300, fail = 340 } } }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A window opens: say what it wants",
|
||||
on = "branchOpen",
|
||||
act = { { "setText", entity = "prompt", text = '"MOVE " .. event.move' } } },
|
||||
|
||||
{ note = "Made it",
|
||||
on = "branchTaken",
|
||||
act = { { "addScore", amount = 500 },
|
||||
{ "addVar", name = "taken", amount = 1 },
|
||||
{ "setText", entity = "prompt", text = '""' } } },
|
||||
|
||||
{ note = "Missed it: a life gone",
|
||||
on = "branchMissed",
|
||||
act = { { "addVar", name = "lives", amount = -1 },
|
||||
{ "addVar", name = "missed", amount = 1 },
|
||||
{ "flash", r = 255, g = 40, b = 40, seconds = 0.3 },
|
||||
{ "setText", entity = "prompt", text = '""' } } },
|
||||
|
||||
{ note = "The death clip is over: back to the cavern, or game over",
|
||||
on = "frameReached", frame = 380,
|
||||
when = { { "test", expr = "lives > 0" } },
|
||||
act = { { "discTo", frame = 100 } } },
|
||||
|
||||
{ note = "Out of lives",
|
||||
on = "frameReached", frame = 380,
|
||||
when = { { "test", expr = "lives <= 0" } },
|
||||
act = { { "submitScore", board = "default" },
|
||||
{ "gameOver" } } },
|
||||
|
||||
{ note = "A coin",
|
||||
on = "pressed", switch = "SWITCH_COIN1",
|
||||
act = { { "credit" } } },
|
||||
|
||||
{ note = "Continue",
|
||||
on = "pressed", switch = "SWITCH_START1",
|
||||
when = { { "test", expr = "credits > 0 and lives <= 0" } },
|
||||
act = { { "addVar", name = "credits", amount = -1 },
|
||||
{ "setVar", name = "lives", value = 3 },
|
||||
{ "restart" } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " lives " .. lives .. " credits " .. credits' } } }
|
||||
}
|
||||
}
|
||||
81
testScripts/author/fps.game
Normal file
81
testScripts/author/fps.game
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
-- A first-person shooter, from the catalogue: a character the keys move relative to a camera on
|
||||
-- its eyes that the mouse turns, a gun that fires from the middle of the picture, and enemies
|
||||
-- that walk the navigation mesh after the hero and bite when they reach it.
|
||||
return {
|
||||
title = "Author test: first person",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 60, g = 60, b = 80 }, fog = { r = 10, g = 10, b = 16, near = 20, far = 50 }, fov = 70 }, { kind = "overlay" } },
|
||||
vars = { score = 0, health = 5, bites = 0 },
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 40, h = 0.4, d = 40, r = 70, g = 70, b = 80 }, behaviours = { { kind = "solid" } } },
|
||||
wall = { look = { kind = "mesh", shape = "box", w = 1, h = 3, d = 40, r = 110, g = 60, b = 60 }, behaviours = { { kind = "solid" } } },
|
||||
hero = { look = { kind = "none", w = 0.6, h = 1.7, d = 0.6 },
|
||||
behaviours = { { kind = "keys", player = 1, left = "A", right = "D", up = "W", down = "S" },
|
||||
{ kind = "character", speed = 4, jump = 5, radius = 0.3, height = 1.7 } } },
|
||||
eyes = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "first", target = "hero", eye = 1.5 } } },
|
||||
gun = { look = { kind = "none" }, vars = { ammo = 30 }, behaviours = { { kind = "gun", player = 1, ammo = 30, reload = "key", aim = "centre" } } },
|
||||
zombie = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.01, w = 0.8, h = 0.9, d = 1.4 },
|
||||
vars = { health = 2 },
|
||||
behaviours = { { kind = "health", max = 2, ragdoll = true, linger = 1.5 },
|
||||
{ kind = "walker", speed = 2.5, radius = 0.4, follow = "hero", every = 0.4, stopAt = 1.6 },
|
||||
{ kind = "animator", idle = "Survey", walk = "Walk", attack = "Run", fade = 0.2 },
|
||||
{ kind = "target", zones = { { name = "head", bone = "b_Head_05", w = 0.5, h = 0.5, d = 0.5 } } },
|
||||
{ kind = "timer", name = "bite", after = 1.2, start = false } } },
|
||||
hive = { look = { kind = "none" }, behaviours = { { kind = "spawner", spawns = "zombie", every = 2, max = 4, total = 6, points = { { x = -8, y = 0, z = -12 }, { x = 8, y = 0, z = -12 }, { x = 0, y = 0, z = -16 } } } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "arena",
|
||||
navFrom = { "floor" },
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.2, z = 0 },
|
||||
{ kind = "wall", id = "wallL", x = -12, y = 1.5, z = 0 },
|
||||
{ kind = "wall", id = "wallR", x = 12, y = 1.5, z = 0 },
|
||||
{ kind = "hero", id = "hero", x = 0, y = 0.2, z = 8 },
|
||||
{ kind = "eyes", id = "eyes", x = 0, y = 1.7, z = 8 },
|
||||
{ kind = "gun", id = "gun1", x = 0, y = 0, z = 0 },
|
||||
{ kind = "hive", id = "hive", x = 0, y = 0, z = -14 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A zombie walks from the moment it is made",
|
||||
on = "spawn", kind = "zombie",
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "A shot lands",
|
||||
on = "hit", kind = "zombie",
|
||||
act = { { "damage", entity = "self", amount = 1 }, { "addScore", amount = 50 } } },
|
||||
|
||||
{ note = "A headshot",
|
||||
on = "hit", kind = "zombie",
|
||||
when = { { "hitPart", part = "head" } },
|
||||
act = { { "damage", entity = "self", amount = 2 }, { "addScore", amount = 150 } } },
|
||||
|
||||
{ note = "It reaches the hero and rears up",
|
||||
on = "arrived", kind = "zombie",
|
||||
act = { { "setState", entity = "self", state = "attack" }, { "timerStart", entity = "self", name = "bite", after = 1.2 } } },
|
||||
|
||||
{ note = "The bite lands unless it was killed",
|
||||
on = "timer", name = "bite", kind = "zombie",
|
||||
when = { { "inState", entity = "self", state = "attack" } },
|
||||
act = { { "addVar", name = "health", amount = -1 }, { "addVar", name = "bites", amount = 1 }, { "flash", r = 255, g = 40, b = 40, seconds = 0.2 },
|
||||
{ "timerStart", entity = "self", name = "bite", after = 1.2 } } },
|
||||
|
||||
{ note = "Reload on R",
|
||||
on = "pressed", key = "R",
|
||||
act = { { "reload", entity = "gun1" } } },
|
||||
|
||||
{ note = "Out of health",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "health <= 0" }, { "once", tag = "over", scope = "game" } },
|
||||
act = { { "gameOver" } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " health " .. health .. " ammo " .. gun1.ammo .. " zombies " .. count("zombie")' } } }
|
||||
}
|
||||
}
|
||||
61
testScripts/author/gunvideo.game
Normal file
61
testScripts/author/gunvideo.game
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
-- Genre two of milestone 1: a light gun game over video. Hitboxes keyed to disc frames, a gun
|
||||
-- that hits or misses, ammo and a reload, a prompt at a frame, and the disc as the picture -- Mad
|
||||
-- Dog McCree in a table. The boxes follow nothing in the menu video; they are where the test
|
||||
-- expects them.
|
||||
return {
|
||||
title = "Author test: gun over video",
|
||||
players = 1,
|
||||
layers = { { kind = "disc", file = "Singe/menuBackground.mkv", start = 100 }, { kind = "overlay" } },
|
||||
vars = { score = 0, misses = 0 },
|
||||
|
||||
kinds = {
|
||||
gun = { look = { kind = "none" },
|
||||
vars = { ammo = 3 },
|
||||
behaviours = { { kind = "gun", player = 1, ammo = 3, reload = "offscreen" } } },
|
||||
bandit = { look = { kind = "none" },
|
||||
vars = { health = 1 },
|
||||
behaviours = { { kind = "hitbox", track = "bandit" }, { kind = "health", max = 1 } } },
|
||||
hostage = { look = { kind = "none" },
|
||||
behaviours = { { kind = "hitbox", track = "hostage" } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "street",
|
||||
entities = {
|
||||
{ kind = "gun", id = "gun1", x = 0, y = 0 },
|
||||
{ kind = "bandit", id = "bandit", x = 0, y = 0 },
|
||||
{ kind = "hostage", id = "hostage", x = 0, y = 0 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
},
|
||||
tracks = {
|
||||
{ name = "bandit", key = "frame", boxes = { { at = 120, x = 100, y = 200, w = 60, h = 100 }, { at = 220, x = 400, y = 200, w = 60, h = 100 } } },
|
||||
{ name = "hostage", key = "frame", boxes = { { at = 100, x = 560, y = 240, w = 50, h = 80 }, { at = 300, x = 560, y = 240, w = 50, h = 80 } } }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "The bandit is shot",
|
||||
on = "hit", kind = "bandit",
|
||||
act = { { "addScore", amount = 100 },
|
||||
{ "damage", entity = "self", amount = 1 } } },
|
||||
|
||||
{ note = "The hostage is shot",
|
||||
on = "hit", kind = "hostage",
|
||||
act = { { "addScore", amount = -200 },
|
||||
{ "flash", r = 255, g = 40, b = 40, seconds = 0.3 } } },
|
||||
|
||||
{ note = "A miss on the picture",
|
||||
on = "miss",
|
||||
when = { { "test", expr = "not event.offscreen" } },
|
||||
act = { { "addVar", name = "misses", amount = 1 } } },
|
||||
|
||||
{ note = "Draw!",
|
||||
on = "frameReached", frame = 120,
|
||||
act = { { "say", text = '"DRAW!"', seconds = 1 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " ammo " .. gun1.ammo' } } }
|
||||
}
|
||||
}
|
||||
17
testScripts/author/hud.rml
Normal file
17
testScripts/author/hud.rml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<rml>
|
||||
<head>
|
||||
<title>hud</title>
|
||||
<link type="text/rcss" href="Singe/gui.rcss"/>
|
||||
<style>
|
||||
body { pointer-events: none; font-family: FreeSans; font-size: 18px; color: #f0f0f8; }
|
||||
#score { position: absolute; left: 12px; top: 8px; }
|
||||
#lives { position: absolute; right: 12px; top: 8px; }
|
||||
#say { position: absolute; left: 0px; right: 0px; top: 200px; text-align: center; font-size: 28px; color: #ffd84a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="score">0</div>
|
||||
<div id="lives">3</div>
|
||||
<div id="say"></div>
|
||||
</body>
|
||||
</rml>
|
||||
48
testScripts/author/maze.game
Normal file
48
testScripts/author/maze.game
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- A maze, from the catalogue: corridors that are a walk area, a hero sent by clicks, dots that
|
||||
-- are eaten on touch, and a ghost that follows the hero across the mesh.
|
||||
return {
|
||||
title = "Author test: maze",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "overlay" } },
|
||||
vars = { score = 0, eaten = 0 },
|
||||
|
||||
kinds = {
|
||||
walls = { look = { kind = "grid", file = "testScripts/checker.png", tile = 32, columns = 2, map = "1 1 1 1 1 1 1 1 1 1 1; 1 0 0 0 0 0 0 0 0 0 1; 1 0 1 1 1 0 1 1 1 0 1; 1 0 0 0 0 0 0 0 0 0 1; 1 1 1 1 1 1 1 1 1 1 1" } },
|
||||
hero = { look = { kind = "box", w = 24, h = 24, r = 255, g = 230, b = 60 }, behaviours = { { kind = "walker", speed = 140, radius = 6 } } },
|
||||
ghost = { look = { kind = "box", w = 24, h = 24, r = 90, g = 120, b = 255 }, behaviours = { { kind = "walker", speed = 90, radius = 6, follow = "hero", every = 0.3, stopAt = 10 } } },
|
||||
dot = { look = { kind = "box", w = 8, h = 8, r = 255, g = 255, b = 255 } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "maze",
|
||||
walk = { { 40, 140, 360, 140, 360, 220, 40, 220 }, { 40, 220, 360, 220, 360, 300, 40, 300 } },
|
||||
entities = {
|
||||
{ kind = "walls", id = "walls", x = 8, y = 100 },
|
||||
{ kind = "dot", id = "dot1", x = 150, y = 180 },
|
||||
{ kind = "dot", id = "dot2", x = 250, y = 180 },
|
||||
{ kind = "dot", id = "dot3", x = 250, y = 260 },
|
||||
{ kind = "hero", id = "hero", x = 60, y = 180 },
|
||||
{ kind = "ghost", id = "ghost", x = 340, y = 260 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A click sends the hero",
|
||||
on = "pressed", switch = "SWITCH_BUTTON3", interrupt = true,
|
||||
act = { { "walkToPointer", entity = "hero", player = 1 } } },
|
||||
|
||||
{ note = "A dot is eaten",
|
||||
on = "collision", a = "hero", b = "dot",
|
||||
act = { { "destroy", entity = "other" }, { "addScore", amount = 10 }, { "addVar", name = "eaten", amount = 1 } } },
|
||||
|
||||
{ note = "Caught",
|
||||
on = "collision", a = "ghost", b = "hero",
|
||||
act = { { "setVar", name = "caught", value = true } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " eaten " .. eaten' } } }
|
||||
}
|
||||
}
|
||||
43
testScripts/author/painted.game
Normal file
43
testScripts/author/painted.game
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- A painted room in 3D, the shape Grim Fandango has: the backdrop is a picture on a flat mesh, a
|
||||
-- pillar that is only in the picture is an occluder -- an invisible box that hides what walks
|
||||
-- behind it -- and a character crosses the room behind it and comes out the other side.
|
||||
return {
|
||||
title = "Author test: painted room",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 120, g = 120, b = 130 }, sun = false, fov = 50 }, { kind = "overlay" } },
|
||||
vars = { score = 0 },
|
||||
|
||||
kinds = {
|
||||
painting = { look = { kind = "mesh", shape = "box", w = 16, h = 9, d = 0.2, r = 255, g = 255, b = 255, texture = "testScripts/hills.png", unlit = true } },
|
||||
pillar = { look = { kind = "mesh", shape = "box", w = 1.6, h = 5, d = 1.6, occluder = true } },
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 20, h = 0.2, d = 12, r = 90, g = 90, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
hero = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.012, clip = "Walk", w = 0.8, h = 1, d = 1.5 },
|
||||
behaviours = { { kind = "seek", target = "far", speed = 2.2, stopAt = 0.3 } } },
|
||||
marker = { look = { kind = "none" } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "fixed", lookX = 0, lookY = 1.2, lookZ = -4 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "hall",
|
||||
entities = {
|
||||
{ kind = "painting", id = "painting", x = 0, y = 3, z = -9 },
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.1, z = -4 },
|
||||
{ kind = "pillar", id = "pillar", x = 0, y = 2.5, z = -4 },
|
||||
{ kind = "hero", id = "hero", x = -5, y = 0, z = -6 },
|
||||
{ kind = "marker", id = "far", x = 5, y = 0, z = -6, ry = 90, scale = 2 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 2.2, z = 5 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "Across the room",
|
||||
on = "arrived", kind = "hero",
|
||||
act = { { "setVar", name = "across", value = true } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"the hero is at " .. floor(hero.x * 10) / 10' } } }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +1,62 @@
|
|||
-- A game description: data, not code. Forge/AuthorCompile.singe turns it into a Singe game.
|
||||
--
|
||||
-- Genre one of two. This one uses a single world2d layer and shares nothing with the QTE beside
|
||||
-- it -- no disc, no branching, no time windows -- which is the point: if the core can express
|
||||
-- both, it is not secretly a platformer engine or secretly a laserdisc engine.
|
||||
-- Genre one of the two the first slice was built on. A single world2d room, a character that runs
|
||||
-- and jumps, a prize, and a readout. It shares nothing with the QTE beside it -- no disc, no
|
||||
-- branching, no time windows -- which is the point: if the core can express both, it is not
|
||||
-- secretly a platformer engine or secretly a laserdisc engine.
|
||||
return {
|
||||
title = "Author test: platformer",
|
||||
title = "Author test: platformer",
|
||||
layers = { { kind = "world2d", gravity = 1500 } },
|
||||
vars = { score = 0 },
|
||||
|
||||
layers = {
|
||||
{ kind = "world2d", gravity = 1500 }
|
||||
kinds = {
|
||||
ground = { look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
ledge = { look = { kind = "box", w = 180, h = 20, r = 60, g = 70, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
prize = { look = { kind = "box", w = 18, h = 18, r = 255, g = 200, b = 60 }, behaviours = { { kind = "drift", vx = 0, vy = 0 } } },
|
||||
hero = { look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 }, behaviours = { { kind = "platformer", speed = 210, jump = 620 } } },
|
||||
readout = { look = { kind = "text", text = "score 0", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
entities = {
|
||||
{ id = "ground", x = 360, y = 440, look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
|
||||
behaviours = { { kind = "solid" } } },
|
||||
{ id = "ledge", x = 520, y = 330, look = { kind = "box", w = 180, h = 20, r = 60, g = 70, b = 90 },
|
||||
behaviours = { { kind = "solid" } } },
|
||||
{ id = "prize", x = 520, y = 300, look = { kind = "box", w = 18, h = 18, r = 255, g = 200, b = 60 },
|
||||
behaviours = { { kind = "drift", vx = 0, vy = 0 } } },
|
||||
{ id = "hero", x = 120, y = 380, look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
|
||||
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } },
|
||||
{ id = "readout", x = 12, y = 12, look = { kind = "text", text = "score 0", r = 235, g = 235, b = 245 } }
|
||||
rooms = {
|
||||
{ name = "field",
|
||||
entities = {
|
||||
{ kind = "ground", id = "ground", x = 360, y = 440 },
|
||||
{ kind = "ledge", id = "ledge", x = 520, y = 330 },
|
||||
{ kind = "prize", id = "prize", x = 520, y = 300 },
|
||||
{ kind = "hero", id = "hero", x = 120, y = 380 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "Run left",
|
||||
on = "frame",
|
||||
when = { { "keyHeld", key = "LEFT" } },
|
||||
act = { { "run", entity = "hero", direction = -1 } } },
|
||||
|
||||
{ note = "Run right",
|
||||
on = "frame",
|
||||
when = { { "keyHeld", key = "RIGHT" } },
|
||||
act = { { "run", entity = "hero", direction = 1 } } },
|
||||
|
||||
{ note = "Jump, but only with ground underfoot",
|
||||
on = "frame",
|
||||
when = { { "keyHeld", key = "SPACE" }, { "onGround", entity = "hero" } },
|
||||
act = { { "jump", entity = "hero" } } },
|
||||
|
||||
{ note = "Take the prize once",
|
||||
when = { { "touching", entity = "hero", other = "prize" }, { "once", tag = "prize" } },
|
||||
{ note = "Take the prize",
|
||||
on = "collision", a = "hero", b = "prize",
|
||||
act = { { "addScore", amount = 100 },
|
||||
{ "show", entity = "prize", visible = false },
|
||||
{ "setFlag", flag = "won", value = true } } },
|
||||
{ "destroy", entity = "other" },
|
||||
{ "setVar", name = "won", value = true } } },
|
||||
|
||||
{ note = "Fallen off the world: back to the start",
|
||||
on = "frame",
|
||||
when = { { "below", entity = "hero", y = 520 } },
|
||||
act = { { "moveTo", entity = "hero", x = 120, y = 380 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
when = {},
|
||||
act = { { "setText", entity = "readout", text = '"score " .. AUTHOR_SCORE' } } }
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score' } } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
testScripts/author/platformer3d.game
Normal file
65
testScripts/author/platformer3d.game
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
-- Genre two of milestone 2: a 3D platformer. A character on the engine's controller, a camera
|
||||
-- that follows, floors and ledges, a prize that is a trigger, and a fall that puts the hero back.
|
||||
-- Nothing in it is shared with the rail shooter but the scene itself.
|
||||
return {
|
||||
title = "Author test: 3D platformer",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 80, g = 80, b = 100 }, fov = 55 }, { kind = "overlay" } },
|
||||
vars = { score = 0 },
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 30, h = 0.4, d = 12, r = 90, g = 110, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
ledge = { look = { kind = "mesh", shape = "box", w = 4, h = 0.4, d = 4, r = 110, g = 90, b = 70 }, behaviours = { { kind = "solid" } } },
|
||||
prize = { look = { kind = "mesh", shape = "sphere", w = 0.6, r = 255, g = 200, b = 60 }, behaviours = { { kind = "trigger" } } },
|
||||
hero = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.008, w = 0.6, h = 0.9, d = 0.6 },
|
||||
behaviours = { { kind = "keys", player = 1, left = "LEFT", right = "RIGHT", up = "UP", down = "DOWN" },
|
||||
{ kind = "character", speed = 4, jump = 6.5, radius = 0.3, height = 0.9 },
|
||||
{ kind = "animator", idle = "Survey", walk = "Walk" } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "follow", target = "hero", x = 0, y = 3, z = 7, lookY = 0.6, lag = 0.2 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "field",
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.2, z = 0 },
|
||||
{ kind = "ledge", id = "ledge", x = 6, y = 1.2, z = 0 },
|
||||
{ kind = "prize", id = "prize", x = 6, y = 2.0, z = 0 },
|
||||
{ kind = "hero", id = "hero", x = -4, y = 0.5, z = 0 },
|
||||
{ kind = "camera", id = "camera", x = -4, y = 3, z = 7 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "Walking sets the state the animator plays",
|
||||
on = "frame", each = "hero",
|
||||
when = { { "test", expr = "self.dx != 0 or self.dy != 0" } },
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "Standing still",
|
||||
on = "frame", each = "hero",
|
||||
when = { { "test", expr = "self.dx == 0 and self.dy == 0" } },
|
||||
act = { { "setState", entity = "self", state = "idle" } } },
|
||||
|
||||
{ note = "Jump, with ground underfoot",
|
||||
on = "frame",
|
||||
when = { { "keyHeld", key = "SPACE" }, { "onGround", entity = "hero" } },
|
||||
act = { { "jump", entity = "hero" } } },
|
||||
|
||||
{ note = "The prize is taken",
|
||||
on = "enter", a = "prize", b = "hero",
|
||||
act = { { "destroy", entity = "self" },
|
||||
{ "addScore", amount = 100 },
|
||||
{ "setVar", name = "won", value = true } } },
|
||||
|
||||
{ note = "Fallen off the world: back to the start",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "hero.y < -5" } },
|
||||
act = { { "moveTo", entity = "hero", x = -4, y = 0.5, z = 0 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score' } } }
|
||||
}
|
||||
}
|
||||
42
testScripts/author/pool.game
Normal file
42
testScripts/author/pool.game
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- The long tail, in 3D: water a body floats in, a boat pushed by its propeller, and a soft
|
||||
-- body -- the pieces a boat game or a physics toy needs.
|
||||
return {
|
||||
title = "Author test: the pool",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 90, g = 100, b = 120 }, fov = 55 }, { kind = "overlay" } },
|
||||
vars = { score = 0 },
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 40, h = 0.4, d = 40, r = 90, g = 90, b = 80 }, behaviours = { { kind = "solid" } } },
|
||||
pool = { look = { kind = "mesh", shape = "box", w = 20, h = 3, d = 20, r = 60, g = 120, b = 200 }, behaviours = { { kind = "water", density = 1 } } },
|
||||
ball = { look = { kind = "mesh", shape = "sphere", w = 0.8, r = 220, g = 80, b = 80 }, behaviours = { { kind = "body", shape = "sphere", mass = 2, buoyancy = 2 } } },
|
||||
boat = { look = { kind = "mesh", shape = "box", w = 1.6, h = 0.4, d = 2.6, r = 200, g = 170, b = 90 },
|
||||
behaviours = { { kind = "body", mass = 120, buoyancy = 3 }, { kind = "vehicle", type = "boat", mass = 120, thrust = 600 } } },
|
||||
balloon = { look = { kind = "mesh", shape = "sphere", w = 1, r = 240, g = 220, b = 80 }, behaviours = { { kind = "soft", type = "body", pressure = 2, mass = 0.6 } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "fixed", lookX = 0, lookY = 1, lookZ = 0 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "pool",
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -3.2, z = 0 },
|
||||
{ kind = "pool", id = "pool", x = 0, y = -1.5, z = 0 },
|
||||
{ kind = "ball", id = "ball", x = 3, y = 3, z = 2 },
|
||||
{ kind = "boat", id = "boat", x = -4, y = 0.4, z = 6 },
|
||||
{ kind = "balloon", id = "balloon", x = 6, y = 4, z = -6 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 10, z = 16 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "The boat runs its propeller",
|
||||
on = "frame", each = "boat",
|
||||
act = { { "setVar", entity = "self", name = "dy", value = -1 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"ball y " .. floor(ball.y * 10) / 10 .. " boat z " .. floor(boat.z * 10) / 10' } } }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,47 @@
|
|||
-- Genre two of two: a quick-time event over video. No physics, no character, no gravity -- the
|
||||
-- only layers are the disc and a flat overlay, and the rules talk about disc frames and windows.
|
||||
-- Genre two of the first slice: a quick-time event over video. No physics, no character, no
|
||||
-- gravity -- the only layers are the disc and a flat overlay, and the rules talk about disc frames
|
||||
-- and windows.
|
||||
return {
|
||||
title = "Author test: QTE over video",
|
||||
title = "Author test: QTE over video",
|
||||
layers = { { kind = "disc", file = "Singe/menuBackground.mkv", start = 180 }, { kind = "overlay" } },
|
||||
vars = { score = 0 },
|
||||
|
||||
layers = {
|
||||
{ kind = "disc", start = 180 },
|
||||
{ kind = "overlay" }
|
||||
kinds = {
|
||||
prompt = { look = { kind = "text", text = "", r = 255, g = 220, b = 80 } },
|
||||
verdict = { look = { kind = "text", text = "", r = 160, g = 255, b = 180 } },
|
||||
readout = { look = { kind = "text", text = "score 0", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
entities = {
|
||||
{ id = "prompt", x = 250, y = 60, look = { kind = "text", text = "", r = 255, g = 220, b = 80 } },
|
||||
{ id = "verdict", x = 250, y = 100, look = { kind = "text", text = "", r = 160, g = 255, b = 180 } },
|
||||
{ id = "readout", x = 12, y = 12, look = { kind = "text", text = "score 0", r = 235, g = 235, b = 245 } }
|
||||
rooms = {
|
||||
{ name = "scene",
|
||||
entities = {
|
||||
{ kind = "prompt", id = "prompt", x = 250, y = 60 },
|
||||
{ kind = "verdict", id = "verdict", x = 250, y = 100 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "The window is open: ask for the button",
|
||||
on = "frame",
|
||||
when = { { "discBetween", from = 200, to = 260 } },
|
||||
act = { { "setText", entity = "prompt", text = '"PRESS!"' } } },
|
||||
|
||||
{ note = "Answered in time, once",
|
||||
on = "frame",
|
||||
when = { { "discBetween", from = 200, to = 260 }, { "switchHeld", switch = "SWITCH_BUTTON1" }, { "once", tag = "hit" } },
|
||||
act = { { "setFlag", flag = "hit", value = true },
|
||||
act = { { "setVar", name = "hit", value = true },
|
||||
{ "addScore", amount = 250 },
|
||||
{ "setText", entity = "verdict", text = '"HIT"' } } },
|
||||
|
||||
{ note = "The window has closed without an answer",
|
||||
when = { { "discBetween", from = 260, to = 320 }, { "once", tag = "judged" } },
|
||||
on = "frameReached", frame = 260,
|
||||
when = { { "test", expr = "not hit" } },
|
||||
act = { { "setText", entity = "prompt", text = '""' },
|
||||
{ "lua", code = 'if not AUTHOR_FLAG["hit"] then authorSetText("verdict", "MISS") end' } } },
|
||||
{ "setText", entity = "verdict", text = '"MISS"' } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
when = {},
|
||||
act = { { "setText", entity = "readout", text = '"score " .. AUTHOR_SCORE' } } }
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score' } } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
testScripts/author/racing.game
Normal file
66
testScripts/author/racing.game
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
-- Genre one of milestone 4: racing. A car on the engine's vehicle physics driven by the keys,
|
||||
-- a rival that drives a track of points on its own, checkpoints that are triggers, laps counted
|
||||
-- by rules, and a camera that follows.
|
||||
return {
|
||||
title = "Author test: racing",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 90, g = 90, b = 110 }, fov = 60 }, { kind = "overlay" } },
|
||||
vars = { score = 0, laps = 0, next = 1 },
|
||||
|
||||
kinds = {
|
||||
ground = { look = { kind = "mesh", shape = "box", w = 120, h = 0.4, d = 200, r = 90, g = 100, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
car = { look = { kind = "mesh", shape = "box", w = 1.6, h = 0.6, d = 3.2, r = 220, g = 60, b = 60 },
|
||||
behaviours = { { kind = "keys", player = 1, left = "LEFT", right = "RIGHT", up = "UP", down = "DOWN" },
|
||||
{ kind = "vehicle", type = "car", torque = 900, mass = 800 } } },
|
||||
rival = { look = { kind = "mesh", shape = "box", w = 1.6, h = 0.6, d = 3.2, r = 60, g = 90, b = 220 },
|
||||
behaviours = { { kind = "vehicle", type = "car", torque = 700, mass = 800 },
|
||||
{ kind = "racer", track = "lap", throttle = 0.5, reach = 4 } } },
|
||||
gate = { look = { kind = "none", w = 10, h = 3, d = 1 }, vars = { index = 1 }, behaviours = { { kind = "trigger" } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "follow", target = "car", x = 0, y = 4, z = 9, lookY = 0.5, lag = 0.3 } } },
|
||||
bar = { look = { kind = "mesh", shape = "box", w = 4, h = 0.3, d = 0.3, r = 200, g = 200, b = 60 },
|
||||
behaviours = { { kind = "body", mass = 20 }, { kind = "joint", type = "hinge", ax = -8, ay = 2, az = -20, dx = 0, dy = 0, dz = 1 } } },
|
||||
hover = { look = { kind = "mesh", shape = "box", w = 1.2, h = 0.4, d = 2, r = 60, g = 220, b = 200 },
|
||||
behaviours = { { kind = "body", mass = 100, friction = 0 }, { kind = "thrust", force = 900, turn = 0.5 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "track",
|
||||
entities = {
|
||||
{ kind = "ground", id = "ground", x = 0, y = -0.2, z = -40 },
|
||||
{ kind = "car", id = "car", x = 0, y = 0.8, z = 10 },
|
||||
{ kind = "rival", id = "rival", x = 4, y = 0.8, z = 12 },
|
||||
{ kind = "gate", id = "gate1", x = 0, y = 1.5, z = -10, vars = { index = 1 } },
|
||||
{ kind = "gate", id = "gate2", x = 0, y = 1.5, z = -35, vars = { index = 2 } },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 4, z = 19 },
|
||||
{ kind = "bar", id = "bar", x = -6, y = 2, z = -20 },
|
||||
{ kind = "hover", id = "hover", x = 8, y = 0.4, z = 10 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
},
|
||||
tracks = {
|
||||
{ name = "lap", key = "time", points = { { at = 0, x = 4, y = 0, z = -10 }, { at = 1, x = 4, y = 0, z = -35 }, { at = 2, x = 4, y = 0, z = -70 } } }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "Through the next gate",
|
||||
on = "enter", a = "gate", b = "car",
|
||||
when = { { "test", expr = "self.index == next" } },
|
||||
act = { { "addVar", name = "next", amount = 1 },
|
||||
{ "addScore", amount = 10 } } },
|
||||
|
||||
{ note = "Both gates passed: a lap",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "next > 2" } },
|
||||
act = { { "addVar", name = "laps", amount = 1 },
|
||||
{ "setVar", name = "next", value = 1 } } },
|
||||
|
||||
{ note = "The hovercraft pushes forward on its own",
|
||||
on = "frame", each = "hover",
|
||||
act = { { "setVar", entity = "self", name = "dy", value = -1 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"speed " .. floor(car.speed) .. " next gate " .. next .. " laps " .. laps' } } }
|
||||
}
|
||||
}
|
||||
116
testScripts/author/railshooter.game
Normal file
116
testScripts/author/railshooter.game
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
-- Genre one of milestone 2: a rail shooter, House of the Dead in a table. A camera on a rail
|
||||
-- that stops at encounters, zombies (the Fox, standing in) that spawn at each stop, walk to the
|
||||
-- camera, and bite on a timer unless they are killed, a gun that hits what the pointer is over --
|
||||
-- and which part -- ragdolls, and a level end.
|
||||
return {
|
||||
title = "Author test: rail shooter",
|
||||
players = 1,
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 70, g = 70, b = 90 }, fog = { r = 12, g = 12, b = 22, near = 25, far = 60 }, fov = 60 },
|
||||
{ kind = "overlay" } },
|
||||
vars = { score = 0, misses = 0, health = 3, bites = 0, stops = 0 },
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 40, h = 0.2, d = 100, r = 70, g = 75, b = 90 }, behaviours = { { kind = "solid" } } },
|
||||
wall = { look = { kind = "mesh", shape = "box", w = 1, h = 3, d = 12, r = 120, g = 60, b = 60 }, behaviours = { { kind = "solid" } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "rail", track = "rail" } } },
|
||||
gun = { look = { kind = "none" }, vars = { ammo = 6 }, behaviours = { { kind = "gun", player = 1, ammo = 6, reload = "offscreen" } } },
|
||||
zombie = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.01, w = 0.8, h = 0.9, d = 1.4 },
|
||||
vars = { health = 2 },
|
||||
behaviours = { { kind = "health", max = 2, ragdoll = true, linger = 2 },
|
||||
{ kind = "seek", target = "camera", speed = 1.6, stopAt = 2.2 },
|
||||
{ kind = "animator", idle = "Survey", walk = "Walk", attack = "Run", fade = 0.2 },
|
||||
{ kind = "target", zones = { { name = "head", bone = "b_Head_05", w = 0.5, h = 0.5, d = 0.5 } } },
|
||||
{ kind = "timer", name = "bite", after = 1.5, start = false } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "street",
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.1, z = -30 },
|
||||
{ kind = "wall", id = "wallL", x = -6, y = 1.5, z = -12 },
|
||||
{ kind = "wall", id = "wallR", x = 6, y = 1.5, z = -12 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 1.6, z = 0 },
|
||||
{ kind = "gun", id = "gun1", x = 0, y = 0, z = 0 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
},
|
||||
tracks = {
|
||||
{ name = "rail", key = "time",
|
||||
points = { { at = 0, x = 0, y = 1.6, z = 0, look = { 0, 1.2, -10 } },
|
||||
{ at = 4, x = 0, y = 1.6, z = -6, look = { 0, 1.2, -16 }, stop = "hall" },
|
||||
{ at = 10, x = 2, y = 1.6, z = -16, look = { 2, 1.2, -26 }, stop = "yard" },
|
||||
{ at = 14, x = 2, y = 1.6, z = -22, look = { 2, 1.2, -32 } } } }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "The hall: three come out of the dark",
|
||||
on = "stopped", name = "hall",
|
||||
act = { { "addVar", name = "stops", amount = 1 },
|
||||
{ "spawn", kind = "zombie", x = -2, y = 0, z = -14 },
|
||||
{ "spawn", kind = "zombie", x = 2, y = 0, z = -15 },
|
||||
{ "spawn", kind = "zombie", x = 0, y = 0, z = -17 } } },
|
||||
|
||||
{ note = "The yard: four more",
|
||||
on = "stopped", name = "yard",
|
||||
act = { { "addVar", name = "stops", amount = 1 },
|
||||
{ "spawn", kind = "zombie", x = -1, y = 0, z = -24 },
|
||||
{ "spawn", kind = "zombie", x = 4, y = 0, z = -25 },
|
||||
{ "spawn", kind = "zombie", x = 2, y = 0, z = -27 },
|
||||
{ "spawn", kind = "zombie", x = 0, y = 0, z = -28 } } },
|
||||
|
||||
{ note = "A zombie starts walking the moment it is made",
|
||||
on = "spawn", kind = "zombie",
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "A shot lands",
|
||||
on = "hit", kind = "zombie",
|
||||
act = { { "damage", entity = "self", amount = 1 },
|
||||
{ "addScore", amount = 100 } } },
|
||||
|
||||
{ note = "A headshot lands harder",
|
||||
on = "hit", kind = "zombie",
|
||||
when = { { "hitPart", part = "head" } },
|
||||
act = { { "damage", entity = "self", amount = 2 },
|
||||
{ "addScore", amount = 300 } } },
|
||||
|
||||
{ note = "It reaches the camera and rears up",
|
||||
on = "arrived", kind = "zombie",
|
||||
act = { { "setState", entity = "self", state = "attack" },
|
||||
{ "timerStart", entity = "self", name = "bite", after = 1.5 } } },
|
||||
|
||||
{ note = "The bite lands unless it was killed in time",
|
||||
on = "timer", name = "bite", kind = "zombie",
|
||||
when = { { "inState", entity = "self", state = "attack" } },
|
||||
act = { { "addVar", name = "health", amount = -1 },
|
||||
{ "addVar", name = "bites", amount = 1 },
|
||||
{ "flash", r = 255, g = 40, b = 40, seconds = 0.3 },
|
||||
{ "shake", amount = 0.3, seconds = 0.4 },
|
||||
{ "timerStart", entity = "self", name = "bite", after = 1.5 } } },
|
||||
|
||||
{ note = "A miss on the picture",
|
||||
on = "miss",
|
||||
when = { { "test", expr = "not event.offscreen" } },
|
||||
act = { { "addVar", name = "misses", amount = 1 } } },
|
||||
|
||||
{ note = "The stop is clear: move on",
|
||||
on = "frame",
|
||||
when = { { "waiting", entity = "camera" }, { "test", expr = 'count("zombie") == 0' } },
|
||||
act = { { "pathNext", entity = "camera" } } },
|
||||
|
||||
{ note = "Out of health",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "health <= 0" }, { "once", tag = "over", scope = "game" } },
|
||||
act = { { "say", text = '"YOU ARE DEAD"', seconds = 2 }, { "gameOver" } } },
|
||||
|
||||
{ note = "The end of the rail",
|
||||
on = "railEnd",
|
||||
act = { { "setVar", name = "done", value = true },
|
||||
{ "say", text = '"STAGE CLEAR"', seconds = 2 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " health " .. health .. " ammo " .. gun1.ammo' } } }
|
||||
}
|
||||
}
|
||||
50
testScripts/author/rhythm.game
Normal file
50
testScripts/author/rhythm.game
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- A rhythm game, from the catalogue: a track plays, notes are windows in time, and a press inside
|
||||
-- one scores while one outside is a miss. The notes are a track of points keyed by time, so the
|
||||
-- editor's timeline lays them out.
|
||||
return {
|
||||
title = "Author test: rhythm",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "overlay" }, { kind = "music", file = "testScripts/tune.mp3", volume = 60 } },
|
||||
vars = { score = 0, hits = 0, misses = 0, early = 0 },
|
||||
|
||||
kinds = {
|
||||
lane = { look = { kind = "box", w = 60, h = 400, r = 40, g = 40, b = 60 } },
|
||||
marker = { look = { kind = "box", w = 80, h = 6, r = 200, g = 200, b = 220 } },
|
||||
note = { look = { kind = "box", w = 50, h = 16, r = 255, g = 200, b = 60 },
|
||||
behaviours = { { kind = "drift", vx = 0, vy = 200 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "stage",
|
||||
entities = {
|
||||
{ kind = "lane", id = "lane", x = 360, y = 240 },
|
||||
{ kind = "marker", id = "marker", x = 360, y = 400 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
},
|
||||
tracks = {
|
||||
{ name = "notes", key = "time", points = { { at = 1.0 }, { at = 2.0 }, { at = 2.5 }, { at = 3.5 } } }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A note falls two seconds before its time",
|
||||
on = "frame", each = "lane",
|
||||
when = { { "every", seconds = 0.5, tag = "spawn" }, { "test", expr = "time < 2" } },
|
||||
act = { { "spawn", kind = "note", x = 360, y = 0 } } },
|
||||
|
||||
{ note = "A press on a note at the marker",
|
||||
on = "pressed", key = "SPACE", each = "note",
|
||||
when = { { "test", expr = "abs(self.y - 400) < 24" } },
|
||||
act = { { "destroy", entity = "self" }, { "addScore", amount = 100 }, { "addVar", name = "hits", amount = 1 } } },
|
||||
|
||||
{ note = "A note that falls past the marker is a miss",
|
||||
on = "frame", each = "note",
|
||||
when = { { "test", expr = "self.y > 440" } },
|
||||
act = { { "destroy", entity = "self" }, { "addVar", name = "misses", amount = 1 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " hits " .. hits .. " misses " .. misses' } } }
|
||||
}
|
||||
}
|
||||
77
testScripts/author/shmup.game
Normal file
77
testScripts/author/shmup.game
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
-- Genre one of milestone 1: a shoot-em-up. Kinds spawned by the dozen, bullets that collide,
|
||||
-- health, a spawner, a HUD document, sound, and a game over -- none of which the first slice could
|
||||
-- say. Everything is a box, so the description needs no art.
|
||||
return {
|
||||
title = "Author test: shoot-em-up",
|
||||
layers = { { kind = "overlay" }, { kind = "hud", document = "testScripts/author/hud.rml", bind = { score = "score", lives = "lives" } } },
|
||||
vars = { score = 0, lives = 3 },
|
||||
|
||||
kinds = {
|
||||
ship = { look = { kind = "box", w = 28, h = 20, r = 120, g = 220, b = 255 },
|
||||
vars = { health = 1 },
|
||||
behaviours = { { kind = "keys", player = 1, left = "LEFT", right = "RIGHT", up = "UP", down = "DOWN", fire = "SPACE" },
|
||||
{ kind = "mover", speed = 260, clamp = true },
|
||||
{ kind = "shooter", spawns = "shot", rate = 0.15, offsetY = -14 },
|
||||
{ kind = "health", max = 1, keep = true },
|
||||
{ kind = "sound", hit = "testScripts/crackle.wav" } } },
|
||||
shot = { look = { kind = "box", w = 4, h = 10, r = 255, g = 255, b = 160 },
|
||||
behaviours = { { kind = "projectile", vx = 0, vy = -520, life = 3 } } },
|
||||
raider = { look = { kind = "box", w = 26, h = 18, r = 240, g = 80, b = 80 },
|
||||
vars = { health = 2 },
|
||||
behaviours = { { kind = "drift", vx = 0, vy = 90 },
|
||||
{ kind = "health", max = 2 },
|
||||
{ kind = "sound", death = "testScripts/crackle.wav" } } },
|
||||
hive = { look = { kind = "none" },
|
||||
behaviours = { { kind = "spawner", spawns = "raider", every = 0.6, max = 6, total = 12,
|
||||
points = { { x = 100, y = -20 }, { x = 300, y = -20 }, { x = 500, y = -20 }, { x = 640, y = -20 } } } } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "space",
|
||||
entities = {
|
||||
{ kind = "ship", id = "ship", x = 360, y = 420 },
|
||||
{ kind = "hive", id = "hive", x = 360, y = -20 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A shot lands on a raider",
|
||||
on = "collision", a = "shot", b = "raider",
|
||||
act = { { "destroy", entity = "self" },
|
||||
{ "damage", entity = "other", amount = 1 },
|
||||
{ "addScore", amount = 10 } } },
|
||||
|
||||
{ note = "A raider dies",
|
||||
on = "death", kind = "raider",
|
||||
act = { { "emit", entity = "self", count = 24, r = 255, g = 120, b = 60 },
|
||||
{ "addScore", amount = 50 } } },
|
||||
|
||||
{ note = "A raider rams the ship",
|
||||
on = "collision", a = "raider", b = "ship",
|
||||
act = { { "destroy", entity = "self" },
|
||||
{ "damage", entity = "other", amount = 1 } } },
|
||||
|
||||
{ note = "The ship is lost",
|
||||
on = "death", kind = "ship",
|
||||
act = { { "addVar", name = "lives", amount = -1 },
|
||||
{ "flash", r = 255, g = 60, b = 60, seconds = 0.3 },
|
||||
{ "setVar", entity = "self", name = "health", value = 1 },
|
||||
{ "moveTo", entity = "self", x = 360, y = 420 } } },
|
||||
|
||||
{ note = "Out of lives",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "lives <= 0" } },
|
||||
act = { { "gameOver" } } },
|
||||
|
||||
{ note = "A raider slips past the bottom",
|
||||
on = "frame", each = "raider",
|
||||
when = { { "test", expr = "self.y > 500" } },
|
||||
act = { { "destroy", entity = "self" } } },
|
||||
|
||||
{ note = "The wave is done",
|
||||
on = "frame",
|
||||
when = { { "test", expr = 'count("raider") == 0 and hive.made >= 12' }, { "once", tag = "wave", scope = "game" } },
|
||||
act = { { "setVar", name = "cleared", value = true },
|
||||
{ "say", text = '"WAVE CLEAR"', seconds = 2 } } }
|
||||
}
|
||||
}
|
||||
48
testScripts/author/space.game
Normal file
48
testScripts/author/space.game
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- A space shooter, from the catalogue: a ship on a body pushed by thrust and turned by the keys,
|
||||
-- bolts it fires, rocks that come at it, and a camera behind. No gravity.
|
||||
return {
|
||||
title = "Author test: space",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", gravity = 0, ambient = { r = 40, g = 40, b = 70 }, fov = 65 }, { kind = "overlay" } },
|
||||
vars = { score = 0, rocks = 0 },
|
||||
|
||||
kinds = {
|
||||
ship = { look = { kind = "mesh", shape = "box", w = 1.2, h = 0.4, d = 2.4, r = 200, g = 210, b = 240 },
|
||||
behaviours = { { kind = "body", mass = 50, friction = 0 },
|
||||
{ kind = "thrust", force = 400, turn = 1.2 },
|
||||
{ kind = "keys", player = 1, left = "LEFT", right = "RIGHT", up = "UP", down = "DOWN", fire = "SPACE" },
|
||||
{ kind = "shooter", spawns = "bolt", rate = 0.2, offsetY = 0 } } },
|
||||
bolt = { look = { kind = "mesh", shape = "sphere", w = 0.3, r = 255, g = 240, b = 120 },
|
||||
behaviours = { { kind = "projectile", vx = 0, vy = 0, vz = -30, life = 3 } } },
|
||||
rock = { look = { kind = "mesh", shape = "sphere", w = 2, r = 130, g = 110, b = 90 },
|
||||
vars = { health = 1 },
|
||||
behaviours = { { kind = "projectile", vx = 0, vy = 0, vz = 6, life = 20 }, { kind = "health", max = 1 } } },
|
||||
belt = { look = { kind = "none" }, behaviours = { { kind = "spawner", spawns = "rock", every = 0.8, max = 8, total = 12, points = { { x = -4, y = 0, z = -60 }, { x = 0, y = 0, z = -60 }, { x = 4, y = 0, z = -60 }, { x = 2, y = 1, z = -60 } } } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "follow", target = "ship", x = 0, y = 3, z = 8, lookY = 0, lag = 0.2 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "belt",
|
||||
entities = {
|
||||
{ kind = "ship", id = "ship", x = 0, y = 0, z = 0 },
|
||||
{ kind = "belt", id = "belt", x = 0, y = 0, z = -60 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 3, z = 8 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A bolt meets a rock",
|
||||
on = "collision", a = "bolt", b = "rock",
|
||||
act = { { "destroy", entity = "self" }, { "damage", entity = "other", amount = 1 } } },
|
||||
|
||||
{ note = "A rock is broken",
|
||||
on = "death", kind = "rock",
|
||||
act = { { "addScore", amount = 25 }, { "addVar", name = "rocks", amount = 1 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " rocks " .. rocks' } } }
|
||||
}
|
||||
}
|
||||
73
testScripts/author/tail.game
Normal file
73
testScripts/author/tail.game
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- The long tail, in 2D: a sprite sheet stepped by state, a map of tiles, and MIDI notes as an
|
||||
-- event -- the pieces a rhythm game or a maze game needs.
|
||||
return {
|
||||
title = "Author test: the long tail",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "overlay" } },
|
||||
vars = { score = 0, notes = 0 },
|
||||
|
||||
kinds = {
|
||||
runner = { look = { kind = "sprite", file = "testScripts/strip.png", frames = 5, anchor = "feet" },
|
||||
behaviours = { { kind = "frames", fps = 10, states = "idle=1-1, walk=3-5, walkLeft=1-2" },
|
||||
{ kind = "keys", player = 1, left = "LEFT", right = "RIGHT" },
|
||||
{ kind = "mover", speed = 120, clamp = true } } },
|
||||
-- The same sheet, drawn facing right only: facing left mirrors it, and rz turns a frame.
|
||||
mirrored = { look = { kind = "sprite", file = "testScripts/strip.png", frames = 5, anchor = "feet" },
|
||||
behaviours = { { kind = "frames", fps = 10, states = "idle=1-1, walk=3-5" },
|
||||
{ kind = "keys", player = 1, left = "LEFT", right = "RIGHT" },
|
||||
{ kind = "mover", speed = 120, clamp = true },
|
||||
{ kind = "sound", hit = "testScripts/crackle.wav", volume = 30 } } },
|
||||
floor = { look = { kind = "grid", file = "testScripts/checker.png", tile = 32, columns = 2, map = "1 2 1 2 1; 2 1 2 1 2; 3 4 3 4 3" } },
|
||||
spinner = { look = { kind = "sprite", file = "testScripts/checker.png" }, behaviours = { { kind = "spin", rate = 90 } } },
|
||||
fire = { look = { kind = "particles", r = 255, g = 140, b = 40, rate = 60, size = 10, speed = 80, life = 0.8 } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "stage",
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 200, y = 300 },
|
||||
{ kind = "runner", id = "runner", x = 360, y = 300 },
|
||||
{ kind = "mirrored", id = "mirror", x = 360, y = 420 },
|
||||
{ kind = "mirrored", id = "tilted", x = 120, y = 200, rz = 30 },
|
||||
{ kind = "spinner", id = "spinner", x = 560, y = 200, rz = 45 },
|
||||
{ kind = "fire", id = "fire", x = 640, y = 400 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "Moving means walking",
|
||||
on = "frame", each = "runner",
|
||||
when = { { "test", expr = "self.dx != 0" } },
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "Still means idle",
|
||||
on = "frame", each = "runner",
|
||||
when = { { "test", expr = "self.dx == 0" } },
|
||||
act = { { "setState", entity = "self", state = "idle" } } },
|
||||
|
||||
{ note = "The mirrored one walks the same",
|
||||
on = "frame", each = "mirrored",
|
||||
when = { { "test", expr = "self.dx != 0" } },
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "Middle C is a quiet click as well",
|
||||
on = "midi", pitch = 60,
|
||||
act = { { "playSound", file = "testScripts/crackle.wav", volume = 40 } } },
|
||||
|
||||
{ note = "Middle C scores",
|
||||
on = "midi", pitch = 60,
|
||||
act = { { "addScore", amount = 10 },
|
||||
{ "addVar", name = "notes", amount = 1 } } },
|
||||
|
||||
{ note = "Any other note is counted",
|
||||
on = "midi",
|
||||
when = { { "test", expr = "event.pitch != 60" } },
|
||||
act = { { "addVar", name = "notes", amount = 1 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"frame " .. runner.frame .. " " .. runner.state .. " score " .. score .. " notes " .. notes' } } }
|
||||
}
|
||||
}
|
||||
76
testScripts/author/thirdperson.game
Normal file
76
testScripts/author/thirdperson.game
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
-- Third-person action, from the catalogue: a character the keys move relative to a camera that
|
||||
-- orbits it under the mouse, enemies that walk the mesh after it and bite, and a swing of the
|
||||
-- sword on a key at whatever is in reach.
|
||||
return {
|
||||
title = "Author test: third person",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 80, g = 80, b = 100 }, fov = 60 }, { kind = "overlay" } },
|
||||
vars = { score = 0, health = 5, swings = 0 },
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 40, h = 0.4, d = 40, r = 80, g = 100, b = 80 }, behaviours = { { kind = "solid" } } },
|
||||
hero = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.008, w = 0.6, h = 0.9, d = 0.6 },
|
||||
behaviours = { { kind = "keys", player = 1, left = "A", right = "D", up = "W", down = "S" },
|
||||
{ kind = "character", speed = 4, jump = 5, radius = 0.3, height = 0.9 },
|
||||
{ kind = "animator", idle = "Survey", walk = "Walk" } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "orbit", target = "hero", y = 1.2, z = 6, lookY = 0.6 } } },
|
||||
enemy = { look = { kind = "model", file = "testScripts/Models/Fox.glb", scale = 0.01, w = 0.8, h = 0.9, d = 1.4 },
|
||||
vars = { health = 2 },
|
||||
behaviours = { { kind = "health", max = 2, ragdoll = true, linger = 1 },
|
||||
{ kind = "walker", speed = 2, radius = 0.4, follow = "hero", every = 0.4, stopAt = 1.6 },
|
||||
{ kind = "animator", idle = "Survey", walk = "Walk", attack = "Run" },
|
||||
{ kind = "timer", name = "bite", after = 1.5, start = false } } },
|
||||
hive = { look = { kind = "none" }, behaviours = { { kind = "spawner", spawns = "enemy", every = 1.5, max = 3, total = 4, points = { { x = -8, y = 0, z = -8 }, { x = 8, y = 0, z = -8 } } } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "field",
|
||||
navFrom = { "floor" },
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.2, z = 0 },
|
||||
{ kind = "hero", id = "hero", x = 0, y = 0.2, z = 4 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 2, z = 10 },
|
||||
{ kind = "hive", id = "hive", x = 0, y = 0, z = -8 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "An enemy walks from the moment it is made",
|
||||
on = "spawn", kind = "enemy",
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "Moving means walking",
|
||||
on = "frame", each = "hero",
|
||||
when = { { "test", expr = "self.dx != 0 or self.dy != 0" } },
|
||||
act = { { "setState", entity = "self", state = "walk" } } },
|
||||
|
||||
{ note = "Still means idle",
|
||||
on = "frame", each = "hero",
|
||||
when = { { "test", expr = "self.dx == 0 and self.dy == 0" } },
|
||||
act = { { "setState", entity = "self", state = "idle" } } },
|
||||
|
||||
{ note = "A swing of the sword hits whatever is in reach",
|
||||
on = "pressed", key = "SPACE", each = "enemy",
|
||||
when = { { "test", expr = 'distance(self, "hero") < 2.2' } },
|
||||
act = { { "damage", entity = "self", amount = 1 }, { "addVar", name = "swings", amount = 1 } } },
|
||||
|
||||
{ note = "A kill",
|
||||
on = "death", kind = "enemy",
|
||||
act = { { "addScore", amount = 100 } } },
|
||||
|
||||
{ note = "It reaches the hero and rears up",
|
||||
on = "arrived", kind = "enemy",
|
||||
act = { { "setState", entity = "self", state = "attack" }, { "timerStart", entity = "self", name = "bite", after = 1.5 } } },
|
||||
|
||||
{ note = "The bite lands unless it was killed",
|
||||
on = "timer", name = "bite", kind = "enemy",
|
||||
when = { { "inState", entity = "self", state = "attack" } },
|
||||
act = { { "addVar", name = "health", amount = -1 }, { "timerStart", entity = "self", name = "bite", after = 1.5 } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"score " .. score .. " health " .. health' } } }
|
||||
}
|
||||
}
|
||||
70
testScripts/author/towerdefence.game
Normal file
70
testScripts/author/towerdefence.game
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
-- Genre two of milestone 4: tower defence. Creeps that walk a lane on the navigation mesh in
|
||||
-- waves, turrets placed with a click on the floor that fire at the nearest creep in range,
|
||||
-- projectiles that collide, gold and lives. Nothing here is shared with the racing game but the
|
||||
-- scene.
|
||||
return {
|
||||
title = "Author test: tower defence",
|
||||
size = { w = 720, h = 480 },
|
||||
layers = { { kind = "scene3d", ambient = { r = 80, g = 90, b = 80 }, fov = 55 }, { kind = "overlay" } },
|
||||
vars = { score = 0, gold = 120, lives = 5, killed = 0 },
|
||||
|
||||
kinds = {
|
||||
floor = { look = { kind = "mesh", shape = "box", w = 30, h = 0.4, d = 24, r = 80, g = 110, b = 80 }, behaviours = { { kind = "solid" } } },
|
||||
creep = { look = { kind = "mesh", shape = "box", w = 0.6, h = 0.8, d = 0.6, r = 120, g = 200, b = 90 },
|
||||
vars = { health = 3 },
|
||||
behaviours = { { kind = "walker", speed = 2.5, radius = 0.3 },
|
||||
{ kind = "patrol", points = { { x = -10, y = 0, z = -6 }, { x = 10, y = 0, z = -6 }, { x = 10, y = 0, z = 6 }, { x = -10, y = 0, z = 6 } } },
|
||||
{ kind = "health", max = 3 } } },
|
||||
hive = { look = { kind = "none" }, behaviours = { { kind = "spawner", spawns = "creep", every = 1.2, max = 8, total = 10, points = { { x = -12, y = 0, z = -6 } } } } },
|
||||
turret = { look = { kind = "mesh", shape = "cylinder", w = 0.8, h = 1.2, r = 70, g = 90, b = 220 },
|
||||
behaviours = { { kind = "turret", targets = "creep", range = 6, rate = 0.5, spawns = "bolt", speed = 14 } } },
|
||||
bolt = { look = { kind = "mesh", shape = "sphere", w = 0.25, r = 255, g = 230, b = 80 },
|
||||
behaviours = { { kind = "projectile", life = 2 } } },
|
||||
camera = { look = { kind = "none" }, behaviours = { { kind = "camera", mode = "fixed", lookX = 0, lookY = 0, lookZ = 0 } } },
|
||||
readout = { look = { kind = "text", text = "", r = 235, g = 235, b = 245 } }
|
||||
},
|
||||
|
||||
rooms = {
|
||||
{ name = "field",
|
||||
navFrom = { "floor" },
|
||||
entities = {
|
||||
{ kind = "floor", id = "floor", x = 0, y = -0.2, z = 0 },
|
||||
{ kind = "hive", id = "hive", x = -12, y = 0, z = -6 },
|
||||
{ kind = "camera", id = "camera", x = 0, y = 20, z = 14 },
|
||||
{ kind = "readout", id = "readout", x = 12, y = 12, z = 0 }
|
||||
} }
|
||||
},
|
||||
|
||||
rules = {
|
||||
{ note = "A click on the floor builds a turret, for fifty gold",
|
||||
on = "pressed", switch = "SWITCH_BUTTON3",
|
||||
when = { { "test", expr = "gold >= 50" } },
|
||||
act = { { "spawnAtPointer", kind = "turret", player = 1 },
|
||||
{ "addVar", name = "gold", amount = -50 } } },
|
||||
|
||||
{ note = "A bolt lands",
|
||||
on = "collision", a = "bolt", b = "creep",
|
||||
act = { { "destroy", entity = "self" },
|
||||
{ "damage", entity = "other", amount = 1 } } },
|
||||
|
||||
{ note = "A creep dies",
|
||||
on = "death", kind = "creep",
|
||||
act = { { "addVar", name = "gold", amount = 20 },
|
||||
{ "addScore", amount = 10 },
|
||||
{ "addVar", name = "killed", amount = 1 } } },
|
||||
|
||||
{ note = "A creep reaches the end of the lane",
|
||||
on = "patrolEnd", kind = "creep",
|
||||
act = { { "destroy", entity = "self" },
|
||||
{ "addVar", name = "lives", amount = -1 } } },
|
||||
|
||||
{ note = "Out of lives",
|
||||
on = "frame",
|
||||
when = { { "test", expr = "lives <= 0" }, { "once", tag = "over", scope = "game" } },
|
||||
act = { { "gameOver" } } },
|
||||
|
||||
{ note = "The readout, every frame",
|
||||
on = "frame",
|
||||
act = { { "setText", entity = "readout", text = '"gold " .. gold .. " lives " .. lives .. " killed " .. killed .. " creeps " .. count("creep")' } } }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
-- Authoring tools (PLAN section 58): compiles both sample descriptions and plays the platformer.
|
||||
-- Forge (FORGE.md): compiles all four sample descriptions and plays the platformer.
|
||||
--
|
||||
-- The two descriptions share nothing -- one is physics with a character, the other is disc frames
|
||||
-- and time windows -- so compiling both here is the test that the core has no genre baked into it.
|
||||
-- The descriptions share nothing -- physics with a character, disc frames and time windows, a
|
||||
-- shoot-em-up of spawned kinds, a gun over video -- so compiling all of them here is the test
|
||||
-- that the core has no genre baked into it.
|
||||
-- Nothing in this scene knows what a platformer is; it drives keys and looks at the result.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
|
|
@ -13,10 +14,16 @@ fontSelect(font)
|
|||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
built.platformer = authorBuild("testScripts/author/platformer.game", out .. "platformer.singe")
|
||||
built.qte = authorBuild("testScripts/author/qte.game", out .. "qte.singe")
|
||||
debugPrint("AUTHOR built " .. built.platformer)
|
||||
debugPrint("AUTHOR built " .. built.qte)
|
||||
for _, name in ipairs({ "platformer", "qte", "shmup", "gunvideo" }) do
|
||||
local game = authorLoad("testScripts/author/" .. name .. ".game")
|
||||
local problems = authorCheck(game)
|
||||
|
||||
built[name] = authorBuild("testScripts/author/" .. name .. ".game", out .. name .. ".singe")
|
||||
debugPrint("AUTHOR built " .. built[name] .. " with " .. #problems .. " problems")
|
||||
for _, problem in ipairs(problems) do
|
||||
debugPrint("AUTHOR FAIL " .. name .. ": " .. problem)
|
||||
end
|
||||
end
|
||||
|
||||
-- Both must at least load as Lua; a compiler that emits something unparseable is the failure this
|
||||
-- catches earliest, and it costs nothing to check both even though only one is played.
|
||||
|
|
@ -75,7 +82,7 @@ function onOverlayUpdate()
|
|||
|
||||
-- The engine's own text over the authored game, so a screenshot says what it is looking at.
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored platformer, %.1fs, score %d", t, AUTHOR_SCORE))
|
||||
fontPrint(12, 450, string.format("authored platformer, %.1fs, score %d", t, AUTHOR_VARS.score))
|
||||
|
||||
-- Four shots at the moments that matter, found by watching rather than by timing: the start,
|
||||
-- the run, the jump, and the frame after the prize was taken.
|
||||
|
|
@ -83,11 +90,11 @@ function onOverlayUpdate()
|
|||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if AUTHOR_FLAG["won"] and won == 0 then
|
||||
if AUTHOR_VARS.won and won == 0 then
|
||||
won = t
|
||||
end
|
||||
if (won > 0 and t > won + 0.6) or (t > GIVE_UP) then
|
||||
debugPrint("AUTHOR RESULT score=" .. AUTHOR_SCORE .. " flagWon=" .. tostring(AUTHOR_FLAG["won"]) .. " after=" .. string.format("%.1fs", t))
|
||||
debugPrint("AUTHOR RESULT score=" .. AUTHOR_VARS.score .. " flagWon=" .. tostring(AUTHOR_VARS.won) .. " after=" .. string.format("%.1fs", t))
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- Authoring tools (PLAN section 58), genre two: the QTE description, played over the disc.
|
||||
-- Forge (FORGE.md), genre two of the first slice: the QTE description, played over the disc.
|
||||
--
|
||||
-- Scene 52 plays the platformer. This one shares no vocabulary with it beyond the three nouns --
|
||||
-- no physics, no character, no gravity, and every rule is about disc frames -- which is what makes
|
||||
|
|
@ -43,7 +43,7 @@ function onOverlayUpdate()
|
|||
singeScreenshot()
|
||||
end
|
||||
if disc >= 310 or frames > 1200 then
|
||||
debugPrint("AUTHOR QTE RESULT score=" .. AUTHOR_SCORE .. " hit=" .. tostring(AUTHOR_FLAG["hit"]))
|
||||
debugPrint("AUTHOR QTE RESULT score=" .. AUTHOR_VARS.score .. " hit=" .. tostring(AUTHOR_VARS.hit))
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
-- Forge (PLAN section 58): opened on a real description, driven, saved, and the
|
||||
-- result compiled and played.
|
||||
-- Forge (FORGE.md): opened on a real description, driven, saved, and the result compiled and
|
||||
-- played.
|
||||
--
|
||||
-- Driven by calling the editor's own functions rather than by a pointer, the way the menu's
|
||||
-- sanitizer scene drives the menu: what is being tested is that moving something in the editor
|
||||
-- reaches the description, survives a save, and changes the game that comes out. The pointer path
|
||||
-- itself was settled by the spike in PLAN section 58 and needs a real X server to exercise.
|
||||
-- itself was settled by the spike in FORGE.md section 1 and needs a real X server to exercise.
|
||||
-- As a library: the tail of Forge.singe that launches it is skipped, because this scene
|
||||
-- drives it rather than being it.
|
||||
FORGE_LIBRARY = true
|
||||
|
|
@ -47,8 +47,8 @@ local function step()
|
|||
if index == nil then
|
||||
debugPrint("FORGE FAIL nothing under the prize's own position")
|
||||
else
|
||||
before = FORGE.game.entities[index].x
|
||||
debugPrint("FORGE picked " .. FORGE.game.entities[index].id .. " at x=" .. before)
|
||||
before = forgeRoom().entities[index].x
|
||||
debugPrint("FORGE picked " .. forgeRoom().entities[index].id .. " at x=" .. before)
|
||||
forgePress(520, 300)
|
||||
end
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ local function step()
|
|||
|
||||
elseif stage == 3 then
|
||||
forgeRelease()
|
||||
local moved = FORGE.game.entities[forgePick(300, 300)]
|
||||
local moved = forgeRoom().entities[forgePick(300, 300)]
|
||||
|
||||
debugPrint("FORGE moved to x=" .. moved.x .. " dirty=" .. tostring(FORGE.dirty))
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ local function step()
|
|||
local reloaded = authorLoad(work)
|
||||
local found = nil
|
||||
|
||||
for _, e in ipairs(reloaded.entities) do
|
||||
for _, e in ipairs(reloaded.rooms[1].entities) do
|
||||
if e.id == "prize" then
|
||||
found = e.x
|
||||
end
|
||||
|
|
@ -84,7 +84,7 @@ local function step()
|
|||
local built = forgeBuild(out .. "edited.singe")
|
||||
local text = assert(io.open(built, "r")):read("a")
|
||||
|
||||
if text:find('id = "prize", x = 300') then
|
||||
if text:find('id = "prize", kind = "prize", x = 300') then
|
||||
debugPrint("FORGE the compiled game carries the new position")
|
||||
else
|
||||
debugPrint("FORGE FAIL the compiled game does not carry the new position")
|
||||
|
|
@ -135,7 +135,7 @@ local function step()
|
|||
elseif stage == 9 then
|
||||
-- And now the same press reaches the hero, which is the whole point of a movable panel.
|
||||
forgePress(120, 380)
|
||||
if FORGE.selected ~= nil and FORGE.game.entities[FORGE.selected].id == "hero" then
|
||||
if FORGE.selected ~= nil and forgeRoom().entities[FORGE.selected].id == "hero" then
|
||||
debugPrint("FORGE the hero is reachable once the panel has moved")
|
||||
else
|
||||
debugPrint("FORGE FAIL the hero is still unreachable")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- The event sheet (PLAN section 58): rules edited, not just entities moved.
|
||||
-- The event sheet (FORGE.md): rules edited, not just entities moved.
|
||||
--
|
||||
-- Driven by keys, which is how the editor is meant to be used and how the bundled menu's document
|
||||
-- has always been driven. What is asserted is that an edit made through the rule editor reaches
|
||||
|
|
@ -50,7 +50,8 @@ end
|
|||
|
||||
local function step()
|
||||
if stage == 1 then
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB) -- entities to kinds,
|
||||
key(SCANCODE.TAB) -- kinds to rules.
|
||||
debugPrint("RULES mode is now " .. FORGE.mode .. ", rules " .. #FORGE.game.rules)
|
||||
|
||||
elseif stage == 2 then
|
||||
|
|
@ -77,7 +78,7 @@ local function step()
|
|||
|
||||
if after == before then
|
||||
debugPrint("RULES FAIL the new rule did not reach the compiled game")
|
||||
elseif after:find('authorKeyHeld%(SCANCODE%.UP%.value%)') and after:find('authorOnGround%("hero"%)') and after:find('authorJump%("hero"%)') then
|
||||
elseif after:find('authorKeyHeld%(SCANCODE%.UP%.value%)') and after:find('authorOnGround%(authorEntity%("hero"%)%)') and after:find('authorJump%(authorEntity%("hero"%)%)') then
|
||||
debugPrint("RULES the compiled game carries the new rule")
|
||||
else
|
||||
debugPrint("RULES FAIL the compiled game is missing part of the new rule")
|
||||
|
|
@ -116,9 +117,10 @@ local function step()
|
|||
elseif stage == 7 then
|
||||
-- A number has to come back a number: "100" and 100 compile to different source, so a
|
||||
-- retyped value that turned into a string would stop the description round-tripping.
|
||||
-- Rule 4 is "Take the prize once"; its third part is addScore, whose value is amount.
|
||||
-- Rule 4 is "Take the prize", on a collision; its first part is addScore, whose value is
|
||||
-- amount.
|
||||
FORGE.rule = 4
|
||||
FORGE.part = 3
|
||||
FORGE.part = 1
|
||||
key(SCANCODE.RETURN)
|
||||
for c in string.gmatch("250", ".") do
|
||||
key(SCANCODE.A, string.byte(c))
|
||||
|
|
@ -132,15 +134,15 @@ local function step()
|
|||
debugPrint("RULES FAIL amount is " .. type(amount) .. " " .. tostring(amount))
|
||||
end
|
||||
|
||||
-- And ESC has to put back what was there.
|
||||
FORGE.part = 1
|
||||
-- And ESC has to put back what was there: the second part is destroy, on other.
|
||||
FORGE.part = 2
|
||||
key(SCANCODE.RETURN)
|
||||
key(SCANCODE.A, string.byte("Z"))
|
||||
key(SCANCODE.ESCAPE)
|
||||
if FORGE.game.rules[4].when[1].entity == "hero" then
|
||||
if FORGE.game.rules[4].act[2].entity == "other" then
|
||||
debugPrint("RULES escape put the value back")
|
||||
else
|
||||
debugPrint("RULES FAIL escape left " .. tostring(FORGE.game.rules[4].when[1].entity))
|
||||
debugPrint("RULES FAIL escape left " .. tostring(FORGE.game.rules[4].act[2].entity))
|
||||
end
|
||||
|
||||
elseif stage == 8 then
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- A Forge release is standalone (PLAN section 58).
|
||||
-- A Forge release is standalone (FORGE.md).
|
||||
--
|
||||
-- Building it is half the test; the half that matters is that the result runs somewhere Forge has
|
||||
-- never been. The runtime a compiled game loads is Singe/Author.singe, which whatever Singe the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- Forge as a game (PLAN section 58): the chooser, the engine callbacks, and the editing that
|
||||
-- Forge as a game (FORGE.md): the chooser, the engine callbacks, and the editing that
|
||||
-- needs no pointer -- entities added, renamed and deleted, a rule built from the pickers, undo.
|
||||
--
|
||||
-- Unlike scene54 to 56 this does not set FORGE_LIBRARY: Forge installs its own callbacks, and
|
||||
|
|
@ -40,31 +40,33 @@ local function step()
|
|||
debugPrint("GAME FAIL ENTER on the chooser opened nothing")
|
||||
else
|
||||
opened = FORGE.path
|
||||
debugPrint("GAME opened " .. opened .. " with " .. #FORGE.game.entities .. " entities")
|
||||
debugPrint("GAME opened " .. opened .. " with " .. #forgeRoom().entities .. " entities")
|
||||
end
|
||||
|
||||
elseif stage == 2 then
|
||||
-- Add an entity, name it, and give it a look by typing the fields ENTER walks.
|
||||
local count = #FORGE.game.entities
|
||||
-- Add an entity, name it, and make it a hero by typing the fields ENTER walks.
|
||||
local count = #forgeRoom().entities
|
||||
|
||||
key(SCANCODE.A)
|
||||
if #FORGE.game.entities ~= count + 1 then
|
||||
if #forgeRoom().entities ~= count + 1 then
|
||||
debugPrint("GAME FAIL A did not add an entity")
|
||||
end
|
||||
key(SCANCODE.RETURN) -- id
|
||||
typeIn("coin")
|
||||
key(SCANCODE.RETURN) -- kind
|
||||
typeIn("hero")
|
||||
key(SCANCODE.RETURN) -- x
|
||||
typeIn("300")
|
||||
key(SCANCODE.RETURN) -- y
|
||||
typeIn("200")
|
||||
key(SCANCODE.RETURN) -- kind
|
||||
key(SCANCODE.RETURN) -- z
|
||||
key(SCANCODE.ESCAPE) -- untouched
|
||||
local coin = FORGE.game.entities[#FORGE.game.entities]
|
||||
local coin = forgeRoom().entities[#forgeRoom().entities]
|
||||
|
||||
if coin.id == "coin" and coin.x == 300 and coin.y == 200 and coin.look.kind == "box" then
|
||||
debugPrint("GAME typed an entity in: " .. coin.id .. " at " .. coin.x .. "," .. coin.y)
|
||||
if coin.id == "coin" and coin.kind == "hero" and coin.x == 300 and coin.y == 200 then
|
||||
debugPrint("GAME typed an entity in: " .. coin.id .. " the " .. coin.kind .. " at " .. coin.x .. "," .. coin.y)
|
||||
else
|
||||
debugPrint("GAME FAIL the typed entity is " .. tostring(coin.id) .. " at " .. tostring(coin.x) .. "," .. tostring(coin.y) .. " " .. tostring(coin.look.kind))
|
||||
debugPrint("GAME FAIL the typed entity is " .. tostring(coin.id) .. " the " .. tostring(coin.kind) .. " at " .. tostring(coin.x) .. "," .. tostring(coin.y))
|
||||
end
|
||||
|
||||
elseif stage == 3 then
|
||||
|
|
@ -77,7 +79,7 @@ local function step()
|
|||
key(SCANCODE.RETURN)
|
||||
typeIn("player")
|
||||
key(SCANCODE.ESCAPE) -- ESC puts it back: the hero keeps its name
|
||||
if FORGE.game.entities[FORGE.selected].id ~= "hero" then
|
||||
if forgeRoom().entities[FORGE.selected].id ~= "hero" then
|
||||
debugPrint("GAME FAIL escape did not put the name back")
|
||||
end
|
||||
key(SCANCODE.RETURN)
|
||||
|
|
@ -93,18 +95,22 @@ local function step()
|
|||
end
|
||||
end
|
||||
end
|
||||
if FORGE.game.entities[FORGE.selected].id == "player" and named == 3 then
|
||||
if forgeRoom().entities[FORGE.selected].id == "player" and named == 3 then
|
||||
debugPrint("GAME renamed the hero and " .. named .. " actions followed")
|
||||
else
|
||||
debugPrint("GAME FAIL rename: id " .. tostring(FORGE.game.entities[FORGE.selected].id) .. ", " .. named .. " actions renamed")
|
||||
debugPrint("GAME FAIL rename: id " .. tostring(forgeRoom().entities[FORGE.selected].id) .. ", " .. named .. " actions renamed")
|
||||
end
|
||||
|
||||
elseif stage == 4 then
|
||||
-- A rule from the pickers: N, then C picks the first condition, T the first action.
|
||||
-- A rule from the pickers: N, E picks an event, C a condition, T an action.
|
||||
local rules = #FORGE.game.rules
|
||||
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB) -- kinds,
|
||||
key(SCANCODE.TAB) -- rules.
|
||||
key(SCANCODE.N)
|
||||
key(SCANCODE.E)
|
||||
key(SCANCODE.DOWN)
|
||||
key(SCANCODE.RETURN)
|
||||
key(SCANCODE.C)
|
||||
if FORGE.picker == nil then
|
||||
debugPrint("GAME FAIL C opened no picker")
|
||||
|
|
@ -115,20 +121,21 @@ local function step()
|
|||
key(SCANCODE.RETURN)
|
||||
local rule = FORGE.game.rules[#FORGE.game.rules]
|
||||
|
||||
if #FORGE.game.rules == rules + 1 and #rule.when == 1 and #rule.act == 1 then
|
||||
debugPrint("GAME built a rule from the pickers: when " .. rule.when[1][1] .. " then " .. rule.act[1][1])
|
||||
if #FORGE.game.rules == rules + 1 and #rule.when == 1 and #rule.act == 1 and rule.on ~= "frame" then
|
||||
debugPrint("GAME built a rule from the pickers: on " .. rule.on .. " when " .. rule.when[1][1] .. " then " .. rule.act[1][1])
|
||||
else
|
||||
debugPrint("GAME FAIL the picked rule has " .. #(rule.when or {}) .. " conditions and " .. #(rule.act or {}) .. " actions")
|
||||
end
|
||||
|
||||
elseif stage == 5 then
|
||||
-- Undo takes the action back, then the condition, then the rule; redo brings the rule
|
||||
-- back, and a change by hand ends the redo history.
|
||||
-- Undo takes the action back, then the condition, the event, then the rule; redo brings
|
||||
-- the rule back, and a change by hand ends the redo history.
|
||||
local rules = #FORGE.game.rules
|
||||
|
||||
key(SCANCODE.U)
|
||||
key(SCANCODE.U)
|
||||
key(SCANCODE.U)
|
||||
key(SCANCODE.U)
|
||||
if #FORGE.game.rules == rules - 1 then
|
||||
debugPrint("GAME three undos took the rule back out")
|
||||
else
|
||||
|
|
@ -152,15 +159,16 @@ local function step()
|
|||
|
||||
elseif stage == 6 then
|
||||
-- Duplicate and delete in the entity list.
|
||||
key(SCANCODE.TAB)
|
||||
local count = #FORGE.game.entities
|
||||
key(SCANCODE.TAB) -- tracks,
|
||||
key(SCANCODE.TAB) -- entities.
|
||||
local count = #forgeRoom().entities
|
||||
|
||||
key(SCANCODE.D)
|
||||
key(SCANCODE.DELETE)
|
||||
if #FORGE.game.entities == count then
|
||||
if #forgeRoom().entities == count then
|
||||
debugPrint("GAME duplicate then delete leaves " .. count .. " entities")
|
||||
else
|
||||
debugPrint("GAME FAIL duplicate then delete leaves " .. #FORGE.game.entities)
|
||||
debugPrint("GAME FAIL duplicate then delete leaves " .. #forgeRoom().entities)
|
||||
end
|
||||
|
||||
elseif stage == 7 then
|
||||
|
|
@ -194,7 +202,7 @@ local function step()
|
|||
else
|
||||
local found = false
|
||||
|
||||
for _, entity in ipairs(FORGE.game.entities) do
|
||||
for _, entity in ipairs(forgeRoom().entities) do
|
||||
if entity.id == "player" then
|
||||
found = true
|
||||
end
|
||||
|
|
|
|||
71
testScripts/scene58.singe
Normal file
71
testScripts/scene58.singe
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
-- Forge milestone 1, genre one: the shoot-em-up. Kinds spawned and destroyed by the dozen,
|
||||
-- collision events between kinds, health and death, a spawner, a HUD document bound to vars, a
|
||||
-- sequence (say waits), a flash, and a game over -- driven by what the game is doing.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
dofile(authorBuild("testScripts/author/shmup.game", singeGetDataPath() .. "shmup.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local maxRaiders = 0
|
||||
local maxShots = 0
|
||||
local reported = false
|
||||
local GIVE_UP = 30
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local ship = authorEntity("ship")
|
||||
|
||||
frames = frames + 1
|
||||
-- Hold fire from the start and sweep left and right under the spawn points.
|
||||
if frames == 2 then
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
end
|
||||
if ship then
|
||||
local x = authorX(ship)
|
||||
|
||||
if (x > 500) and authorKeyHeld(SCANCODE.RIGHT.value) then
|
||||
authorKeyUp(0, SCANCODE.RIGHT.value)
|
||||
authorKeyDown(0, SCANCODE.LEFT.value)
|
||||
elseif (x < 120) and authorKeyHeld(SCANCODE.LEFT.value) then
|
||||
authorKeyUp(0, SCANCODE.LEFT.value)
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
elseif not (authorKeyHeld(SCANCODE.LEFT.value) or authorKeyHeld(SCANCODE.RIGHT.value)) then
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
end
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
maxRaiders = math.max(maxRaiders, authorCount("raider"))
|
||||
maxShots = math.max(maxShots, authorCount("shot"))
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored shmup, %.1fs, raiders %d, shots %d, score %d, lives %d", t, authorCount("raider"), authorCount("shot"), AUTHOR_VARS.score, AUTHOR_VARS.lives))
|
||||
|
||||
if (shots == 0 and t > 1.5) or (shots == 1 and t > 4) or (shots == 2 and AUTHOR_VARS.cleared) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (AUTHOR_VARS.cleared and t > 1 and not reported) or (t > GIVE_UP) then
|
||||
reported = true
|
||||
debugPrint(string.format("SHMUP RESULT score=%d lives=%d cleared=%s maxRaiders=%d maxShots=%d after=%.1fs",
|
||||
AUTHOR_VARS.score, AUTHOR_VARS.lives, tostring(AUTHOR_VARS.cleared), maxRaiders, maxShots, t))
|
||||
if maxRaiders < 3 then
|
||||
debugPrint("SHMUP FAIL the spawner never had three raiders up at once")
|
||||
end
|
||||
if AUTHOR_VARS.score <= 0 then
|
||||
debugPrint("SHMUP FAIL nothing was ever hit")
|
||||
end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
93
testScripts/scene59.singe
Normal file
93
testScripts/scene59.singe
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
-- Forge milestone 1, genre two: a light gun over video. Hitboxes keyed to disc frames and
|
||||
-- interpolated between them, a gun that raises hit and miss, ammo that runs out, a reload by a
|
||||
-- shot off the picture, a prompt at a frame, and a penalty for the hostage. The pointer is
|
||||
-- injected through authorSetPointer, the door a test uses instead of a mouse.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/gunvideo.game", singeGetDataPath() .. "gunvideo.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local stage = 0
|
||||
local hits = 0
|
||||
local misses = 0
|
||||
local hostage = 0
|
||||
|
||||
local realEvent = authorEvent
|
||||
function authorEvent(name, event)
|
||||
if name == "hit" then
|
||||
if event.self.kind == "bandit" then hits = hits + 1 elseif event.self.kind == "hostage" then hostage = hostage + 1 end
|
||||
elseif name == "miss" then
|
||||
misses = misses + 1
|
||||
end
|
||||
return realEvent(name, event)
|
||||
end
|
||||
|
||||
|
||||
local function shoot(x, y, on)
|
||||
authorSetPointer(1, x, y, on)
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local disc = discGetFrame()
|
||||
local box = authorTrackBox("bandit")
|
||||
|
||||
frames = frames + 1
|
||||
-- Each shot waits for the disc to reach a frame, so the sequence is the same at any speed.
|
||||
if (stage == 0) and (disc >= 130) and box then
|
||||
-- Frame 130 of 120..220: the bandit box has moved a tenth of the way from x=100 to x=400.
|
||||
debugPrint(string.format("GUN at frame %d the bandit box is at x=%.0f (interpolated from 100 toward 400)", disc, box.x))
|
||||
shoot(box.x + 10, box.y + 10, true) -- hit
|
||||
stage = 1
|
||||
elseif (stage == 1) and (disc >= 150) then
|
||||
shoot(20, 20, true) -- miss on the picture
|
||||
shoot(box.x + 10, box.y + 10, true) -- hit (the bandit is dead; no target, so a miss)
|
||||
stage = 2
|
||||
elseif (stage == 2) and (disc >= 170) then
|
||||
-- Out of ammo now (3 shots taken): a shot on the picture does nothing, off it reloads.
|
||||
local ammo = authorEntity("gun1").vars.ammo
|
||||
|
||||
shoot(580, 260, true) -- hostage, but the gun is empty
|
||||
debugPrint("GUN ammo before reload " .. ammo .. ", hostage hits so far " .. hostage)
|
||||
shoot(-10, -10, false) -- reload
|
||||
debugPrint("GUN ammo after reload " .. authorEntity("gun1").vars.ammo)
|
||||
shoot(580, 260, true) -- hostage
|
||||
stage = 3
|
||||
elseif (stage == 3) and (disc >= 320) then
|
||||
stage = 4
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, "authored gun game, disc frame " .. disc)
|
||||
if box then
|
||||
colorForeground(255, 80, 80, 255)
|
||||
overlayBox(box.x, box.y, box.x + box.w, box.y + box.h)
|
||||
end
|
||||
if (shots == 0 and disc >= 125) or (shots == 1 and disc >= 160) or (shots == 2 and disc >= 200) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if stage == 4 or frames > 1500 then
|
||||
debugPrint(string.format("GUN RESULT score=%d hits=%d misses=%d hostage=%d ammo=%d", AUTHOR_VARS.score, hits, misses, hostage, authorEntity("gun1").vars.ammo))
|
||||
if hits ~= 1 or hostage ~= 1 or misses ~= 2 then
|
||||
debugPrint("GUN FAIL expected 1 hit, 2 misses, 1 hostage")
|
||||
end
|
||||
if authorTrackBoxAt({ boxes = { { at = 0, x = 0, y = 0, w = 10, h = 10 }, { at = 10, x = 100, y = 0, w = 10, h = 10 } } }, 5).x ~= 50 then
|
||||
debugPrint("GUN FAIL interpolation")
|
||||
end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
148
testScripts/scene60.singe
Normal file
148
testScripts/scene60.singe
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
-- The timeline (FORGE.md milestone 1): hitbox tracks keyed to disc frames, edited in Forge. The
|
||||
-- cursor is stepped, keys are made at it, one is picked up off the canvas and dragged, one is
|
||||
-- deleted, and the compiled game carries what was drawn. The description names its video, so the
|
||||
-- editor has a frame to show under the cursor.
|
||||
FORGE_LIBRARY = true
|
||||
dofile("Forge/Forge.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||
local out = singeGetDataPath()
|
||||
local work = out .. "timeline.game"
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
local source = assert(io.open("testScripts/author/gunvideo.game", "r"))
|
||||
local copy = assert(io.open(work, "w"))
|
||||
|
||||
copy:write(source:read("a"))
|
||||
source:close()
|
||||
copy:close()
|
||||
|
||||
if not forgeBegin(work) then
|
||||
debugPrint("TIMELINE FAIL could not open " .. work)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
|
||||
|
||||
local function key(code, character)
|
||||
forgeKey(character or 0, code.value)
|
||||
end
|
||||
|
||||
|
||||
local function step()
|
||||
local track = forgeRoom().tracks[1]
|
||||
|
||||
if stage == 1 then
|
||||
debugPrint("TIMELINE video loaded: " .. tostring(FORGE.video ~= nil) .. "; track '" .. track.name .. "' has " .. #track.boxes .. " keys")
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB)
|
||||
if FORGE.mode ~= "tracks" then
|
||||
debugPrint("TIMELINE FAIL three tabs did not reach the tracks: " .. FORGE.mode)
|
||||
end
|
||||
|
||||
elseif stage == 2 then
|
||||
-- Step and leap the cursor to frame 170, halfway between the bandit's two keys.
|
||||
for _ = 1, 17 do
|
||||
key(SCANCODE.RIGHTBRACKET)
|
||||
end
|
||||
local box = forgeCursorBox()
|
||||
|
||||
debugPrint(string.format("TIMELINE cursor at %d, the bandit box is at x=%.0f (halfway from 100 to 400)", FORGE.cursor, box.x))
|
||||
if math.abs(box.x - 250) > 1 then
|
||||
debugPrint("TIMELINE FAIL interpolation at the cursor")
|
||||
end
|
||||
|
||||
elseif stage == 3 then
|
||||
-- A key at the cursor takes the interpolated box, and the list stays sorted.
|
||||
key(SCANCODE.K)
|
||||
if #track.boxes == 3 and track.boxes[2].at == 170 and track.boxes[2].x == 250 then
|
||||
debugPrint("TIMELINE a key at 170 went in between the two, at x=250")
|
||||
else
|
||||
debugPrint("TIMELINE FAIL after K the track has " .. #track.boxes .. " keys; second at " .. tostring(track.boxes[2] and track.boxes[2].at))
|
||||
end
|
||||
|
||||
elseif stage == 4 then
|
||||
-- Pick the box up off the canvas and drag it, the way a pointer would.
|
||||
local box = forgeCursorBox()
|
||||
|
||||
forgePress(box.x + 5, box.y + 5)
|
||||
forgeDrag(box.x + 65, box.y + 25)
|
||||
forgeRelease()
|
||||
if track.boxes[2].x == 310 and track.boxes[2].y == 220 then
|
||||
debugPrint("TIMELINE dragged the key to " .. track.boxes[2].x .. "," .. track.boxes[2].y)
|
||||
else
|
||||
debugPrint("TIMELINE FAIL the drag left the key at " .. tostring(track.boxes[2].x) .. "," .. tostring(track.boxes[2].y))
|
||||
end
|
||||
|
||||
elseif stage == 5 then
|
||||
-- Type the key's height, then step back a frame: the box between keys follows the new one.
|
||||
key(SCANCODE.RETURN) -- at
|
||||
key(SCANCODE.RETURN) -- x
|
||||
key(SCANCODE.RETURN) -- y
|
||||
key(SCANCODE.RETURN) -- w
|
||||
key(SCANCODE.RETURN) -- h
|
||||
for c in string.gmatch("40", ".") do
|
||||
key(SCANCODE.A, string.byte(c))
|
||||
end
|
||||
key(SCANCODE.RETURN)
|
||||
key(SCANCODE.ESCAPE)
|
||||
key(SCANCODE.COMMA)
|
||||
local box = forgeCursorBox()
|
||||
|
||||
debugPrint(string.format("TIMELINE key height typed to %d; at frame %d the box is %.0f tall", track.boxes[2].h, FORGE.cursor, box.h))
|
||||
|
||||
elseif stage == 6 then
|
||||
-- Undo the typing and the drag, then delete the key. Undo puts a copy of the description
|
||||
-- in place, so the track is looked up again afterwards.
|
||||
key(SCANCODE.U)
|
||||
key(SCANCODE.U)
|
||||
track = forgeRoom().tracks[1]
|
||||
if track.boxes[2].x ~= 250 then
|
||||
debugPrint("TIMELINE FAIL undo did not bring the key back to x=250: " .. tostring(track.boxes[2].x))
|
||||
end
|
||||
FORGE.key = 2
|
||||
key(SCANCODE.DELETE)
|
||||
debugPrint("TIMELINE after two undos and a delete the track has " .. #track.boxes .. " keys")
|
||||
|
||||
elseif stage == 7 then
|
||||
-- And a new key in a new track reaches the compiled game.
|
||||
key(SCANCODE.N)
|
||||
key(SCANCODE.K)
|
||||
key(SCANCODE.S)
|
||||
local built = forgeBuild(out .. "timeline.singe")
|
||||
local text = assert(io.open(built, "r")):read("a")
|
||||
|
||||
if text:find('name = "track3"') and text:find("at = " .. FORGE.cursor) then
|
||||
debugPrint("TIMELINE the compiled game carries the new track and its key")
|
||||
else
|
||||
debugPrint("TIMELINE FAIL the new track is missing from the compiled game")
|
||||
end
|
||||
debugPrint("TIMELINE RESULT done")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
if frames % 14 == 0 and stage < 8 then
|
||||
stage = stage + 1
|
||||
step()
|
||||
end
|
||||
forgeDraw(nil, nil)
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(210, 8, "timeline, stage " .. stage)
|
||||
if frames == 44 or frames == 72 or frames == 100 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames > 130 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
103
testScripts/scene61.singe
Normal file
103
testScripts/scene61.singe
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
-- Forge milestone 2, genre one: the rail shooter. A camera on a rail that stops at encounters,
|
||||
-- zombies that spawn there and walk to it, a gun whose ray says which part it hit, ragdolls, and
|
||||
-- the rail moving on when the stop is clear. The pointer is aimed at each zombie's head through
|
||||
-- sceneProject, which is what a light gun would do with a person behind it.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/railshooter.game", singeGetDataPath() .. "railshooter.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local hits = 0
|
||||
local heads = 0
|
||||
local kills = 0
|
||||
local stopped = 0
|
||||
local ended = false
|
||||
local lastShot = 0
|
||||
local GIVE_UP = 60
|
||||
|
||||
local realEvent = authorEvent
|
||||
function authorEvent(name, event)
|
||||
if name == "hit" then
|
||||
hits = hits + 1
|
||||
if event.part == "head" then
|
||||
heads = heads + 1
|
||||
end
|
||||
elseif name == "death" then
|
||||
kills = kills + 1
|
||||
elseif name == "stopped" then
|
||||
stopped = stopped + 1
|
||||
elseif name == "railEnd" then
|
||||
ended = true
|
||||
end
|
||||
return realEvent(name, event)
|
||||
end
|
||||
|
||||
|
||||
local function fire(x, y, on)
|
||||
authorSetPointer(1, x, y, on)
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local gun = authorEntity("gun1")
|
||||
|
||||
frames = frames + 1
|
||||
-- Aim at the nearest living zombie's head and fire, no faster than a person would; reload
|
||||
-- off the picture when empty.
|
||||
if gun and (t - lastShot > 0.25) then
|
||||
local nearest, best = nil, math.huge
|
||||
|
||||
for _, zombie in ipairs(authorEach("zombie")) do
|
||||
if zombie.vars.state ~= "dead" then
|
||||
local far = authorDistance(zombie, authorEntity("camera"))
|
||||
|
||||
if far < best then
|
||||
nearest, best = zombie, far
|
||||
end
|
||||
end
|
||||
end
|
||||
if gun.vars.ammo <= 0 then
|
||||
fire(-10, -10, false)
|
||||
lastShot = t
|
||||
elseif nearest then
|
||||
local head = nodeFind("b_Head_05", nearest.model)
|
||||
local hx, hy, hz = nodeGetWorldPosition(head)
|
||||
local sx, sy, _, inFront = sceneProject(hx, hy, hz)
|
||||
|
||||
if inFront then
|
||||
fire(sx, sy, true)
|
||||
lastShot = t
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored rail shooter, %.1fs, zombies %d, kills %d, heads %d, stops %d", t, authorCount("zombie"), kills, heads, stopped))
|
||||
if (shots == 0 and t > 5) or (shots == 1 and kills > 0) or (shots == 2 and stopped == 2) or (shots == 3 and ended) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (ended and t > 1) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("RAIL RESULT score=%d health=%d bites=%d stops=%d kills=%d hits=%d heads=%d ended=%s after=%.1fs",
|
||||
AUTHOR_VARS.score, AUTHOR_VARS.health, AUTHOR_VARS.bites, stopped, kills, hits, heads, tostring(ended), t))
|
||||
if stopped < 2 then debugPrint("RAIL FAIL the rail did not reach both stops") end
|
||||
if kills < 7 then debugPrint("RAIL FAIL not every zombie was killed") end
|
||||
if heads == 0 then debugPrint("RAIL FAIL no shot was reported on the head") end
|
||||
if not ended then debugPrint("RAIL FAIL the rail never ended") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
64
testScripts/scene62.singe
Normal file
64
testScripts/scene62.singe
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
-- Forge milestone 2, genre two: the 3D platformer. A character on the controller moving
|
||||
-- relative to a following camera, a jump onto a ledge, and a prize that is a trigger. Driven by
|
||||
-- what the hero is doing, as scene52 drives the 2D one.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/platformer3d.game", singeGetDataPath() .. "platformer3d.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local jumped = false
|
||||
local won = 0
|
||||
local GIVE_UP = 25
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local hero = authorEntity("hero")
|
||||
local hx, hy, hz = authorPosition(hero)
|
||||
|
||||
frames = frames + 1
|
||||
if frames == 2 then
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
end
|
||||
-- Jump once the ledge is near and there is ground underfoot.
|
||||
if (not jumped) and (hx > 3.2) and authorOnGround(hero) then
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
jumped = true
|
||||
elseif jumped then
|
||||
authorKeyUp(0, SCANCODE.SPACE.value)
|
||||
end
|
||||
-- Past the prize, turn back so the ledge is not overshot for good.
|
||||
if hx > 7.5 then
|
||||
authorKeyUp(0, SCANCODE.RIGHT.value)
|
||||
authorKeyDown(0, SCANCODE.LEFT.value)
|
||||
elseif hx < 5 and jumped and AUTHOR_VARS.won == nil then
|
||||
authorKeyUp(0, SCANCODE.LEFT.value)
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored 3D platformer, %.1fs, hero %.1f %.1f %.1f, state %s, score %d", t, hx, hy, hz, hero.vars.state, AUTHOR_VARS.score))
|
||||
if (shots == 0 and t > 0.5) or (shots == 1 and jumped and not authorOnGround(hero)) or (shots == 2 and won > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if AUTHOR_VARS.won and won == 0 then
|
||||
won = t
|
||||
end
|
||||
if (won > 0 and t > won + 0.5) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("PLATFORM3D RESULT score=%d won=%s hero=%.1f,%.1f after=%.1fs", AUTHOR_VARS.score, tostring(AUTHOR_VARS.won), hx, hy, t))
|
||||
if not AUTHOR_VARS.won then debugPrint("PLATFORM3D FAIL the prize was never taken") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
139
testScripts/scene63.singe
Normal file
139
testScripts/scene63.singe
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
-- The 3D viewport (FORGE.md milestone 2): a room with a scene3d layer is shown as the scene,
|
||||
-- built from the kinds' looks under an orbiting editor camera. Entities are picked and dragged
|
||||
-- by where they project, the camera orbits by key, and a rail point is put where the camera
|
||||
-- stands.
|
||||
FORGE_LIBRARY = true
|
||||
dofile("Forge/Forge.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||
local out = singeGetDataPath()
|
||||
local work = out .. "rail.game"
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
local source = assert(io.open("testScripts/author/railshooter.game", "r"))
|
||||
local copy = assert(io.open(work, "w"))
|
||||
|
||||
copy:write(source:read("a"))
|
||||
source:close()
|
||||
copy:close()
|
||||
|
||||
if not forgeBegin(work) then
|
||||
debugPrint("VIEW FAIL could not open " .. work)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
local wall = nil
|
||||
|
||||
|
||||
local function key(code, character)
|
||||
forgeKey(character or 0, code.value)
|
||||
end
|
||||
|
||||
|
||||
local function step()
|
||||
local room = forgeRoom()
|
||||
|
||||
if stage == 1 then
|
||||
local built = 0
|
||||
|
||||
for _ in pairs(FORGE.scene and FORGE.scene.nodes or {}) do
|
||||
built = built + 1
|
||||
end
|
||||
debugPrint("VIEW the preview holds " .. built .. " of " .. #room.entities .. " entities")
|
||||
if built ~= #room.entities then
|
||||
debugPrint("VIEW FAIL the preview is incomplete")
|
||||
end
|
||||
|
||||
elseif stage == 2 then
|
||||
-- Pick the left wall by where it projects.
|
||||
for index, entry in ipairs(room.entities) do
|
||||
if entry.id == "wallL" then
|
||||
wall = index
|
||||
end
|
||||
end
|
||||
local entry = room.entities[wall]
|
||||
local sx, sy, _, inFront = sceneProject(entry.x, entry.y, entry.z)
|
||||
|
||||
forgePress(sx, sy)
|
||||
if FORGE.selected == wall then
|
||||
debugPrint("VIEW picked " .. entry.id .. " at its projection " .. math.floor(sx) .. "," .. math.floor(sy))
|
||||
else
|
||||
debugPrint("VIEW FAIL the press at the wall selected " .. tostring(FORGE.selected) .. " (in front " .. tostring(inFront) .. ")")
|
||||
end
|
||||
-- Drag it a little to the right on screen and let go: it moves across the floor.
|
||||
forgeDrag(sx + 40, sy)
|
||||
forgeRelease()
|
||||
if (entry.y == 1.5) and ((entry.x ~= -6) or (entry.z ~= -12)) then
|
||||
debugPrint(string.format("VIEW dragged it to %.1f, %.1f, %.1f, staying on its floor", entry.x, entry.y, entry.z))
|
||||
else
|
||||
debugPrint(string.format("VIEW FAIL the drag left it at %.1f, %.1f, %.1f", entry.x, entry.y, entry.z))
|
||||
end
|
||||
|
||||
elseif stage == 3 then
|
||||
local yaw = FORGE.scene.yaw
|
||||
local far = FORGE.scene.distance
|
||||
|
||||
key(SCANCODE.J)
|
||||
key(SCANCODE.J)
|
||||
key(SCANCODE.I)
|
||||
debugPrint(string.format("VIEW orbit: yaw %d to %d, distance %.1f to %.1f", yaw, FORGE.scene.yaw, far, FORGE.scene.distance))
|
||||
|
||||
elseif stage == 4 then
|
||||
-- A rail point where the camera stands, at a typed time.
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB)
|
||||
local track = room.tracks[1]
|
||||
local count = #track.points
|
||||
|
||||
key(SCANCODE.K)
|
||||
key(SCANCODE.RETURN) -- at
|
||||
for c in string.gmatch("20", ".") do
|
||||
key(SCANCODE.A, string.byte(c))
|
||||
end
|
||||
key(SCANCODE.RETURN) -- commits at, moves on to x
|
||||
key(SCANCODE.ESCAPE)
|
||||
track = forgeRoom().tracks[1]
|
||||
local last = track.points[#track.points]
|
||||
|
||||
if (#track.points == count + 1) and (last.at == 20) and (last.look ~= nil) then
|
||||
debugPrint(string.format("VIEW a rail point went in at %d: %.1f, %.1f, %.1f looking at %s", last.at, last.x, last.y, last.z, tostring(last.look[1] or last.look.x)))
|
||||
else
|
||||
debugPrint("VIEW FAIL the rail has " .. #track.points .. " points; the last is at " .. tostring(last and last.at))
|
||||
end
|
||||
|
||||
elseif stage == 5 then
|
||||
key(SCANCODE.S)
|
||||
local reloaded = authorLoad(work)
|
||||
local built = forgeBuild(out .. "rail.singe")
|
||||
local text = assert(io.open(built, "r")):read("a")
|
||||
|
||||
debugPrint("VIEW saved: the rail has " .. #reloaded.rooms[1].tracks[1].points .. " points on disc; compiled carries at = 20: " .. tostring(text:find("at = 20") ~= nil))
|
||||
debugPrint("VIEW RESULT done")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
if frames % 14 == 0 and stage < 6 then
|
||||
stage = stage + 1
|
||||
step()
|
||||
end
|
||||
forgeDraw(nil, nil)
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(210, 8, "3D viewport, stage " .. stage)
|
||||
if frames == 20 or frames == 50 or frames == 80 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames > 100 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
145
testScripts/scene64.singe
Normal file
145
testScripts/scene64.singe
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
-- Forge milestone 3, genre one: the adventure in a painted room. Walk areas baked to a
|
||||
-- navigation mesh, hotspots, verbs and the sentence line, the parser, an inventory, a dialogue
|
||||
-- whose choice changes the world, a cut-scene into a second room, and a save and load. Driven the
|
||||
-- way a player drives it: a verb, a click on a hotspot, a typed line, a numbered choice.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 16)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/adventure2d.game", singeGetDataPath() .. "adventure2d.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
local shots = 0
|
||||
local said = {}
|
||||
local GIVE_UP = 90
|
||||
|
||||
local realSay = authorSay
|
||||
function authorSay(text, seconds)
|
||||
said[#said + 1] = tostring(text)
|
||||
return realSay(text, seconds)
|
||||
end
|
||||
|
||||
|
||||
local function clickOn(verb, id)
|
||||
local target = authorEntity(id)
|
||||
local bx, by, bw, bh = authorBounds(target)
|
||||
|
||||
authorSetVerb(verb)
|
||||
authorSetPointer(1, bx + bw / 2, by + bh / 2, true)
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
end
|
||||
|
||||
|
||||
local function lastSaid()
|
||||
return said[#said] or "(nothing)"
|
||||
end
|
||||
|
||||
|
||||
-- Each stage waits for the game to settle -- no sequence running, no line up, nobody walking --
|
||||
-- or for a dialogue to offer its choices, before the next.
|
||||
local function idle()
|
||||
local hero = authorEntity("hero")
|
||||
|
||||
if authorChoices() ~= nil then
|
||||
return true
|
||||
end
|
||||
|
||||
return (not authorBusy()) and ((hero == nil) or not hero.walking)
|
||||
end
|
||||
|
||||
|
||||
local steps = {
|
||||
function()
|
||||
debugPrint("ADV the hall has a navigation mesh: " .. tostring(authorRoomNav() ~= nil) .. "; verb is " .. tostring(AUTHOR_VARS.verb))
|
||||
clickOn("look", "door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV look at the door said: " .. lastSaid())
|
||||
clickOn("take", "key")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV took the key: has it " .. tostring(authorHas("key")) .. ", key gone " .. tostring(authorEntity("key") == nil) .. ", hero at " .. math.floor(authorX(authorEntity("hero"))) .. "," .. math.floor(authorY(authorEntity("hero"))))
|
||||
clickOn("open", "door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV the guard objects: " .. lastSaid())
|
||||
clickOn("talk", "guard")
|
||||
end,
|
||||
function()
|
||||
-- The dialogue is up: the first choice, then the one the key allows.
|
||||
debugPrint("ADV the guard says: " .. lastSaid())
|
||||
authorKeyDown(0, SCANCODE.MAIN_1.value)
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV and then: " .. lastSaid())
|
||||
authorKeyDown(0, SCANCODE.MAIN_1.value)
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV allowed now: " .. tostring(AUTHOR_VARS.allowed) .. "; last line: " .. lastSaid())
|
||||
authorSaid("look at the door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV typed 'look at the door': " .. lastSaid())
|
||||
authorSaid("frobnicate the key")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV typed nonsense: " .. lastSaid())
|
||||
clickOn("open", "door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV after the door: room " .. AUTHOR_ROOM.name .. ", score " .. AUTHOR_VARS.score .. ", outside " .. tostring(AUTHOR_VARS.outside))
|
||||
authorSaveGame(1)
|
||||
authorRestart()
|
||||
debugPrint("ADV restarted: room " .. AUTHOR_ROOM.name .. ", score " .. AUTHOR_VARS.score)
|
||||
authorLoadGame(1)
|
||||
debugPrint("ADV loaded: room " .. AUTHOR_ROOM.name .. ", score " .. AUTHOR_VARS.score .. ", has key " .. tostring(authorHas("key")))
|
||||
end,
|
||||
function()
|
||||
if AUTHOR_ROOM.name == "yard" and AUTHOR_VARS.score == 3 and authorHas("key") then
|
||||
debugPrint("ADV RESULT done")
|
||||
else
|
||||
debugPrint("ADV FAIL room " .. AUTHOR_ROOM.name .. " score " .. AUTHOR_VARS.score)
|
||||
end
|
||||
singeQuit()
|
||||
end
|
||||
}
|
||||
|
||||
local waited = 0
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
|
||||
frames = frames + 1
|
||||
-- Move on when the game is idle for a moment, or after a while regardless.
|
||||
if idle() then
|
||||
waited = waited + 1
|
||||
else
|
||||
waited = 0
|
||||
end
|
||||
if (stage < #steps) and ((waited > 12) or (frames == 20)) then
|
||||
stage = stage + 1
|
||||
waited = 0
|
||||
steps[stage]()
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(300, 450, string.format("adventure, stage %d, %.1fs", stage, t))
|
||||
if (shots == 0 and stage == 2) or (shots == 1 and stage == 5) or (shots == 2 and stage == 10) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if t > GIVE_UP then
|
||||
debugPrint("ADV FAIL gave up at stage " .. stage)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
147
testScripts/scene65.singe
Normal file
147
testScripts/scene65.singe
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
-- Forge milestone 3, genre two: the same adventure in a modelled room from a fixed camera. The Walk areas baked to a
|
||||
-- baked from the floor and a hotspot is what the ray through the pointer meets. Driven as scene64
|
||||
-- is, with the pointer placed by where each hotspot projects.
|
||||
--
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 16)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/adventure3d.game", singeGetDataPath() .. "adventure3d.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
local shots = 0
|
||||
local said = {}
|
||||
local GIVE_UP = 90
|
||||
|
||||
local realSay = authorSay
|
||||
function authorSay(text, seconds)
|
||||
said[#said + 1] = tostring(text)
|
||||
return realSay(text, seconds)
|
||||
end
|
||||
|
||||
|
||||
local function clickOn(verb, id)
|
||||
local target = authorEntity(id)
|
||||
local x, y, z = authorPosition(target)
|
||||
local w, h = authorSize(target)
|
||||
local sx, sy = sceneProject(x, y + h / 2, z)
|
||||
|
||||
authorSetVerb(verb)
|
||||
authorSetPointer(1, sx, sy, true)
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
end
|
||||
|
||||
|
||||
local function lastSaid()
|
||||
return said[#said] or "(nothing)"
|
||||
end
|
||||
|
||||
|
||||
-- Each stage waits for the game to settle -- no sequence running, no line up, nobody walking --
|
||||
-- or for a dialogue to offer its choices, before the next.
|
||||
local function idle()
|
||||
local hero = authorEntity("hero")
|
||||
|
||||
if authorChoices() ~= nil then
|
||||
return true
|
||||
end
|
||||
|
||||
return (not authorBusy()) and ((hero == nil) or not hero.walking)
|
||||
end
|
||||
|
||||
|
||||
local steps = {
|
||||
function()
|
||||
debugPrint("ADV3 the hall has a navigation mesh: " .. tostring(authorRoomNav() ~= nil) .. "; verb is " .. tostring(AUTHOR_VARS.verb))
|
||||
clickOn("look", "door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 look at the door said: " .. lastSaid())
|
||||
clickOn("take", "key")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 took the key: has it " .. tostring(authorHas("key")) .. ", key gone " .. tostring(authorEntity("key") == nil) .. ", hero at " .. string.format("%.1f", authorX(authorEntity("hero"))) .. "," .. string.format("%.1f", authorField(authorEntity("hero"), "z")))
|
||||
clickOn("open", "door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 the guard objects: " .. lastSaid())
|
||||
clickOn("talk", "guard")
|
||||
end,
|
||||
function()
|
||||
-- The dialogue is up: the first choice, then the one the key allows.
|
||||
debugPrint("ADV3 the guard says: " .. lastSaid())
|
||||
authorKeyDown(0, SCANCODE.MAIN_1.value)
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 and then: " .. lastSaid())
|
||||
authorKeyDown(0, SCANCODE.MAIN_1.value)
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 allowed now: " .. tostring(AUTHOR_VARS.allowed) .. "; last line: " .. lastSaid())
|
||||
authorSaid("look at the door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 typed 'look at the door': " .. lastSaid())
|
||||
authorSaid("frobnicate the key")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 typed nonsense: " .. lastSaid())
|
||||
clickOn("open", "door")
|
||||
end,
|
||||
function()
|
||||
debugPrint("ADV3 after the door: room " .. AUTHOR_ROOM.name .. ", score " .. AUTHOR_VARS.score .. ", outside " .. tostring(AUTHOR_VARS.outside))
|
||||
authorSaveGame(1)
|
||||
authorRestart()
|
||||
debugPrint("ADV3 restarted: room " .. AUTHOR_ROOM.name .. ", score " .. AUTHOR_VARS.score)
|
||||
authorLoadGame(1)
|
||||
debugPrint("ADV3 loaded: room " .. AUTHOR_ROOM.name .. ", score " .. AUTHOR_VARS.score .. ", has key " .. tostring(authorHas("key")))
|
||||
end,
|
||||
function()
|
||||
if AUTHOR_ROOM.name == "yard" and AUTHOR_VARS.score == 3 and authorHas("key") then
|
||||
debugPrint("ADV3 RESULT done")
|
||||
else
|
||||
debugPrint("ADV3 FAIL room " .. AUTHOR_ROOM.name .. " score " .. AUTHOR_VARS.score)
|
||||
end
|
||||
singeQuit()
|
||||
end
|
||||
}
|
||||
|
||||
local waited = 0
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
|
||||
frames = frames + 1
|
||||
-- Move on when the game is idle for a moment, or after a while regardless.
|
||||
if idle() then
|
||||
waited = waited + 1
|
||||
else
|
||||
waited = 0
|
||||
end
|
||||
if (stage < #steps) and ((waited > 12) or (frames == 20)) then
|
||||
stage = stage + 1
|
||||
waited = 0
|
||||
steps[stage]()
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(300, 450, string.format("adventure in 3D, stage %d, %.1fs", stage, t))
|
||||
if (shots == 0 and stage == 2) or (shots == 1 and stage == 5) or (shots == 2 and stage == 10) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if t > GIVE_UP then
|
||||
debugPrint("ADV3 FAIL gave up at stage " .. stage)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
55
testScripts/scene66.singe
Normal file
55
testScripts/scene66.singe
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
-- Forge milestone 4, genre one: racing. The car is driven by held keys, the rival drives the
|
||||
-- track of points on its own, a checkpoint is a trigger the car enters, and a lap is two of them.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/racing.game", singeGetDataPath() .. "racing.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local topSpeed = 0
|
||||
local rivalStart = nil
|
||||
local GIVE_UP = 40
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local car = authorEntity("car")
|
||||
local rival = authorEntity("rival")
|
||||
local cx, cy, cz = authorPosition(car)
|
||||
local rx, ry, rz = authorPosition(rival)
|
||||
|
||||
frames = frames + 1
|
||||
rivalStart = rivalStart or rz
|
||||
if frames == 2 then
|
||||
authorKeyDown(0, SCANCODE.UP.value)
|
||||
end
|
||||
topSpeed = math.max(topSpeed, car.vars.speed or 0)
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored racing, %.1fs, car z %.1f speed %.1f, rival z %.1f, gate %d, laps %d", t, cz, car.vars.speed or 0, rz, AUTHOR_VARS.next, AUTHOR_VARS.laps))
|
||||
if (shots == 0 and t > 1) or (shots == 1 and AUTHOR_VARS.next == 2) or (shots == 2 and AUTHOR_VARS.laps > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (AUTHOR_VARS.laps > 0 and t > 1) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("RACE RESULT laps=%d next=%d topSpeed=%.1f carZ=%.1f rivalMoved=%.1f after=%.1fs", AUTHOR_VARS.laps, AUTHOR_VARS.next, topSpeed, cz, rivalStart - rz, t))
|
||||
if topSpeed < 3 then debugPrint("RACE FAIL the car never got going") end
|
||||
if AUTHOR_VARS.laps < 1 then debugPrint("RACE FAIL no lap was completed") end
|
||||
if rivalStart - rz < 5 then debugPrint("RACE FAIL the rival did not drive") end
|
||||
local hz = authorField(authorEntity("hover"), "z")
|
||||
|
||||
debugPrint(string.format("RACE the hovercraft under thrust is at z %.1f (from 10); the hinged bar is at y %.1f", hz, authorField(authorEntity("bar"), "y")))
|
||||
if hz > 8 then debugPrint("RACE FAIL the thrust did not move the hovercraft") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
64
testScripts/scene67.singe
Normal file
64
testScripts/scene67.singe
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
-- Forge milestone 4, genre two: tower defence. Creeps walk a lane on the navigation mesh in
|
||||
-- waves, turrets are placed by clicks on the floor -- the pointer put where a lane point projects
|
||||
-- -- and fire projectiles that collide with them.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/towerdefence.game", singeGetDataPath() .. "towerdefence.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local placed = 0
|
||||
local maxCreeps = 0
|
||||
local GIVE_UP = 45
|
||||
|
||||
|
||||
local function place(x, y, z)
|
||||
local sx, sy, _, inFront = sceneProject(x, y, z)
|
||||
|
||||
if inFront then
|
||||
authorSetPointer(1, sx, sy, true)
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
placed = placed + 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
|
||||
frames = frames + 1
|
||||
-- Two turrets beside the lane, once the scene has drawn so projection works.
|
||||
if frames == 10 then
|
||||
place(0, 0, -3.5)
|
||||
elseif frames == 12 then
|
||||
place(10, 0, 0)
|
||||
end
|
||||
maxCreeps = math.max(maxCreeps, authorCount("creep"))
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored tower defence, %.1fs, creeps %d, turrets %d, killed %d, gold %d, lives %d", t, authorCount("creep"), authorCount("turret"), AUTHOR_VARS.killed, AUTHOR_VARS.gold, AUTHOR_VARS.lives))
|
||||
if (shots == 0 and t > 3) or (shots == 1 and AUTHOR_VARS.killed > 0) or (shots == 2 and t > 20) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
local hive = authorEntity("hive")
|
||||
|
||||
if (t > 8 and hive and hive.vars.made >= 10 and authorCount("creep") == 0) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("TD RESULT turrets=%d killed=%d gold=%d lives=%d maxCreeps=%d after=%.1fs", authorCount("turret"), AUTHOR_VARS.killed, AUTHOR_VARS.gold, AUTHOR_VARS.lives, maxCreeps, t))
|
||||
if authorCount("turret") < 2 then debugPrint("TD FAIL the clicks did not place two turrets") end
|
||||
if AUTHOR_VARS.killed == 0 then debugPrint("TD FAIL no creep was killed") end
|
||||
if maxCreeps < 3 then debugPrint("TD FAIL the waves never had three creeps walking") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
75
testScripts/scene68.singe
Normal file
75
testScripts/scene68.singe
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
-- Forge milestone 5, genre one: branching video. A window on the disc answered in time sends
|
||||
-- the disc to the success clip; one missed sends it to the death clip and takes a life; lives
|
||||
-- run out, the score goes to the board, the results card shows, a coin and start continue.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
-- The master service, stubbed: what is sent is what is checked.
|
||||
local submitted = {}
|
||||
function masterSubmitScore(gameId, value, board, meta)
|
||||
submitted[#submitted + 1] = { value = value, board = board }
|
||||
end
|
||||
function masterPump() end
|
||||
|
||||
dofile(authorBuild("testScripts/author/fmv.game", singeGetDataPath() .. "fmv.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local moved = false
|
||||
local continued = false
|
||||
local jumps = {}
|
||||
local lastFrame = -1
|
||||
local before = { taken = 0, missed = 0 }
|
||||
local GIVE_UP = 1500
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local disc = discGetFrame()
|
||||
|
||||
frames = frames + 1
|
||||
-- The disc jumping backward or far forward is a branch being taken.
|
||||
if (lastFrame >= 0) and ((disc < lastFrame) or (disc > lastFrame + 20)) then
|
||||
jumps[#jumps + 1] = lastFrame .. ">" .. disc
|
||||
end
|
||||
lastFrame = disc
|
||||
-- Answer the first window, in time; miss the second on purpose.
|
||||
if (disc >= 130) and (disc < 150) and not moved and (AUTHOR_VARS.taken == 0) then
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
authorKeyUp(0, SCANCODE.RIGHT.value)
|
||||
moved = true
|
||||
end
|
||||
-- Once it is over, a coin and a start continue. What the run counted is kept first, since
|
||||
-- the restart begins the vars again.
|
||||
if AUTHOR_VARS.lives <= 0 and not continued and disc >= 385 then
|
||||
before = { taken = AUTHOR_VARS.taken, missed = AUTHOR_VARS.missed }
|
||||
authorSwitchDown(SWITCH_COIN1)
|
||||
authorSwitchUp(SWITCH_COIN1)
|
||||
authorSwitchDown(SWITCH_START1)
|
||||
authorSwitchUp(SWITCH_START1)
|
||||
continued = true
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored branching video, frame %d, taken %d, missed %d, lives %d", disc, AUTHOR_VARS.taken, AUTHOR_VARS.missed, AUTHOR_VARS.lives))
|
||||
if (shots == 0 and disc >= 125) or (shots == 1 and AUTHOR_VARS.taken > 0) or (shots == 2 and AUTHOR_VARS.lives <= 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (continued and frames > 60) or (frames > GIVE_UP) then
|
||||
debugPrint(string.format("FMV RESULT taken=%d missed=%d lives=%d credits=%d submitted=%d jumps=%s", before.taken, before.missed, AUTHOR_VARS.lives, AUTHOR_VARS.credits, #submitted, table.concat(jumps, " ")))
|
||||
if before.taken ~= 1 then debugPrint("FMV FAIL the first move was not taken") end
|
||||
if before.missed ~= 3 then debugPrint("FMV FAIL three misses should have spent the lives") end
|
||||
if #submitted ~= 1 then debugPrint("FMV FAIL the score was not sent once") end
|
||||
if not continued or AUTHOR_VARS.lives ~= 3 then debugPrint("FMV FAIL the continue did not restart with three lives") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
179
testScripts/scene69.singe
Normal file
179
testScripts/scene69.singe
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
-- The editor's milestone 6 (FORGE.md): a picker narrowed by typing, a file field offering the
|
||||
-- game's files, walk areas and hotspot outlines drawn corner by corner, the dialogue outline, and
|
||||
-- building with the current room first. Driven by keys and canvas presses, as the others are.
|
||||
FORGE_LIBRARY = true
|
||||
dofile("Forge/Forge.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||
local out = singeGetDataPath()
|
||||
local work = out .. "adv.game"
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
local source = assert(io.open("testScripts/author/adventure2d.game", "r"))
|
||||
local copy = assert(io.open(work, "w"))
|
||||
|
||||
copy:write(source:read("a"))
|
||||
source:close()
|
||||
copy:close()
|
||||
|
||||
if not forgeBegin(work) then
|
||||
debugPrint("EDIT FAIL could not open " .. work)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
|
||||
|
||||
local function key(code, character)
|
||||
forgeKey(character or 0, code.value)
|
||||
end
|
||||
|
||||
|
||||
local function typeIn(text)
|
||||
for c in string.gmatch(text, ".") do
|
||||
key(SCANCODE.A, string.byte(c))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function step()
|
||||
if stage == 1 then
|
||||
-- A rule's condition picked by typing part of its name.
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.TAB)
|
||||
key(SCANCODE.N)
|
||||
key(SCANCODE.C)
|
||||
typeIn("keyH")
|
||||
local shown = forgePickItems()
|
||||
|
||||
key(SCANCODE.RETURN)
|
||||
local rule = FORGE.game.rules[#FORGE.game.rules]
|
||||
|
||||
if #shown == 1 and rule.when[1][1] == "keyHeld" then
|
||||
debugPrint("EDIT typing keyH left one condition and ENTER took it: " .. rule.when[1][1])
|
||||
else
|
||||
debugPrint("EDIT FAIL the narrowed picker showed " .. #shown .. " and took " .. tostring(rule.when[1] and rule.when[1][1]))
|
||||
end
|
||||
|
||||
elseif stage == 2 then
|
||||
-- A file field offers the files under the game's directory.
|
||||
key(SCANCODE.TAB) -- tracks
|
||||
key(SCANCODE.TAB) -- dialogues
|
||||
key(SCANCODE.TAB) -- entities
|
||||
key(SCANCODE.TAB) -- kinds
|
||||
FORGE.kindName = "hero"
|
||||
forgeKindSet(FORGE.game.kinds.hero, "look", "sprite")
|
||||
FORGE.lastField = "look.anchor"
|
||||
key(SCANCODE.RETURN) -- look.file: the picker
|
||||
if FORGE.picker == nil then
|
||||
debugPrint("EDIT FAIL a file field did not open the picker")
|
||||
end
|
||||
typeIn("testScripts/strip")
|
||||
key(SCANCODE.RETURN)
|
||||
debugPrint("EDIT the hero's sprite file is now " .. tostring(FORGE.game.kinds.hero.look.file))
|
||||
if FORGE.game.kinds.hero.look.file ~= "testScripts/strip.png" then
|
||||
debugPrint("EDIT FAIL the file picker did not set the file")
|
||||
end
|
||||
forgeKindSet(FORGE.game.kinds.hero, "look", "box")
|
||||
|
||||
elseif stage == 3 then
|
||||
-- A walk area drawn corner by corner, then a hotspot's outline.
|
||||
key(SCANCODE.TAB) -- rules
|
||||
key(SCANCODE.TAB) -- tracks
|
||||
key(SCANCODE.TAB) -- dialogues
|
||||
key(SCANCODE.TAB) -- entities
|
||||
forgeSelect(nil)
|
||||
local walks = #(forgeRoom().walk or {})
|
||||
|
||||
key(SCANCODE.V)
|
||||
forgePress(300, 320)
|
||||
forgePress(500, 320)
|
||||
forgePress(400, 400)
|
||||
key(SCANCODE.V)
|
||||
if #forgeRoom().walk == walks + 1 and #forgeRoom().walk[walks + 1] == 6 then
|
||||
debugPrint("EDIT a walk area of three corners was drawn")
|
||||
else
|
||||
debugPrint("EDIT FAIL the room has " .. #(forgeRoom().walk or {}) .. " walk areas")
|
||||
end
|
||||
for index, entry in ipairs(forgeRoom().entities) do
|
||||
if entry.id == "door" then
|
||||
forgeSelect(index)
|
||||
end
|
||||
end
|
||||
key(SCANCODE.V)
|
||||
forgePress(570, 180)
|
||||
forgePress(630, 180)
|
||||
forgePress(630, 300)
|
||||
forgePress(570, 300)
|
||||
key(SCANCODE.RETURN)
|
||||
local spot = FORGE.game.kinds.door.behaviours[1]
|
||||
|
||||
if spot.polygon and #spot.polygon == 8 then
|
||||
debugPrint("EDIT the door's outline has four corners")
|
||||
else
|
||||
debugPrint("EDIT FAIL the door's outline is " .. tostring(spot.polygon and #spot.polygon))
|
||||
end
|
||||
|
||||
elseif stage == 4 then
|
||||
-- The dialogue outline: a node added and named, a choice added, an action picked for it.
|
||||
key(SCANCODE.TAB) -- kinds
|
||||
key(SCANCODE.TAB) -- rules
|
||||
key(SCANCODE.TAB) -- tracks
|
||||
key(SCANCODE.TAB) -- dialogues
|
||||
debugPrint("EDIT the outline starts on dialogue '" .. tostring(FORGE.dialogue) .. "' with " .. #forgeDialogueRows() .. " rows")
|
||||
key(SCANCODE.A) -- a node in the guard's dialogue
|
||||
key(SCANCODE.RETURN) -- its name
|
||||
typeIn("bye")
|
||||
key(SCANCODE.RETURN) -- who
|
||||
key(SCANCODE.ESCAPE)
|
||||
key(SCANCODE.A) -- a choice in it
|
||||
key(SCANCODE.T)
|
||||
typeIn("addSc")
|
||||
key(SCANCODE.RETURN)
|
||||
local node = FORGE.game.dialogues.guard.nodes.bye
|
||||
|
||||
if node and node.choices[1] and node.choices[1].act[1][1] == "addScore" then
|
||||
debugPrint("EDIT a node 'bye' with a choice that adds to the score went into the guard's dialogue")
|
||||
else
|
||||
debugPrint("EDIT FAIL the node is " .. tostring(node) .. "; rows " .. #forgeDialogueRows())
|
||||
end
|
||||
|
||||
elseif stage == 5 then
|
||||
-- Saved, and built with the second room first.
|
||||
key(SCANCODE.S)
|
||||
local built = forgeBuild(out .. "adv.singe", 2)
|
||||
local text = assert(io.open(built, "r")):read("a")
|
||||
local yard = text:find('name = "yard"', 1, true)
|
||||
local hall = text:find('name = "hall"', 1, true)
|
||||
|
||||
debugPrint("EDIT built from the yard: yard before hall " .. tostring(yard ~= nil and hall ~= nil and yard < hall) .. "; the dialogue carries bye: " .. tostring(text:find('"bye"', 1, true) ~= nil))
|
||||
if not (yard and hall and yard < hall) then
|
||||
debugPrint("EDIT FAIL the built game does not start in the yard")
|
||||
end
|
||||
debugPrint("EDIT RESULT done")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
if frames % 14 == 0 and stage < 6 then
|
||||
stage = stage + 1
|
||||
step()
|
||||
end
|
||||
forgeDraw(nil, nil)
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(210, 8, "editor, stage " .. stage)
|
||||
if frames == 30 or frames == 58 or frames == 72 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames > 100 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
82
testScripts/scene70.singe
Normal file
82
testScripts/scene70.singe
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
-- The long tail (FORGE.md milestone 7), in 2D: a sprite sheet's frames stepped by state, a
|
||||
-- grid of tiles drawn from a sheet, and MIDI notes reaching the rules as an event.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/tail.game", singeGetDataPath() .. "tail.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local seen = {}
|
||||
local leftSeen = {}
|
||||
local shots = 0
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local runner = authorEntity("runner")
|
||||
local t = authorTime()
|
||||
|
||||
frames = frames + 1
|
||||
if frames == 2 then
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
elseif frames == 45 then
|
||||
authorKeyUp(0, SCANCODE.RIGHT.value)
|
||||
authorKeyDown(0, SCANCODE.LEFT.value)
|
||||
elseif frames == 60 then
|
||||
authorMidi(0x90, 60, 100)
|
||||
authorMidi(0x91, 64, 90)
|
||||
authorMidi(0x80, 60, 0)
|
||||
end
|
||||
if frames > 48 then
|
||||
leftSeen[runner.vars.frame] = true
|
||||
else
|
||||
seen[runner.vars.frame] = true
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored long tail, %.1fs, frame %d, %s", t, runner.vars.frame, runner.vars.state))
|
||||
if (shots == 0 and frames == 20) or (shots == 1 and frames == 70) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames > 90 then
|
||||
local distinct = 0
|
||||
|
||||
for _ in pairs(seen) do
|
||||
distinct = distinct + 1
|
||||
end
|
||||
local floor = authorEntity("floor")
|
||||
local w, h = authorSize(floor)
|
||||
|
||||
debugPrint(string.format("TAIL RESULT frames=%d state=%s score=%d notes=%d grid=%dx%d", distinct, runner.vars.state, AUTHOR_VARS.score, AUTHOR_VARS.notes, w, h))
|
||||
if distinct < 3 then debugPrint("TAIL FAIL the walk cycle did not step through its frames") end
|
||||
local leftOnly = true
|
||||
|
||||
for frame in pairs(leftSeen) do
|
||||
if frame > 2 then
|
||||
leftOnly = false
|
||||
end
|
||||
end
|
||||
debugPrint("TAIL facing left used only the walkLeft frames: " .. tostring(leftOnly))
|
||||
local spinner = authorEntity("spinner")
|
||||
|
||||
debugPrint(string.format("TAIL the spinner began at 45 and is at %.0f degrees; the fire has an emitter: %s", spinner.vars.angle, tostring(authorEntity("fire").emitter ~= nil)))
|
||||
if spinner.vars.angle < 100 then debugPrint("TAIL FAIL the spinner did not turn") end
|
||||
local problems = authorCheck({ kinds = { thing = { look = { kind = "box", widthh = 3 }, behaviours = { { kind = "drift", vxx = 1 } } } }, rooms = { { name = "r", entities = {} } }, rules = { { on = "pressed", keyy = "A" } } })
|
||||
|
||||
debugPrint("TAIL the checker names " .. #problems .. " misspelt fields: " .. table.concat(problems, " | "))
|
||||
if #problems ~= 3 then debugPrint("TAIL FAIL expected three misspellings to be named") end
|
||||
if not leftOnly then debugPrint("TAIL FAIL facing left drew frames outside walkLeft") end
|
||||
if AUTHOR_VARS.score ~= 10 or AUTHOR_VARS.notes ~= 2 then debugPrint("TAIL FAIL the notes were not counted: score " .. AUTHOR_VARS.score .. " notes " .. AUTHOR_VARS.notes) end
|
||||
if w ~= 160 or h ~= 96 then debugPrint("TAIL FAIL the grid is not 5 by 3 tiles of 32") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
49
testScripts/scene71.singe
Normal file
49
testScripts/scene71.singe
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
-- The long tail (FORGE.md milestone 7), in 3D: a body floating in water, a boat under its
|
||||
-- propeller, and a soft body.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/pool.game", singeGetDataPath() .. "pool.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local boatStart = nil
|
||||
local lowest = math.huge
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local ball = authorEntity("ball")
|
||||
local boat = authorEntity("boat")
|
||||
local by = authorY(ball)
|
||||
|
||||
frames = frames + 1
|
||||
boatStart = boatStart or authorField(boat, "z")
|
||||
if t > 2 then
|
||||
lowest = math.min(lowest, by)
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored pool, %.1fs, ball y %.2f, boat z %.1f", t, by, authorField(boat, "z")))
|
||||
if (shots == 0 and t > 1) or (shots == 1 and t > 4) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if t > 6 then
|
||||
local moved = boatStart - authorField(boat, "z")
|
||||
|
||||
debugPrint(string.format("POOL RESULT ballLowest=%.2f ballNow=%.2f boatMoved=%.1f balloon=%s", lowest, by, moved, tostring(authorEntity("balloon") ~= nil)))
|
||||
if lowest < -1.5 then debugPrint("POOL FAIL the ball sank to the bottom") end
|
||||
if moved < 2 then debugPrint("POOL FAIL the boat did not move") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
96
testScripts/scene72.singe
Normal file
96
testScripts/scene72.singe
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
-- The first-person shooter from FORGE.md's catalogue: a camera on the hero's eyes turned by mouse
|
||||
-- motion, a gun firing from the middle of the picture, keys moving relative to where the camera
|
||||
-- looks, and enemies walking the mesh after the hero. The mouse is injected to turn toward the
|
||||
-- nearest zombie's head before each shot.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/fps.game", singeGetDataPath() .. "fps.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local hits = 0
|
||||
local kills = 0
|
||||
local lastShot = 0
|
||||
local walked = false
|
||||
local startZ = nil
|
||||
local GIVE_UP = 45
|
||||
local LOOK_SPEED = 0.2
|
||||
|
||||
local realEvent = authorEvent
|
||||
function authorEvent(name, event)
|
||||
if name == "hit" then hits = hits + 1 elseif name == "death" then kills = kills + 1 end
|
||||
return realEvent(name, event)
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local hero = authorEntity("hero")
|
||||
local eyes = authorEntity("eyes")
|
||||
local hx, hy, hz = authorPosition(hero)
|
||||
|
||||
frames = frames + 1
|
||||
startZ = startZ or hz
|
||||
-- A step forward first, to show the keys move relative to the view.
|
||||
if frames == 5 then
|
||||
authorKeyDown(0, SCANCODE.W.value)
|
||||
elseif frames == 40 then
|
||||
authorKeyUp(0, SCANCODE.W.value)
|
||||
walked = true
|
||||
end
|
||||
-- Turn toward the nearest zombie's head and fire.
|
||||
if walked and (t - lastShot > 0.3) then
|
||||
local nearest, best = nil, math.huge
|
||||
|
||||
for _, zombie in ipairs(authorEach("zombie")) do
|
||||
if zombie.vars.state ~= "dead" then
|
||||
local far = authorDistance3D(zombie, hero)
|
||||
|
||||
if far < best then
|
||||
nearest, best = zombie, far
|
||||
end
|
||||
end
|
||||
end
|
||||
if nearest then
|
||||
local head = nodeFind("b_Head_05", nearest.model)
|
||||
local tx, ty, tz = nodeGetWorldPosition(head)
|
||||
local ex, ey, ez = authorPosition(eyes)
|
||||
local wantYaw = math.deg(math.atan(-(tx - ex), -(tz - ez)))
|
||||
local wantPitch = math.deg(math.atan(ty - ey, math.sqrt((tx - ex) ^ 2 + (tz - ez) ^ 2)))
|
||||
local dYaw = (eyes.yaw - wantYaw + 540) % 360 - 180
|
||||
local dPitch = eyes.pitch - wantPitch
|
||||
|
||||
authorMouseMoved(0, 0, dYaw / LOOK_SPEED, dPitch / LOOK_SPEED)
|
||||
if math.abs(dYaw) < 2 then
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
lastShot = t
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored first person, %.1fs, yaw %.0f, hits %d, kills %d, zombies %d", t, eyes.yaw, hits, kills, authorCount("zombie")))
|
||||
if (shots == 0 and t > 1) or (shots == 1 and kills > 0) or (shots == 2 and kills >= 4) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
local hive = authorEntity("hive")
|
||||
|
||||
if (hive.vars.made >= 6 and authorCount("zombie") == 0 and t > 5) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("FPS RESULT kills=%d hits=%d health=%d bites=%d movedForward=%.1f after=%.1fs", kills, hits, AUTHOR_VARS.health, AUTHOR_VARS.bites, startZ - hz, t))
|
||||
if kills < 4 then debugPrint("FPS FAIL fewer than four zombies were killed") end
|
||||
if startZ - hz < 1 then debugPrint("FPS FAIL W did not move the hero forward") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
53
testScripts/scene73.singe
Normal file
53
testScripts/scene73.singe
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
-- Arcade physics (FORGE.md's catalogue): a ball on a 2D body bouncing off static bricks and a
|
||||
-- kinematic paddle the keys move; bricks broken by collision rules.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
dofile(authorBuild("testScripts/author/breakout.game", singeGetDataPath() .. "breakout.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local paddleStart = nil
|
||||
local GIVE_UP = 30
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local ball = authorEntity("ball")
|
||||
local paddle = authorEntity("paddle")
|
||||
local bx, by = authorPosition(ball)
|
||||
local px = authorX(paddle)
|
||||
|
||||
frames = frames + 1
|
||||
paddleStart = paddleStart or px
|
||||
-- The paddle chases the ball, which is what keeps it in play.
|
||||
if bx < px - 10 then
|
||||
authorKeyUp(0, SCANCODE.RIGHT.value)
|
||||
authorKeyDown(0, SCANCODE.LEFT.value)
|
||||
elseif bx > px + 10 then
|
||||
authorKeyUp(0, SCANCODE.LEFT.value)
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored breakout, %.1fs, ball %.0f,%.0f, broken %d, lives %d", t, bx, by, AUTHOR_VARS.broken, AUTHOR_VARS.lives))
|
||||
if (shots == 0 and t > 0.5) or (shots == 1 and AUTHOR_VARS.broken > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (AUTHOR_VARS.broken >= 2) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("BREAKOUT RESULT broken=%d lives=%d paddleMoved=%s after=%.1fs", AUTHOR_VARS.broken, AUTHOR_VARS.lives, tostring(math.abs(px - paddleStart) > 5), t))
|
||||
if AUTHOR_VARS.broken < 1 then debugPrint("BREAKOUT FAIL no brick was broken") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
51
testScripts/scene74.singe
Normal file
51
testScripts/scene74.singe
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
-- A maze (FORGE.md's catalogue): a tile map for the walls, corridors that are a walk area, a
|
||||
-- hero sent by a click, dots eaten on touch, and a ghost that follows the hero across the mesh.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
dofile(authorBuild("testScripts/author/maze.game", singeGetDataPath() .. "maze.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local gapStart = nil
|
||||
local GIVE_UP = 25
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local hero = authorEntity("hero")
|
||||
local ghost = authorEntity("ghost")
|
||||
local gap = authorDistance(hero, ghost)
|
||||
|
||||
frames = frames + 1
|
||||
gapStart = gapStart or gap
|
||||
if frames == 10 then
|
||||
-- A click at the far end of the top corridor.
|
||||
authorSetPointer(1, 340, 180, true)
|
||||
authorSwitchDown(SWITCH_BUTTON3)
|
||||
authorSwitchUp(SWITCH_BUTTON3)
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored maze, %.1fs, hero %.0f,%.0f, ghost gap %.0f, eaten %d", t, authorX(hero), authorY(hero), gap, AUTHOR_VARS.eaten))
|
||||
if (shots == 0 and t > 0.5) or (shots == 1 and AUTHOR_VARS.eaten > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (AUTHOR_VARS.eaten >= 2 and gap < gapStart - 50) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("MAZE RESULT eaten=%d gap=%.0f (from %.0f) caught=%s after=%.1fs", AUTHOR_VARS.eaten, gap, gapStart, tostring(AUTHOR_VARS.caught), t))
|
||||
if AUTHOR_VARS.eaten < 2 then debugPrint("MAZE FAIL the hero did not eat two dots on its way") end
|
||||
if gap >= gapStart - 50 then debugPrint("MAZE FAIL the ghost did not close on the hero") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
77
testScripts/scene75.singe
Normal file
77
testScripts/scene75.singe
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
-- Third-person action (FORGE.md's catalogue): a camera orbiting the hero under injected mouse
|
||||
-- motion, keys moving the hero relative to it, enemies following across the mesh, and a sword
|
||||
-- swung by a key at what is in reach.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/thirdperson.game", singeGetDataPath() .. "thirdperson.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local kills = 0
|
||||
local yawStart = nil
|
||||
local startX = nil
|
||||
local lastSwing = 0
|
||||
local GIVE_UP = 40
|
||||
|
||||
local realEvent = authorEvent
|
||||
function authorEvent(name, event)
|
||||
if name == "death" then kills = kills + 1 end
|
||||
return realEvent(name, event)
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local hero = authorEntity("hero")
|
||||
local camera = authorEntity("camera")
|
||||
local hx, hy, hz = authorPosition(hero)
|
||||
|
||||
frames = frames + 1
|
||||
yawStart = yawStart or camera.yaw
|
||||
startX = startX or hx
|
||||
-- Orbit the camera a quarter turn over the first second, then walk forward for two.
|
||||
if frames >= 5 and frames < 35 then
|
||||
authorMouseMoved(0, 0, 15, 0)
|
||||
elseif frames == 35 then
|
||||
authorKeyDown(0, SCANCODE.W.value)
|
||||
elseif frames == 155 then
|
||||
authorKeyUp(0, SCANCODE.W.value)
|
||||
end
|
||||
-- Swing at anything close.
|
||||
if (t - lastSwing > 0.4) then
|
||||
for _, enemy in ipairs(authorEach("enemy")) do
|
||||
if (enemy.vars.state ~= "dead") and (authorDistance3D(enemy, hero) < 2.2) then
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
authorKeyUp(0, SCANCODE.SPACE.value)
|
||||
lastSwing = t
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored third person, %.1fs, yaw %.0f, hero %.1f,%.1f, kills %d, swings %d", t, camera.yaw, hx, hz, kills, AUTHOR_VARS.swings))
|
||||
if (shots == 0 and frames == 40) or (shots == 1 and kills > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
local hive = authorEntity("hive")
|
||||
|
||||
if (hive.vars.made >= 4 and authorCount("enemy") == 0 and t > 5) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("TPS RESULT kills=%d swings=%d health=%d yawTurned=%.0f movedX=%.1f after=%.1fs", kills, AUTHOR_VARS.swings, AUTHOR_VARS.health, camera.yaw - yawStart, hx - startX, t))
|
||||
if kills < 3 then debugPrint("TPS FAIL fewer than three enemies were cut down") end
|
||||
if math.abs(camera.yaw - yawStart) < 60 then debugPrint("TPS FAIL the camera did not orbit") end
|
||||
if math.abs(hx - startX) < 1 then debugPrint("TPS FAIL walking forward after the orbit did not move the hero sideways in the world") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
49
testScripts/scene76.singe
Normal file
49
testScripts/scene76.singe
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
-- A space shooter (FORGE.md's catalogue): a ship under thrust with no gravity, bolts fired
|
||||
-- forward, rocks coming at it broken by collision rules, and a camera behind.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/space.game", singeGetDataPath() .. "space.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local startZ = nil
|
||||
local maxBolts = 0
|
||||
local GIVE_UP = 40
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local ship = authorEntity("ship")
|
||||
local sx, sy, sz = authorPosition(ship)
|
||||
|
||||
frames = frames + 1
|
||||
startZ = startZ or sz
|
||||
if frames == 3 then
|
||||
authorKeyDown(0, SCANCODE.UP.value)
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
end
|
||||
maxBolts = math.max(maxBolts, authorCount("bolt"))
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored space, %.1fs, ship z %.1f, bolts %d, rocks %d, broken %d", t, sz, authorCount("bolt"), authorCount("rock"), AUTHOR_VARS.rocks))
|
||||
if (shots == 0 and t > 2) or (shots == 1 and AUTHOR_VARS.rocks > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (AUTHOR_VARS.rocks >= 3) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("SPACE RESULT broken=%d movedForward=%.1f maxBolts=%d after=%.1fs", AUTHOR_VARS.rocks, startZ - sz, maxBolts, t))
|
||||
if AUTHOR_VARS.rocks < 1 then debugPrint("SPACE FAIL no rock was broken") end
|
||||
if startZ - sz < 3 then debugPrint("SPACE FAIL the thrust did not move the ship") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
52
testScripts/scene77.singe
Normal file
52
testScripts/scene77.singe
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
-- A rhythm game (FORGE.md's catalogue): notes that fall to a marker, a press inside the window
|
||||
-- scoring and one outside missing, over a music layer.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
dofile(authorBuild("testScripts/author/rhythm.game", singeGetDataPath() .. "rhythm.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local pressed = {}
|
||||
local GIVE_UP = 8
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
|
||||
frames = frames + 1
|
||||
-- Press when a note is at the marker, once per note, and once at nothing.
|
||||
for _, note in ipairs(authorEach("note")) do
|
||||
if (math.abs(authorY(note) - 400) < 12) and not pressed[note.id] then
|
||||
pressed[note.id] = true
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
authorKeyUp(0, SCANCODE.SPACE.value)
|
||||
end
|
||||
end
|
||||
if frames == 10 then
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
authorKeyUp(0, SCANCODE.SPACE.value)
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored rhythm, %.1fs, notes %d, hits %d, misses %d, music %s", t, authorCount("note"), AUTHOR_VARS.hits, AUTHOR_VARS.misses, tostring(musicIsPlaying())))
|
||||
if (shots == 0 and t > 1.5) or (shots == 1 and AUTHOR_VARS.hits > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if t > GIVE_UP then
|
||||
debugPrint(string.format("RHYTHM RESULT hits=%d misses=%d score=%d", AUTHOR_VARS.hits, AUTHOR_VARS.misses, AUTHOR_VARS.score))
|
||||
if AUTHOR_VARS.hits < 3 then debugPrint("RHYTHM FAIL fewer than three notes were hit") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
43
testScripts/scene78.singe
Normal file
43
testScripts/scene78.singe
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- Bowling (FORGE.md's catalogue): a ball shoved down a lane by a press, pins that are bodies
|
||||
-- counted as they fall, and a trigger at the end of the lane.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/bowling.game", singeGetDataPath() .. "bowling.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local GIVE_UP = 20
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local ball = authorEntity("ball")
|
||||
|
||||
frames = frames + 1
|
||||
if frames == 5 then
|
||||
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||
authorKeyUp(0, SCANCODE.SPACE.value)
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored bowling, %.1fs, ball z %.1f, pins down %d", t, authorField(ball, "z"), AUTHOR_VARS.down))
|
||||
if (shots == 0 and t > 0.5) or (shots == 1 and AUTHOR_VARS.down > 0) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if (AUTHOR_VARS.done and t > 3) or (t > GIVE_UP) then
|
||||
debugPrint(string.format("BOWL RESULT down=%d done=%s ballZ=%.1f after=%.1fs", AUTHOR_VARS.down, tostring(AUTHOR_VARS.done), authorField(ball, "z"), t))
|
||||
if AUTHOR_VARS.down < 1 then debugPrint("BOWL FAIL no pin fell") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
51
testScripts/scene79.singe
Normal file
51
testScripts/scene79.singe
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
-- Occluders (FORGE.md): a painted room in 3D with a pillar that is only in the picture. The
|
||||
-- pillar is an invisible box that writes depth alone, so the picture shows through it and the hero
|
||||
-- disappears behind it on the way across. Three shots: before, behind, after.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/painted.game", singeGetDataPath() .. "painted.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
local GIVE_UP = 12
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local t = authorTime()
|
||||
local hero = authorEntity("hero")
|
||||
local hx = authorX(hero)
|
||||
|
||||
frames = frames + 1
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("authored painted room, %.1fs, hero x %.1f, pillar %s", t, hx, hero.material and "" or "is an occluder"))
|
||||
if (shots == 0 and hx > -3.5) or (shots == 1 and hx > -0.3) or (shots == 2 and hx > 3) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if AUTHOR_VARS.across or (t > GIVE_UP) then
|
||||
local pillar = authorEntity("pillar")
|
||||
|
||||
local _, ry, _ = nodeGetRotation(authorEntity("far").node)
|
||||
|
||||
debugPrint(string.format("PAINTED RESULT across=%s heroX=%.1f occluderCall=%s pillarMaterial=%s markerYaw=%.0f", tostring(AUTHOR_VARS.across), hx, tostring(type(materialSetOccluder) == "function"), tostring(pillar.material ~= nil), ry))
|
||||
if math.abs(ry - 90) > 1 then debugPrint("PAINTED FAIL the marker's ry did not reach its node") end
|
||||
local sx = nodeGetScale(authorEntity("far").node)
|
||||
local w = authorSize(authorEntity("far"))
|
||||
|
||||
debugPrint(string.format("PAINTED the marker's scale of 2 gives node scale %.1f and a counted width of %.1f", sx, w))
|
||||
if math.abs(sx - 2) > 0.01 then debugPrint("PAINTED FAIL the scale did not reach the node") end
|
||||
if not AUTHOR_VARS.across then debugPrint("PAINTED FAIL the hero did not cross") end
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
131
testScripts/scene80.singe
Normal file
131
testScripts/scene80.singe
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
-- Gizmos in the 3D viewport (FORGE.md): the selected entity carries three arms along the world
|
||||
-- axes, and dragging an arm's handle moves the entity along that axis alone -- the pointer's
|
||||
-- travel along the arm, as a fraction of the arm, is the distance moved.
|
||||
FORGE_LIBRARY = true
|
||||
dofile("Forge/Forge.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||
local out = singeGetDataPath()
|
||||
local work = out .. "gizmo.game"
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
local source = assert(io.open("testScripts/author/railshooter.game", "r"))
|
||||
local copy = assert(io.open(work, "w"))
|
||||
|
||||
copy:write(source:read("a"))
|
||||
source:close()
|
||||
copy:close()
|
||||
|
||||
if not forgeBegin(work) then
|
||||
debugPrint("GIZMO FAIL could not open " .. work)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
|
||||
|
||||
local function handle(axis)
|
||||
for _, item in ipairs(forgeGizmoHandles()) do
|
||||
if item.axis == axis then
|
||||
return item
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
local function step()
|
||||
local room = forgeRoom()
|
||||
|
||||
if stage == 1 then
|
||||
for index, entry in ipairs(room.entities) do
|
||||
if entry.id == "wallL" then
|
||||
forgeSelect(index)
|
||||
end
|
||||
end
|
||||
local arms = forgeGizmoHandles()
|
||||
|
||||
debugPrint("GIZMO the selected wall shows " .. #arms .. " arms")
|
||||
if #arms ~= 4 then
|
||||
debugPrint("GIZMO FAIL expected three axis arms and the turn handle")
|
||||
end
|
||||
|
||||
elseif stage == 2 then
|
||||
-- Drag the Y handle the length of its own arm: the wall rises one arm's length (1.5) and
|
||||
-- nothing else about it changes.
|
||||
local entry = room.entities[FORGE.selected]
|
||||
local was = { x = entry.x, y = entry.y, z = entry.z }
|
||||
local arm = handle("y")
|
||||
|
||||
forgePress(arm.tx, arm.ty)
|
||||
forgeDrag(arm.tx + (arm.tx - arm.sx), arm.ty + (arm.ty - arm.sy))
|
||||
forgeRelease()
|
||||
debugPrint(string.format("GIZMO the Y arm took the wall from %.1f to %.1f; x %.1f to %.1f, z %.1f to %.1f", was.y, entry.y, was.x, entry.x, was.z, entry.z))
|
||||
if math.abs(entry.y - (was.y + 1.5)) > 0.2 or entry.x ~= was.x or entry.z ~= was.z then
|
||||
debugPrint("GIZMO FAIL the Y drag did not move along Y alone")
|
||||
end
|
||||
|
||||
elseif stage == 3 then
|
||||
-- And the X handle, dragged half an arm back.
|
||||
local entry = room.entities[FORGE.selected]
|
||||
local was = entry.x
|
||||
local arm = handle("x")
|
||||
|
||||
forgePress(arm.tx, arm.ty)
|
||||
forgeDrag(arm.tx - (arm.tx - arm.sx) / 2, arm.ty - (arm.ty - arm.sy) / 2)
|
||||
forgeRelease()
|
||||
debugPrint(string.format("GIZMO the X arm took the wall from %.1f to %.1f", was, entry.x))
|
||||
if math.abs(entry.x - (was - 0.75)) > 0.2 then
|
||||
debugPrint("GIZMO FAIL the X drag did not move half an arm back")
|
||||
end
|
||||
forgeKey(0, SCANCODE.U.value)
|
||||
forgeKey(0, SCANCODE.U.value)
|
||||
-- Undo puts a copy of the description in place, so the room is looked up again.
|
||||
local back = forgeRoom().entities[FORGE.selected]
|
||||
|
||||
debugPrint(string.format("GIZMO two undos: the wall is back at x %.1f, y %.1f", back.x, back.y))
|
||||
if back.x ~= -6 or back.y ~= 1.5 then
|
||||
debugPrint("GIZMO FAIL undo did not put the wall back")
|
||||
end
|
||||
|
||||
elseif stage == 4 then
|
||||
-- The turn handle: dragged a quarter turn round the entity on screen.
|
||||
local entry = forgeRoom().entities[FORGE.selected]
|
||||
local arm = handle("ry")
|
||||
local dx, dy = arm.tx - arm.sx, arm.ty - arm.sy
|
||||
|
||||
forgePress(arm.tx, arm.ty)
|
||||
forgeDrag(arm.sx - dy, arm.sy + dx)
|
||||
forgeRelease()
|
||||
debugPrint(string.format("GIZMO the turn handle set ry to %s", tostring(entry.ry)))
|
||||
if (entry.ry == nil) or (math.abs(((entry.ry - 270 + 180) % 360) - 180) > 15) then
|
||||
debugPrint("GIZMO FAIL a quarter turn clockwise on screen should read about 270")
|
||||
end
|
||||
debugPrint("GIZMO RESULT done")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
if frames % 14 == 0 and stage < 5 then
|
||||
stage = stage + 1
|
||||
step()
|
||||
end
|
||||
forgeDraw(nil, nil)
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(210, 8, "gizmos, stage " .. stage)
|
||||
if frames == 20 or frames == 34 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames > 80 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
73
testScripts/scene81.singe
Normal file
73
testScripts/scene81.singe
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- Facing and turning sprites in a Forge game: a sheet drawn facing right only is mirrored by
|
||||
-- the engine while its instance faces left (the runner beside it has frames drawn both ways and
|
||||
-- is not), a sheet entity with an rz draws its frame turned, and clips play at a volume of their
|
||||
-- own under the game's.
|
||||
dofile("Forge/AuthorCompile.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
dofile(authorBuild("testScripts/author/tail.game", singeGetDataPath() .. "facing.singe"))
|
||||
|
||||
local gameUpdate = onOverlayUpdate
|
||||
local frames = 0
|
||||
local shots = 0
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
local runner = authorEntity("runner")
|
||||
local mirror = authorEntity("mirror")
|
||||
local tilted = authorEntity("tilted")
|
||||
|
||||
frames = frames + 1
|
||||
if frames == 2 then
|
||||
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||
elseif frames == 30 then
|
||||
authorKeyUp(0, SCANCODE.RIGHT.value)
|
||||
authorKeyDown(0, SCANCODE.LEFT.value)
|
||||
elseif frames == 40 then
|
||||
-- The playSound action with a volume, and the sound behaviour's, both under the master.
|
||||
authorMidi(0x90, 60, 100)
|
||||
authorBehaviourEvent(mirror, "hit")
|
||||
local channel = soundPlay(soundLoad("testScripts/crackle.wav"), 0, 20)
|
||||
|
||||
soundSetChannelVolume(channel, 50)
|
||||
soundSetVolume(40)
|
||||
debugPrint("FACING a clip on channel " .. channel .. " at its own volume under a master of " .. soundGetVolume())
|
||||
end
|
||||
|
||||
local r = gameUpdate()
|
||||
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(12, 450, string.format("facing: runner %s, mirror %s%s, tilted %d degrees", tostring(runner.vars.facing), tostring(mirror.vars.facing), authorMirrored(mirror) and " (mirrored)" or "", tilted.vars.angle or 0))
|
||||
if (shots == 0 and frames == 20) or (shots == 1 and frames == 55) then
|
||||
shots = shots + 1
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames == 20 then
|
||||
debugPrint(string.format("FACING going right: runner mirrored %s, mirror mirrored %s", tostring(authorMirrored(runner)), tostring(authorMirrored(mirror))))
|
||||
if authorMirrored(runner) or authorMirrored(mirror) then
|
||||
debugPrint("FACING FAIL nothing should be mirrored while facing right")
|
||||
end
|
||||
debugPrint(string.format("FACING the tilted sheet has an image of its own: %s, turned %d", tostring(tilted.ownSprite ~= nil), tilted.ownAngle or -1))
|
||||
if (tilted.ownSprite == nil) or (tilted.ownAngle ~= 30) then
|
||||
debugPrint("FACING FAIL the sheet entity with rz 30 should draw a turned frame")
|
||||
end
|
||||
elseif frames == 55 then
|
||||
debugPrint(string.format("FACING going left: runner mirrored %s (drawn %s), mirror mirrored %s", tostring(authorMirrored(runner)), tostring(runner.drawnFacing), tostring(authorMirrored(mirror))))
|
||||
if authorMirrored(runner) or not authorMirrored(mirror) then
|
||||
debugPrint("FACING FAIL the sheet without left frames should be mirrored, the one with them not")
|
||||
end
|
||||
if mirror.ownSprite == nil then
|
||||
debugPrint("FACING FAIL the mirrored instance should draw from an image of its own")
|
||||
end
|
||||
debugPrint("FACING RESULT done")
|
||||
end
|
||||
if frames > 70 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
111
testScripts/scene82.singe
Normal file
111
testScripts/scene82.singe
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
-- The 2D canvas's turn handle and the kinds panel's thumbnails (FORGE.md): the selected entity
|
||||
-- carries a yellow arm the way its rz points, dragged round it to turn it, as the scene's ry arm
|
||||
-- does; and each kind in the panel shows what it looks like beside its name.
|
||||
FORGE_LIBRARY = true
|
||||
dofile("Forge/Forge.singe")
|
||||
|
||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||
local out = singeGetDataPath()
|
||||
local work = out .. "turn.game"
|
||||
|
||||
fontSelect(font)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
overlaySetResolution(720, 480)
|
||||
|
||||
local source = assert(io.open("testScripts/author/tail.game", "r"))
|
||||
local copy = assert(io.open(work, "w"))
|
||||
|
||||
copy:write(source:read("a"))
|
||||
source:close()
|
||||
copy:close()
|
||||
|
||||
if not forgeBegin(work) then
|
||||
debugPrint("TURN FAIL could not open " .. work)
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
local frames = 0
|
||||
local stage = 0
|
||||
|
||||
|
||||
local function selectEntity(id)
|
||||
for index, entry in ipairs(forgeRoom().entities) do
|
||||
if entry.id == id then
|
||||
forgeSelect(index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function step()
|
||||
if stage == 1 then
|
||||
selectEntity("spinner")
|
||||
local arm = forgeTurnHandle()
|
||||
|
||||
debugPrint(string.format("TURN the spinner, rz 45, shows its handle at %.0f, %.0f from %.0f, %.0f", arm and arm.tx or -1, arm and arm.ty or -1, arm and arm.sx or -1, arm and arm.sy or -1))
|
||||
if (arm == nil) or (arm.tx <= arm.sx) or (arm.ty <= arm.sy) then
|
||||
debugPrint("TURN FAIL a handle at 45 degrees lies down and right of the entity")
|
||||
end
|
||||
|
||||
elseif stage == 2 then
|
||||
-- A quarter turn clockwise round the entity: the handle's offset turned 90 degrees.
|
||||
local entry = forgeRoom().entities[FORGE.selected]
|
||||
local arm = forgeTurnHandle()
|
||||
local dx, dy = arm.tx - arm.sx, arm.ty - arm.sy
|
||||
|
||||
forgePress(arm.tx, arm.ty)
|
||||
forgeDrag(arm.sx - dy, arm.sy + dx)
|
||||
forgeRelease()
|
||||
debugPrint(string.format("TURN a quarter turn clockwise took rz from 45 to %s", tostring(entry.rz)))
|
||||
if (entry.rz == nil) or (math.abs(entry.rz - 135) > 3) then
|
||||
debugPrint("TURN FAIL expected about 135")
|
||||
end
|
||||
|
||||
elseif stage == 3 then
|
||||
-- Undo puts it back, and a sheet entity with an rz draws its first frame turned.
|
||||
forgeKey(0, SCANCODE.U.value)
|
||||
local back = forgeRoom().entities[FORGE.selected]
|
||||
|
||||
debugPrint("TURN undone, rz " .. tostring(back.rz))
|
||||
if back.rz ~= 45 then
|
||||
debugPrint("TURN FAIL undo should give 45 back")
|
||||
end
|
||||
selectEntity("tilted")
|
||||
|
||||
elseif stage == 4 then
|
||||
-- The kinds panel: a thumbnail in front of every name.
|
||||
while FORGE.mode ~= "kinds" do
|
||||
forgeKey(0, SCANCODE.TAB.value)
|
||||
end
|
||||
forgeRefresh()
|
||||
local rml = rmlui.contexts["gui" .. FORGE.gui].documents["forge"]:GetElementById("list").inner_rml
|
||||
local images = select(2, rml:gsub("<img", ""))
|
||||
local swatch = select(2, rml:gsub("swatch", ""))
|
||||
|
||||
debugPrint(string.format("TURN the kinds panel shows %d sprite thumbnails and %d swatches", images, swatch))
|
||||
if (images < 2) or (swatch < 1) then
|
||||
debugPrint("TURN FAIL the sprite kinds want images and the fire wants a swatch")
|
||||
end
|
||||
debugPrint("TURN RESULT done")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
frames = frames + 1
|
||||
if frames % 14 == 0 and stage < 5 then
|
||||
stage = stage + 1
|
||||
step()
|
||||
end
|
||||
forgeDraw(nil, nil)
|
||||
colorForeground(255, 255, 255, 255)
|
||||
fontPrint(210, 8, "2D turn handle, stage " .. stage)
|
||||
if frames == 20 or frames == 62 then
|
||||
singeScreenshot()
|
||||
end
|
||||
if frames > 80 then
|
||||
singeQuit()
|
||||
end
|
||||
|
||||
return OVERLAY_UPDATED
|
||||
end
|
||||
68
util/forgeVocabulary.lua
Normal file
68
util/forgeVocabulary.lua
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
-- Writes the Forge vocabulary -- every layer, look, behaviour, event, condition, and action the
|
||||
-- manifest in assets/Forge/Author.singe declares -- as AsciiDoc, for docs/Forge.adoc to include.
|
||||
-- The manifest is the one source of truth; this keeps the manual from drifting from it.
|
||||
--
|
||||
-- lua5.4 util/forgeVocabulary.lua > docs/ForgeVocabulary.adoc
|
||||
--
|
||||
-- Author.singe touches nothing of the engine when it is loaded, only when a game begins, so plain
|
||||
-- Lua can read it.
|
||||
|
||||
local function keysOf(t)
|
||||
local keys = {}
|
||||
|
||||
for key in pairs(t or {}) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
table.sort(keys)
|
||||
|
||||
return keys
|
||||
end
|
||||
|
||||
|
||||
local function paramsText(params)
|
||||
local parts = {}
|
||||
|
||||
for _, key in ipairs(keysOf(params)) do
|
||||
parts[#parts + 1] = "`" .. key .. "` (" .. params[key] .. ")"
|
||||
end
|
||||
if #parts == 0 then
|
||||
return "--"
|
||||
end
|
||||
|
||||
return table.concat(parts, ", ")
|
||||
end
|
||||
|
||||
|
||||
local function section(title, set, extra)
|
||||
print("=== " .. title)
|
||||
print("")
|
||||
print('[cols="1,3,2' .. (extra and ',1' or '') .. '"]')
|
||||
print("|===")
|
||||
print("| Name | What it does | Parameters" .. (extra and (" | " .. extra) or ""))
|
||||
print("")
|
||||
for _, name in ipairs(keysOf(set)) do
|
||||
local entry = set[name]
|
||||
|
||||
print("| `" .. name .. "`")
|
||||
print("| " .. (entry.help or ""))
|
||||
print("| " .. paramsText(entry.params or entry.filter))
|
||||
if extra then
|
||||
print("| " .. (entry.waits and "yes" or ""))
|
||||
end
|
||||
print("")
|
||||
end
|
||||
print("|===")
|
||||
print("")
|
||||
end
|
||||
|
||||
|
||||
dofile("assets/Forge/Author.singe")
|
||||
|
||||
print("// Generated by util/forgeVocabulary.lua from assets/Forge/Author.singe. Do not edit.")
|
||||
print("")
|
||||
section("Layers", AUTHOR.layers)
|
||||
section("Looks", AUTHOR.looks)
|
||||
section("Behaviours", AUTHOR.behaviours)
|
||||
section("Events", AUTHOR.events)
|
||||
section("Conditions", AUTHOR.conditions)
|
||||
section("Actions", AUTHOR.actions, "Waits")
|
||||
Loading…
Add table
Reference in a new issue