singe/assets/Forge/Author.singe
2026-09-22 18:32:07 -05:00

13628 lines
418 KiB
Text

--[[
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*
--]]
-- The runtime an authored game is compiled against (FORGE.md).
--
-- A game made with Forge is not interpreted: Forge/AuthorCompile.singe turns its description into
-- ordinary Lua, and this file is the library that Lua calls. Rules become real functions, so
-- nothing walks a table every frame, and the result can be opened in ZeroBrane and edited by hand
-- like any other game.
--
-- The nouns (FORGE.md section 3):
--
-- layers which of the engine's own layers the game uses. A genre is only a choice of layers.
-- types what a thing is: a look, behaviours, and vars. Everything placed or spawned is
-- an instance of a type.
-- rooms where things are. A room keeps its state when it is left unless it says reset.
-- rules a trigger, conditions, and actions. Actions that take time run as a coroutine.
--
-- AUTHOR is also the manifest the compiler and the editor read: every layer, look, behaviour,
-- condition, action, and event declares its parameters and the Lua it emits, so a new kind of
-- game is a set of entries here rather than a new release of anything.
--
-- The manifest's emit functions receive their parameters already as Lua source fragments -- the
-- compiler has turned a number into a number, an expression into Lua, an entity name into a lookup
-- -- so an entry only concatenates. The parameter types (FORGE.md section 3):
--
-- number a number, or an expression that makes one
-- expression any expression
-- string text, quoted
-- boolean true or false
-- entity "self", "other", or an entity id
-- type a type's name
-- scancode a key, from SCANCODE
-- switch a switch, from SWITCH_*
-- file a file name
-- state a state name
-- track a track's name
-- room a room's name
-- lua Lua, as written
AUTHOR = {
layers = {},
looks = {},
behaviours = {},
conditions = {},
actions = {},
events = {}
}
AUTHOR_GAME = nil -- The description handed to authorBegin.
AUTHOR_VARS = {} -- Game scope vars: score, lives, flags, whatever the game declared.
AUTHOR_LIVE = {} -- Instances in the current room, in draw order.
AUTHOR_BY_ID = {} -- Live instances by id.
AUTHOR_BY_NODE = {} -- Live instances by node, for what physics and rays report.
AUTHOR_3D = false -- Whether the game has a scene3d layer.
AUTHOR_MOUSE = { dx = 0, dy = 0 } -- Relative motion this frame, for a camera that looks.
AUTHOR_ROOM = nil -- The current room's table.
AUTHOR_STARTED = 0 -- singeGetTicks() when the game began.
AUTHOR_CONTROLS = true -- False while a cut-scene has the controls.
AUTHOR_FONT = nil
AUTHOR_FONT_POINTS = 18
local GRAVITY_DEFAULT = 1400 -- Overlay units a second squared; a 480 tall screen wants about this.
local PLAYER_RADIUS = 0.5 -- Fractions of an entity's box, for the capsule a platformer stands in.
local MOUSE_LEFT = nil -- Filled from SWITCH_BUTTON3 once Framework is in: the gun's trigger.
local SAY_SECONDS_MIN = 1.5 -- A line stays up at least this long,
local SAY_PER_CHAR = 0.05 -- plus this per character.
local SOUND_MAX_VOLUME = 63 -- soundPlay's loudest; Forge volumes are 0 to 100 of it.
local PLATFORMER_STATES = { idle = true, walk = true, jump = true, fall = true } -- The states a platformer sets itself.
local AUTHOR_GROUND = { r = 18, g = 18, b = 26 } -- Behind a flat game with no disc: the editor's own dark.
local SPAWN_MARGIN = 40 -- How far off the overlay a projectile may go before it is dropped.
local FRAME_CAP = 0.1 -- The longest step a frame may take, so a stall does not teleport.
local RAY_REACH = 500 -- How far a gun's ray goes, in world units.
local FAR_AWAY = 300 -- A 3D projectile past this from the origin is dropped.
local LOOK_SPEED = 0.2 -- Degrees of turn per pixel of mouse motion.
local PITCH_LIMIT = 80 -- Degrees a looking camera may tilt.
local models = {} -- Loaded models by file.
local meshes = {} -- Built primitives by description.
local cameraNode = nil -- The node the scene is drawn from, when a camera behaviour has one.
local NAV_SCALE = 0.01 -- Overlay pixels to navigation units in a 2D room: a 720 wide room is 7.2 across.
local NAV_RADIUS_2D = 0.08 -- Agents in a 2D room, in navigation units: eight pixels.
local NAV_HEIGHT_2D = 0.5
local WALK_TIMEOUT = 12 -- Seconds a walkTo waits before giving up on an unreachable point.
local FADE_DEFAULT = 0.6 -- Seconds a fade takes when the rule does not say.
local CHOICE_KEYS = { "MAIN_1", "MAIN_2", "MAIN_3", "MAIN_4", "MAIN_5", "MAIN_6", "MAIN_7", "MAIN_8", "MAIN_9" }
local dialogues = {} -- Compiled dialogues by name, from authorDialogues.
local talking = nil -- The dialogue on screen: { lines, choices, chosen }.
local fadeLevel = 0 -- 0 clear, 1 black.
local parserLayer = nil -- The parser layer, when the game has one.
AUTHOR_TYPING = "" -- What has been typed at the parser's prompt.
local bezel = nil -- The score bezel layer, when the game has one.
local results = nil -- The results card after gameOver: { score, best }.
local masterReady = false -- Whether the master service's script has been loaded.
local BEST_KEY = "forge.best"
local rules = {} -- Compiled rules by event name.
local rulesReady = false -- authorRules has been called; before it, events are kept for it.
local pending = {} -- Events raised before the rules arrived: the first room's start.
local pairsToTest = {} -- Kind pairs the collision rules ask about.
local sequences = {} -- Running coroutines, by rule and instance.
local timers = {} -- Runtime timers: { instance, name, at, every }.
local onceSeen = {} -- once tags, by scope.
local sounds = {} -- Loaded clips by file.
local fonts = {} -- Loaded fonts by file and size.
local playing = {} -- What soundPlay is playing, by channel: the file and the instance that played it, for soundDone.
local sprites = {} -- Loaded images by file.
local pointers = {} -- Injected pointer positions by player, for tests.
local pressedNow = {} -- Keys and switches that went down this frame.
local lastTime = 0
local lastFrame = -1
local nextSerial = 1
local hud = nil -- { gui, document, bind }
local sayLine = nil -- { text, until }
local flashLine = nil -- { r, g, b, until, seconds }
local gameOver = false
local music = nil
-- ===== Small helpers ==========================================================================
-- Seconds since the game started. Every rule that talks about time uses this, so a game is
-- reproducible under --deterministic rather than depending on how fast the machine draws.
function authorTime()
return (singeGetTicks() - AUTHOR_STARTED) / 1000.0
end
function authorEntity(id)
return AUTHOR_BY_ID[id]
end
-- Every live instance of a type, in draw order.
function authorEach(typeName)
local list = {}
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.alive and (instance.type == typeName) then
list[#list + 1] = instance
end
end
return list
end
function authorCount(typeName)
return #authorEach(typeName)
end
-- Where an instance is, in overlay coordinates. Everything is a node, whether or not the scene is
-- drawing: nodes exist without a GPU, which is what lets a 2D game run on a machine with none.
function authorPosition(instance)
local x, y, z = nodeGetPosition(instance.node)
return x, y, z
end
function authorX(instance)
local x = nodeGetPosition(instance.node)
return x
end
function authorY(instance)
local _, y = nodeGetPosition(instance.node)
return y
end
-- An instance's field, for expressions: x, y, z, id, type, state, or one of its vars.
function authorField(instance, name)
if instance == nil then
return nil
end
if name == "x" then
return authorX(instance)
elseif name == "y" then
return authorY(instance)
elseif name == "z" then
local _, _, z = nodeGetPosition(instance.node)
return z
elseif (name == "id") or (name == "type") then
return instance[name]
end
return instance.vars[name]
end
-- The distance between two instances -- either may be given by id -- across the picture in 2D
-- and through the scene in 3D.
function authorDistance(a, b)
if type(a) == "string" then
a = AUTHOR_BY_ID[a]
end
if type(b) == "string" then
b = AUTHOR_BY_ID[b]
end
if (a == nil) or (b == nil) then
return math.huge
end
local ax, ay, az = authorPosition(a)
local bx, by, bz = authorPosition(b)
if AUTHOR_3D then
return math.sqrt((ax - bx) ^ 2 + (ay - by) ^ 2 + (az - bz) ^ 2)
end
return math.sqrt((ax - bx) ^ 2 + (ay - by) ^ 2)
end
function authorRandom(low, high)
if high == nil then
return math.random() * (low or 1)
end
return low + math.random() * (high - low)
end
function authorHas(item)
for _, held in ipairs(AUTHOR_VARS.inventory or {}) do
if held == item then
return true
end
end
return false
end
-- How big an instance is, from its look and its own scale. Every look answers.
function authorSize(instance)
local look = AUTHOR.looks[instance.look.kind]
local scale = instance.scale or 1
local w, h, d
if look and look.size then
w, h, d = look.size(instance)
else
w, h, d = instance.look.w or 0, instance.look.h or 0, instance.look.d or 0
end
return w * scale, h * scale, (d or 0) * scale
end
-- The look's own size, before the entity's scale: what a body on the node wants, since bodies
-- take the node's scale themselves.
function authorUnscaledSize(instance)
local w, h, d = authorSize(instance)
local scale = instance.scale or 1
return w / scale, h / scale, d / scale
end
-- An instance's box in overlay coordinates, centred on it, or standing on it when its look says
-- anchor feet, and scaled with depth in a room that scales.
function authorBounds(instance)
local x, y = authorPosition(instance)
local w, h = authorSize(instance)
local scale = authorDepthScaleOf(instance)
w = w * scale
h = h * scale
if instance.look.anchor == "feet" then
return x - w / 2, y - h, w, h
end
return x - w / 2, y - h / 2, w, h
end
-- How big a thing at a height on the picture is drawn, in a room whose scaleBy says things
-- further up the picture are further away. 1 everywhere else.
-- Where an instance sorts in a room that sorts by depth: its y, except that a line of text is a
-- readout and stays in front of everything.
function authorDepthY(instance)
if instance.look.kind == "text" then
return math.huge
end
return authorY(instance)
end
-- The room's depth scale for an instance: what stands on the floor (anchor feet) shrinks with
-- distance; a backdrop, a readout, or a thing floating in the air is drawn as it is.
function authorDepthScaleOf(instance)
if instance.look.anchor ~= "feet" then
return 1
end
return authorDepthScale(select(2, authorPosition(instance)))
end
function authorDepthScale(y)
local table_ = AUTHOR_ROOM and AUTHOR_ROOM.scaleBy or nil
if (table_ == nil) or (#table_ < 2) then
return 1
end
local before, after
for _, key in ipairs(table_) do
if (key.y <= y) and ((before == nil) or (key.y > before.y)) then
before = key
end
if (key.y >= y) and ((after == nil) or (key.y < after.y)) then
after = key
end
end
before = before or after
after = after or before
if (before == after) or (after.y == before.y) then
return before.scale
end
return before.scale + (after.scale - before.scale) * (y - before.y) / (after.y - before.y)
end
-- A key named in a behaviour's parameters, as the engine delivers it. Rules have theirs compiled;
-- behaviours get the description's own text and resolve it here. Public, for a game's own vocabulary.
function authorKeyValue(name)
if type(name) == "string" then
return SCANCODE[name] and SCANCODE[name].value or nil
end
return name
end
-- A switch named the same way.
function authorSwitchValue(name)
if type(name) == "string" then
return _G[name]
end
return name
end
-- A file of the game's own -- its vocabulary, a script it brought -- beside the description
-- first, then beside the built script: a built script can share a name with a file of the
-- game's, and loading the built one would be loading the game inside itself.
function authorBeside(name)
if not name:match("^[/\\]") and not name:match("^%a:") then
for _, folder in ipairs({ AUTHOR_SOURCE_DIR or "", AUTHOR_DIR or "" }) do
if folder ~= "" then
local probe = io.open(folder .. name, "rb")
if probe ~= nil then
probe:close()
return folder .. name
end
end
end
end
return name
end
-- A game's own vocabulary: a file of Lua that adds looks, behaviours, conditions, actions,
-- layers, or events to the manifest for this game alone, written as the manifest's own are.
-- The description names it (vocabulary = "Vocabulary.singe"); the runtime, the compiler, and
-- the editor each load it before they read the manifest, and a release carries it. What it
-- added is remembered so the editor can forget it when the game is closed.
AUTHOR_ADDED = {}
function authorVocabulary(name, folder)
local path = (folder and (folder ~= "") and (io.open(folder .. name, "rb") ~= nil)) and (folder .. name) or authorBeside(name)
local had = {}
for set, entries in pairs(AUTHOR) do
had[set] = {}
for key in pairs(entries) do
had[set][key] = true
end
end
local chunk, why = loadfile(path)
if chunk == nil then
debugPrint("Author: vocabulary " .. tostring(name) .. " did not load: " .. tostring(why))
return false
end
local ok, err = xpcall(chunk, debug.traceback)
if not ok then
debugPrint("Author: vocabulary " .. tostring(name) .. ": " .. tostring(err):sub(1, 800))
return false
end
for set, entries in pairs(AUTHOR) do
for key in pairs(entries) do
if not (had[set] and had[set][key]) then
AUTHOR_ADDED[#AUTHOR_ADDED + 1] = { set = set, key = key }
end
end
end
return true
end
-- Takes out what a game's vocabulary added, when the editor closes the game.
function authorVocabularyForget()
for _, added in ipairs(AUTHOR_ADDED) do
if AUTHOR[added.set] then
AUTHOR[added.set][added.key] = nil
end
end
AUTHOR_ADDED = {}
end
-- Where a file the description names really is. A game built in the editor names files as the
-- engine sees them from the games directory (Forge/kit/hero.png); a released game carries its
-- files beside itself and names them from there (kit/hero.png), and the compiled script says
-- where it is in AUTHOR_DIR. The name is tried beside the game first, then as it is.
function authorFile(name)
if not name:match("^[/\\]") and not name:match("^%a:") then
for _, folder in ipairs({ AUTHOR_DIR or "", AUTHOR_SOURCE_DIR or "" }) do
if folder ~= "" then
local probe = io.open(folder .. name, "rb")
if probe ~= nil then
probe:close()
return folder .. name
end
end
end
end
return name
end
-- A picture loaded once per file -- or, with a frame count, as a sheet of that many frames across,
-- which is its own sprite since the engine keeps the frames with the handle.
local function spriteFor(file, frames)
local key = file .. "#" .. tostring(frames or 1)
if sprites[key] == nil then
if (frames or 1) > 1 then
sprites[key] = spriteLoadFrames(frames, authorFile(file))
else
sprites[key] = spriteLoad(authorFile(file))
end
end
return sprites[key]
end
local function soundFor(file)
if sounds[file] == nil then
sounds[file] = soundLoad(authorFile(file))
end
return sounds[file]
end
function fontFor(file, size)
local key = file .. "@" .. size
if fonts[key] == nil then
fonts[key] = fontLoad(authorFile(file), size)
end
return fonts[key]
end
-- ===== Layers =================================================================================
--
-- Each declares what it needs of the engine. Nothing here knows what kind of game is being made.
AUTHOR.layers.world2d = {
help = "A flat world with gravity: physics in the XY plane, drawn into the overlay.",
params = { gravity = "number" },
begin = function(layer)
physicsSetEnabled(true)
physicsSet2D(true)
physicsSetGravity(0, layer.gravity or GRAVITY_DEFAULT, 0)
end
}
AUTHOR.layers.overlay = {
help = "Flat drawing over everything: sprites, scores, prompts.",
params = {},
begin = function() end
}
AUTHOR.layers.disc = {
help = "The video the game is played over. The file is what the editor scrubs; games.dat names it when the game runs.",
params = { file = "file", start = "number" },
begin = function(layer)
discPlay()
if layer.start then
discSkipToFrame(layer.start)
end
end
}
AUTHOR.layers.hud = {
help = "An RmlUi document over everything. bind maps element ids to game vars, refreshed as they change.",
params = { document = "file", bind = "table" },
begin = function(layer)
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
hud = { gui = gui, document = guiLoad(gui, authorFile(layer.document)), bind = layer.bind or {}, shown = {} }
end
}
AUTHOR.layers.music = {
help = "A track that plays from the start, looping.",
params = { file = "file", volume = "number" },
begin = function(layer)
music = musicLoad(authorFile(layer.file))
if layer.volume then
musicSetVolume(layer.volume)
end
musicPlay(music, -1)
end
}
AUTHOR.layers.scene3d = {
help = "The 3D scene: physics in three axes, a sun, a sky, fog. Positions are world units.",
params = { gravity = "number", sky = "file", ambient = "table", fog = "table", sun = "table", fov = "number", exposure = "number" },
begin = function(layer)
local ok, err = pcall(sceneEnable, true)
if not ok then
debugPrint("Author: the 3D scene could not start: " .. tostring(err))
end
AUTHOR_3D = true
physicsSetEnabled(true)
physicsSet2D(false)
physicsSetGravity(0, layer.gravity or -9.8, 0)
if layer.sky then
sceneSetSky(authorFile(layer.sky))
end
if layer.ambient then
sceneSetAmbient(layer.ambient.r or 40, layer.ambient.g or 40, layer.ambient.b or 50)
end
if layer.fog then
sceneSetFog(layer.fog.r or 0, layer.fog.g or 0, layer.fog.b or 0, layer.fog.near or 30, layer.fog.far or 80)
end
if layer.exposure then
sceneSetExposure(layer.exposure)
end
if layer.sun ~= false then
local sun = layer.sun or {}
local light = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(light, sun.x or -5, sun.y or 10, sun.z or 6)
nodeLookAt(light, 0, 0, 0)
lightSetIntensity(light, sun.intensity or 1.5)
lightSetShadow(light, sun.shadow ~= false)
end
cameraSetPerspective(layer.fov or 60, 0.1, 500)
end
}
AUTHOR.layers.bezel = {
help = "The arcade score panel around the picture, showing the vars score (and score2), lives (and lives2), and credits as they change.",
params = { twin = "boolean" },
begin = function(layer)
bezel = { twin = layer.twin, shown = {} }
scoreBezelEnable(true)
if layer.twin then
scoreBezelTwinScoreOn(true)
end
end
}
AUTHOR.layers.parser = {
help = "A text parser, the Sierra way: a prompt the player types at, and words the game knows. What is typed raises the said event.",
params = { prompt = "string", words = "table", ignore = "table" },
begin = function(layer)
parserLayer = layer
keyboardSetMode(MODE_FULL)
end
}
-- ===== Looks ==================================================================================
--
-- How an instance is drawn. A look draws itself and says how big it is; it never knows which
-- layer it is on. The 3D looks build nodes under the instance's own and draw nothing themselves:
-- the scene draws them.
AUTHOR.looks.box = {
help = "A filled rectangle, centred on the instance -- or standing on it, with anchor feet.",
params = { w = "number", h = "number", r = "number", g = "number", b = "number", anchor = "string" },
choices = { anchor = { "feet" } },
size = function(instance) return instance.look.w or 20, instance.look.h or 20 end,
draw = function(instance)
local look = instance.look
local x, y = authorPosition(instance)
local w, h = authorSize(instance)
local scale = authorDepthScaleOf(instance)
local top = (look.anchor == "feet") and (y - h * scale) or (y - h * scale / 2)
colorForeground(look.r or 255, look.g or 255, look.b or 255, 255)
-- overlayBox outlines; a solid block is the outline drawn every row, which is cheap at
-- these sizes and saves the game needing an image for a placeholder.
for row = math.floor(top), math.floor(top + h * scale) do
overlayLine(x - w * scale / 2, row, x + w * scale / 2, row)
end
end
}
AUTHOR.looks.sprite = {
help = "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. The var copies draws it that many times in a row, step pixels apart (a row of bullets, of hearts): negative steps go left.",
params = { file = "file", frames = "number", anchor = "string", faces = "string", step = "number" },
choices = { anchor = { "feet" }, faces = { "right", "left", "none" } },
load = function(instance)
instance.sprite = spriteFor(instance.look.file, instance.look.frames)
end,
size = function(instance)
if instance.sprite == nil then
return 0, 0
end
if (instance.look.frames or 1) > 1 then
return spriteFrameWidth(instance.sprite), spriteFrameHeight(instance.sprite)
end
return spriteGetWidth(instance.sprite), spriteGetHeight(instance.sprite)
end,
draw = function(instance)
local x, y = authorPosition(instance)
local w, h = authorSize(instance)
local scale = authorDepthScaleOf(instance) * (instance.scale or 1)
local top = (instance.look.anchor == "feet") and (y - h * scale) or (y - h * scale / 2)
local frames = instance.look.frames or 1
local frame = instance.vars.frame or 1
local angle = instance.vars.angle or 0
local mirror = authorMirrored(instance)
if (angle ~= 0) or mirror then
-- A turned or mirrored sprite gets an image of its own, so it does not turn every other
-- instance drawn from the same file, and the image is rebuilt only when something
-- about it changes.
local own = instance.ownSprite
if own == nil then
own = (frames > 1) and spriteLoadFrames(frames, authorFile(instance.look.file)) or spriteLoad(authorFile(instance.look.file))
instance.ownSprite = own
end
spriteFlip(own, mirror, false)
if frames > 1 then
if (instance.ownAngle ~= angle) or (instance.ownFrame ~= frame) then
spriteRotateFrame(own, angle, frame)
instance.ownAngle = angle
instance.ownFrame = frame
end
spriteDrawRotatedFrame(own, x, top + h * scale / 2, scale)
else
spriteRotate(own, angle)
spriteScale(own, scale)
spriteDraw(own, x, top + h * scale / 2, true)
end
elseif (frames > 1) or (scale ~= 1) then
spriteDrawFrame(instance.sprite, x - w * scale / 2, top, frame, scale)
else
local copies = instance.vars.copies or 1
local step = instance.look.step or w
for copy = 0, copies - 1 do
spriteDraw(instance.sprite, x - w / 2 + copy * step, top)
end
end
end
}
AUTHOR.looks.particles = {
help = "A steady stream of particles from the instance: a fire, smoke, a thruster, rain. Colour, size, speed, and life as an emitter takes them.",
params = { r = "number", g = "number", b = "number", rate = "number", size = "number", speed = "number", life = "number", spread = "number", gravity = "number" },
load = function(instance)
local look = instance.look
instance.emitter = AUTHOR_3D and emitterNew(instance.node) or emitterNew()
emitterSetColor(instance.emitter, look.r or 255, look.g or 200, look.b or 80, 255, look.r or 255, look.g or 200, look.b or 80, 0)
emitterSetRate(instance.emitter, look.rate or 40)
emitterSetLife(instance.emitter, (look.life or 1) * 0.6, look.life or 1)
emitterSetSpread(instance.emitter, look.spread or 30)
if look.size then
emitterSetSize(instance.emitter, look.size, look.size * 0.2)
end
if look.speed then
emitterSetSpeed(instance.emitter, look.speed * 0.6, look.speed)
end
if look.gravity then
emitterSetGravity(instance.emitter, 0, look.gravity, 0)
end
emitterStart(instance.emitter)
end,
size = function(instance) return instance.look.size or 16, instance.look.size or 16, instance.look.size or 16 end,
draw = function(instance)
if not AUTHOR_3D then
local x, y = authorPosition(instance)
emitterSetPosition(instance.emitter, x, y)
end
end
}
AUTHOR.looks.text = {
help = "A line of text, its top left at the instance -- or centred on it, standing on it (feet), or hanging from it (top). The var text is what it says. A font file and size of its own, or the game's.",
params = { text = "string", r = "number", g = "number", b = "number", font = "file", size = "number", anchor = "string" },
choices = { anchor = { "centre", "feet", "top" } },
load = function(instance)
instance.vars.text = instance.vars.text or instance.look.text or ""
if instance.look.font then
instance.font = fontFor(instance.look.font, instance.look.size or AUTHOR_FONT_POINTS)
end
end,
size = function(instance)
-- Measured once per change: rendering text to measure it is not free.
if instance.measured ~= instance.vars.text then
local sprite
if instance.font then
fontSelect(instance.font)
end
sprite = fontToSprite(instance.vars.text ~= "" and instance.vars.text or " ")
instance.textW = spriteGetWidth(sprite)
instance.textH = spriteGetHeight(sprite)
instance.measured = instance.vars.text
spriteUnload(sprite)
if instance.font then
fontSelect(AUTHOR_FONT)
end
end
return instance.textW, instance.textH
end,
draw = function(instance)
local x, y = authorPosition(instance)
local w, h = authorSize(instance)
local anchor = instance.look.anchor
if anchor == "centre" then
x, y = x - w / 2, y - h / 2
elseif anchor == "feet" then
x, y = x - w / 2, y - h
elseif anchor == "top" then
x = x - w / 2
end
colorForeground(instance.look.r or 255, instance.look.g or 255, instance.look.b or 255, 255)
if instance.vars.text ~= "" then
if instance.font then
fontSelect(instance.font)
end
fontPrint(x, y, instance.vars.text)
if instance.font then
fontSelect(AUTHOR_FONT)
end
end
end
}
AUTHOR.looks.none = {
help = "Nothing drawn: a trigger, a spawner, a hitbox over video, a camera.",
params = { w = "number", h = "number", d = "number" },
size = function(instance) return instance.look.w or 0, instance.look.h or 0, instance.look.d or 0 end,
draw = function() end
}
local function modelFor(file)
if models[file] == nil then
models[file] = modelLoad(authorFile(file))
end
return models[file]
end
-- Every node under a model instance is the instance's, so a ray or a contact that reports a mesh
-- node or a bone finds its way back without asking the engine for parents (which would end the
-- game on a node a rule had already deleted).
local function claimNodes(instance, node)
AUTHOR_BY_NODE[node] = instance
instance.nodes[#instance.nodes + 1] = node
for _, child in ipairs(nodeGetChildren(node) or {}) do
claimNodes(instance, child)
end
end
AUTHOR.looks.model = {
help = "A glTF model under the instance, scaled and turned, playing a clip. w, h, d say how big it counts as.",
params = { file = "file", scale = "number", clip = "string", rx = "number", ry = "number", rz = "number", w = "number", h = "number", d = "number" },
load = function(instance)
local look = instance.look
instance.model = modelInstance(modelFor(look.file))
claimNodes(instance, instance.model)
nodeSetParent(instance.model, instance.node)
nodeSetScale(instance.model, look.scale or 1)
nodeSetRotation(instance.model, look.rx or 0, look.ry or 0, look.rz or 0)
if look.clip then
animationPlay(instance.model, look.clip, true)
end
end,
size = function(instance) return instance.look.w or 1, instance.look.h or 1, instance.look.d or 1 end,
draw = function() end
}
-- A primitive mesh, shared by every instance that asks for the same one.
local function meshFor(look)
local key = table.concat({ look.shape or "box", look.w or 1, look.h or 1, look.d or 1 }, "/")
if meshes[key] == nil then
if look.shape == "sphere" then
meshes[key] = meshSphere((look.w or 1) / 2, 24)
elseif look.shape == "cylinder" then
meshes[key] = meshCylinder((look.w or 1) / 2, look.h or 1, 24)
elseif look.shape == "plane" then
meshes[key] = meshPlane(look.w or 1, look.d or 1)
else
meshes[key] = meshBox(look.w or 1, look.h or 1, look.d or 1)
end
end
return meshes[key]
end
AUTHOR.looks.mesh = {
help = "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.",
params = { shape = "string", w = "number", h = "number", d = "number", r = "number", g = "number", b = "number", texture = "file", unlit = "boolean", metallic = "number", roughness = "number", occluder = "boolean" },
choices = { shape = { "box", "sphere", "cylinder", "plane" } },
load = function(instance)
local look = instance.look
local material = materialNew()
materialSetColor(material, look.r or 200, look.g or 200, look.b or 200)
-- A KTX2 texture goes straight on; any other picture is loaded as a sprite first, which is
-- what materialSetTexture takes for those.
if look.texture and look.texture ~= "" then
if look.texture:lower():match("%.ktx2$") then
materialSetTexture(material, authorFile(look.texture))
else
materialSetTexture(material, spriteFor(look.texture))
end
end
if look.unlit then
materialSetUnlit(material, true)
end
if look.metallic then
materialSetMetallic(material, look.metallic)
end
if look.roughness then
materialSetRoughness(material, look.roughness)
end
-- The editor's preview keeps an occluder visible, so it can be placed; the game hides it.
if look.occluder and not instance.preview then
materialSetOccluder(material, true)
end
instance.material = material
nodeSetMesh(instance.node, meshFor(look), material)
end,
size = function(instance)
local look = instance.look
if look.shape == "plane" then
return look.w or 1, 0.01, look.d or 1
end
return look.w or 1, (look.shape == "sphere") and (look.w or 1) or (look.h or 1), (look.shape == "sphere") and (look.w or 1) or (look.d or 1)
end,
draw = function() end
}
AUTHOR.looks.light = {
help = "A point or spot light, or a sun, on the instance.",
params = { type = "string", r = "number", g = "number", b = "number", intensity = "number", range = "number", shadow = "boolean" },
choices = { type = { "point", "spot", "sun" } },
load = function(instance)
local look = instance.look
local kind = (look.type == "spot") and LIGHT_SPOT or ((look.type == "sun") and LIGHT_DIRECTIONAL or LIGHT_POINT)
local light = lightNew(kind, instance.node)
lightSetColor(light, look.r or 255, look.g or 255, look.b or 255)
lightSetIntensity(light, look.intensity or 1)
if look.range then
lightSetRange(light, look.range)
end
if look.shadow then
lightSetShadow(light, true)
end
instance.light = light
end,
size = function() return 0.5, 0.5, 0.5 end,
draw = function() end
}
AUTHOR.looks.billboard = {
help = "A sprite in the scene, always facing the camera: a tree, a puff, a health bar.",
params = { file = "file", height = "number", w = "number", h = "number", d = "number" },
load = function(instance)
instance.sprite = spriteFor(instance.look.file)
nodeSetSprite(instance.node, instance.sprite, instance.look.height or 1)
nodeSetBillboard(instance.node, BILLBOARD_Y)
end,
size = function(instance) return instance.look.w or 1, instance.look.h or instance.look.height or 1, instance.look.d or 1 end,
draw = function() end
}
AUTHOR.looks.text3d = {
help = "A line of text standing in the scene. The var text is what it says.",
params = { text = "string", height = "number" },
load = function(instance)
instance.vars.text = instance.vars.text or instance.look.text or ""
instance.shown = instance.vars.text
nodeSetText(instance.node, instance.vars.text, instance.look.height or 0.5)
nodeSetBillboard(instance.node, BILLBOARD_Y)
end,
size = function(instance) return 1, instance.look.height or 0.5, 0.1 end,
draw = function(instance)
if instance.shown ~= instance.vars.text then
instance.shown = instance.vars.text
nodeSetText(instance.node, instance.vars.text, instance.look.height or 0.5)
end
end
}
AUTHOR.looks.grid = {
help = "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.",
params = { file = "file", tile = "number", columns = "number", map = "string" },
load = function(instance)
local look = instance.look
instance.sprite = spriteFor(look.file)
instance.rows = {}
for row in string.gmatch(tostring(look.map or ""), "[^;]+") do
local cells = {}
for number in string.gmatch(row, "%d+") do
cells[#cells + 1] = tonumber(number)
end
instance.rows[#instance.rows + 1] = cells
end
end,
size = function(instance)
local tile = instance.look.tile or 32
local width = 0
for _, row in ipairs(instance.rows or {}) do
width = math.max(width, #row)
end
return width * tile, #(instance.rows or {}) * tile
end,
draw = function(instance)
local look = instance.look
local tile = look.tile or 32
local columns = look.columns or 1
local x, y = authorPosition(instance)
for r, row in ipairs(instance.rows) do
for c, cell in ipairs(row) do
if cell > 0 then
local sx = ((cell - 1) % columns) * tile
local sy = math.floor((cell - 1) / columns) * tile
spriteDrawGrid(instance.sprite, x + (c - 1) * tile, y + (r - 1) * tile, sx, sy, tile, tile)
end
end
end
end
}
-- ===== Behaviours =============================================================================
--
-- Each is a bundle over engine calls that already exist and are tested. A platformer is a
-- checkbox over the character controller, not a reimplementation of one. attach runs once when
-- the instance is made, step every frame with the seconds since the last one, and on(event) when
-- something happens to the instance.
AUTHOR.behaviours.solid = {
help = "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.",
params = { moving = "boolean" },
attach = function(instance, params)
local w, h, d = authorUnscaledSize(instance)
local kind = params.moving and BODY_KINEMATIC or BODY_STATIC
if AUTHOR_3D then
bodyNew(instance.node, kind, SHAPE_BOX, w, h, d)
else
bodyNew(instance.node, kind, SHAPE_BOX, w, h, w)
end
instance.hasBody = true
end
}
AUTHOR.behaviours.body = {
help = "A thing physics moves: a crate, a ball, debris. Mass, bounce, friction, and buoyancy as Jolt takes them.",
params = { shape = "string", mass = "number", bounce = "number", friction = "number", buoyancy = "number" },
choices = { shape = { "box", "sphere" } },
attach = function(instance, params)
local w, h, d = authorUnscaledSize(instance)
if params.shape == "sphere" then
bodyNew(instance.node, BODY_DYNAMIC, SHAPE_SPHERE, w / 2)
elseif AUTHOR_3D then
bodyNew(instance.node, BODY_DYNAMIC, SHAPE_BOX, w, h, d)
else
bodyNew(instance.node, BODY_DYNAMIC, SHAPE_BOX, w, h, w)
end
if params.mass then
bodySetMass(instance.node, params.mass)
end
if params.bounce then
bodySetBounce(instance.node, params.bounce)
end
if params.friction then
bodySetFriction(instance.node, params.friction)
end
if params.buoyancy then
bodySetBuoyancy(instance.node, params.buoyancy)
end
instance.hasBody = true
end
}
AUTHOR.behaviours.trigger = {
help = "A volume that reports what enters and leaves it, as the enter and leave events. A checkpoint, a doorway, a prize.",
params = {},
attach = function(instance)
local w, h, d = authorUnscaledSize(instance)
if AUTHOR_3D then
bodyNew(instance.node, BODY_KINEMATIC, SHAPE_BOX, w, h, d)
else
bodyNew(instance.node, BODY_KINEMATIC, SHAPE_BOX, w, h, w)
end
bodySetTrigger(instance.node, true)
instance.hasBody = true
end
}
AUTHOR.behaviours.character = {
help = "Walks, falls, and jumps in the scene: the engine's character controller. Moves by the vars dx and dy, relative to the camera.",
params = { speed = "number", jump = "number", radius = "number", height = "number" },
attach = function(instance, params)
local w, h = authorSize(instance)
instance.speed = params.speed or 4
instance.jump = params.jump or 6
instance.player = true
playerNew(instance.node, SHAPE_CAPSULE, params.radius or (w * PLAYER_RADIUS), params.height or h)
end,
step = function(instance)
local dx = instance.vars.dx or 0
local dy = instance.vars.dy or 0
local fx, fz, rx, rz = authorCameraAxes()
local vx = (rx * dx - fx * dy) * instance.speed
local vz = (rz * dx - fz * dy) * instance.speed
playerMove(instance.node, vx, vz)
if ((dx ~= 0) or (dy ~= 0)) and instance.model then
local x, y, z = authorPosition(instance)
nodeLookAt(instance.model, x - vx, y, z - vz)
end
end
}
AUTHOR.behaviours.seek = {
help = "Moves straight toward a target -- an entity's id, or \"camera\" -- and raises arrived within stopAt of it.",
params = { target = "string", speed = "number", stopAt = "number", fly = "boolean" },
attach = function(instance, params)
instance.seek = params
end,
step = function(instance, dt)
local p = instance.seek
local tx, ty, tz = authorTargetPosition(p.target)
if tx == nil then
return
end
local x, y, z = authorPosition(instance)
local dx, dy, dz = tx - x, ty - y, tz - z
if not p.fly then
dy = 0
end
local far = math.sqrt(dx * dx + dy * dy + dz * dz)
if far <= (p.stopAt or 1) then
if not instance.arrived then
instance.arrived = true
authorEvent("arrived", { self = instance })
end
return
end
instance.arrived = false
local step = math.min((p.speed or 1) * dt, far)
nodeSetPosition(instance.node, x + dx / far * step, y + dy / far * step, z + dz / far * step)
if instance.model then
nodeLookAt(instance.model, x - dx, y, z - dz)
end
end
}
AUTHOR.behaviours.animator = {
help = "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.",
params = { idle = "string", walk = "string", attack = "string", hit = "string", dead = "string", fade = "number", loop = "string" },
attach = function(instance, params)
instance.animator = params
instance.playing = nil
end,
step = function(instance)
local p = instance.animator
local state = instance.vars.state
local clip = p[state]
if instance.model == nil then
return
end
if (clip ~= nil) and (clip ~= instance.playing) then
local loops = (state == "idle") or (state == "walk") or (state == "run") or ((p.loop or ""):find(state, 1, true) ~= nil)
animationPlay(instance.model, clip, loops, 1, p.fade or 0.15)
instance.playing = clip
instance.watch = not loops
elseif instance.watch and not animationIsPlaying(instance.model) then
instance.watch = false
authorEvent("animationDone", { self = instance, state = state })
end
end
}
AUTHOR.behaviours.camera = {
help = "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.",
params = { mode = "string", target = "string", x = "number", y = "number", z = "number", lookX = "number", lookY = "number", lookZ = "number", track = "track", lag = "number", eye = "number" },
choices = { mode = { "fixed", "follow", "first", "orbit", "rail" } },
attach = function(instance, params)
instance.camera = params
instance.yaw = 0
instance.pitch = 0
cameraNode = instance.node
cameraSet(instance.node)
if params.mode == "rail" then
instance.rail = { t = 0, waiting = nil, passed = {} }
end
if (params.mode == "fixed") or (params.mode == nil) then
nodeLookAt(instance.node, params.lookX or 0, params.lookY or 0, params.lookZ or 0)
end
end,
step = function(instance, dt)
local p = instance.camera
if p.mode == "follow" then
local tx, ty, tz = authorTargetPosition(p.target)
if tx then
local x, y, z = authorPosition(instance)
local k = (p.lag and p.lag > 0) and math.min(1, dt / p.lag) or 1
nodeSetPosition(instance.node, x + (tx + (p.x or 0) - x) * k, y + (ty + (p.y or 3) - y) * k, z + (tz + (p.z or 6) - z) * k)
nodeLookAt(instance.node, tx, ty + (p.lookY or 0.5), tz)
end
elseif p.mode == "orbit" then
local tx, ty, tz = authorTargetPosition(p.target)
if tx then
instance.yaw = instance.yaw - AUTHOR_MOUSE.dx * LOOK_SPEED
instance.pitch = math.max(-PITCH_LIMIT, math.min(PITCH_LIMIT, instance.pitch + AUTHOR_MOUSE.dy * LOOK_SPEED))
local far = p.z or 6
local yaw = math.rad(instance.yaw)
local pitch = math.rad(instance.pitch)
nodeSetPosition(instance.node, tx + math.sin(yaw) * math.cos(pitch) * far, ty + (p.y or 1) + math.sin(pitch) * far, tz + math.cos(yaw) * math.cos(pitch) * far)
nodeLookAt(instance.node, tx, ty + (p.lookY or 0.5), tz)
end
elseif p.mode == "first" then
local target = authorTargetInstance(p.target)
if target then
local tx, ty, tz = authorPosition(target)
instance.yaw = instance.yaw - AUTHOR_MOUSE.dx * LOOK_SPEED
instance.pitch = math.max(-PITCH_LIMIT, math.min(PITCH_LIMIT, instance.pitch - AUTHOR_MOUSE.dy * LOOK_SPEED))
nodeSetPosition(instance.node, tx, ty + (p.eye or 0.7), tz)
nodeSetRotation(instance.node, instance.pitch, instance.yaw, 0)
end
elseif p.mode == "rail" then
authorRailStep(instance, dt)
end
end
}
AUTHOR.behaviours.platformer = {
help = "Runs, falls, and jumps: the engine's character controller in 2D. Rules say which way with run and jump. Its point is its feet, so its look stands on it. Sets the var facing, and the var state to idle, walk, jump, or fall unless a rule set another.",
params = { speed = "number", jump = "number" },
attach = function(instance, params)
local w, h = authorSize(instance)
instance.speed = params.speed or 200
instance.jump = params.jump or 500
instance.player = true
-- A player stands on its node, so the node is the feet; the look is drawn to match.
instance.look.anchor = "feet"
playerNew(instance.node, SHAPE_CAPSULE, w * PLAYER_RADIUS, h)
end,
step = function(instance)
local drive = instance.vars.drive
local state = instance.vars.state
-- The rules say which way; this clears it each frame so releasing a key stops the run.
playerMove(instance.node, drive * instance.speed)
instance.vars.drive = 0
-- Which way it faces and what it is doing, for a sprite look and a frames behaviour: a
-- state a rule set (hit, dead) is left alone until the rule sets another.
if drive ~= 0 then
instance.vars.facing = (drive < 0) and "left" or "right"
end
if (state == nil) or PLATFORMER_STATES[state] then
if not playerIsOnGround(instance.node) then
local _, vy = playerGetVelocity(instance.node)
instance.vars.state = (vy < 0) and "jump" or "fall"
else
instance.vars.state = (drive ~= 0) and "walk" or "idle"
end
end
end
}
AUTHOR.behaviours.drift = {
help = "Moves steadily, for a cloud, a platform, or a target.",
params = { vx = "number", vy = "number" },
attach = function(instance, params)
instance.vars.vx = instance.vars.vx or params.vx or 0
instance.vars.vy = instance.vars.vy or params.vy or 0
end,
step = function(instance, dt)
local x, y, z = authorPosition(instance)
nodeSetPosition(instance.node, x + instance.vars.vx * dt, y + instance.vars.vy * dt, z)
end
}
AUTHOR.behaviours.keys = {
help = "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.",
params = { player = "number", left = "scancode", right = "scancode", up = "scancode", down = "scancode", fire = "scancode" },
attach = function(instance, params)
instance.keys = {
left = authorKeyValue(params.left),
right = authorKeyValue(params.right),
up = authorKeyValue(params.up),
down = authorKeyValue(params.down),
fire = authorKeyValue(params.fire)
}
instance.vars.dx = 0
instance.vars.dy = 0
end,
step = function(instance)
local k = instance.keys
local dx = 0
local dy = 0
if AUTHOR_CONTROLS then
if k.left and authorKeyHeld(k.left) then dx = dx - 1 end
if k.right and authorKeyHeld(k.right) then dx = dx + 1 end
if k.up and authorKeyHeld(k.up) then dy = dy - 1 end
if k.down and authorKeyHeld(k.down) then dy = dy + 1 end
end
instance.vars.dx = dx
instance.vars.dy = dy
instance.vars.fire = AUTHOR_CONTROLS and (k.fire ~= nil) and authorKeyHeld(k.fire)
end
}
AUTHOR.behaviours.mover = {
help = "Moves by the vars dx and dy at a speed, kept on the screen when clamp is set. Pair it with keys.",
params = { speed = "number", clamp = "boolean" },
attach = function(instance, params)
instance.moveSpeed = params.speed or 200
instance.clamp = params.clamp
end,
step = function(instance, dt)
local x, y, z = authorPosition(instance)
local w, h = authorSize(instance)
x = x + (instance.vars.dx or 0) * instance.moveSpeed * dt
y = y + (instance.vars.dy or 0) * instance.moveSpeed * dt
if (instance.vars.dx or 0) ~= 0 then
instance.vars.facing = (instance.vars.dx < 0) and "left" or "right"
end
if instance.clamp then
x = math.max(w / 2, math.min(overlayGetWidth() - w / 2, x))
y = math.max(h / 2, math.min(overlayGetHeight() - h / 2, y))
end
nodeSetPosition(instance.node, x, y, z)
end
}
AUTHOR.behaviours.shooter = {
help = "Spawns a type from an offset while the var fire is set (or always, with auto), no faster than the rate.",
params = { spawns = "type", rate = "number", offsetX = "number", offsetY = "number", auto = "boolean" },
attach = function(instance, params)
instance.shooter = params
instance.lastShot = -math.huge
end,
step = function(instance)
local p = instance.shooter
if (p.auto or instance.vars.fire) and (authorTime() - instance.lastShot >= (p.rate or 0.2)) then
local x, y = authorPosition(instance)
instance.lastShot = authorTime()
authorSpawn(p.spawns, x + (p.offsetX or 0), y + (p.offsetY or 0), 0, { owner = instance.id })
end
end
}
AUTHOR.behaviours.projectile = {
help = "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.",
params = { vx = "number", vy = "number", vz = "number", life = "number", aim = "boolean" },
attach = function(instance, params)
instance.vars.vx = instance.vars.vx or params.vx or 0
instance.vars.vy = instance.vars.vy or params.vy or (AUTHOR_3D and 0 or -400)
instance.vars.vz = instance.vars.vz or params.vz or 0
instance.dies = authorTime() + (params.life or 5)
if params.aim and not AUTHOR_3D then
instance.vars.angle = math.deg(math.atan(instance.vars.vx, -instance.vars.vy))
end
end,
step = function(instance, dt)
local x, y, z = authorPosition(instance)
x = x + instance.vars.vx * dt
y = y + instance.vars.vy * dt
z = z + instance.vars.vz * dt
nodeSetPosition(instance.node, x, y, z)
if authorTime() > instance.dies then
authorDestroy(instance)
elseif AUTHOR_3D then
if (math.abs(x) > FAR_AWAY) or (math.abs(y) > FAR_AWAY) or (math.abs(z) > FAR_AWAY) then
authorDestroy(instance)
end
elseif (x < -SPAWN_MARGIN) or (y < -SPAWN_MARGIN) or (x > overlayGetWidth() + SPAWN_MARGIN) or (y > overlayGetHeight() + SPAWN_MARGIN) then
authorDestroy(instance)
end
end
}
AUTHOR.behaviours.health = {
help = "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.",
params = { max = "number", keep = "boolean", ragdoll = "boolean", linger = "number" },
attach = function(instance, params)
instance.vars.health = instance.vars.health or params.max or 1
instance.keepDead = params.keep or params.ragdoll
instance.linger = params.linger or 4
if params.ragdoll and instance.model then
ragdollNew(instance.model)
instance.ragdoll = true
end
end,
on = function(instance, event, data)
if (event == "death") and instance.ragdoll then
ragdollActivate(instance.model)
authorTimerStart(instance, "gone", instance.linger)
instance.isTarget = false
elseif (event == "timer") and (data.name == "gone") then
authorDestroy(instance)
end
end
}
AUTHOR.behaviours.timer = {
help = "Raises the timer event with its name after some seconds, or every so many. start false waits for timerStart.",
params = { name = "string", after = "number", every = "number", start = "boolean" },
attach = function(instance, params)
if params.start ~= false then
authorTimerStart(instance, params.name, params.after or params.every, params.every)
end
end
}
AUTHOR.behaviours.spawner = {
help = "Makes instances of a type every so many seconds at its own position or at one of its points, up to max alive at once and total in all.",
params = { spawns = "type", every = "number", max = "number", total = "number", points = "table", pick = "string" },
choices = { pick = { "turn", "random" } },
attach = function(instance, params)
instance.spawner = params
instance.nextAt = authorTime() + (params.every or 1)
instance.vars.made = 0 -- A var, so a rule can ask how many it has made.
instance.point = 0
end,
step = function(instance)
local p = instance.spawner
if (authorTime() < instance.nextAt) or (p.total and instance.vars.made >= p.total) then
return
end
if p.max and (authorCount(p.spawns) >= p.max) then
return
end
local x, y, z = authorPosition(instance)
if p.points and #p.points > 0 then
local at
if p.pick == "random" then
at = math.random(#p.points)
else
instance.point = (instance.point % #p.points) + 1
at = instance.point
end
x = p.points[at].x or p.points[at][1]
y = p.points[at].y or p.points[at][2]
z = p.points[at].z or p.points[at][3] or z
end
instance.nextAt = authorTime() + (p.every or 1)
instance.vars.made = instance.vars.made + 1
authorSpawn(p.spawns, x, y, z, { spawner = instance.id })
end
}
AUTHOR.behaviours.sound = {
help = "A clip per event: spawn, hit, miss, death, fire (a gun's trigger), or any event the instance sees. Values are file names. volume is the clips' own, 0 to 100, under the game's.",
params = { spawn = "file", hit = "file", miss = "file", death = "file", fire = "file", pressed = "file", volume = "number" },
attach = function(instance, params)
instance.sounds = params
if params.spawn then
authorPlaySound(params.spawn, params.volume, instance)
end
end,
on = function(instance, event)
local file = instance.sounds[event]
if file and (event ~= "spawn") then
authorPlaySound(file, instance.sounds.volume, instance)
end
end
}
AUTHOR.behaviours.gun = {
help = "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.",
params = { player = "number", trigger = "switch", ammo = "number", reload = "string", aim = "string" },
choices = { reload = { "offscreen" }, aim = { "centre" } },
attach = function(instance, params)
instance.gun = params
instance.trigger = authorSwitchValue(params.trigger)
instance.vars.ammo = instance.vars.ammo or params.ammo or 6
instance.vars.player = params.player or 1
end
}
AUTHOR.behaviours.pointer = {
help = "Follows a player's pointer: a crosshair, a hand. Hidden when the engine says a real gun wants no crosshair drawn.",
params = { player = "number" },
attach = function(instance, params)
instance.pointerOf = params.player or 1
end,
step = function(instance)
local x, y = authorPointer(instance.pointerOf)
authorMoveTo(instance, x, y)
instance.visible = singeWantsCrosshairs()
end
}
AUTHOR.behaviours.target = {
help = "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.",
params = { zones = "table" },
attach = function(instance, params)
instance.isTarget = true
if AUTHOR_3D and not instance.hasBody then
-- The body sits on a child node raised by half the height, since a model stands on its
-- origin and a box centred on the feet would be half underground.
local w, h, d = authorSize(instance)
local box = nodeNew()
nodeSetParent(box, instance.node)
nodeSetPosition(box, 0, h / 2, 0)
nodeSetName(box, "body")
bodyNew(box, BODY_KINEMATIC, SHAPE_BOX, w, h, d)
bodySetTrigger(box, true)
AUTHOR_BY_NODE[box] = instance
instance.nodes[#instance.nodes + 1] = box
end
for _, zone in ipairs(params.zones or {}) do
local bone = instance.model and nodeFind(zone.bone, instance.model) or nil
if bone then
local box = nodeNew()
nodeSetParent(box, bone)
nodeSetName(box, zone.name)
AUTHOR_BY_NODE[box] = instance
instance.nodes[#instance.nodes + 1] = box
instance.zones = instance.zones or {}
instance.zones[#instance.zones + 1] = { node = box, name = zone.name, reach = math.max(zone.w or 0.3, zone.h or 0.3, zone.d or 0.3) / 2 }
else
debugPrint("Author: " .. instance.id .. " has no bone called '" .. tostring(zone.bone) .. "' for zone " .. tostring(zone.name))
end
end
end
}
AUTHOR.behaviours.walker = {
help = "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.",
params = { speed = "number", radius = "number", follow = "string", every = "number", stopAt = "number" },
attach = function(instance, params)
instance.walker = params
instance.repath = 0
end,
step = function(instance)
local nav = authorRoomNav()
if nav == nil then
return
end
if instance.agent == nil then
local x, y, z = authorPosition(instance)
if AUTHOR_3D then
instance.navNode = instance.node
else
instance.navNode = nodeNew()
nodeSetPosition(instance.navNode, x * NAV_SCALE, 0, y * NAV_SCALE)
end
instance.agent = navAgentNew(nav.nav, instance.navNode, instance.walker.radius or nav.radius, nav.height, (instance.walker.speed or 120) * nav.scale)
AUTHOR_AGENTS[instance.agent] = instance
end
if not AUTHOR_3D then
local nx, _, nz = nodeGetPosition(instance.navNode)
nodeSetPosition(instance.node, nx / NAV_SCALE, nz / NAV_SCALE, 0)
end
-- Following: the target's position is asked for again every so often, and the walk ends
-- within stopAt of it, where a seek would.
if instance.walker.follow and (authorTime() >= instance.repath) then
local tx, ty, tz = authorTargetPosition(instance.walker.follow)
instance.repath = authorTime() + (instance.walker.every or 0.5)
if tx then
local x, y, z = authorPosition(instance)
local far = AUTHOR_3D and math.sqrt((tx - x) ^ 2 + (tz - z) ^ 2) or math.sqrt((tx - x) ^ 2 + (ty - y) ^ 2)
if far > (instance.walker.stopAt or 1.5) then
authorWalkStart(instance, tx, ty, tz)
elseif instance.walking then
navAgentStop(instance.agent)
instance.walking = false
instance.vars.state = "idle"
authorEvent("arrived", { self = instance })
end
end
end
if instance.walking then
if navAgentIsArrived(instance.agent) then
instance.walking = false
instance.vars.state = "idle"
authorEvent("arrived", { self = instance })
else
local vx, vy, vz = navAgentGetVelocity(instance.agent)
if (vx ~= 0) or (vz ~= 0) then
instance.vars.state = "walk"
if instance.model then
local x, y, z = authorPosition(instance)
nodeLookAt(instance.model, x - vx, y, z - vz)
end
instance.vars.facing = (vx < 0) and "left" or "right"
end
end
end
end
}
AUTHOR.behaviours.patrol = {
help = "Walks a list of points in turn -- a creep's lane, a guard's round -- looping or stopping, and raises patrolEnd at the last.",
params = { points = "table", loop = "boolean" },
attach = function(instance, params)
instance.patrol = params
instance.leg = 0
end,
step = function(instance)
local p = instance.patrol
if (instance.agent == nil) or instance.walking or (p.points == nil) or (#p.points == 0) then
return
end
if instance.leg >= #p.points then
if p.loop then
instance.leg = 0
elseif not instance.patrolDone then
instance.patrolDone = true
authorEvent("patrolEnd", { self = instance })
return
else
return
end
end
instance.leg = instance.leg + 1
local point = p.points[instance.leg]
authorWalkStart(instance, point.x or point[1], point.y or point[2] or 0, point.z or point[3] or 0)
end
}
AUTHOR.behaviours.vehicle = {
help = "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.",
params = { type = "string", wheelX = "number", wheelZ = "number", radius = "number", width = "number", suspension = "number", torque = "number", mass = "number", thrust = "number" },
choices = { type = { "car", "motorcycle", "tank", "boat" } },
attach = function(instance, params)
local w, h, d = authorSize(instance)
local kind = ({ car = VEHICLE_CAR, motorcycle = VEHICLE_MOTORCYCLE, tank = VEHICLE_TANK, boat = VEHICLE_BOAT })[params.type or "car"] or VEHICLE_CAR
if not instance.hasBody then
bodyNew(instance.node, BODY_DYNAMIC, SHAPE_BOX, w, h, d)
instance.hasBody = true
end
bodySetMass(instance.node, params.mass or 800)
vehicleNew(instance.node, kind)
if kind ~= VEHICLE_BOAT then
local wx = params.wheelX or (w / 2)
local wz = params.wheelZ or (d / 2 - 0.2)
local radius = params.radius or 0.35
local wheels = (kind == VEHICLE_MOTORCYCLE) and { { 0, -wz }, { 0, wz } } or { { -wx, -wz }, { wx, -wz }, { -wx, wz }, { wx, wz } }
for _, at in ipairs(wheels) do
local wheel = nodeNew()
nodeSetParent(wheel, instance.node)
nodeSetPosition(wheel, at[1], -h / 2, at[2])
instance.nodes[#instance.nodes + 1] = wheel
vehicleAddWheel(instance.node, wheel, radius, params.width or 0.2, params.suspension or 0.3)
end
if params.torque then
vehicleSetEngine(instance.node, params.torque, 6000)
end
elseif params.thrust then
vehicleSetThrust(instance.node, params.thrust, 0, -0.2, d / 2)
end
instance.vehicle = true
end,
step = function(instance)
local dx = instance.vars.dx or 0
local dy = instance.vars.dy or 0
vehicleDrive(instance.node, -dy, dx, instance.vars.brake or 0)
instance.vars.speed = vehicleGetSpeed(instance.node)
end
}
AUTHOR.behaviours.racer = {
help = "Drives a vehicle round a track of points on its own: steering toward the next, throttling by how straight the road is.",
params = { track = "track", throttle = "number", reach = "number" },
attach = function(instance, params)
instance.racer = params
instance.leg = 1
end,
step = function(instance)
local track = authorTrackNamed(instance.racer.track or "")
local points = track and track.points or {}
if (#points == 0) or not instance.vehicle then
return
end
local point = points[instance.leg]
local x, y, z = authorPosition(instance)
local dx, dz = (point.x or 0) - x, (point.z or 0) - z
local far = math.sqrt(dx * dx + dz * dz)
if far < (instance.racer.reach or 3) then
instance.leg = (instance.leg % #points) + 1
return
end
-- The chassis' heading from its yaw; the steering is the signed angle to the point.
local _, yaw, _ = nodeGetRotation(instance.node)
local heading = math.rad(yaw)
local fx, fz = -math.sin(heading), -math.cos(heading)
local cross = fx * dz - fz * dx
local dot = fx * dx + fz * dz
local angle = math.atan(cross, dot)
instance.vars.dx = math.max(-1, math.min(1, -angle * 2))
instance.vars.dy = -(instance.racer.throttle or 0.6) * ((dot > 0) and 1 or 0.3)
end
}
AUTHOR.behaviours.thrust = {
help = "A body pushed along its nose by the var dy and turned by dx: a hovercraft, a ship, a plane without lift.",
params = { force = "number", turn = "number" },
attach = function(instance, params)
instance.thrust = params
end,
step = function(instance)
if not instance.hasBody then
return
end
local _, yaw, _ = nodeGetRotation(instance.node)
local heading = math.rad(yaw)
local push = -(instance.vars.dy or 0) * (instance.thrust.force or 500)
bodyApplyForce(instance.node, -math.sin(heading) * push, 0, -math.cos(heading) * push)
bodySetAngularVelocity(instance.node, 0, -(instance.vars.dx or 0) * (instance.thrust.turn or 1.5), 0)
end
}
AUTHOR.behaviours.joint = {
help = "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.",
params = { type = "string", other = "string", ax = "number", ay = "number", az = "number", dx = "number", dy = "number", dz = "number" },
choices = { type = { "hinge", "ball", "slider" } },
attach = function(instance, params)
instance.jointWanted = params
end,
step = function(instance)
-- Made on the first frame, once every body of the room exists.
local p = instance.jointWanted
if p == nil then
return
end
instance.jointWanted = nil
local other = p.other and AUTHOR_BY_ID[p.other] or nil
local node = other and other.node or -1
local x, y, z = authorPosition(instance)
if p.type == "ball" then
instance.joint = jointBall(instance.node, node, p.ax or x, p.ay or y, p.az or z)
elseif p.type == "slider" then
instance.joint = jointSlider(instance.node, node, p.ax or x, p.ay or y, p.az or z, p.dx or 1, p.dy or 0, p.dz or 0)
else
instance.joint = jointHinge(instance.node, node, p.ax or x, p.ay or y, p.az or z, p.dx or 0, p.dy or 1, p.dz or 0)
end
end
}
AUTHOR.behaviours.turret = {
help = "Fires at the nearest instance of a type within range, no faster than the rate: spawns a projectile aimed at it, or, with no projectile, does the damage itself.",
params = { targets = "type", range = "number", rate = "number", spawns = "type", damage = "number", speed = "number" },
attach = function(instance, params)
instance.turret = params
instance.lastShot = -math.huge
end,
step = function(instance)
local p = instance.turret
if authorTime() - instance.lastShot < (p.rate or 1) then
return
end
local nearest, best = nil, p.range or 6
for _, target in ipairs(authorEach(p.targets or "")) do
local far = authorDistance3D(instance, target)
if far < best then
nearest, best = target, far
end
end
if nearest == nil then
return
end
instance.lastShot = authorTime()
if p.spawns then
local x, y, z = authorPosition(instance)
local tx, ty, tz = authorPosition(nearest)
local speed = p.speed or 12
local dx, dy, dz = tx - x, ty - y, tz - z
local far = math.max(0.001, math.sqrt(dx * dx + dy * dy + dz * dz))
authorSpawn(p.spawns, x, y, z, { vars = { vx = dx / far * speed, vy = dy / far * speed, vz = dz / far * speed }, owner = instance.id })
else
authorDamage(nearest, p.damage or 1)
end
authorBehaviourEvent(instance, "fire")
end
}
AUTHOR.behaviours.branching = {
help = "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. A branch with mash wants the move that many times inside the window; one with hold wants it held that many frames. Raises branchOpen, branchTaken, and branchMissed.",
params = { track = "track" },
attach = function(instance, params)
instance.branching = params
instance.branchAt = nil
end,
step = function(instance)
local track = authorTrackNamed(instance.branching.track or "")
local frame = discGetFrame()
if track == nil then
return
end
for _, branch in ipairs(track.branches or {}) do
if (frame >= branch.from) and (frame < branch.to) then
if instance.branchAt ~= branch then
instance.branchAt = branch
instance.decided = false
instance.mashed = 0
instance.held = 0
authorEvent("branchOpen", { self = instance, move = branch.move, mash = branch.mash, hold = branch.hold })
end
if not instance.decided then
local key = authorKeyValue(branch.move)
local switch = authorSwitchValue(branch.switch)
local made = (key and authorKeyPressed(key)) or (switch and pressedNow[switch])
local down = (key and AUTHOR_KEYS[key]) or (switch and AUTHOR_SWITCHES[switch])
if branch.mash then
-- Mash: the move so many times before the window closes.
if made then
instance.mashed = instance.mashed + 1
end
made = (instance.mashed >= branch.mash)
elseif branch.hold then
-- Hold: the move kept down for so many frames of the window.
instance.held = down and (instance.held + 1) or 0
made = (instance.held >= branch.hold)
end
if made then
instance.decided = true
if branch.success then
discSkipToFrame(branch.success)
end
authorEvent("branchTaken", { self = instance, move = branch.move })
end
end
elseif (instance.branchAt == branch) and (frame >= branch.to) and not instance.decided then
instance.decided = true
if branch.fail then
discSkipToFrame(branch.fail)
end
authorEvent("branchMissed", { self = instance, move = branch.move })
end
end
end
}
AUTHOR.behaviours.spin = {
help = "Turns steadily: the var angle in 2D (a sprite turns with it), the node about Y in 3D, in degrees a second.",
params = { rate = "number" },
attach = function(instance, params)
instance.spinRate = params.rate or 90
instance.vars.angle = instance.vars.angle or 0
end,
step = function(instance, dt)
if AUTHOR_3D then
local rx, ry, rz = nodeGetRotation(instance.node)
nodeSetRotation(instance.node, rx, (ry + instance.spinRate * dt) % 360, rz)
else
instance.vars.angle = (instance.vars.angle + instance.spinRate * dt) % 360
end
end
}
AUTHOR.behaviours.frames = {
help = "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.",
params = { fps = "number", states = "string" },
attach = function(instance, params)
instance.frameRanges = {}
for state, from, to in string.gmatch(tostring(params.states or ""), "(%w+)%s*=%s*(%d+)%s*-%s*(%d+)") do
instance.frameRanges[state] = { tonumber(from), tonumber(to) }
end
instance.fps = params.fps or 8
instance.frameClock = 0
instance.vars.frame = instance.vars.frame or 1
end,
step = function(instance, dt)
local state = instance.vars.state or "idle"
local range = instance.frameRanges[state]
local facing = instance.vars.facing
local faced = facing and instance.frameRanges[state .. facing:sub(1, 1):upper() .. facing:sub(2)]
-- Frames drawn the way the instance faces are used over the plain ones, and the look is
-- told, so it does not mirror them as well.
instance.drawnFacing = faced and facing or nil
range = faced or range or instance.frameRanges.idle
if range == nil then
return
end
instance.frameClock = instance.frameClock + dt * instance.fps
local count = range[2] - range[1] + 1
local frame = range[1] + (math.floor(instance.frameClock) % count)
instance.vars.frame = frame
end
}
AUTHOR.behaviours.water = {
help = "Fills a mesh look with water: its top is the surface, and bodies inside float or sink by their buoyancy.",
params = { density = "number", drag = "number" },
attach = function(instance, params)
local w, h, d = authorSize(instance)
bodyNew(instance.node, BODY_STATIC, SHAPE_BOX, w, h, d)
bodySetWater(instance.node, params.density or 1, params.drag or 0.5)
instance.hasBody = true
end
}
AUTHOR.behaviours.soft = {
help = "A mesh look made soft: a cloth, or a body inflated to a pressure.",
params = { type = "string", pressure = "number", stiffness = "number", mass = "number" },
choices = { type = { "cloth", "body" } },
attach = function(instance, params)
softNew(instance.node, (params.type == "cloth") and SOFT_CLOTH or SOFT_BODY)
if params.pressure then
softSetPressure(instance.node, params.pressure)
end
if params.stiffness then
softSetStiffness(instance.node, params.stiffness)
end
if params.mass then
softSetMass(instance.node, params.mass)
end
instance.soft = true
end
}
AUTHOR.behaviours.hotspot = {
help = "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.",
params = { name = "string", polygon = "table", walkX = "number", walkY = "number", walkZ = "number" },
attach = function(instance, params)
instance.hotspot = params
instance.polygon = authorNumbers(params.polygon)
if AUTHOR_3D and not instance.hasBody then
local w, h, d = authorSize(instance)
local box = nodeNew()
nodeSetParent(box, instance.node)
nodeSetPosition(box, 0, h / 2, 0)
bodyNew(box, BODY_KINEMATIC, SHAPE_BOX, w, h, d)
bodySetTrigger(box, true)
AUTHOR_BY_NODE[box] = instance
instance.nodes[#instance.nodes + 1] = box
end
end
}
AUTHOR.behaviours.hitbox = {
help = "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. A track with step = true keeps a box only on the frames it keys.",
params = { track = "track" },
attach = function(instance, params)
instance.hitTrack = params.track
instance.isTarget = true
end
}
-- ===== Events =================================================================================
--
-- What a rule can be triggered by. filter names the parameters a rule may give to narrow it;
-- the runtime matches them against the event's context.
AUTHOR.events.frame = { help = "Every frame.", filter = {} }
AUTHOR.events.roomStart = { help = "A room has been entered.", filter = { room = "room" } }
AUTHOR.events.roomEnd = { help = "A room is about to be left.", filter = { room = "room" } }
AUTHOR.events.pressed = { help = "A key or a switch went down.", filter = { key = "scancode", switch = "switch", player = "number" } }
AUTHOR.events.released = { help = "A key or a switch came up.", filter = { key = "scancode", switch = "switch", player = "number" } }
AUTHOR.events.collision = { help = "An instance of type a began touching one of type b (self is a, other is b).", filter = { a = "type", b = "type" } }
AUTHOR.events.hit = { help = "A gun shot landed on self; other is the gun.", filter = { type = "type", player = "number" } }
AUTHOR.events.miss = { help = "A gun shot hit nothing; self is the gun. event.offscreen says whether it left the screen.", filter = { player = "number" } }
AUTHOR.events.death = { help = "self's health reached zero.", filter = { type = "type" } }
AUTHOR.events.spawn = { help = "self has just been made.", filter = { type = "type" } }
AUTHOR.events.timer = { help = "A timer on self went off; event.name says which.", filter = { name = "string", type = "type" } }
AUTHOR.events.frameReached = { help = "The disc passed a frame.", filter = { frame = "number" } }
AUTHOR.events.gameOver = { help = "The game ended.", filter = {} }
AUTHOR.events.enter = { help = "An instance of type b entered a trigger of type a (self is the trigger, other what entered).", filter = { a = "type", b = "type" } }
AUTHOR.events.leave = { help = "An instance of type b left a trigger of type a.", filter = { a = "type", b = "type" } }
AUTHOR.events.arrived = { help = "A seeking instance reached its target.", filter = { type = "type" } }
AUTHOR.events.stopped = { help = "A rail camera reached a stop; event.name says which. pathNext moves it on.", filter = { name = "string" } }
AUTHOR.events.railEnd = { help = "A rail camera reached the end of its track.", filter = {} }
AUTHOR.events.verb = { help = "A verb was used on a hotspot (self), with an item or not: \"use key on door\". The most specific rule wins.", filter = { verb = "string", target = "string", item = "string" }, exclusive = true }
AUTHOR.events.said = { help = "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.", filter = { verb = "string", noun = "string", second = "string" }, exclusive = true }
AUTHOR.events.branchOpen = { help = "A branch's window opened; event.move says what it wants, event.mash how many times, event.hold for how many frames.", filter = {} }
AUTHOR.events.branchTaken = { help = "The move was made in time and the disc went to the success frame.", filter = {} }
AUTHOR.events.branchMissed = { help = "The window closed without the move and the disc went to the fail frame.", filter = {} }
AUTHOR.events.midi = { help = "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.)", filter = { pitch = "number" } }
AUTHOR.events.patrolEnd = { help = "A patrolling instance reached the last of its points.", filter = { type = "type" } }
AUTHOR.events.animationDone = { help = "A clip that does not loop ended on self; event.state says which state it was for.", filter = { type = "type", state = "state" } }
AUTHOR.events.soundDone = { help = "A clip that playSound or a sound behaviour started has ended; event.file says which, and self is the instance that played it.", filter = { file = "file", type = "type" } }
-- ===== Conditions =============================================================================
--
-- Every entry emits a Lua expression. Parameters arrive as Lua fragments.
AUTHOR.conditions.keyHeld = {
help = "A key is down.",
params = { key = "scancode" },
emit = function(p) return "authorKeyHeld(" .. p.key .. ")" end
}
AUTHOR.conditions.switchHeld = {
help = "A pad, gun, or mouse switch is down.",
params = { switch = "switch" },
emit = function(p) return "authorSwitchHeld(" .. p.switch .. ")" end
}
AUTHOR.conditions.keyPressed = {
help = "A key went down this frame.",
params = { key = "scancode" },
emit = function(p) return "authorKeyPressed(" .. p.key .. ")" end
}
AUTHOR.conditions.test = {
help = "An expression is true: self.health <= 0, count(\"zombie\") == 0, score > 1000.",
params = { expr = "expression" },
emit = function(p) return "(" .. p.expr .. ")" end
}
AUTHOR.conditions.timeBetween = {
help = "The game is between two moments, in seconds.",
params = { from = "number", to = "number" },
emit = function(p) return "(authorTime() >= " .. p.from .. " and authorTime() < " .. p.to .. ")" end
}
AUTHOR.conditions.discBetween = {
help = "The disc is between two frames -- the window a QTE is answered in.",
params = { from = "number", to = "number" },
emit = function(p) return "authorDiscBetween(" .. p.from .. ", " .. p.to .. ")" end
}
AUTHOR.conditions.onGround = {
help = "A platformer has ground under it.",
params = { entity = "entity" },
emit = function(p) return "authorOnGround(" .. p.entity .. ")" end
}
AUTHOR.conditions.touching = {
help = "Two instances overlap.",
params = { entity = "entity", other = "entity" },
emit = function(p) return "authorTouching(" .. p.entity .. ", " .. p.other .. ")" end
}
AUTHOR.conditions.below = {
help = "An instance has fallen past a line -- a pit, or the bottom of the screen.",
params = { entity = "entity", y = "number" },
emit = function(p) return "(authorY(" .. p.entity .. ") > " .. p.y .. ")" end
}
AUTHOR.conditions.inState = {
help = "An instance's state is this.",
params = { entity = "entity", state = "state" },
emit = function(p) return "authorInState(" .. p.entity .. ", " .. p.state .. ")" end
}
AUTHOR.conditions.pointerIn = {
help = "A player's pointer is over an instance.",
params = { entity = "entity", player = "number" },
emit = function(p) return "authorPointerIn(" .. p.entity .. ", " .. p.player .. ")" end
}
AUTHOR.conditions.pointerOffscreen = {
help = "A player's pointer is off the picture, which is how a light gun reloads.",
params = { player = "number" },
emit = function(p) return "authorPointerOffscreen(" .. p.player .. ")" end
}
AUTHOR.conditions.onScreen = {
help = "An instance's box is at least partly on the overlay.",
params = { entity = "entity" },
emit = function(p) return "authorOnScreen(" .. p.entity .. ")" end
}
AUTHOR.conditions.every = {
help = "True once every so many seconds, for a rule that fires steadily.",
params = { seconds = "number", tag = "string" },
emit = function(p) return "authorEvery(" .. p.seconds .. ", " .. p.tag .. ")" end
}
AUTHOR.conditions.chance = {
help = "True with a probability, 0 to 1.",
params = { p = "number" },
emit = function(p) return "(math.random() < " .. p.p .. ")" end
}
AUTHOR.conditions.hitPart = {
help = "The shot landed on a named zone of the target: head, weakspot.",
params = { part = "string" },
emit = function(p) return "(event[\"part\"] == " .. p.part .. ")" end
}
AUTHOR.conditions.waiting = {
help = "A rail camera is held at a stop.",
params = { entity = "entity" },
emit = function(p) return "authorWaiting(" .. p.entity .. ")" end
}
AUTHOR.conditions.has = {
help = "The inventory holds an item.",
params = { item = "string" },
emit = function(p) return "authorHas(" .. p.item .. ")" end
}
AUTHOR.conditions.hover = {
help = "A player's pointer is over the named hotspot.",
params = { target = "string", player = "number" },
defaults = { player = 1 },
emit = function(p) return "(authorHover(" .. p.player .. ") == " .. p.target .. ")" end
}
AUTHOR.conditions.once = {
help = "Only the first time this rule would run -- per instance and per room unless scope is \"game\".",
params = { tag = "string", scope = "string" },
optional = { scope = true },
emit = function(p) return "authorOnce(" .. p.tag .. ", self, " .. p.scope .. ")" end
}
-- ===== Actions ================================================================================
--
-- Every entry emits a Lua statement. waits marks the ones that take time: a rule with any of
-- them runs as a coroutine, and the action yields until it is done.
AUTHOR.actions.run = {
help = "Drive a platformer left (-1) or right (1) this frame.",
params = { entity = "entity", direction = "number" },
emit = function(p) return "authorRun(" .. p.entity .. ", " .. p.direction .. ")" end
}
AUTHOR.actions.jump = {
help = "Ask a platformer to jump.",
params = { entity = "entity" },
emit = function(p) return "authorJump(" .. p.entity .. ")" end
}
AUTHOR.actions.moveTo = {
help = "Put an instance somewhere.",
params = { entity = "entity", x = "number", y = "number", z = "number" },
defaults = { z = 0 },
emit = function(p) return "authorMoveTo(" .. p.entity .. ", " .. p.x .. ", " .. p.y .. ", " .. p.z .. ")" end
}
AUTHOR.actions.setText = {
help = "Change what a text instance says.",
params = { entity = "entity", text = "expression" },
emit = function(p) return "authorSetVar(" .. p.entity .. ", \"text\", tostring(" .. p.text .. "))" end
}
AUTHOR.actions.show = {
help = "Show or hide an instance.",
params = { entity = "entity", visible = "boolean" },
emit = function(p) return "authorShow(" .. p.entity .. ", " .. p.visible .. ")" end
}
AUTHOR.actions.addScore = {
help = "Add to the score.",
params = { amount = "number" },
emit = function(p) return "authorAddVar(nil, \"score\", " .. p.amount .. ")" end
}
AUTHOR.actions.setVar = {
help = "Set a var on an instance, or on the game with no entity.",
params = { entity = "entity", name = "string", value = "expression" },
optional = { entity = true },
emit = function(p) return "authorSetVar(" .. p.entity .. ", " .. p.name .. ", " .. p.value .. ")" end
}
AUTHOR.actions.addVar = {
help = "Add to a var on an instance, or on the game with no entity.",
params = { entity = "entity", name = "string", amount = "number" },
optional = { entity = true },
emit = function(p) return "authorAddVar(" .. p.entity .. ", " .. p.name .. ", " .. p.amount .. ")" end
}
AUTHOR.actions.setState = {
help = "Change an instance's state.",
params = { entity = "entity", state = "state" },
emit = function(p) return "authorSetVar(" .. p.entity .. ", \"state\", " .. p.state .. ")" end
}
AUTHOR.actions.damage = {
help = "Take from an instance's health; at zero it dies.",
params = { entity = "entity", amount = "number" },
emit = function(p) return "authorDamage(" .. p.entity .. ", " .. p.amount .. ")" end
}
AUTHOR.actions.spawn = {
help = "Make an instance of a type at a point.",
params = { type = "type", x = "number", y = "number", z = "number" },
defaults = { z = 0 },
emit = function(p) return "authorSpawn(" .. p.type .. ", " .. p.x .. ", " .. p.y .. ", " .. p.z .. ")" end
}
AUTHOR.actions.destroy = {
help = "Remove an instance.",
params = { entity = "entity" },
emit = function(p) return "authorDestroy(" .. p.entity .. ")" end
}
AUTHOR.actions.playSound = {
help = "Play a clip, at a volume of its own (0 to 100) under the game's.",
params = { file = "file", volume = "number" },
defaults = { volume = 100 },
emit = function(p) return "authorPlaySound(" .. p.file .. ", " .. p.volume .. ", self)" end
}
AUTHOR.actions.playMusic = {
help = "Play a track, looping.",
params = { file = "file" },
emit = function(p) return "authorPlayMusic(" .. p.file .. ")" end
}
AUTHOR.actions.stopMusic = {
help = "Stop the track that is playing.",
params = {},
emit = function() return "authorStopMusic()" end
}
AUTHOR.actions.emit = {
help = "A burst of particles at an instance: an explosion, sparks, a puff.",
params = { entity = "entity", count = "number", r = "number", g = "number", b = "number", speed = "number" },
defaults = { count = 20, r = 255, g = 200, b = 60, speed = 150 },
emit = function(p) return "authorEmit(" .. p.entity .. ", " .. p.count .. ", " .. p.r .. ", " .. p.g .. ", " .. p.b .. ", " .. p.speed .. ")" end
}
AUTHOR.actions.say = {
help = "Show a line for a while, and wait for it. With a hud it goes in the element called say; otherwise it is drawn.",
params = { text = "expression", seconds = "number" },
optional = { seconds = true },
waits = true,
emit = function(p) return "authorSay(" .. p.text .. ", " .. p.seconds .. ")" end
}
AUTHOR.actions.wait = {
help = "Wait some seconds before the next action.",
params = { seconds = "number" },
waits = true,
emit = function(p) return "authorWaitSeconds(" .. p.seconds .. ")" end
}
AUTHOR.actions.flash = {
help = "Flash the screen a colour.",
params = { r = "number", g = "number", b = "number", seconds = "number" },
defaults = { r = 255, g = 255, b = 255, seconds = 0.2 },
emit = function(p) return "authorFlash(" .. p.r .. ", " .. p.g .. ", " .. p.b .. ", " .. p.seconds .. ")" end
}
AUTHOR.actions.timerStart = {
help = "Start a named timer on an instance.",
params = { entity = "entity", name = "string", after = "number" },
emit = function(p) return "authorTimerStart(" .. p.entity .. ", " .. p.name .. ", " .. p.after .. ")" end
}
AUTHOR.actions.goTo = {
help = "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.",
params = { room = "room", entity = "entity", x = "number", y = "number", z = "number", at = "string" },
optional = { entity = true, x = true, y = true, z = true, at = true },
emit = function(p) return "authorGoTo(" .. p.room .. ", " .. p.entity .. ", " .. p.x .. ", " .. p.y .. ", " .. p.z .. ", " .. p.at .. ")" end
}
AUTHOR.actions.reload = {
help = "Fill a gun's ammo.",
params = { entity = "entity" },
emit = function(p) return "authorReload(" .. p.entity .. ")" end
}
AUTHOR.actions.discTo = {
help = "Send the disc to a frame -- the branch a QTE takes.",
params = { frame = "number" },
emit = function(p) return "discSkipToFrame(" .. p.frame .. ")" end
}
AUTHOR.actions.discPlay = {
help = "Play the disc from where it stands.",
params = {},
emit = function() return "discPlay()" end
}
AUTHOR.actions.discPause = {
help = "Hold the disc on its frame: a still behind a title.",
params = {},
emit = function() return "discPause()" end
}
AUTHOR.actions.gameOver = {
help = "End the game: rules stop, the gameOver event fires, restart begins again.",
params = {},
emit = function() return "authorGameOver()" end
}
AUTHOR.actions.restart = {
help = "Begin the game again from its first room.",
params = {},
emit = function() return "authorRestart()" end
}
AUTHOR.actions.pathNext = {
help = "Send a rail camera on from its stop.",
params = { entity = "entity" },
emit = function(p) return "authorPathNext(" .. p.entity .. ")" end
}
AUTHOR.actions.playAnimation = {
help = "Play a model's clip, once or looping.",
params = { entity = "entity", clip = "string", loop = "boolean" },
defaults = { loop = false },
emit = function(p) return "authorPlayAnimation(" .. p.entity .. ", " .. p.clip .. ", " .. p.loop .. ")" end
}
AUTHOR.actions.lookAt = {
help = "Turn an instance toward another.",
params = { entity = "entity", target = "entity" },
emit = function(p) return "authorLookAt(" .. p.entity .. ", " .. p.target .. ")" end
}
AUTHOR.actions.push = {
help = "Shove a body, or a ragdoll's bone, along a direction.",
params = { entity = "entity", x = "number", y = "number", z = "number", bone = "string" },
defaults = { x = 0, y = 0, z = 0 },
optional = { bone = true },
emit = function(p) return "authorPush(" .. p.entity .. ", " .. p.x .. ", " .. p.y .. ", " .. p.z .. ", " .. p.bone .. ")" end
}
AUTHOR.actions.shake = {
help = "Shake the camera for a moment.",
params = { amount = "number", seconds = "number" },
defaults = { amount = 0.3, seconds = 0.4 },
emit = function(p) return "authorShake(" .. p.amount .. ", " .. p.seconds .. ")" end
}
AUTHOR.actions.cameraCut = {
help = "Draw the scene from another camera instance.",
params = { entity = "entity" },
emit = function(p) return "authorCameraCut(" .. p.entity .. ")" end
}
AUTHOR.actions.spawnAtPointer = {
help = "Make an instance of a type where the player's pointer is: on the overlay in 2D, on the floor the ray meets in 3D.",
params = { type = "type", player = "number" },
defaults = { player = 1 },
emit = function(p) return "authorSpawnAtPointer(" .. p.type .. ", " .. p.player .. ")" end
}
AUTHOR.actions.walkTo = {
help = "Send a walker to a point, and wait until it gets there.",
params = { entity = "entity", x = "number", y = "number", z = "number" },
defaults = { z = 0 },
waits = true,
emit = function(p) return "authorWalkTo(" .. p.entity .. ", " .. p.x .. ", " .. p.y .. ", " .. p.z .. ")" end
}
AUTHOR.actions.walkToPointer = {
help = "Send a walker where the player's pointer is, and wait.",
params = { entity = "entity", player = "number" },
defaults = { player = 1 },
waits = true,
emit = function(p) return "authorWalkToPointer(" .. p.entity .. ", " .. p.player .. ")" end
}
AUTHOR.actions.walkToHotspot = {
help = "Send a walker to a hotspot's walk point (the one the verb was used on, with self), and wait.",
params = { entity = "entity", target = "entity" },
waits = true,
emit = function(p) return "authorWalkToHotspot(" .. p.entity .. ", " .. p.target .. ")" end
}
AUTHOR.actions.face = {
help = "Turn a walker to face left or right, or toward an instance.",
params = { entity = "entity", direction = "string", target = "entity" },
optional = { direction = true, target = true },
emit = function(p) return "authorFace(" .. p.entity .. ", " .. p.direction .. ", " .. p.target .. ")" end
}
AUTHOR.actions.give = {
help = "Put an item in the inventory.",
params = { item = "string" },
emit = function(p) return "authorGive(" .. p.item .. ")" end
}
AUTHOR.actions.take = {
help = "Take an item out of the inventory.",
params = { item = "string" },
emit = function(p) return "authorTake(" .. p.item .. ")" end
}
AUTHOR.actions.setVerb = {
help = "Choose the verb the next click uses.",
params = { verb = "string" },
emit = function(p) return "authorSetVerb(" .. p.verb .. ")" end
}
AUTHOR.actions.nextVerb = {
help = "Cycle to the next verb in the game's list.",
params = {},
emit = function() return "authorNextVerb()" end
}
AUTHOR.actions.useItem = {
help = "Choose an inventory item for the next click: \"use key on ...\".",
params = { item = "string" },
emit = function(p) return "authorUseItem(" .. p.item .. ")" end
}
AUTHOR.actions.talk = {
help = "Run a dialogue from the game's dialogues, and wait for it to end.",
params = { dialogue = "string" },
waits = true,
emit = function(p) return "authorTalk(" .. p.dialogue .. ")" end
}
AUTHOR.actions.fade = {
help = "Fade the picture to black, or back (with out false), and wait.",
params = { seconds = "number", out = "boolean" },
defaults = { seconds = FADE_DEFAULT, out = true },
waits = true,
emit = function(p) return "authorFade(" .. p.seconds .. ", " .. p.out .. ")" end
}
AUTHOR.actions.saveGame = {
help = "Keep the whole game -- room, positions, vars, inventory -- in a numbered slot.",
params = { slot = "number" },
defaults = { slot = 1 },
emit = function(p) return "authorSaveGame(" .. p.slot .. ")" end
}
AUTHOR.actions.loadGame = {
help = "Bring a slot back.",
params = { slot = "number" },
defaults = { slot = 1 },
emit = function(p) return "authorLoadGame(" .. p.slot .. ")" end
}
AUTHOR.actions.die = {
help = "The Sierra death: a message, then the game starts over.",
params = { text = "expression" },
waits = true,
emit = function(p) return "authorDie(" .. p.text .. ")" end
}
AUTHOR.actions.submitScore = {
help = "Send the score to the master service's board for this game, queued until it can go.",
params = { board = "string" },
emit = function(p) return "authorSubmitScore(" .. (p.board or '"default"') .. ")" end
}
AUTHOR.actions.credit = {
help = "A coin: one more credit.",
params = {},
emit = function() return "authorAddVar(nil, \"credits\", 1)" end
}
AUTHOR.actions.lua = {
help = "Anything the vocabulary cannot say. The way out, and it is meant to be used.",
params = { code = "lua" },
emit = function(p) return p.code end
}
-- ===== Instances ==============================================================================
local function nextId(typeName)
local id = typeName .. "#" .. nextSerial
nextSerial = nextSerial + 1
return id
end
local function copyOf(value)
local out
if type(value) ~= "table" then
return value
end
out = {}
for key, item in pairs(value) do
out[key] = copyOf(item)
end
return out
end
-- Makes an instance of a type. overrides is an entities entry: id, x, y, z, and vars.
local function make(typeName, x, y, z, overrides)
local def = AUTHOR_GAME.types[typeName]
local instance
if def == nil then
debugPrint("Author: no type called '" .. tostring(typeName) .. "'")
return nil
end
overrides = overrides or {}
instance = {
id = overrides.id or nextId(typeName),
type = typeName,
def = def,
look = copyOf(def.look or { kind = "none" }),
behaviours = def.behaviours or {},
vars = copyOf(def.vars or {}),
visible = true,
alive = true,
node = nodeNew(),
nodes = {}
}
for key, value in pairs(overrides.vars or {}) do
instance.vars[key] = copyOf(value)
end
for key, value in pairs(overrides) do
if (key ~= "id") and (key ~= "x") and (key ~= "y") and (key ~= "z") and (key ~= "rx") and (key ~= "ry") and (key ~= "rz") and (key ~= "scale") and (key ~= "type") and (key ~= "vars") then
instance.vars[key] = copyOf(value)
end
end
instance.vars.drive = 0
instance.vars.state = instance.vars.state or "idle"
nodeSetPosition(instance.node, x or 0, y or 0, z or 0)
if overrides.rx or overrides.ry or overrides.rz then
nodeSetRotation(instance.node, overrides.rx or 0, overrides.ry or 0, overrides.rz or 0)
end
-- A scale on the entity scales its node, and with it its look, its body (bodies take the node's
-- scale), and how big it counts as.
instance.scale = overrides.scale or 1
if instance.scale ~= 1 then
nodeSetScale(instance.node, instance.scale)
end
if overrides.rz and not AUTHOR_3D then
instance.vars.angle = overrides.rz
end
local look = AUTHOR.looks[instance.look.kind]
if look == nil then
debugPrint("Author: no look called '" .. tostring(instance.look.kind) .. "' on " .. instance.id .. "; drawing a box")
instance.look.kind = "box"
look = AUTHOR.looks.box
end
if look.load then
look.load(instance)
end
AUTHOR_LIVE[#AUTHOR_LIVE + 1] = instance
AUTHOR_BY_ID[instance.id] = instance
AUTHOR_BY_NODE[instance.node] = instance
for _, b in ipairs(instance.behaviours) do
local behaviour = AUTHOR.behaviours[b.kind]
if behaviour == nil then
debugPrint("Author: no behaviour called '" .. tostring(b.kind) .. "'")
elseif behaviour.attach then
behaviour.attach(instance, b)
end
end
return instance
end
function authorSpawn(typeName, x, y, z, overrides)
local instance = make(typeName, x, y, z, overrides)
if instance ~= nil then
authorEvent("spawn", { self = instance })
end
return instance
end
-- Removes an instance now. Its node goes with it, and any body or player on the node.
function authorDestroy(instance)
if (instance == nil) or not instance.alive then
return
end
instance.alive = false
AUTHOR_BY_ID[instance.id] = nil
AUTHOR_BY_NODE[instance.node] = nil
for _, node in ipairs(instance.nodes) do
AUTHOR_BY_NODE[node] = nil
end
if instance.ragdoll then
ragdollDelete(instance.model)
end
for at = #AUTHOR_LIVE, 1, -1 do
if AUTHOR_LIVE[at] == instance then
table.remove(AUTHOR_LIVE, at)
end
end
for at = #timers, 1, -1 do
if timers[at].instance == instance then
table.remove(timers, at)
end
end
if instance.emitter then
emitterDelete(instance.emitter)
end
if instance.ownSprite then
spriteUnload(instance.ownSprite)
end
if instance.agent then
navAgentDelete(instance.agent)
AUTHOR_AGENTS[instance.agent] = nil
end
if instance.navNode and (instance.navNode ~= instance.node) then
nodeDelete(instance.navNode)
end
nodeDelete(instance.node)
end
-- ===== The runtime the generated Lua calls ====================================================
function authorKeyHeld(scancode)
return AUTHOR_KEYS[scancode] == true
end
function authorKeyPressed(scancode)
return pressedNow[scancode] == true
end
function authorSwitchHeld(switch)
return AUTHOR_SWITCHES[switch] == true
end
function authorDiscBetween(from, to)
local frame = discGetFrame()
return (frame >= from) and (frame < to)
end
function authorOnGround(instance)
return (instance ~= nil) and instance.player and playerIsOnGround(instance.node)
end
function authorTouching(a, b)
if (a == nil) or (b == nil) then
return false
end
if AUTHOR_3D then
local ax, ay, az = authorPosition(a)
local bx, by, bz = authorPosition(b)
local aw, ah, ad = authorSize(a)
local bw, bh, bd = authorSize(b)
return (math.abs(ax - bx) * 2 < aw + bw) and (math.abs(ay - by) * 2 < ah + bh) and (math.abs(az - bz) * 2 < (ad or aw) + (bd or bw))
end
local ax, ay, aw, ah = authorBounds(a)
local bx, by, bw, bh = authorBounds(b)
return collideRects(ax, ay, aw, ah, bx, by, bw, bh)
end
function authorInState(instance, state)
return (instance ~= nil) and (instance.vars.state == state)
end
function authorOnScreen(instance)
if instance == nil then
return false
end
local x, y, w, h = authorBounds(instance)
return collideRects(x, y, w, h, 0, 0, overlayGetWidth(), overlayGetHeight())
end
-- True the first time only, per instance and room by default; "game" makes it once ever.
function authorOnce(tag, instance, scope)
local key = tag
if scope ~= "game" then
key = (AUTHOR_ROOM and AUTHOR_ROOM.name or "") .. "/" .. (instance and instance.id or "") .. "/" .. tag
end
if onceSeen[key] then
return false
end
onceSeen[key] = true
return true
end
local everyLast = {}
function authorEvery(seconds, tag)
local now = authorTime()
if (everyLast[tag] == nil) or (now - everyLast[tag] >= seconds) then
everyLast[tag] = now
return true
end
return false
end
function authorRun(instance, direction)
if instance then
instance.vars.drive = direction
end
end
function authorJump(instance)
if instance and instance.player then
playerJump(instance.node, instance.jump)
end
end
function authorMoveTo(instance, x, y, z)
if instance == nil then
return
end
if instance.player then
playerSetPosition(instance.node, x, y, z or 0)
else
nodeSetPosition(instance.node, x, y, z or 0)
end
end
function authorShow(instance, visible)
if instance then
instance.visible = visible
end
end
-- A var on an instance, or on the game with nil.
function authorSetVar(instance, name, value)
if instance == nil then
AUTHOR_VARS[name] = value
elseif instance.alive then
instance.vars[name] = value
end
end
function authorAddVar(instance, name, amount)
local vars = instance and instance.vars or AUTHOR_VARS
vars[name] = (vars[name] or 0) + amount
end
function authorDamage(instance, amount)
if (instance == nil) or not instance.alive then
return
end
instance.vars.health = (instance.vars.health or 1) - amount
authorBehaviourEvent(instance, "hit")
if instance.vars.health <= 0 then
instance.vars.state = "dead"
authorBehaviourEvent(instance, "death")
authorEvent("death", { self = instance })
if not instance.keepDead then
authorDestroy(instance)
end
end
end
-- Whether a sprite look is drawn mirrored: its art faces one way (faces: right unless said), the
-- instance faces the other, and its sheet has no frames drawn that way (the frames behaviour says
-- so by drawnFacing). Facing up or down never mirrors.
function authorMirrored(instance)
local faces = instance.look.faces or "right"
local opposite = (faces == "right") and "left" or ((faces == "left") and "right" or nil)
if (opposite == nil) or (instance.vars.facing ~= opposite) then
return false
end
return instance.drawnFacing ~= opposite
end
-- A clip at a Forge volume, 0 to 100, under the engine's master.
-- Plays a clip at a volume of its own, remembering who played it so soundDone can say so.
function authorPlaySound(file, volume, owner)
local channel = soundPlay(soundFor(file), 0, math.floor(math.max(0, math.min(100, volume or 100)) * SOUND_MAX_VOLUME / 100 + 0.5))
if channel and (channel >= 0) then
playing[channel] = { file = file, self = owner }
end
return channel
end
-- The engine says a channel has finished: soundDone for the clip that was on it, with the
-- instance that played it as self.
function authorSoundDone(channel)
local was = playing[channel]
if was == nil then
return
end
playing[channel] = nil
authorEvent("soundDone", { self = was.self, file = was.file })
end
function authorPlayMusic(file)
if music then
musicStop(music)
end
music = musicLoad(authorFile(file))
musicPlay(music, -1)
end
function authorStopMusic()
if music then
musicStop(music)
end
end
function authorEmit(instance, count, r, g, b, speed)
if instance == nil then
return
end
local x, y = authorPosition(instance)
if instance.emitter == nil then
instance.emitter = emitterNew()
emitterSetLife(instance.emitter, 0.3, 0.7)
emitterSetSpread(instance.emitter, 360)
emitterSetSize(instance.emitter, 6, 1)
end
emitterSetPosition(instance.emitter, x, y)
emitterSetColor(instance.emitter, r, g, b, 255, r, g, b, 0)
emitterSetSpeed(instance.emitter, speed * 0.5, speed)
emitterBurst(instance.emitter, count)
end
function authorFlash(r, g, b, seconds)
flashLine = { r = r, g = g, b = b, seconds = seconds, till = authorTime() + seconds }
end
function authorTimerStart(instance, name, after, every)
if instance == nil then
return
end
timers[#timers + 1] = { instance = instance, name = name, at = authorTime() + (after or 0), every = every }
end
function authorReload(instance)
if instance and instance.gun then
instance.vars.ammo = instance.gun.ammo or 6
authorBehaviourEvent(instance, "reload")
end
end
function authorGameOver()
if gameOver then
return
end
gameOver = true
-- The results card, and the best score kept for next time.
local best = math.max(AUTHOR_VARS.best or 0, AUTHOR_VARS.score or 0)
AUTHOR_VARS.best = best
saveSet(BEST_KEY, best)
results = { score = AUTHOR_VARS.score or 0, best = best }
authorEvent("gameOver", {})
end
-- The master service's client is loaded when a score is first sent, and pumped from then on. A
-- test may supply masterSubmitScore itself.
function authorSubmitScore(board)
if not masterReady then
if masterSubmitScore == nil then
dofile("Singe/Net.singe")
dofile("Singe/Master.singe")
masterLoad()
end
masterReady = true
end
masterSubmitScore(singeGetGameId(), AUTHOR_VARS.score or 0, board)
end
local function updateBezel()
if bezel == nil then
return
end
local shows = { { "score", 1 }, { "lives", 1 }, { "score2", 2 }, { "lives2", 2 } }
for _, item in ipairs(shows) do
local value = AUTHOR_VARS[item[1]]
if (value ~= nil) and (bezel.shown[item[1]] ~= value) then
bezel.shown[item[1]] = value
if item[1]:sub(1, 5) == "score" then
scoreBezelScore(item[2], value)
else
scoreBezelLives(item[2], value)
end
end
end
if (AUTHOR_VARS.credits ~= nil) and (bezel.shown.credits ~= AUTHOR_VARS.credits) then
bezel.shown.credits = AUTHOR_VARS.credits
scoreBezelCredits(AUTHOR_VARS.credits)
end
end
-- ===== The scene ==============================================================================
-- Where a target named in a behaviour is: "camera", an entity's id, or nothing.
function authorTargetInstance(name)
if name == "camera" then
return cameraNode and AUTHOR_BY_NODE[cameraNode] or nil
end
return AUTHOR_BY_ID[name or ""]
end
function authorTargetPosition(name)
local target = authorTargetInstance(name)
if target == nil then
return nil
end
return authorPosition(target)
end
-- The camera's forward and right on the ground, for a character that moves relative to the view.
-- Without a camera, world axes: forward is -Z.
function authorCameraAxes()
local camera = cameraNode and AUTHOR_BY_NODE[cameraNode] or nil
if (camera == nil) or (camera.camera == nil) then
return 0, -1, 1, 0
end
local yaw
if camera.camera.mode == "first" or camera.camera.mode == "orbit" then
yaw = math.rad(camera.yaw)
else
local x, _, z = authorPosition(camera)
local tx, _, tz = authorTargetPosition(camera.camera.target)
if tx == nil then
return 0, -1, 1, 0
end
yaw = math.atan(x - tx, z - tz)
end
-- Forward is away from the camera, right is ninety degrees round from it.
local fx, fz = -math.sin(yaw), -math.cos(yaw)
return fx, fz, -fz, fx
end
-- The instance a node belongs to. Every node an instance owns is registered when it is made, so
-- this never asks the engine -- which matters because physics can report a node a rule has already
-- deleted, a prize destroyed as it is entered, and asking about that node would end the game.
function authorInstanceOf(node)
return node and AUTHOR_BY_NODE[node] or nil
end
function authorWaiting(instance)
return (instance ~= nil) and (instance.rail ~= nil) and (instance.rail.waiting ~= nil)
end
function authorPathNext(instance)
if instance and instance.rail then
instance.rail.waiting = nil
end
end
function authorPlayAnimation(instance, clip, loop)
if instance and instance.model then
animationPlay(instance.model, clip, loop)
instance.playing = clip
end
end
function authorLookAt(instance, target)
if instance and target then
local x, y, z = authorPosition(target)
nodeLookAt(instance.model or instance.node, x, y, z)
end
end
function authorPush(instance, x, y, z, bone)
if instance == nil then
return
end
if instance.ragdoll and bone then
ragdollApplyImpulse(instance.model, bone, x, y, z)
elseif instance.hasBody then
bodyApplyImpulse(instance.node, x, y, z)
end
end
local shaking = nil
function authorShake(amount, seconds)
shaking = { amount = amount, till = authorTime() + seconds }
end
function authorCameraCut(instance)
if instance then
cameraNode = instance.node
cameraSet(instance.node)
end
end
-- One step along a rail: the camera track's points are positions by time, each looking at a
-- point or an entity, and a point may be a stop that holds the rail until pathNext.
function authorRailStep(instance, dt)
local rail = instance.rail
local track = authorTrackNamed(instance.camera.track or "")
local points = track and track.points or {}
if #points == 0 then
return
end
if rail.waiting == nil then
rail.t = rail.t + dt
end
local before, after
for _, point in ipairs(points) do
if (point.at <= rail.t) and ((before == nil) or (point.at > before.at)) then
before = point
end
if (point.at >= rail.t) and ((after == nil) or (point.at < after.at)) then
after = point
end
end
if before == nil then
before = after
end
if after == nil then
after = before
if not rail.ended then
rail.ended = true
authorEvent("railEnd", { self = instance })
end
end
-- A stop holds the rail the moment it is reached, once.
if before.stop and not rail.passed[before] and (rail.t >= before.at) then
rail.passed[before] = true
rail.waiting = before.stop
rail.t = before.at
authorEvent("stopped", { self = instance, name = before.stop })
authorWavesAtStop(before.stop)
end
local k = 0
if (after ~= before) and (after.at > before.at) then
k = (rail.t - before.at) / (after.at - before.at)
end
local x = before.x + (after.x - before.x) * k
local y = before.y + (after.y - before.y) * k
local z = before.z + (after.z - before.z) * k
if shaking then
if authorTime() >= shaking.till then
shaking = nil
else
x = x + (math.random() - 0.5) * shaking.amount
y = y + (math.random() - 0.5) * shaking.amount
end
end
nodeSetPosition(instance.node, x, y, z)
local look = before.look
if type(look) == "string" then
local lx, ly, lz = authorTargetPosition(look)
if lx then
nodeLookAt(instance.node, lx, ly, lz)
end
elseif type(look) == "table" then
local lx = look.x or look[1] or 0
local ly = look.y or look[2] or 0
local lz = look.z or look[3] or 0
if (after ~= before) and (type(after.look) == "table") then
lx = lx + ((after.look.x or after.look[1] or 0) - lx) * k
ly = ly + ((after.look.y or after.look[2] or 0) - ly) * k
lz = lz + ((after.look.z or after.look[3] or 0) - lz) * k
end
nodeLookAt(instance.node, lx, ly, lz)
end
end
-- Physics reports a contact between two bodies; the rules hear about it by type.
function authorCollision(nodeA, nodeB, x, y, z, speed)
local a = authorInstanceOf(nodeA)
local b = authorInstanceOf(nodeB)
if a and b then
authorEvent("collision", { self = a, other = b, x = x, y = y, z = z, speed = speed })
authorEvent("collision", { self = b, other = a, x = x, y = y, z = z, speed = speed })
end
end
function authorTrigger(trigger, other, entered)
local a = authorInstanceOf(trigger)
local b = authorInstanceOf(other)
if a and b then
authorEvent(entered and "enter" or "leave", { self = a, other = b })
end
end
-- A MIDI message from the input port: note-on with a velocity is a note, and the rules hear it.
function authorMidi(status, data1, data2)
local kind = math.floor((status or 0) / 16)
if (kind == 9) and ((data2 or 0) > 0) then
authorEvent("midi", { pitch = data1, velocity = data2, channel = (status % 16) + 1 })
end
end
function authorMouseMoved(x, y, xr, yr, device)
AUTHOR_MOUSE.dx = AUTHOR_MOUSE.dx + (xr or 0)
AUTHOR_MOUSE.dy = AUTHOR_MOUSE.dy + (yr or 0)
-- A game loop that hears everything gets the pointer too (a map-mode game that shoots),
-- with the device it moved on, for a game of two guns.
authorFan("pointer", { x = x, y = y, xrel = xr, yrel = yr, device = device })
end
-- ===== Walking, hotspots, verbs, and the parser ===============================================
--
-- The adventure's pieces (FORGE.md section 3). A room's walk areas become a navigation mesh so
-- walking in a painted room and in a modelled one is the same agent; a hotspot is where a verb
-- lands; the sentence line is built from the verb, the hotspot under the pointer, and the item in
-- hand; the parser turns a typed line into the same kind of event.
AUTHOR_AGENTS = {} -- Instances by navigation agent, for onNavArrived.
-- Numbers from a table, or from text like "10,20, 30,40" typed in the editor.
function authorNumbers(value)
if type(value) == "table" then
return value
end
if type(value) ~= "string" then
return nil
end
local out = {}
for number in string.gmatch(value, "-?[%d.]+") do
out[#out + 1] = tonumber(number)
end
return (#out > 0) and out or nil
end
-- Ear clipping: a polygon of x,y pairs into triangles, as vertex indexes counted from 1.
local function triangulate(polygon)
local points = {}
local order = {}
local tris = {}
for at = 1, #polygon - 1, 2 do
points[#points + 1] = { polygon[at], polygon[at + 1] }
order[#order + 1] = #points
end
local function area2(a, b, c)
return (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2])
end
local function inside(p, a, b, c)
local s1 = area2(a, b, p)
local s2 = area2(b, c, p)
local s3 = area2(c, a, p)
return ((s1 >= 0) and (s2 >= 0) and (s3 >= 0)) or ((s1 <= 0) and (s2 <= 0) and (s3 <= 0))
end
local total = 0
for at = 1, #points do
local a = points[at]
local b = points[(at % #points) + 1]
total = total + a[1] * b[2] - b[1] * a[2]
end
local sign = (total >= 0) and 1 or -1
local guard = 0
while (#order > 3) and (guard < 1000) do
local clipped = false
guard = guard + 1
for at = 1, #order do
local i0 = order[((at - 2) % #order) + 1]
local i1 = order[at]
local i2 = order[(at % #order) + 1]
local a, b, c = points[i0], points[i1], points[i2]
if area2(a, b, c) * sign > 0 then
local ear = true
for _, other in ipairs(order) do
if (other ~= i0) and (other ~= i1) and (other ~= i2) and inside(points[other], a, b, c) then
ear = false
end
end
if ear then
tris[#tris + 1] = { i0, i1, i2 }
table.remove(order, at)
clipped = true
break
end
end
end
if not clipped then
break
end
end
if #order == 3 then
tris[#tris + 1] = { order[1], order[2], order[3] }
end
return points, tris
end
-- The current room's navigation mesh, baked once: from its walk polygons in 2D, from the meshes
-- of the entities navFrom names in 3D. nil for a room with neither.
function authorRoomNav()
local room = AUTHOR_ROOM
if room == nil then
return nil
end
if room.nav ~= nil then
return room.nav or nil
end
if room.walk and (#room.walk > 0) then
local nav = navNew(NAV_RADIUS_2D, NAV_HEIGHT_2D, 60, 0.05)
for _, polygon in ipairs(room.walk) do
local points, tris = triangulate(authorNumbers(polygon) or {})
local positions = {}
local indices = {}
for _, point in ipairs(points) do
positions[#positions + 1] = point[1] * NAV_SCALE
positions[#positions + 1] = 0
positions[#positions + 1] = point[2] * NAV_SCALE
end
for _, tri in ipairs(tris) do
-- Wound so the face looks up, whichever way the author drew the outline.
local a, b, c = points[tri[1]], points[tri[2]], points[tri[3]]
local up = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2])
if up > 0 then
indices[#indices + 1] = tri[1]
indices[#indices + 1] = tri[3]
indices[#indices + 1] = tri[2]
else
indices[#indices + 1] = tri[1]
indices[#indices + 1] = tri[2]
indices[#indices + 1] = tri[3]
end
end
if #indices > 0 then
local node = nodeNew()
nodeSetMesh(node, meshNew(positions, nil, nil, indices))
nodeSetVisible(node, false)
navAddNode(nav, node)
end
end
navBuild(nav)
room.nav = { nav = nav, scale = NAV_SCALE, radius = NAV_RADIUS_2D, height = NAV_HEIGHT_2D }
elseif room.navFrom then
local nav = navNew(0.3, 1.0, 45, 0.4)
for _, id in ipairs(room.navFrom) do
local floor = AUTHOR_BY_ID[id]
if floor then
navAddNode(nav, floor.node)
end
end
navBuild(nav)
room.nav = { nav = nav, scale = 1, radius = 0.3, height = 1.0 }
else
room.nav = false
end
return room.nav or nil
end
-- Sends a walker somewhere. Answers whether it set off.
function authorWalkStart(instance, x, y, z)
local nav = authorRoomNav()
if (instance == nil) or (instance.agent == nil) or (nav == nil) then
return false
end
local tx, ty, tz
if AUTHOR_3D then
tx, ty, tz = x, y, z or 0
else
tx, ty, tz = x * NAV_SCALE, 0, y * NAV_SCALE
end
local sx, sy, sz = navNearest(nav.nav, tx, ty, tz)
if sx == nil then
return false
end
if navAgentMoveTo(instance.agent, sx, sy, sz) then
instance.walking = true
instance.vars.state = "walk"
return true
end
return false
end
-- Sends a walker somewhere and waits until it arrives, or gives up after a while.
function authorWalkTo(instance, x, y, z)
if authorWalkStart(instance, x, y, z) then
local till = authorTime() + WALK_TIMEOUT
authorWait(function() return (not instance.alive) or (not instance.walking) or (authorTime() > till) end)
end
end
function authorDistance3D(a, b)
local ax, ay, az = authorPosition(a)
local bx, by, bz = authorPosition(b)
return math.sqrt((ax - bx) ^ 2 + (ay - by) ^ 2 + (az - bz) ^ 2)
end
function authorSpawnAtPointer(typeName, player)
local x, y, z = authorPointerWorld(player)
if x ~= nil then
return authorSpawn(typeName, x, y, z)
end
return nil
end
-- The world point under a player's pointer: the overlay point in 2D, the floor the ray hits in 3D.
function authorPointerWorld(player)
local px, py, on = authorPointer(player)
if not on then
return nil
end
if not AUTHOR_3D then
return px, py, 0
end
local x0, y0, z0 = sceneUnproject(px, py, 0)
local x1, y1, z1 = sceneUnproject(px, py, 10)
local node, hx, hy, hz = physicsRaycast(x0, y0, z0, x1 - x0, y1 - y0, z1 - z0, RAY_REACH)
if node == nil then
return nil
end
return hx, hy, hz
end
function authorWalkToPointer(instance, player)
local x, y, z = authorPointerWorld(player)
if x ~= nil then
authorWalkTo(instance, x, y, z)
end
end
function authorWalkToHotspot(instance, target)
if (target == nil) or (target.hotspot == nil) then
return
end
local p = target.hotspot
local x, y, z = authorPosition(target)
authorWalkTo(instance, p.walkX or x, p.walkY or y, p.walkZ or z)
end
function authorFace(instance, direction, target)
if instance == nil then
return
end
if target then
local x, y, z = authorPosition(instance)
local tx, ty, tz = authorPosition(target)
instance.vars.facing = (tx < x) and "left" or "right"
if instance.model then
nodeLookAt(instance.model, tx, y, tz)
end
elseif direction then
instance.vars.facing = direction
end
end
function authorNavArrived(agent)
local instance = AUTHOR_AGENTS[agent]
if instance and instance.walking then
instance.walking = false
instance.vars.state = "idle"
authorEvent("arrived", { self = instance })
end
end
-- The hotspot under a player's pointer, or nil. The topmost in 2D; what the ray meets in 3D.
function authorHoverInstance(player)
local px, py, on = authorPointer(player)
if not on then
return nil
end
if AUTHOR_3D then
local x0, y0, z0 = sceneUnproject(px, py, 0)
local x1, y1, z1 = sceneUnproject(px, py, 10)
local node = physicsRaycast(x0, y0, z0, x1 - x0, y1 - y0, z1 - z0, RAY_REACH)
local hit = node and AUTHOR_BY_NODE[node] or nil
return (hit and hit.hotspot) and hit or nil
end
for at = #AUTHOR_LIVE, 1, -1 do
local instance = AUTHOR_LIVE[at]
if instance.alive and instance.visible and instance.hotspot then
if instance.polygon then
if collidePointPolygon(px, py, instance.polygon) then
return instance
end
else
local x, y, w, h = authorBounds(instance)
if collidePointRect(px, py, x, y, w, h) then
return instance
end
end
end
end
return nil
end
function authorHover(player)
local instance = authorHoverInstance(player)
return instance and (instance.hotspot.name or instance.id) or nil
end
function authorGive(item)
AUTHOR_VARS.inventory = AUTHOR_VARS.inventory or {}
if not authorHas(item) then
AUTHOR_VARS.inventory[#AUTHOR_VARS.inventory + 1] = item
end
end
function authorTake(item)
for at = #(AUTHOR_VARS.inventory or {}), 1, -1 do
if AUTHOR_VARS.inventory[at] == item then
table.remove(AUTHOR_VARS.inventory, at)
end
end
if AUTHOR_VARS.item == item then
AUTHOR_VARS.item = nil
end
end
function authorSetVerb(verb)
AUTHOR_VARS.verb = verb
AUTHOR_VARS.item = nil
end
function authorNextVerb()
local verbs = AUTHOR_GAME.verbs or {}
local at = 0
for index, verb in ipairs(verbs) do
if verb == AUTHOR_VARS.verb then
at = index
end
end
AUTHOR_VARS.verb = verbs[(at % #verbs) + 1]
AUTHOR_VARS.item = nil
end
function authorUseItem(item)
AUTHOR_VARS.verb = "use"
AUTHOR_VARS.item = item
end
-- The sentence line, rebuilt every frame: "use key with door".
local function updateSentence()
if AUTHOR_GAME.verbs == nil then
return
end
local hover = authorHover(1) or ""
local verb = AUTHOR_VARS.verb or AUTHOR_GAME.verbs[1] or ""
local line = verb
AUTHOR_VARS.hover = hover
if AUTHOR_VARS.item then
line = line .. " " .. AUTHOR_VARS.item .. ((hover ~= "") and " with " or "")
elseif hover ~= "" then
line = line .. " "
end
AUTHOR_VARS.sentence = line .. hover
end
-- A press of the pointer's button in a game with verbs: on a hotspot it is the verb event, with
-- the most specific rule winning; elsewhere it is left to the pressed rules (a walk, usually).
local function verbPress(player)
local target = authorHoverInstance(player)
if target == nil then
return false
end
local verb = AUTHOR_VARS.verb or AUTHOR_GAME.verbs[1]
local item = AUTHOR_VARS.item
authorEvent("verb", { self = target, verb = verb, target = target.hotspot.name or target.id, item = item, player = player })
AUTHOR_VARS.verb = AUTHOR_GAME.verbs[1]
AUTHOR_VARS.item = nil
return true
end
-- A word as the parser's words table knows it, or nil.
local function canonical(word)
for name, synonyms in pairs(parserLayer.words or {}) do
if name == word then
return name
end
for _, synonym in ipairs(synonyms) do
if synonym == word then
return name
end
end
end
return nil
end
-- A typed line: verb, noun, and a second noun after a preposition, as the words table knows
-- them. A word it does not know is reported as unknown, so a rule can say so.
function authorSaid(text)
local words = {}
local ignore = {}
local unknown = nil
local known = {}
for _, word in ipairs(parserLayer.ignore or { "the", "a", "an", "at", "to" }) do
ignore[word] = true
end
for word in string.gmatch(string.lower(text), "%a+") do
if not ignore[word] then
words[#words + 1] = word
end
end
for _, word in ipairs(words) do
local name = canonical(word)
if name then
known[#known + 1] = name
elseif unknown == nil then
unknown = word
end
end
AUTHOR_VARS.typed = text
authorEvent("said", { verb = known[1], noun = known[2], second = known[3], unknown = unknown, text = text })
end
-- ===== Dialogue ===============================================================================
--
-- A dialogue is nodes of a line and choices; the compiler hands them over with each choice's
-- condition and actions as functions. talk runs one from its start and waits for it.
function authorDialogues(list)
dialogues = list
end
function authorTalk(name)
local dialogue = dialogues[name]
if dialogue == nil then
debugPrint("Author: no dialogue called '" .. tostring(name) .. "'")
return
end
local node = dialogue.nodes[dialogue.start]
local was = AUTHOR_CONTROLS
AUTHOR_CONTROLS = false
while node ~= nil do
if node.text then
authorSay(((node.who and (node.who .. ": ")) or "") .. node.text, node.seconds)
end
local offered = {}
for index, choice in ipairs(node.choices or {}) do
local key = name .. "/" .. tostring(node.name) .. "/" .. index
if ((choice.when == nil) or choice.when()) and not (choice.once and onceSeen[key]) then
offered[#offered + 1] = { choice = choice, key = key }
end
end
if #offered == 0 then
node = node.next and dialogue.nodes[node.next] or nil
else
talking = { offered = offered, chosen = nil }
authorWait(function() return talking.chosen ~= nil end)
local picked = offered[talking.chosen]
talking = nil
if picked.choice.once then
onceSeen[picked.key] = true
end
if picked.choice.run then
picked.choice.run()
end
node = picked.choice.next and dialogue.nodes[picked.choice.next] or nil
end
end
AUTHOR_CONTROLS = was
end
-- Whether anything is still happening that a player would wait for: a sequence running, a line
-- on screen. A dialogue waiting for a choice is not busy; it is waiting for the player.
function authorBusy()
if talking ~= nil then
return false
end
if sayLine ~= nil then
return true
end
return next(sequences) ~= nil
end
-- The choices a dialogue is offering, or nil.
function authorChoices()
if talking == nil then
return nil
end
local texts = {}
for index, offer in ipairs(talking.offered) do
texts[index] = offer.choice.text
end
return texts
end
-- For tests and for a pointer: pick a choice by number.
function authorChoose(index)
if talking and talking.offered[index] then
talking.chosen = index
end
end
function authorFade(seconds, out)
local from = fadeLevel
local to = out and 1 or 0
local till = authorTime() + seconds
local start = authorTime()
authorWait(function()
local now = authorTime()
fadeLevel = from + (to - from) * math.min(1, (now - start) / seconds)
return now >= till
end)
fadeLevel = to
end
function authorDie(text)
authorSay(text, nil)
authorFade(1, true)
authorRestart()
fadeLevel = 0
end
-- ===== Saving =================================================================================
--
-- The whole game -- which room, where everything in every visited room is, every var, the
-- inventory -- as one string under a slot. Coroutines cannot be saved, so a save taken during a
-- sequence records the world as it stands and the sequence does not resume.
local function sourceOf(value)
if type(value) == "table" then
local parts = {}
for key, item in pairs(value) do
local name = (type(key) == "string") and (key:match("^[%a_][%w_]*$") and key or ("[" .. string.format("%q", key) .. "]")) or ("[" .. tostring(key) .. "]")
parts[#parts + 1] = name .. " = " .. sourceOf(item)
end
return "{ " .. table.concat(parts, ", ") .. " }"
elseif type(value) == "string" then
return string.format("%q", value)
end
return tostring(value)
end
local function snapshotRoom(list)
local entities = {}
for _, instance in ipairs(list) do
if instance.alive then
local x, y, z = authorPosition(instance)
entities[#entities + 1] = { id = instance.id, type = instance.type, x = x, y = y, z = z, vars = instance.vars }
end
end
return entities
end
function authorSaveGame(slot)
local rooms = {}
for _, room in ipairs(AUTHOR_GAME.rooms) do
if room == AUTHOR_ROOM then
rooms[room.name] = snapshotRoom(AUTHOR_LIVE)
elseif room.kept then
rooms[room.name] = snapshotRoom(room.kept)
end
end
saveSet("forge.slot" .. tostring(slot), "return " .. sourceOf({ room = AUTHOR_ROOM.name, vars = AUTHOR_VARS, rooms = rooms, once = onceSeen }))
end
function authorLoadGame(slot)
local text = saveGet("forge.slot" .. tostring(slot))
if text == nil then
return false
end
local chunk = load(text, "save", "t", {})
if chunk == nil then
return false
end
local saved = chunk()
authorRestart()
AUTHOR_VARS = saved.vars
onceSeen = saved.once or {}
for _, room in ipairs(AUTHOR_GAME.rooms) do
room.saved = saved.rooms[room.name]
if room.kept then
for _, instance in ipairs(room.kept) do
nodeDelete(instance.node)
end
room.kept = nil
end
end
for at = #AUTHOR_LIVE, 1, -1 do
authorDestroy(AUTHOR_LIVE[at])
end
AUTHOR_ROOM = nil
authorGoTo(saved.room)
return true
end
-- ===== Sequences ==============================================================================
--
-- An action that takes time yields from the rule's coroutine until it is done; authorWait is what
-- it yields through. Outside a coroutine (a rule the compiler saw no waiting action in) the wait
-- is skipped and the action's effect stands, so nothing breaks when a rule is edited by hand.
function authorWait(predicate)
if not coroutine.isyieldable() then
return
end
while not predicate() do
coroutine.yield()
end
end
function authorWaitSeconds(seconds)
local till = authorTime() + seconds
authorWait(function() return authorTime() >= till end)
end
function authorSay(text, seconds)
text = tostring(text or "")
seconds = seconds or (SAY_SECONDS_MIN + #text * SAY_PER_CHAR)
sayLine = { text = text, till = authorTime() + seconds }
if hud then
guiSetValue(hud.gui, hud.document, "say", text)
end
authorWait(function() return authorTime() >= sayLine.till end)
end
-- ===== Rules ==================================================================================
--
-- The compiler hands over the rules once; each is { on, filter, each, waits, controls, run }. run
-- takes (self, other, event) and does the conditions and actions. A rule with waits runs as a
-- coroutine kept until it finishes; the same rule on the same instance does not start again while
-- one is running unless the rule says interrupt.
function authorRules(list)
rules = {}
pairsToTest = {}
for _, rule in ipairs(list) do
rules[rule.on] = rules[rule.on] or {}
rules[rule.on][#rules[rule.on] + 1] = rule
if rule.on == "collision" then
pairsToTest[#pairsToTest + 1] = { a = rule.filter.a, b = rule.filter.b }
end
end
-- The first room was built by authorBegin, before this; what it raised -- roomStart, a spawn
-- for every entity placed in it -- is delivered now that there are rules to hear it.
rulesReady = true
for _, held in ipairs(pending) do
authorEvent(held.name, held.event)
end
pending = {}
end
local runRule
-- Runs a rule for an event: once, with the event's own instance as self, or once per instance
-- of the rule's type when the event has none. Answers whether it ran for anything.
local function dispatch(rule, event)
if rule.each and (event.self == nil) then
local any = false
for _, instance in ipairs(authorEach(rule.each)) do
if runRule(rule, instance, event.other, event) then
any = true
end
end
return any
end
return runRule(rule, event.self, event.other, event)
end
-- Runs a rule. Answers whether its conditions held -- a sequence that yielded has passed them,
-- since the conditions come before the first action.
function runRule(rule, self, other, event)
if not rule.waits then
return rule.run(self, other, event) == true
end
local key = tostring(rule) .. "/" .. (self and self.id or "")
if sequences[key] and not rule.interrupt then
return false
end
local co = coroutine.create(function()
local held
if rule.controls == false then
AUTHOR_CONTROLS = false
end
held = rule.run(self, other, event)
if rule.controls == false then
AUTHOR_CONTROLS = true
end
return held
end)
sequences[key] = co
local ok, held = coroutine.resume(co)
if not ok then
debugPrint("Author: " .. tostring(held))
sequences[key] = nil
return false
end
if coroutine.status(co) == "dead" then
sequences[key] = nil
return held == true
end
return true
end
local function matches(rule, event)
for name, want in pairs(rule.filter) do
if name == "type" then
if (event.self == nil) or (event.self.type ~= want) then
return false
end
elseif name == "a" then
if (event.self == nil) or (event.self.type ~= want) then
return false
end
elseif name == "b" then
if (event.other == nil) or (event.other.type ~= want) then
return false
end
elseif event[name] ~= want then
return false
end
end
-- each on an event about an instance narrows it to that type; on an event about nothing in
-- particular (a key, a room, the disc) it fans the rule out over every instance of the type.
if rule.each and (event.self ~= nil) and (event.self.type ~= rule.each) then
return false
end
-- An event belongs to the room it happened in: a press that sends the game to the next room
-- is not also a press in that room.
if rule.room and (event.room ~= rule.room) then
return false
end
return true
end
-- Something happened. event carries self, other, and whatever the event has to say. An
-- exclusive event -- a verb, a typed line -- runs only the most specific rules that match: "use
-- key on door" beats "use anything on door" beats "use".
-- An event about nothing in particular -- a press, a room, the disc -- reaches the behaviours
-- that asked to hear everything: a game loop reading the switches.
function authorFan(name, event)
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.alive and instance.hearsAll then
authorBehaviourEvent(instance, name, event)
end
end
end
function authorEvent(name, event)
local list = rules[name]
local entry = AUTHOR.events[name]
if gameOver and (name ~= "gameOver") and (name ~= "pressed") then
return
end
event.room = event.room or (AUTHOR_ROOM and AUTHOR_ROOM.name)
if event.self then
authorBehaviourEvent(event.self, name, event)
else
authorFan(name, event)
end
if not rulesReady then
pending[#pending + 1] = { name = name, event = event }
return
end
if list == nil then
return
end
if entry and entry.exclusive then
-- Tiers by how many of the event's parameters a rule names, a rule with conditions
-- ranking above one without among the same parameters; the most specific tier whose
-- conditions hold is the one that acts, and the rest are left alone.
local tiers = {}
for _, rule in ipairs(list) do
if matches(rule, event) then
local score = rule.guarded and 1 or 0
for _ in pairs(rule.filter) do
score = score + 2
end
tiers[score] = tiers[score] or {}
tiers[score][#tiers[score] + 1] = rule
end
end
for score = 7, 0, -1 do
local ran = false
for _, rule in ipairs(tiers[score] or {}) do
if dispatch(rule, event) then
ran = true
end
end
if ran then
return
end
end
return
end
for _, rule in ipairs(list) do
if matches(rule, event) then
dispatch(rule, event)
end
end
end
-- The behaviours hear about their instance's events too: a sound plays its clip on hit.
function authorBehaviourEvent(instance, name, event)
for _, b in ipairs(instance.behaviours) do
local behaviour = AUTHOR.behaviours[b.kind]
if behaviour and behaviour.on then
behaviour.on(instance, name, event or {})
end
end
end
local function runFrameRules()
local list = rules.frame
local room = AUTHOR_ROOM and AUTHOR_ROOM.name
if (list == nil) or gameOver then
return
end
-- The frame belongs to the room it began in: a rule that leaves the room does not hand the
-- rest of the frame to the next room's rules.
for _, rule in ipairs(list) do
if (rule.room == nil) or (room == rule.room) then
if rule.each then
for _, instance in ipairs(authorEach(rule.each)) do
runRule(rule, instance, nil, {})
end
else
runRule(rule, nil, nil, {})
end
end
end
end
local function resumeSequences()
local running = {}
-- A snapshot, because a resumed rule may start another sequence, and adding to a table while
-- pairs walks it is not allowed.
for key, co in pairs(sequences) do
running[#running + 1] = { key = key, co = co }
end
for _, entry in ipairs(running) do
if coroutine.status(entry.co) == "dead" then
sequences[entry.key] = nil
elseif sequences[entry.key] == entry.co then
local ok, err = coroutine.resume(entry.co)
if not ok then
debugPrint("Author: " .. tostring(err))
sequences[entry.key] = nil
elseif coroutine.status(entry.co) == "dead" then
sequences[entry.key] = nil
end
end
end
end
-- ===== Rooms ==================================================================================
local function roomNamed(name)
for _, room in ipairs(AUTHOR_GAME.rooms) do
if room.name == name then
return room
end
end
return nil
end
local function buildRoom(room)
local from = room.saved or room.entities or {}
room.saved = nil
for _, entry in ipairs(from) do
make(entry.type, entry.x, entry.y, entry.z, entry)
end
authorRoomNav()
end
-- Leaves the current room, keeping its instances if it wants to be remembered. Kept instances
-- keep their nodes; they are only taken out of the live list and drawn no more.
local function leaveRoom()
if AUTHOR_ROOM == nil then
return
end
authorEvent("roomEnd", { room = AUTHOR_ROOM.name })
if AUTHOR_ROOM.reset then
for at = #AUTHOR_LIVE, 1, -1 do
authorDestroy(AUTHOR_LIVE[at])
end
AUTHOR_ROOM.kept = nil
else
AUTHOR_ROOM.kept = AUTHOR_LIVE
for _, instance in ipairs(AUTHOR_LIVE) do
nodeSetVisible(instance.node, false)
end
end
-- Agents belong to the room's mesh; a walker makes a new one where it arrives.
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.agent then
navAgentDelete(instance.agent)
AUTHOR_AGENTS[instance.agent] = nil
instance.agent = nil
if instance.navNode and (instance.navNode ~= instance.node) then
nodeDelete(instance.navNode)
instance.navNode = nil
end
end
end
AUTHOR_LIVE = {}
AUTHOR_BY_ID = {}
timers = {}
-- Sequences stay: the cut-scene that walked through the door is the one changing rooms, and
-- it has a fade-in and a score still to do on the other side.
end
function authorGoTo(name, instance, x, y, z, at)
local room = roomNamed(name)
if room == nil then
debugPrint("Author: no room called '" .. tostring(name) .. "'")
return
end
local carried = instance and instance.alive and instance or nil
if carried then
-- The one that travels comes out of the room it leaves before it is stashed.
for at = #AUTHOR_LIVE, 1, -1 do
if AUTHOR_LIVE[at] == carried then
table.remove(AUTHOR_LIVE, at)
end
end
end
leaveRoom()
AUTHOR_ROOM = room
if room.kept then
AUTHOR_LIVE = room.kept
room.kept = nil
for _, kept in ipairs(AUTHOR_LIVE) do
AUTHOR_BY_ID[kept.id] = kept
nodeSetVisible(kept.node, true)
if kept.camera then
cameraNode = kept.node
cameraSet(kept.node)
end
end
else
buildRoom(room)
end
if carried then
-- An instance of the same id already in the room gives way to the one arriving.
local standing = AUTHOR_BY_ID[carried.id]
if standing and (standing ~= carried) then
authorDestroy(standing)
end
AUTHOR_LIVE[#AUTHOR_LIVE + 1] = carried
AUTHOR_BY_ID[carried.id] = carried
AUTHOR_BY_NODE[carried.node] = carried
for _, node in ipairs(carried.nodes) do
AUTHOR_BY_NODE[node] = carried
end
if at and AUTHOR_BY_ID[at] then
local ax, ay, az = authorPosition(AUTHOR_BY_ID[at])
authorMoveTo(carried, ax, ay, az)
elseif x ~= nil then
authorMoveTo(carried, x, y, z)
end
end
for _, spawned in ipairs(AUTHOR_LIVE) do
if spawned ~= carried and not spawned.begun then
spawned.begun = true
authorEvent("spawn", { self = spawned })
end
end
authorWavesReset()
authorEvent("roomStart", { room = name })
end
function authorRestart()
for _, room in ipairs(AUTHOR_GAME.rooms) do
if room.kept then
for _, instance in ipairs(room.kept) do
nodeDelete(instance.node)
end
room.kept = nil
end
end
for at = #AUTHOR_LIVE, 1, -1 do
authorDestroy(AUTHOR_LIVE[at])
end
AUTHOR_ROOM = nil
cameraNode = nil
sequences = {}
AUTHOR_VARS = copyOf(AUTHOR_GAME.vars or {})
AUTHOR_VARS.score = AUTHOR_VARS.score or 0
if AUTHOR_GAME.verbs then
AUTHOR_VARS.verb = AUTHOR_GAME.verbs[1]
AUTHOR_VARS.inventory = AUTHOR_VARS.inventory or {}
end
AUTHOR_TYPING = ""
talking = nil
fadeLevel = 0
results = nil
AUTHOR_VARS.best = saveGet(BEST_KEY, 0)
onceSeen = {}
gameOver = false
sayLine = nil
AUTHOR_STARTED = singeGetTicks()
lastTime = 0
lastFrame = -1
authorGoTo(AUTHOR_GAME.rooms[1].name)
end
-- ===== Pointers and guns ======================================================================
-- A player's pointer in overlay coordinates, and whether it is on the picture. A test can set it.
-- The pointer's place, for expressions: pointerX(1) < 360.
function authorPointerX(player)
local x = authorPointer(player)
return x
end
function authorPointerY(player)
local _, y = authorPointer(player)
return y
end
function authorPointer(player)
local device = (player or 1) - 1
if pointers[player or 1] then
return pointers[player or 1].x, pointers[player or 1].y, pointers[player or 1].on
end
if device >= mouseHowMany() then
device = 0
end
local x, y = mouseGetPosition(device)
return x, y, (x >= 0) and (y >= 0) and (x < overlayGetWidth()) and (y < overlayGetHeight())
end
-- For tests: where a player's pointer is, and whether it counts as on the picture.
function authorSetPointer(player, x, y, on)
if x == nil then
pointers[player] = nil
else
pointers[player] = { x = x, y = y, on = (on ~= false) }
end
end
function authorPointerIn(instance, player)
if instance == nil then
return false
end
local px, py, on = authorPointer(player)
local x, y, w, h = authorHitShape(instance)
return on and (x ~= nil) and collidePointRect(px, py, x, y, w, h)
end
function authorPointerOffscreen(player)
local _, _, on = authorPointer(player)
return not on
end
-- What a gun can hit an instance in: its track's box at this frame when it has a hitbox
-- behaviour, its look's box otherwise. nil when the track has no box now.
function authorHitShape(instance)
if instance.hitTrack then
local box = authorTrackBox(instance.hitTrack)
if box == nil then
return nil
end
return box.x, box.y, box.w, box.h
end
return authorBounds(instance)
end
-- A shot from a player's gun. The topmost instance under the pointer is hit; nothing is a miss.
local function fire(gun)
local player = gun.vars.player
local px, py, on = authorPointer(player)
if gun.gun.aim == "centre" then
px, py, on = overlayGetWidth() / 2, overlayGetHeight() / 2, true
end
if gun.vars.ammo <= 0 then
authorBehaviourEvent(gun, "empty")
if (gun.gun.reload == "offscreen") and not on then
authorReload(gun)
end
return
end
if (gun.gun.reload == "offscreen") and not on then
authorReload(gun)
return
end
gun.vars.ammo = gun.vars.ammo - 1
authorBehaviourEvent(gun, "fire")
if on and AUTHOR_3D then
-- A ray through the pointer, from the near plane out: what it hits is a body, and the
-- body's node says which instance -- and which zone of it, when it has zones.
local x0, y0, z0 = sceneUnproject(px, py, 0)
local x1, y1, z1 = sceneUnproject(px, py, 10)
local node, hx, hy, hz = physicsRaycast(x0, y0, z0, x1 - x0, y1 - y0, z1 - z0, RAY_REACH)
local target = node and authorInstanceOf(node) or nil
local casts = 0
-- What the ray meets first may be something a shot passes through: the hero's own body
-- in first person, a trigger, a wall of glass. The cast goes on from just past it, a few
-- times, before the shot counts as a miss.
while node and not (target and target.isTarget) and (casts < 4) do
local dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
local len = math.sqrt(dx * dx + dy * dy + dz * dz)
casts = casts + 1
x0, y0, z0 = hx + dx / len * 0.05, hy + dy / len * 0.05, hz + dz / len * 0.05
x1, y1, z1 = x0 + dx, y0 + dy, z0 + dz
node, hx, hy, hz = physicsRaycast(x0, y0, z0, dx, dy, dz, RAY_REACH)
target = node and authorInstanceOf(node) or nil
end
if target and target.alive and target.isTarget then
-- The body box is what the ray meets first; a zone inside it -- the head -- is found by
-- how close the ray passes to the zone's centre.
local part = "body"
local dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
local len = math.sqrt(dx * dx + dy * dy + dz * dz)
dx, dy, dz = dx / len, dy / len, dz / len
for _, zone in ipairs(target.zones or {}) do
local zx, zy, zz = nodeGetWorldPosition(zone.node)
local t = (zx - x0) * dx + (zy - y0) * dy + (zz - z0) * dz
local cx, cy, cz = x0 + dx * t - zx, y0 + dy * t - zy, z0 + dz * t - zz
if (t > 0) and (math.sqrt(cx * cx + cy * cy + cz * cz) <= zone.reach) then
part = zone.name
end
end
authorEvent("hit", { self = target, other = gun, player = player, x = hx, y = hy, z = hz, part = part })
return
end
elseif on then
for at = #AUTHOR_LIVE, 1, -1 do
local target = AUTHOR_LIVE[at]
if target.alive and target.visible and target.isTarget then
local x, y, w, h = authorHitShape(target)
if (x ~= nil) and collidePointRect(px, py, x, y, w, h) then
authorEvent("hit", { self = target, other = gun, player = player, x = px, y = py })
return
end
end
end
end
authorEvent("miss", { self = gun, player = player, offscreen = not on, x = px, y = py })
end
-- ===== Tracks =================================================================================
--
-- A track is a list of keys by frame or time. A box track answers the box at the current key
-- position, interpolated between keys, nil outside them.
function authorTrackNamed(name)
for _, track in ipairs(AUTHOR_ROOM and AUTHOR_ROOM.tracks or {}) do
if track.name == name then
return track
end
end
for _, track in ipairs(AUTHOR_GAME.tracks or {}) do
if track.name == name then
return track
end
end
return nil
end
local function trackNow(track)
if track.key == "frame" then
return discGetFrame()
end
return authorTime()
end
-- The interpolated box of a track at a position along it.
function authorTrackBoxAt(track, at)
local keys = track.boxes or {}
local before, after
-- A stepped track keeps a box only on the frames it keys: hitboxes read off a film frame
-- by frame (the American Laser Games files) are exact there and absent between.
if track.step then
for _, key in ipairs(keys) do
if key.at == at then
return { x = key.x, y = key.y, w = key.w, h = key.h }
end
end
return nil
end
for _, key in ipairs(keys) do
if key.at <= at and ((before == nil) or (key.at > before.at)) then
before = key
end
if key.at >= at and ((after == nil) or (key.at < after.at)) then
after = key
end
end
if (before == nil) or (after == nil) then
return nil
end
if (before == after) or (after.at == before.at) then
return { x = before.x, y = before.y, w = before.w, h = before.h }
end
local t = (at - before.at) / (after.at - before.at)
return {
x = before.x + (after.x - before.x) * t,
y = before.y + (after.y - before.y) * t,
w = before.w + (after.w - before.w) * t,
h = before.h + (after.h - before.h) * t
}
end
function authorTrackBox(name)
local track = authorTrackNamed(name)
if track == nil then
return nil
end
return authorTrackBoxAt(track, trackNow(track))
end
-- ===== Waves ==================================================================================
--
-- A track of spawns: keys by time, by disc frame, or by a rail camera's stop, each making count
-- instances of a type, one every so many seconds, at an entity named in from -- at each of a
-- comma-separated list of them in turn -- or at the key's own point. A key begins once per
-- visit to the room.
local waves = {} -- The waves under way: { key, made, wait }.
local started = {} -- The keys begun this visit, by key.
local function waveBegin(key)
started[key] = true
waves[#waves + 1] = { key = key, made = 0, wait = 0 }
end
-- Where a wave's next instance goes.
local function wavePlace(wave)
local from = wave.key.from
local names = {}
if type(from) == "table" then
names = from
elseif type(from) == "string" then
for name in from:gmatch("[^,%s]+") do
names[#names + 1] = name
end
end
if #names > 0 then
local x, y, z = authorTargetPosition(names[(wave.made % #names) + 1])
if x then
return x, y, z
end
end
return wave.key.x or 0, wave.key.y or 0, wave.key.z or 0
end
-- Every track of spawns, the room's and the game's.
local function waveTracks()
local found = {}
for _, list in ipairs({ AUTHOR_ROOM and AUTHOR_ROOM.tracks or {}, AUTHOR_GAME.tracks or {} }) do
for _, track in ipairs(list) do
if track.spawns then
found[#found + 1] = track
end
end
end
return found
end
local function updateWaves(dt)
-- Keys by time or by frame begin when the track passes them.
for _, track in ipairs(waveTracks()) do
if track.key ~= "stop" then
local now = trackNow(track)
for _, key in ipairs(track.spawns) do
if not started[key] and (type(key.at) == "number") and (now >= key.at) then
waveBegin(key)
end
end
end
end
-- Each wave under way makes its next instance when its wait is up.
for index = #waves, 1, -1 do
local wave = waves[index]
wave.wait = wave.wait - dt
if wave.wait <= 0 then
local x, y, z = wavePlace(wave)
authorSpawn(wave.key.type, x, y, z)
wave.made = wave.made + 1
wave.wait = wave.key.every or 0
if wave.made >= (wave.key.count or 1) then
table.remove(waves, index)
end
end
end
end
-- A rail camera reached a stop: the waves keyed by that stop begin.
function authorWavesAtStop(name)
for _, track in ipairs(waveTracks()) do
if track.key == "stop" then
for _, key in ipairs(track.spawns) do
if not started[key] and (key.at == name) then
waveBegin(key)
end
end
end
end
end
-- A room begun: its waves start over.
function authorWavesReset()
waves = {}
started = {}
end
-- ===== Timers and the disc ====================================================================
local function updateTimers()
local now = authorTime()
for at = #timers, 1, -1 do
local timer = timers[at]
-- A timer's rule may destroy an instance and take its timers out from under this loop.
if timer == nil then
-- Nothing left at this index.
elseif timer.instance.alive and (now >= timer.at) then
if timer.every then
timer.at = now + timer.every
else
table.remove(timers, at)
end
authorEvent("timer", { self = timer.instance, name = timer.name })
elseif not timer.instance.alive then
table.remove(timers, at)
end
end
end
local function updateDisc()
if (discGetState == nil) or (AUTHOR_GAME.disc == false) then
return
end
local frame = discGetFrame()
local list = rules.frameReached
local room = AUTHOR_ROOM and AUTHOR_ROOM.name
if list and (lastFrame >= 0) and (frame > lastFrame) then
for _, rule in ipairs(list) do
local want = rule.filter.frame
if want and (want > lastFrame) and (want <= frame) and ((rule.room == nil) or (room == rule.room)) then
runRule(rule, nil, nil, { frame = frame, room = room })
end
end
end
lastFrame = frame
end
-- ===== Collisions =============================================================================
--
-- The rules say which types they care about; only those pairs are tested, and a pair that goes on
-- touching is reported once.
local touchingNow = {}
local function checkCollisions()
local seen = {}
for _, pair in ipairs(pairsToTest) do
for _, a in ipairs(authorEach(pair.a)) do
for _, b in ipairs(authorEach(pair.b)) do
if (a ~= b) and a.alive and b.alive and authorTouching(a, b) then
local key = a.id .. "|" .. b.id
seen[key] = true
if not touchingNow[key] then
authorEvent("collision", { self = a, other = b })
end
end
end
end
end
touchingNow = seen
end
-- ===== HUD ====================================================================================
local function updateHud()
if hud == nil then
return
end
for id, var in pairs(hud.bind) do
local value = AUTHOR_VARS[var]
if type(value) == "table" then
value = table.concat(value, ", ")
else
value = tostring(value or "")
end
if hud.shown[id] ~= value then
hud.shown[id] = value
guiSetValue(hud.gui, hud.document, id, value)
end
end
if sayLine and (authorTime() >= sayLine.till) then
sayLine = nil
guiSetValue(hud.gui, hud.document, "say", "")
end
end
-- ===== The frame ==============================================================================
-- Builds the game from its description: layers, vars, and the first room. The compiled game
-- calls this once, then authorRules with what it compiled, then authorFrame every frame.
function authorBegin(game)
-- A font, because the text look draws with fontPrint and fontPrint ends the game when none is
-- selected. A game that wants its own calls fontSelect afterwards.
if AUTHOR_FONT == nil then
AUTHOR_FONT = fontLoad("Singe/FreeSansBold.ttf", AUTHOR_FONT_POINTS)
fontSelect(AUTHOR_FONT)
fontQuality(FONT_QUALITY_BLENDED)
end
MOUSE_LEFT = SWITCH_BUTTON3
AUTHOR_GAME = game
-- The overlay the game was drawn for. Without it the engine's default stands, which over a
-- disc is the disc's own size.
if game.size then
overlaySetResolution(game.size.w, game.size.h)
end
game.types = game.types or {}
game.rooms = game.rooms or { { name = "main", entities = {} } }
game.disc = false
for _, layer in ipairs(game.layers or {}) do
local kind = AUTHOR.layers[layer.kind]
if kind == nil then
debugPrint("Author: no layer called '" .. tostring(layer.kind) .. "'")
else
if layer.kind == "disc" then
game.disc = true
end
kind.begin(layer)
end
end
-- What is behind the overlay: the disc or the scene when the game has one, and a dark ground
-- of its own otherwise, so a flat game never plays over whatever video the engine was given.
if game.disc or AUTHOR_3D then
colorBackground(0, 0, 0, 0)
else
colorBackground(AUTHOR_GROUND.r, AUTHOR_GROUND.g, AUTHOR_GROUND.b, 255)
end
if (game.players or 1) > 1 then
mouseSetMode(MOUSE_MANY)
end
authorRestart()
end
-- One frame: timers, behaviours, sequences, the frame rules, collisions, the HUD, and everything
-- drawn in the order it was made. The generated game calls this from onOverlayUpdate.
function authorFrame()
local now = authorTime()
local dt = math.min(now - lastTime, FRAME_CAP)
lastTime = now
updateTimers()
updateDisc()
updateWaves(dt)
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.alive then
for _, b in ipairs(instance.behaviours) do
local behaviour = AUTHOR.behaviours[b.kind]
if behaviour and behaviour.step then
behaviour.step(instance, dt)
end
end
end
end
resumeSequences()
runFrameRules()
checkCollisions()
updateSentence()
updateHud()
updateBezel()
if masterReady and (masterPump ~= nil) then
masterPump()
end
pressedNow = {}
AUTHOR_MOUSE.dx = 0
AUTHOR_MOUSE.dy = 0
overlayClear()
if AUTHOR_ROOM and AUTHOR_ROOM.depthSort then
-- Further up the picture is further away, so what is lower is drawn last, over it.
local order = {}
for at, instance in ipairs(AUTHOR_LIVE) do
order[at] = instance
instance.order = at
end
table.sort(order, function(a, b)
local ay, by = authorDepthY(a), authorDepthY(b)
if ay == by then
return a.order < b.order
end
return ay < by
end)
for _, instance in ipairs(order) do
if instance.alive and instance.visible then
AUTHOR.looks[instance.look.kind].draw(instance)
end
end
else
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.alive and instance.visible then
AUTHOR.looks[instance.look.kind].draw(instance)
end
end
end
-- Particles in a 2D room are drawn on request, unlike the scene's.
if not AUTHOR_3D then
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.alive and instance.emitter then
emitterDraw(instance.emitter)
end
end
end
if talking then
-- The choices, numbered, low on the picture (or in the hud's choices element).
local lines = {}
for index, offer in ipairs(talking.offered) do
lines[#lines + 1] = index .. ". " .. offer.choice.text
end
if hud then
guiSetValue(hud.gui, hud.document, "choices", table.concat(lines, "\n"))
else
colorForeground(255, 230, 120, 255)
for index, line in ipairs(lines) do
fontPrint(40, overlayGetHeight() - 40 - (#lines - index + 1) * 22, line)
end
end
elseif hud and hud.choicesShown then
guiSetValue(hud.gui, hud.document, "choices", "")
end
if hud then
hud.choicesShown = (talking ~= nil)
end
if parserLayer then
colorForeground(235, 235, 245, 255)
fontPrint(12, overlayGetHeight() - 30, (parserLayer.prompt or ">") .. AUTHOR_TYPING .. "_")
end
if results then
local middle = overlayGetHeight() / 2
colorForeground(255, 230, 120, 255)
fontPrint(overlayGetWidth() / 2 - 60, middle - 30, "GAME OVER")
colorForeground(235, 235, 245, 255)
fontPrint(overlayGetWidth() / 2 - 60, middle, "score " .. results.score)
fontPrint(overlayGetWidth() / 2 - 60, middle + 24, "best " .. results.best)
end
if fadeLevel > 0 then
colorForeground(0, 0, 0, math.floor(255 * fadeLevel))
for row = 0, overlayGetHeight() - 1 do
overlayLine(0, row, overlayGetWidth(), row)
end
end
if flashLine then
if now >= flashLine.till then
flashLine = nil
else
local alpha = math.floor(255 * (flashLine.till - now) / flashLine.seconds)
colorForeground(flashLine.r, flashLine.g, flashLine.b, alpha)
for row = 0, overlayGetHeight() - 1, 2 do
overlayLine(0, row, overlayGetWidth(), row)
end
end
end
if sayLine and (hud == nil) then
if now >= sayLine.till then
sayLine = nil
else
colorForeground(255, 255, 255, 255)
fontPrint(overlayGetWidth() / 2 - #sayLine.text * 4, overlayGetHeight() - 60, sayLine.text)
end
end
if hud then
guiDraw(hud.gui)
end
end
-- ===== Input ==================================================================================
--
-- Held state, because rules ask "is this key down" rather than "was it just pressed", and edges,
-- because some do. The generated game points the engine's callbacks straight at these.
AUTHOR_KEYS = {}
AUTHOR_SWITCHES = {}
function authorKeyDown(keysym, scancode)
AUTHOR_KEYS[scancode] = true
pressedNow[scancode] = true
-- A dialogue's choices take the number keys first.
if talking then
for index, name in ipairs(CHOICE_KEYS) do
if scancode == SCANCODE[name].value then
authorChoose(index)
return
end
end
end
-- The parser takes the typing keys.
if parserLayer and AUTHOR_CONTROLS then
if scancode == SCANCODE.RETURN.value then
local line = AUTHOR_TYPING
AUTHOR_TYPING = ""
if line ~= "" then
authorSaid(line)
end
return
elseif scancode == SCANCODE.BACKSPACE.value then
AUTHOR_TYPING = string.sub(AUTHOR_TYPING, 1, -2)
return
elseif (keysym >= 32) and (keysym < 127) then
AUTHOR_TYPING = AUTHOR_TYPING .. string.char(keysym)
return
end
end
if AUTHOR_CONTROLS then
local px, py = authorPointer(1)
authorEvent("pressed", { key = scancode, x = px, y = py })
end
end
function authorKeyUp(keysym, scancode)
AUTHOR_KEYS[scancode] = nil
authorEvent("released", { key = scancode })
end
-- In MODE_FULL the engine hands every key to onInputPressed as its keysym, on the same channel
-- as the switches, and four keysyms are switch numbers too: BACKSPACE is 8 (SWITCH_BUTTON3),
-- TAB 9 (SWITCH_COIN1), RETURN 13, and ESCAPE 27. The key is down at that moment and the switch
-- is not, which tells them apart.
local KEY_MASQUERADES = { [8] = "BACKSPACE", [9] = "TAB", [13] = "RETURN", [27] = "ESCAPE" }
function authorSwitchIsKey(what)
local name = KEY_MASQUERADES[what]
return (name ~= nil) and keyboardIsDown(SCANCODE[name].value)
end
-- The engine names the device a switch came from (a mouse or gun by its index); a press with
-- one is that player's, and only that player's gun fires on its trigger.
function authorSwitchDown(what, device)
local player = device and (device + 1) or nil
if authorSwitchIsKey(what) then
return
end
AUTHOR_SWITCHES[what] = true
pressedNow[what] = true
if not AUTHOR_CONTROLS then
return
end
for _, instance in ipairs(AUTHOR_LIVE) do
if instance.alive and instance.gun and (what == (instance.trigger or MOUSE_LEFT)) and ((player == nil) or (instance.vars.player == player)) then
fire(instance)
end
end
if talking and (what == MOUSE_LEFT) then
-- A click picks the choice under the pointer, by its line.
local _, py = authorPointer(1)
local count = #talking.offered
local index = count - math.floor((overlayGetHeight() - 40 - py) / 22)
authorChoose(math.max(1, math.min(count, index)))
return
end
if AUTHOR_GAME.verbs and (what == MOUSE_LEFT) and verbPress(1) then
return
end
local px, py = authorPointer(player or 1)
authorEvent("pressed", { switch = what, x = px, y = py, player = player })
end
function authorSwitchUp(what, device)
if authorSwitchIsKey(what) then
return
end
AUTHOR_SWITCHES[what] = nil
authorEvent("released", { switch = what, player = device and (device + 1) or nil })
end
-- ===== The KarisFramework game loop ===========================================================
--
-- The qte behaviour plays a laserdisc quick-time-event game the way the KarisFramework does:
-- an attract loop, credits and a start, levels of scenes, each scene a run of moves with a
-- window of frames to answer in, a death clip and a life lost for a wrong or missed one, a
-- continue screen, a game over, and the framework's dips deciding how strict, how many lives,
-- and what plays in between.
--
-- The state lives in one table, q, under the framework's own names -- iScore, iLives, iLevel,
-- currentMove, move[], stage[], SCOREMOVE, offsetGetReady, dip_Difficulty -- because a game
-- brings Lua of its own: its settings script declares the tables and a setupMoves that may read
-- the state (Space Ace picks a scene by which levels are beaten), and its Script/addons.singe
-- hooks the loop (startConf, swapLevel, swapScene, swapDeath, specialScore, doLevelSelect).
-- Both run inside a shim whose globals are q, so they play as they do in the framework. The
-- description names them under game.qte (util/forgePortKaris.lua writes it), and carries a
-- snapshot of the same tables for the editor. The states and their numbers are the
-- framework's, so a run of the original and a run of the port trace alike (FORGE.md 14).
local Q = {} -- The loop's functions, one table so the file keeps within Lua's count of locals.
local QTE = {
-- The framework's constants, as globals.singe declares them.
UP = 1, DOWN = 2, LEFT = 3, RIGHT = 4, BUTTON1 = 5, BUTTON2 = 6, BUTTON3 = 7, BUTTON4 = 8,
UPLEFT = 9, UPRIGHT = 10, DOWNLEFT = 11, DOWNRIGHT = 12, ACTUP = 13, ACTDOWN = 14, ACTLEFT = 15, ACTRIGHT = 16,
MASH = 17, MASHMIN = 18, MASHMAX = 19, LETGO = 20, HOLDUP = 21, HOLDDOWN = 22, HOLDLEFT = 23, HOLDRIGHT = 24, HOLDBUT = 25,
DOUBLE = 26, RUN = 27, RUNMIN = 28, RUNMAX = 29, MASH2 = 30, MASH2MIN = 31, MASH2MAX = 32, MULTI = 33, LOOPLEFT = 35, LOOPRIGHT = 36,
CHOOSE = 50, PATH = 51, YESNO = 52, TIMED = 53, MTIMED = 54, WAY = 98, WAYOUT = 99, SKIP = 100,
-- The MazescaterFramework's compound kinds (LINEA): two and three inputs in turn, circles
-- from any of four starts, mashes of one direction or button (MASH3 alternates BUTTON1 and
-- BUTTON3), and a direction with BUTTON2 or BUTTON3, or two directions with a button.
DLB = 149, DRB = 150, ULB = 151, URB = 152, MASH3 = 153, MASH3MIN = 154, MASH3MAX = 155,
UL = 156, UR = 157, DL = 158, DR = 159, LU = 160, RU = 161, LD = 162, RD = 163, LUB = 164, RUB = 165, RDB = 166, LDB = 167, DLB2 = 168, DRB2 = 169,
RL = 170, LR = 171, UD = 172, DU = 173, ULB2 = 174, URB2 = 175, LUB2 = 176, RUB2 = 177, RDB2 = 178, LDB2 = 179,
DLU = 180, DRU = 181, LDR = 182, LUR = 183, RDL = 184, RUL = 185, ULD = 186, URD = 187,
MASHB2 = 188, MASHB2MAX = 189, MASHB2MIN = 190, MASHB3 = 192, MASHB3MAX = 193, MASHB3MIN = 194,
MASHLEFT = 196, MASHLEFTMIN = 197, MASHLEFTMAX = 198, MASHRIGHT = 200, MASHRIGHTMIN = 201, MASHRIGHTMAX = 202,
MASHUP = 204, MASHUPMIN = 205, MASHUPMAX = 206, MASHDOWN = 208, MASHDOWNMIN = 209, MASHDOWNMAX = 210,
LOOPRIGHTL = 212, LOOPRIGHTU = 213, LOOPRIGHTR = 214, LOOPRIGHTD = 215, LOOPLEFTL = 216, LOOPLEFTU = 217, LOOPLEFTR = 218, LOOPLEFTD = 219,
ACT2UP = 220, ACT2DOWN = 221, ACT2LEFT = 222, ACT2RIGHT = 223, ACT3UP = 224, ACT3DOWN = 225, ACT3LEFT = 226, ACT3RIGHT = 227,
ACTUPLEFT = 228, ACTUPRIGHT = 229, ACTDOWNLEFT = 230, ACTDOWNRIGHT = 231, ACT2UPLEFT = 232, ACT2UPRIGHT = 233, ACT2DOWNLEFT = 234, ACT2DOWNRIGHT = 235,
ACT3UPLEFT = 236, ACT3UPRIGHT = 237, ACT3DOWNLEFT = 238, ACT3DOWNRIGHT = 239,
NOMOVE = -1, MOVEPENDING = -2, MOVEFAIL = -3, OUT = 1000,
inputFrmStart = 1, inputFrmEnd = 2, correctMove = 3, moveDeath = 4, moveFrmStart = 7, moveFrmEnd = 8, curDeathStart = 1, curDeathEnd = 2,
TITLE = 1, INTROCLIP = 2, INTROCLIPEND = 3, TOTALSCENES = 4, MIRROR = 5, DTHMIRROR = 6, LVLREPLAY = 7,
LEVELSTARTED = 1, BEATSTATUS = 2, DEATHCOUNT = 3, SCENEID = 1, SCENECOMPLETE = 2, NODEATH = -1, NODEATHSCORE = -2,
lvlSetup = 0, lvlRunning = 1, lvlEnd = 2, lvlPlayDeath = 3, lvlPlayRest = 4,
branch01 = 10, branch02 = 11, branch03 = 12, branch04 = 13, branch05 = 14, branch06 = 15, branch07 = 16, branch08 = 17, branch09 = 18, branch10 = 19, branch11 = 20, branch12 = 21,
levelIntro = 0, levelContinue = 100, levelGameOver = 101, levelChoose = 102, levelNormal = 103, levelService = 104, levelMap = 105, levelHighScore = 106,
levelMovie = 107, levelSave = 108, levelDiffScreen = 109, levelFinish = 110, levelTrophy = 111, levelExit = 112, levelSecret = 1000,
DOPT_FREEPLAY = 0, DOPT_INFINITE_CONTINUES = 4,
WHITE = 1, RED = 2, BLUE = 3, YELLOW = 4, GREEN = 5, ORANGE = 6, PINK = 7, PURPLE = 8, LIGHTBLUE = 9, GREY = 10, BLACK = 11, MISC = 12,
TOP = 1, BOTTOM = 2, MIDDLE = 3
}
-- The compound kinds as data: the inputs of a sequence in turn (a circle is five, back to its
-- start), the switch or pair a mash takes with its count per second of the window, and the
-- flags a combination wants all at once.
local MOVE_FLAG = { [QTE.UP] = "p1UP", [QTE.DOWN] = "p1DOWN", [QTE.LEFT] = "p1LEFT", [QTE.RIGHT] = "p1RIGHT", [QTE.BUTTON1] = "p1BUTTON1", [QTE.BUTTON2] = "p1BUTTON2", [QTE.BUTTON3] = "p1BUTTON3" }
local MAZE_SEQ = {}
local MAZE_MASH = {}
do
local U, D, L, R, B1, B2, B3 = QTE.UP, QTE.DOWN, QTE.LEFT, QTE.RIGHT, QTE.BUTTON1, QTE.BUTTON2, QTE.BUTTON3
MAZE_SEQ = {
[QTE.UL] = { U, L }, [QTE.UR] = { U, R }, [QTE.DL] = { D, L }, [QTE.DR] = { D, R }, [QTE.LU] = { L, U }, [QTE.RU] = { R, U }, [QTE.LD] = { L, D }, [QTE.RD] = { R, D },
[QTE.RL] = { R, L }, [QTE.LR] = { L, R }, [QTE.UD] = { U, D }, [QTE.DU] = { D, U },
[QTE.DLB] = { D, L, B1 }, [QTE.DRB] = { D, R, B1 }, [QTE.ULB] = { U, L, B1 }, [QTE.URB] = { U, R, B1 }, [QTE.LUB] = { L, U, B1 }, [QTE.RUB] = { R, U, B1 }, [QTE.RDB] = { R, D, B1 }, [QTE.LDB] = { L, D, B1 },
[QTE.DLB2] = { D, L, B2 }, [QTE.DRB2] = { D, R, B2 }, [QTE.ULB2] = { U, L, B2 }, [QTE.URB2] = { U, R, B2 }, [QTE.LUB2] = { L, U, B2 }, [QTE.RUB2] = { R, U, B2 }, [QTE.RDB2] = { R, D, B2 }, [QTE.LDB2] = { L, D, B2 },
[QTE.DLU] = { D, L, U }, [QTE.DRU] = { D, R, U }, [QTE.LDR] = { L, D, R }, [QTE.LUR] = { L, U, R }, [QTE.RDL] = { R, D, L }, [QTE.RUL] = { R, U, L }, [QTE.ULD] = { U, L, D }, [QTE.URD] = { U, R, D },
[QTE.LOOPLEFTL] = { L, D, R, U, L }, [QTE.LOOPLEFTD] = { D, R, U, L, D }, [QTE.LOOPLEFTR] = { R, U, L, D, R }, [QTE.LOOPLEFTU] = { U, L, D, R, U },
[QTE.LOOPRIGHTL] = { L, U, R, D, L }, [QTE.LOOPRIGHTU] = { U, R, D, L, U }, [QTE.LOOPRIGHTR] = { R, D, L, U, R }, [QTE.LOOPRIGHTD] = { D, L, U, R, D }
}
for _, mash in ipairs({ { QTE.MASHB2, QTE.MASHB2MIN, QTE.MASHB2MAX, 3, B2 }, { QTE.MASHB3, QTE.MASHB3MIN, QTE.MASHB3MAX, 3, B3 },
{ QTE.MASHLEFT, QTE.MASHLEFTMIN, QTE.MASHLEFTMAX, 6, L }, { QTE.MASHRIGHT, QTE.MASHRIGHTMIN, QTE.MASHRIGHTMAX, 6, R },
{ QTE.MASHUP, QTE.MASHUPMIN, QTE.MASHUPMAX, 6, U }, { QTE.MASHDOWN, QTE.MASHDOWNMIN, QTE.MASHDOWNMAX, 6, D },
{ QTE.MASH3, QTE.MASH3MIN, QTE.MASH3MAX, 6, B1, B3 } }) do
local spec = { min = mash[2], max = mash[3], times = mash[4], left = mash[5], right = mash[6] }
MAZE_MASH[mash[1]], MAZE_MASH[mash[2]], MAZE_MASH[mash[3]] = spec, spec, spec
end
end
local ACT_WANTS = { [QTE.ACTUP] = { "p1BUTTON1", "p1UP" }, [QTE.ACTDOWN] = { "p1BUTTON1", "p1DOWN" }, [QTE.ACTLEFT] = { "p1BUTTON1", "p1LEFT" }, [QTE.ACTRIGHT] = { "p1BUTTON1", "p1RIGHT" },
[QTE.UPLEFT] = { "p1UP", "p1LEFT" }, [QTE.UPRIGHT] = { "p1UP", "p1RIGHT" }, [QTE.DOWNLEFT] = { "p1DOWN", "p1LEFT" }, [QTE.DOWNRIGHT] = { "p1DOWN", "p1RIGHT" },
[QTE.ACT2UP] = { "p1BUTTON2", "p1UP" }, [QTE.ACT2DOWN] = { "p1BUTTON2", "p1DOWN" }, [QTE.ACT2LEFT] = { "p1BUTTON2", "p1LEFT" }, [QTE.ACT2RIGHT] = { "p1BUTTON2", "p1RIGHT" },
[QTE.ACT3UP] = { "p1BUTTON3", "p1UP" }, [QTE.ACT3DOWN] = { "p1BUTTON3", "p1DOWN" }, [QTE.ACT3LEFT] = { "p1BUTTON3", "p1LEFT" }, [QTE.ACT3RIGHT] = { "p1BUTTON3", "p1RIGHT" },
[QTE.ACTUPLEFT] = { "p1BUTTON1", "p1UP", "p1LEFT" }, [QTE.ACTUPRIGHT] = { "p1BUTTON1", "p1UP", "p1RIGHT" }, [QTE.ACTDOWNLEFT] = { "p1BUTTON1", "p1DOWN", "p1LEFT" }, [QTE.ACTDOWNRIGHT] = { "p1BUTTON1", "p1DOWN", "p1RIGHT" },
[QTE.ACT2UPLEFT] = { "p1BUTTON2", "p1UP", "p1LEFT" }, [QTE.ACT2UPRIGHT] = { "p1BUTTON2", "p1UP", "p1RIGHT" }, [QTE.ACT2DOWNLEFT] = { "p1BUTTON2", "p1DOWN", "p1LEFT" }, [QTE.ACT2DOWNRIGHT] = { "p1BUTTON2", "p1DOWN", "p1RIGHT" },
[QTE.ACT3UPLEFT] = { "p1BUTTON3", "p1UP", "p1LEFT" }, [QTE.ACT3UPRIGHT] = { "p1BUTTON3", "p1UP", "p1RIGHT" }, [QTE.ACT3DOWNLEFT] = { "p1BUTTON3", "p1DOWN", "p1LEFT" }, [QTE.ACT3DOWNRIGHT] = { "p1BUTTON3", "p1DOWN", "p1RIGHT" } }
-- The framework's flag for each switch.
QTE.FLAG_OF = nil
local function qteFlagOf(switch)
if QTE.FLAG_OF == nil then
QTE.FLAG_OF = { [SWITCH_UP] = "p1UP", [SWITCH_DOWN] = "p1DOWN", [SWITCH_LEFT] = "p1LEFT", [SWITCH_RIGHT] = "p1RIGHT",
[SWITCH_BUTTON1] = "p1BUTTON1", [SWITCH_BUTTON2] = "p1BUTTON2", [SWITCH_BUTTON3] = "p1BUTTON3", [SWITCH_BUTTON4] = "p1BUTTON4",
[SWITCH_START1] = "p1START1", [SWITCH_START2] = "p1START2", [SWITCH_COIN1] = "p1COIN1", [SWITCH_COIN2] = "p1COIN2",
[SWITCH_SERVICE] = "p1SERVICE" }
end
return QTE.FLAG_OF[switch]
end
-- ----- Timers, sounds, clips ----------------------------------------------------------------
function Q.timerON(q, seconds)
q.timerLimit = seconds
q.timerFrom = authorTime()
end
function Q.timerDue(q)
if q.timerLimit == nil then
return true
end
if authorTime() - q.timerFrom >= q.timerLimit then
q.timerLimit = nil
return true
end
return false
end
function Q.joyDelayON(q, seconds)
q.joyLimit = seconds
q.joyFrom = authorTime()
end
function Q.joyDelayDue(q)
if q.joyLimit == nil then
return true
end
if authorTime() - q.joyFrom >= q.joyLimit then
q.joyLimit = nil
return true
end
return false
end
function Q.setupClip(q, from, to)
q.iFrameStart = from
q.iFrameEnd = to
discSkipToFrame(from)
end
-- A framework sound by its handle name (sndcoin, sndright, ...), loaded from the game's Sounds
-- directory on first use.
function Q.sound(q, name)
local file = q.sounds[name]
if file then
authorPlaySound(file, 100, q.instance)
end
end
-- The secret level's combination on the attract screens: the framework's 3.32b asks for
-- BUTTON4 and UP, the earlier versions for BUTTON2, BUTTON3, UP, and RIGHT.
function Q.secretPressed(q)
if not q.AllowSecret then
return false
end
if q.snapshot.framework == "3.32b" then
return q.p1BUTTON4 and q.p1UP
end
return q.p1BUTTON2 and q.p1BUTTON3 and q.p1UP and q.p1RIGHT
end
-- The MazescaterFramework (LINEA): Karis 3.31c with the compound kinds, a longer look at
-- the hints, a slower MASH2, and every switch counted for a mash.
function Q.mazescater(q)
return (tostring(q.snapshot.framework or "")):find("Mazescater", 1, true) ~= nil
end
function Q.scanInput(q)
if q.p1BUTTON1 then return QTE.BUTTON1 end
if q.p1BUTTON2 then return QTE.BUTTON2 end
if q.p1BUTTON3 then return QTE.BUTTON3 end
if q.p1UP then return QTE.UP end
if q.p1DOWN then return QTE.DOWN end
if q.p1LEFT then return QTE.LEFT end
if q.p1RIGHT then return QTE.RIGHT end
return QTE.NOMOVE
end
function Q.clearInput(q)
q.p1UP, q.p1DOWN, q.p1LEFT, q.p1RIGHT, q.p1BUTTON1, q.p1BUTTON2, q.p1BUTTON3 = false, false, false, false, false, false, false
end
-- ----- Scoring and what was beaten ----------------------------------------------------------
function Q.addPoints(q, thisMuch, thisValue)
q.thisScore = 0
if q.specialScore then
q.specialScore(thisValue)
end
if q.dip_GameType ~= 3 then
local worth = (q.thisScore == 0) and thisMuch or q.thisScore
q.iScore = q.iScore + worth
q.iExtraLife = q.iExtraLife + worth
if (q.EXTRALIFE > 0) and (q.iExtraLife >= q.EXTRALIFE) and (q.dip_GameType == 0) then
q.iExtraLife = 0
if q.iLives < q.dip_LivesPerCredit then
Q.sound(q, "sndvictory")
q.iLives = q.iLives + 1
end
end
if (q.dip_GameType == 1) and (q.iRightMv == q.BarBonusT) and (q.BarBonusT ~= 0) and (q.iLifeBar < q.BarSize) then
q.iRightMv = 0
q.iLifeBar = q.iLifeBar + 1
end
elseif thisValue == 1 then
q.iScore = q.iScore + 1
end
if q.iScore > q.iTop then
q.iTop = q.iScore
end
if q.iScore > 99999999 then
q.iScore = 99999999
end
end
function Q.beatLevel(q, thisLevel)
local k = q.Level[thisLevel][QTE.TOTALSCENES]
return q.scene[thisLevel][k][QTE.SCENECOMPLETE]
end
function Q.beatGame(q)
for k = 1, q.finalstage do
if not q.stage[k][QTE.BEATSTATUS] then
return false
end
end
return true
end
function Q.beatGameWithOneLife(q)
if not Q.beatGame(q) then
return false
end
for k = 1, q.finalstage do
if q.stage[k][QTE.DEATHCOUNT] > 0 then
return false
end
end
return true
end
function Q.beatGameWithOneCredit(q)
return Q.beatGame(q) and (q.iContinues == 0)
end
function Q.newScore(q, score)
for k = 1, 10 do
local entry = q.highScores[k]
if entry and (score >= entry[2]) then
return true
end
end
return false
end
-- ----- Levels: their order, their scenes, their frames --------------------------------------
function Q.initStages(q)
q.stage = {}
q.scene = {}
for k = 1, q.finalstage do
q.scene[k] = {}
q.stage[k] = { false, false, 0 }
q.LvlOrder[k] = k
for i = 1, q.Level[k][QTE.TOTALSCENES] do
q.scene[k][i] = { i, false }
end
end
if q.AllowSecret and q.Level[QTE.levelSecret] then
q.stage[QTE.levelSecret] = { false, false, 0 }
q.scene[QTE.levelSecret] = {}
for i = 1, q.Level[QTE.levelSecret][QTE.TOTALSCENES] do
q.scene[QTE.levelSecret][i] = { i, false }
end
end
end
function Q.doMixSEQ(q)
q.LvlOrder = {}
for i = 1, q.finalstage do
q.LvlOrder[i] = q.PlayOrder[i]
end
end
function Q.doMixTIE(q)
local count = 1
q.LvlOrder = {}
q.LvlOrder[q.finalstage] = q.finalstage
for i = 1, q.Tiers[0][1] do
local size = q.Tiers[0][i + 1]
local tier = q.Tiers[i]
local took = 1
while took <= size do
local pick = math.random(size)
local placed = false
for k = 1, q.finalstage do
if q.LvlOrder[k] == tier[pick] then
placed = true
break
end
end
if not placed then
q.LvlOrder[count] = tier[pick]
count = count + 1
took = took + 1
end
end
end
end
function Q.doMixRND(q)
q.LvlOrder = {}
q.LvlOrder[q.finalstage] = q.finalstage
for w = 1, q.finalstage - 1 do
while true do
local pick = math.random(q.finalstage - 1)
local placed = false
for k = 1, q.finalstage - 1 do
if q.LvlOrder[k] == pick then
placed = true
break
end
end
if not placed then
q.LvlOrder[w] = pick
break
end
end
end
end
function Q.nextLevel(q, thisLevel)
q.iScPlayed, q.iScDeath, q.iTotDeath = 0, 0, 0
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
q.iScene = 0
elseif q.iPath ~= 0 then
q.iLevel = q.iPath
q.iPath = 0
q.iScene = 0
else
for k = 1, q.finalstage - 1 do
if q.LvlOrder[k] == thisLevel then
q.iLevel = q.LvlOrder[k + 1]
q.iScene = 0
break
end
end
end
if q.swapLevel then
q.swapLevel()
end
end
function Q.reOrder(q, thisLevel)
local replay = q.Level[thisLevel][QTE.LVLREPLAY]
local n = 0
for k = 1, q.finalstage - 1 do
if q.LvlOrder[k] == thisLevel then
n = q.LvlOrder[k + 1]
for i = k, replay - 1 do
q.LvlOrder[i] = q.LvlOrder[i + 1]
end
q.LvlOrder[replay] = thisLevel
break
end
end
q.iLevel = n
q.iScene = 0
if q.swapLevel then
q.swapLevel()
end
end
function Q.onToNextLevel(q)
q.bSkipIntroClip = false
q.iLiveSave = q.iLives
q.iScoreSave = q.iScore
q.bAllowSave = true
q.bRes = true
q.iScoreTemp = 0
q.iBonus = 0
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
else
Q.nextLevel(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
function Q.levelReplay(q)
local replay = q.Level[q.iLevel][QTE.LVLREPLAY]
if replay == 0 then
Q.onToNextLevel(q)
elseif replay == 1 then
if not q.stage[q.iLevel][QTE.LEVELSTARTED] then
q.stage[q.iLevel][QTE.LEVELSTARTED] = true
else
Q.onToNextLevel(q)
end
elseif replay > 1 then
q.bSkipIntroClip = false
q.iLiveSave = q.iLives
q.iScoreSave = q.iScore
q.bAllowSave = true
q.bRes = true
q.iScoreTemp = 0
q.iBonus = 0
if not q.stage[q.iLevel][QTE.LEVELSTARTED] then
q.stage[q.iLevel][QTE.LEVELSTARTED] = true
if q.dip_PlayStyle == 3 then
q.currentLevel = QTE.levelMap
else
Q.reOrder(q, q.iLevel)
q.currentLevel = QTE.levelNormal
end
else
if q.dip_PlayStyle == 3 then
q.currentLevel = QTE.levelMap
else
Q.nextLevel(q, q.iLevel)
q.currentLevel = QTE.levelNormal
end
end
q.lvlState = QTE.lvlSetup
end
end
-- The framework's frame arithmetic on a scene's moves.
function Q.setupFrames(q, thisLevel)
local level = q.Level[thisLevel]
local w = q.RelativeFrames and 1 or 0
local flipBy = q.bFlip and level[QTE.MIRROR] or 0
local move = q.move
q.sceneStart = q.sceneStart + w * level[QTE.INTROCLIP] + flipBy
q.sceneEnd = q.sceneEnd + w * level[QTE.INTROCLIP] + flipBy
q.Tlimit = q.sceneStart + 50
for k = 1, q.totalMoves do
move[k][1] = move[k][1] + w * level[QTE.INTROCLIP] + flipBy + q.iPenal
move[k][2] = move[k][2] + w * level[QTE.INTROCLIP] + flipBy
end
for k = 1, q.totalMoves do
local m = move[k]
if q.dip_GameType == 4 then
if not ((m[3] >= QTE.CHOOSE) and (m[3] <= QTE.YESNO)) then
m[3] = QTE.BUTTON1
end
else
if m[3] == q.DOUBLE then
m[3], m[5], m[6] = QTE.MULTI, QTE.BUTTON1, 2
end
if q.dip_Difficulty == 0 then
local easy = { [QTE.ACTUP] = QTE.UP, [QTE.ACTDOWN] = QTE.DOWN, [QTE.ACTLEFT] = QTE.LEFT, [QTE.ACTRIGHT] = QTE.RIGHT,
[QTE.HOLDUP] = QTE.UP, [QTE.HOLDDOWN] = QTE.DOWN, [QTE.HOLDLEFT] = QTE.LEFT, [QTE.HOLDRIGHT] = QTE.RIGHT,
[QTE.MASH] = QTE.BUTTON1, [QTE.MASHMIN] = QTE.BUTTON1, [QTE.MASHMAX] = QTE.BUTTON1, [QTE.LETGO] = QTE.BUTTON1,
[QTE.RUN] = QTE.BUTTON1, [QTE.RUNMIN] = QTE.BUTTON1, [QTE.RUNMAX] = QTE.BUTTON1,
[QTE.LOOPLEFT] = QTE.LEFT, [QTE.LOOPRIGHT] = QTE.RIGHT,
[QTE.ACT2UP] = QTE.UP, [QTE.ACT3UP] = QTE.UP, [QTE.ACT2DOWN] = QTE.DOWN, [QTE.ACT3DOWN] = QTE.DOWN,
[QTE.ACT2LEFT] = QTE.LEFT, [QTE.ACT3LEFT] = QTE.LEFT, [QTE.ACT2RIGHT] = QTE.RIGHT, [QTE.ACT3RIGHT] = QTE.RIGHT,
[QTE.LOOPLEFTL] = QTE.LEFT, [QTE.LOOPLEFTD] = QTE.LEFT, [QTE.LOOPLEFTR] = QTE.LEFT, [QTE.LOOPLEFTU] = QTE.LEFT,
[QTE.LOOPRIGHTL] = QTE.RIGHT, [QTE.LOOPRIGHTD] = QTE.RIGHT, [QTE.LOOPRIGHTR] = QTE.RIGHT, [QTE.LOOPRIGHTU] = QTE.RIGHT }
if m[3] == QTE.MULTI then
m[3] = m[5]
elseif easy[m[3]] then
m[3] = easy[m[3]]
end
else
if q.dip_MashtoRun == 2 then
if m[3] == QTE.RUN then m[3] = QTE.MASH2 elseif m[3] == QTE.RUNMIN then m[3] = QTE.MASH2MIN elseif m[3] == QTE.RUNMAX then m[3] = QTE.MASH2MAX end
elseif q.dip_MashtoRun == 3 then
if (m[3] == QTE.RUN) or (m[3] == QTE.MASH2) then m[3] = QTE.MASH
elseif (m[3] == QTE.RUNMIN) or (m[3] == QTE.MASH2MIN) then m[3] = QTE.MASHMIN
elseif (m[3] == QTE.RUNMAX) or (m[3] == QTE.MASH2MAX) then m[3] = QTE.MASHMAX end
end
if q.dip_HoldtoLoop == 1 then
if m[3] == QTE.LOOPLEFT then m[3] = QTE.HOLDLEFT elseif m[3] == QTE.LOOPRIGHT then m[3] = QTE.HOLDRIGHT end
end
end
end
if q.bFlip then
local mirrored = { [QTE.LEFT] = QTE.RIGHT, [QTE.RIGHT] = QTE.LEFT, [QTE.ACTLEFT] = QTE.ACTRIGHT, [QTE.ACTRIGHT] = QTE.ACTLEFT,
[QTE.UPLEFT] = QTE.UPRIGHT, [QTE.UPRIGHT] = QTE.UPLEFT, [QTE.DOWNLEFT] = QTE.DOWNRIGHT, [QTE.DOWNRIGHT] = QTE.DOWNLEFT,
[QTE.HOLDLEFT] = QTE.HOLDRIGHT, [QTE.HOLDRIGHT] = QTE.HOLDLEFT, [QTE.LOOPLEFT] = QTE.LOOPRIGHT, [QTE.LOOPRIGHT] = QTE.LOOPLEFT }
local function swapSide(value)
if value == QTE.LEFT then return QTE.RIGHT elseif value == QTE.RIGHT then return QTE.LEFT end
return value
end
if mirrored[m[3]] then
m[3] = mirrored[m[3]]
elseif (m[3] == QTE.PATH) and q.path[k] then
for _, slot in ipairs({ 1, 3, 5, 7 }) do
q.path[k][slot] = swapSide(q.path[k][slot])
end
elseif m[3] == QTE.TIMED then
local from, to = m[5] or k, m[6] or k
for p = from, to do
if q.timed[p] then
q.timed[p][1] = swapSide(q.timed[p][1])
q.timed[p][2] = q.timed[p][2] + flipBy
q.timed[p][3] = q.timed[p][3] + flipBy
end
end
elseif m[3] == QTE.MULTI then
m[5] = swapSide(m[5])
end
end
local before = move[k - 1]
local first = (k == 1) or (m[3] == QTE.WAY) or (m[3] == QTE.WAYOUT)
if first then
m[7] = m[1]
elseif ((before[3] == QTE.WAY) or (m[3] == QTE.WAYOUT)) and (before[8] > m[1]) then
m[7] = move[k - 2][2] + 1
else
m[7] = before[2] + 1
end
m[8] = (k < q.totalMoves) and m[2] or q.sceneEnd
end
end
-- The scene's moves: from the game's own setupMoves when its script is loaded, else from the
-- description's snapshot of what setupMoves declared.
function Q.loadMoves(q, thisLevel, thisScene)
q.move, q.path, q.choice, q.timed = {}, {}, {}, {}
q.sceneStart, q.sceneEnd, q.totalMoves = 0, 0, 0
if q.setupMoves then
q.setupMoves(thisLevel, thisScene)
return
end
local level = (thisLevel == QTE.levelSecret) and q.snapshot.levels.secret or q.snapshot.levels[thisLevel]
local play = (level.playBy and level.playBy[q.dip_Difficulty] or level.play)[thisScene]
q.sceneStart = play.start
q.sceneEnd = play.finish
q.totalMoves = #play.moves
for index, m in ipairs(play.moves) do
q.move[index] = { m[1], m[2], m[3], m[4], m[5], m[6] }
end
for index, c in pairs(play.choices or {}) do
q.choice[index] = { c[1], c[2], c[3] }
end
for index, p in pairs(play.paths or {}) do
local copy = {}
for slot = 1, 9 do
copy[slot] = p[slot]
end
q.path[index] = copy
end
for index, t in pairs(play.timed or {}) do
q.timed[index] = { t[1], t[2], t[3], t[4], t[5] }
end
end
function Q.setupLevel(q, thisLevel)
local level = q.Level[thisLevel]
q.iScene = q.iScene + 1
q.iPath = 0
q.iPathAjmp = 0
q.iPathAend = 0
if q.swapScene then
q.swapScene()
end
q.bFlip = false
if q.iScene > level[QTE.TOTALSCENES] then
q.iScene = q.iScene - 1
end
if level[QTE.MIRROR] > 0 then
if math.random(100) <= 50 then
q.bFlip = true
end
end
Q.loadMoves(q, thisLevel, q.iScene)
Q.setupFrames(q, thisLevel)
if q.bAllowSave and (q.dip_GameType ~= 2) and (q.dip_GameType ~= 3) then
q.bAllowSave = false
end
end
-- A wrong or missed move: the death clip and a life gone -- or nothing at all, for a move whose
-- death is negative.
function Q.setupDeathClip(q)
local level = q.Level[q.iLevel]
local curDeath = q.move[q.currentMove][QTE.moveDeath]
if q.swapDeath then
q.swapDeath()
end
q.bShowLvl = false
Q.clearInput(q)
q.bTestMash, q.bTestMashL, q.bTestMashR, q.bTestRunL, q.bTestRunR, q.bTestHold = false, false, false, false, false, false
q.iMash, q.iMulti, q.iLoopStep, q.iLenHold = 0, 0, 0, 0
q.bCalc = true
if curDeath < 0 then
q.lvlState = QTE.lvlPlayRest
return
end
q.lvlState = QTE.lvlPlayDeath
q.iLives = q.iLives - 1
if curDeath == 0 then
curDeath = math.random(q.totalDeath)
end
local clip = q.Death[curDeath]
if q.bFlip then
Q.setupClip(q, clip[1] + level[QTE.DTHMIRROR], clip[2] + level[QTE.DTHMIRROR])
else
Q.setupClip(q, clip[1], clip[2])
end
if not q.dip_Hints and (q.dip_GameType ~= 1) then
Q.sound(q, "sndwrong")
end
if q.dip_Rewind == 1 then
local m = q.move[q.currentMove]
if q.currentMove == 1 then
q.iPauseFrame = m[QTE.inputFrmStart] - 15
q.currentMove = 0
elseif m[QTE.correctMove] == QTE.CHOOSE then
m[QTE.moveDeath] = q.numChoice
q.iPauseFrame = q.move[q.currentMove - 2][QTE.inputFrmEnd] + 1
q.currentMove = q.currentMove - 2
elseif m[QTE.correctMove] == QTE.LETGO then
q.iPauseFrame = q.move[q.currentMove - 1][QTE.inputFrmStart] - 15
q.currentMove = q.currentMove - 2
else
q.iPauseFrame = m[QTE.inputFrmStart] - 15
q.currentMove = q.currentMove - 1
end
end
end
-- ----- The tests of the harder moves ---------------------------------------------------------
function Q.checkHold(q, playerMove, curMove)
local z = Q.scanInput(q)
local m = q.move[q.currentMove]
if (q.currentFrame == m[QTE.inputFrmStart]) and (z == playerMove) then
z = QTE.MOVEFAIL
elseif z == playerMove then
if q.iLenHold >= q.lenCounter then
z = curMove
q.iLenHold = 0
else
z = QTE.MOVEPENDING
if q.bTestHold and (q.currentFrame == q.lastHold) then
q.lastHold = q.currentFrame
elseif q.bTestHold and (q.currentFrame == q.lastHold + 1) then
q.iLenHold = q.iLenHold + 1
q.lastHold = q.currentFrame
else
q.lastHold = q.currentFrame
end
end
else
if z ~= QTE.NOMOVE then
q.iLenHold = 0
z = QTE.MOVEFAIL
else
if q.iLenHold > 0 then
q.iLenHold = q.iLenHold - 1
end
z = QTE.MOVEPENDING
end
end
return z
end
function Q.checkLet(q, playerMove, curMove)
local m = q.move[q.currentMove]
if (q.currentFrame == m[QTE.inputFrmStart]) and (playerMove == QTE.NOMOVE) then
return QTE.MOVEFAIL
elseif (q.currentFrame == m[QTE.inputFrmEnd] - 1) and (playerMove == QTE.NOMOVE) then
return curMove
end
return QTE.MOVEPENDING
end
function Q.checkLoop(q, playerMove, curMove, first, third)
local z = QTE.MOVEPENDING
local firstFlag = (first == QTE.LEFT) and "p1LEFT" or "p1RIGHT"
local thirdFlag = (third == QTE.LEFT) and "p1LEFT" or "p1RIGHT"
if (playerMove == first) and (q.iLoopStep == 0) then
q.iLoopPrev, q.iLoopStep, q[firstFlag] = 0, 1, false
elseif (playerMove == QTE.DOWN) and (q.iLoopPrev == 0) and (q.iLoopStep == 1) then
q.iLoopPrev, q.iLoopStep, q.p1DOWN = 1, 2, false
elseif (playerMove == third) and (q.iLoopPrev == 1) and (q.iLoopStep == 2) then
q.iLoopPrev, q.iLoopStep, q[thirdFlag] = 2, 3, false
elseif (playerMove == QTE.UP) and (q.iLoopPrev == 2) and (q.iLoopStep == 3) then
q.iLoopPrev, q.iLoopStep, q.p1UP = 3, 4, false
elseif (playerMove == first) and (q.iLoopPrev == 3) and (q.iLoopStep == 4) then
z = curMove
q.iLoopPrev, q.iLoopStep, q[firstFlag] = 0, 0, false
end
return z
end
function Q.checkMash(q, playerMove, curMove)
local z = QTE.MOVEPENDING
if playerMove == QTE.BUTTON1 then
q.bTestMash = false
if q.iMash >= q.mashCounter then
z = curMove
q.p1BUTTON1 = false
end
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= QTE.NOMOVE then
q.iMash = 0
z = QTE.MOVEFAIL
end
end
return z
end
function Q.checkMash2(q, playerMove, curMove)
local z = QTE.MOVEPENDING
if (playerMove == QTE.BUTTON1) and q.bTestMashL then
q.bTestMashL, q.bTestMashR = false, true
if q.iMash >= q.mashCounter then
z = curMove
q.p1BUTTON1 = false
end
elseif (playerMove == QTE.BUTTON2) and q.bTestMashR then
q.bTestMashR, q.bTestMashL = false, true
if q.iMash >= q.mashCounter then
z = curMove
q.p1BUTTON2 = false
end
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= QTE.NOMOVE then
q.iMash = 0
z = QTE.MOVEFAIL
end
end
return z
end
-- A sequence of inputs, each on its own frame: the framework's checkul, checkdlb,
-- checkLOOPLEFTU, and their kin, one step at a time through iLoopStep. A circle (five steps)
-- starts over when done; a shorter sequence leaves its count for the next move to clear.
function Q.checkSequence(q, playerMove, curMove, steps)
local z = QTE.MOVEPENDING
local next = q.iLoopStep + 1
if (playerMove == steps[next]) and ((next == 1) or (q.iLoopPrev == next - 2)) then
q.iLoopPrev, q.iLoopStep = next - 1, next
q[MOVE_FLAG[playerMove]] = false
if next == #steps then
z = curMove
if #steps == 5 then
q.iLoopPrev, q.iLoopStep = 0, 0
end
end
end
return z
end
-- A mash of one switch (the framework's checkMashleft and its kin) or of two in turn (its
-- checkMash3): the left flag takes the first, the right flag the second, and any other input
-- ends it.
function Q.checkMashOf(q, playerMove, curMove, spec)
local z = QTE.MOVEPENDING
if (playerMove == spec.left) and q.bTestMashL then
q.bTestMashL, q.bTestMashR = false, true
if q.iMash >= q.mashCounter then
z = curMove
end
elseif (spec.right ~= nil) and (playerMove == spec.right) and q.bTestMashR then
q.bTestMashR, q.bTestMashL = false, true
if q.iMash >= q.mashCounter then
z = curMove
q[MOVE_FLAG[playerMove]] = false
end
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= QTE.NOMOVE then
q.iMash = 0
z = QTE.MOVEFAIL
end
end
return z
end
function Q.checkMulti(q, playerMove, curMove)
local m = q.move[q.currentMove]
local z = QTE.MOVEPENDING
if playerMove == m[5] then
q.bTestMulti = false
if q.iMulti >= m[6] then
z = curMove
end
elseif playerMove ~= QTE.NOMOVE then
z = QTE.MOVEFAIL
end
return z
end
function Q.checkRun(q, playerMove, curMove)
local z = QTE.MOVEPENDING
if playerMove == QTE.LEFT then
q.bTestRunL, q.bTestRunR, q.p1LEFT = false, true, false
if q.iMash >= q.mashCounter then
z = curMove
end
elseif playerMove == QTE.RIGHT then
q.bTestRunR, q.bTestRunL, q.p1RIGHT = false, true, false
if q.iMash >= q.mashCounter then
z = curMove
end
elseif playerMove == QTE.UP then
q.p1UP = false
elseif playerMove == QTE.DOWN then
q.p1DOWN = false
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= QTE.NOMOVE then
q.iMash = 0
z = QTE.MOVEFAIL
end
end
return z
end
function Q.checkSkip(q, playerMove, curMove)
if playerMove ~= QTE.NOMOVE then
Q.clearInput(q)
q.bShowAction = false
discSkipToFrame(q.move[q.currentMove][QTE.inputFrmEnd])
end
return curMove
end
-- ----- The screens ---------------------------------------------------------------------------
function Q.startGame(q)
math.randomseed(KARIS_SEED or os.time())
math.random(100)
if q.bExtendedPlay then
Q.initStages(q)
q.currentLevel = QTE.levelNormal
q.iLevel = QTE.levelSecret
else
if q.iCredits > 0 then
q.iCredits = q.iCredits - 1
end
q.iScore, q.iScoreTemp, q.iBonus, q.iScPlayed, q.iScDeath, q.iTotDeath = 0, 0, 0, 0, 0, 0
if q.currentLevel == QTE.levelContinue then
q.currentLevel = q.iTempLevel
q.iLifeBar = q.BarSize
q.iRightMv, q.iWrongMv = 0, 0
if q.dip_Rewind == 0 then
q.bSwap = true
Q.levelReplay(q)
elseif q.dip_Rewind == 1 then
q.currentMove = q.currentMove + 1
q.bSave = true
discSkipToFrame(q.iPauseFrame)
q.lvlState = QTE.lvlRunning
elseif q.dip_Rewind == 2 then
q.bRes, q.bPath, q.bTime, q.bSwap = true, true, true, true
q.iScene = 0
Q.levelReplay(q)
elseif q.dip_Rewind == 3 then
local after = q.move[q.currentMove + 1]
if q.currentMove == q.totalMoves then
q.bSwap = true
q.scene[q.iLevel][q.iScene][QTE.SCENECOMPLETE] = true
q.iScene, q.currentMove, q.bSave = q.iScene + 1, 1, true
elseif after and (q.currentMove + 1 == q.totalMoves) and ((after[3] == QTE.CHOOSE) or (after[3] == QTE.LETGO) or (after[3] == QTE.PATH) or (after[3] == QTE.YESNO)) then
q.bSwap = true
q.scene[q.iLevel][q.iScene][QTE.SCENECOMPLETE] = true
q.iScene, q.currentMove = q.iScene + 1, 1
else
q.currentMove, q.bSave = q.currentMove + 1, true
end
end
else
Q.initStages(q)
q.bSkipIntroClip = false
q.iPath, q.iPathAend, q.iPathAjmp = 0, 0, 0
q.iContinues, q.iScene, q.currentMove = 0, 0, 1
if (q.dip_GameType == 0) or (q.dip_GameType == 1) or (q.dip_GameType == 4) then
q.iLifeBar = q.BarSize
q.iRightMv, q.iWrongMv = 0, 0
q.iTop = (q.dip_GameType == 1) and q.iTopLB or q.iTopN
if q.dip_PlayStyle == 0 then
Q.doMixSEQ(q)
q.iLevel = q.dip_StartLevel
q.iScene = q.dip_StartScene - 1
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 1 then
Q.doMixRND(q)
q.iLevel = q.LvlOrder[1]
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 2 then
Q.doMixTIE(q)
q.iLevel = q.LvlOrder[1]
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 3 then
q.iLevel = q.PlayOrder[1]
if q.MapStart == 1 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
else
q.currentLevel = QTE.levelMap
end
end
elseif q.dip_GameType == 2 then
q.bShowLives, q.bShowScore = false, false
q.iRightMv, q.iWrongMv = 0, 0
q.iLevel, q.iScene = q.dip_StartLevel, 0
q.currentLevel = QTE.levelNormal
elseif q.dip_GameType == 3 then
Q.doMixSEQ(q)
q.iLevel, q.iScene = 1, 0
q.iTop = q.iTopS
q.currentLevel = QTE.levelNormal
end
if q.startConf then
q.startConf()
end
end
end
if q.IngameDiffchoice and (q.dip_Diffshow == 4) and q.bOneDiff then
q.altState = QTE.lvlSetup
q.currentLevel = QTE.levelDiffScreen
end
if (q.offsetIntroGame ~= 0) and (q.iContinues == 0) and (q.dip_StartScene == 1) then
Q.setupClip(q, q.offsetIntroGame, q.offsetIntroGameend)
q.lvlState = QTE.branch11
else
q.lvlState = QTE.lvlSetup
end
q.bShowAction, q.bShowScore, q.bRes = false, false, true
if q.dip_GameType == 1 then
q.iLives = 1
if q.dip_Difficulty == 0 then
q.BarMinT = q.BarMin
if q.BarBonus > 0 then
q.BarBonusT = q.BarBonus - 1
end
elseif q.dip_Difficulty == 1 then
q.BarMinT, q.BarBonusT = q.BarMin, q.BarBonus
elseif q.dip_Difficulty == 2 then
q.BarMinT, q.BarBonusT = q.BarMin + 1, q.BarBonus
else
q.BarMinT, q.BarBonusT = q.BarMin + 1, q.BarBonus + 1
end
elseif q.dip_GameType == 3 then
q.iLives = 1
else
q.iLives = q.dip_LivesPerCredit
end
q.bShowAction, q.bShowCredits, q.bShowLCD, q.bResetContinue, q.bExtendedPlay = false, false, false, false, false
end
function Q.doIntro(q)
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetIntro01, q.offsetIntro01end)
q.lvlState = QTE.branch01
q.bShowCredits = true
q.bShowLCD = true
q.bShowLives = false
elseif q.lvlState == QTE.branch01 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
discSkipToFrame(q.frameControls)
discPause()
q.bShowLCD, q.bShowCredits = false, false
Q.timerON(q, 10)
q.lvlState = (q.frameSpecial ~= q.frameControls) and QTE.branch02 or QTE.branch03
elseif Q.secretPressed(q) then
q.p1BUTTON2, q.p1BUTTON3, q.p1BUTTON4, q.p1UP, q.p1RIGHT = false, false, false, false, false
q.bExtendedPlay = true
Q.startGame(q)
q.bShowCredits = false
end
elseif q.lvlState == QTE.branch02 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
discSkipToFrame(q.frameSpecial)
discPause()
q.bShowLCD, q.bShowCredits = false, false
Q.timerON(q, 10)
q.lvlState = QTE.branch03
end
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = true, true
Q.setupClip(q, q.offsetIntro02, q.offsetIntro02end)
q.lvlState = QTE.branch04
end
elseif q.lvlState == QTE.branch04 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = false, false
discSkipToFrame(q.frameRankings)
Q.timerON(q, 10)
discPause()
q.lvlState = QTE.branch05
elseif Q.secretPressed(q) then
q.p1BUTTON2, q.p1BUTTON3, q.p1BUTTON4, q.p1UP, q.p1RIGHT = false, false, false, false, false
q.bExtendedPlay = true
Q.startGame(q)
q.bShowCredits = false
end
elseif q.lvlState == QTE.branch05 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = true, true
Q.setupClip(q, q.offsetIntro03, q.offsetIntro03end)
q.lvlState = (q.LvlTrophy3 ~= 0) and QTE.branch06 or QTE.branch09
elseif Q.secretPressed(q) then
q.p1BUTTON2, q.p1BUTTON3, q.p1BUTTON4, q.p1UP, q.p1RIGHT = false, false, false, false, false
q.bExtendedPlay = true
Q.startGame(q)
q.bShowCredits = false
end
elseif q.lvlState == QTE.branch06 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = false, false
discSkipToFrame(q.frameTrophy)
Q.timerON(q, 8)
discPause()
q.lvlState = QTE.branch07
end
elseif q.lvlState == QTE.branch07 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
discSkipToFrame(q.frameTrophy)
Q.timerON(q, 8)
discPause()
q.lvlState = QTE.branch08
end
elseif q.lvlState == QTE.branch08 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = true, true
Q.setupClip(q, q.offsetTitle, q.offsetTitleend)
q.lvlState = QTE.branch09
end
elseif q.lvlState == QTE.branch09 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = false, false
discSkipToFrame(q.frameRankings)
Q.timerON(q, 8)
discPause()
q.lvlState = QTE.branch10
elseif Q.secretPressed(q) then
q.p1BUTTON2, q.p1BUTTON3, q.p1BUTTON4, q.p1UP, q.p1RIGHT = false, false, false, false, false
q.bExtendedPlay = true
Q.startGame(q)
q.bShowCredits = false
end
elseif q.lvlState == QTE.branch10 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = false, false
q.lvlState = QTE.lvlSetup
end
end
if (q.dip_CoinsPerCredit == QTE.DOPT_FREEPLAY) or (q.bShowCredits and (q.iCredits > 0)) then
if q.p1START1 and (q.dip_GameType ~= 5) then
q.p1START1 = false
q.bShowCredits = false
q.i2P = 0
Q.startGame(q)
end
end
end
function Q.doChoose(q)
local m = q.move[q.currentMove]
local addPoints = q.addPoints or Q.addPoints -- Kimmy scores its own way.
q.numChoice = m[QTE.moveDeath]
if q.altState == QTE.lvlSetup then
q.altState = QTE.lvlRunning
q.iChoice = 1
q.bIgnoreJoy = false
q.bShowChoices = true
-- The framework shuffles the options on their first drawing, from the one random stream.
if q.bShuffleOrder then
q.bShuffleOrder = false
if q.numChoice == 2 then
q.optorder = ({ { 1, 2 }, { 2, 1 } })[math.random(2)]
elseif q.numChoice == 3 then
q.optorder = ({ { 1, 2, 3 }, { 2, 3, 1 }, { 3, 1, 2 }, { 1, 3, 2 }, { 2, 1, 3 }, { 3, 2, 1 } })[math.random(6)]
elseif q.numChoice == 4 then
q.optorder = ({ { 1, 2, 3, 4 }, { 4, 2, 3, 1 }, { 3, 4, 1, 2 }, { 1, 3, 2, 4 }, { 2, 1, 4, 3 }, { 4, 3, 2, 1 } })[math.random(6)]
end
end
elseif q.altState == QTE.lvlRunning then
local worth = q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE
if q.currentFrame > m[QTE.inputFrmEnd] then
if q.choice[q.optorder[q.iChoice]][2] == true then
Q.sound(q, "sndright")
q.iRightMv = q.iRightMv + 1
addPoints(q, worth, q.currentMove)
q.iScoreTemp = q.iScoreTemp + worth
q.lvlState = QTE.lvlPlayRest
else
m[QTE.moveDeath] = q.choice[q.optorder[q.iChoice]][3]
q.bShowAction, q.bShowNext = false, false
q.lvlState = QTE.branch02
end
q.bShowChoices = false
elseif (q.currentFrame >= m[QTE.inputFrmStart]) and (q.currentFrame <= m[QTE.inputFrmEnd]) then
local thisMove = QTE.NOMOVE
if q.bIgnoreJoy then
if Q.timerDue(q) then
q.bIgnoreJoy = false
end
else
thisMove = Q.scanInput(q)
end
if thisMove == QTE.UP then
q.p1UP = false
if q.iChoice > 1 then
q.iChoice = q.iChoice - 1
Q.sound(q, "sndcoin")
end
elseif thisMove == QTE.DOWN then
q.p1DOWN = false
if q.iChoice < q.numChoice then
q.iChoice = q.iChoice + 1
Q.sound(q, "sndcoin")
end
elseif thisMove == QTE.BUTTON1 then
q.p1BUTTON1 = false
if q.choice[q.optorder[q.iChoice]][2] == true then
Q.sound(q, "sndright")
q.iRightMv = q.iRightMv + 1
addPoints(q, worth, q.currentMove)
q.iScoreTemp = q.iScoreTemp + worth
discSkipToFrame(m[QTE.inputFrmEnd])
q.lvlState = QTE.lvlPlayRest
else
m[QTE.moveDeath] = q.choice[q.optorder[q.iChoice]][3]
q.bShowAction, q.bShowNext = false, false
if (q.dip_GameType == 2) or (q.dip_GameType == 1) then
discSkipToFrame(m[QTE.inputFrmEnd])
end
q.lvlState = QTE.branch02
end
q.bShowChoices = false
end
end
end
end
-- A scene completed: the last of the game's last level ends the game; any other counts its
-- bonus and moves on. Answers whether the game ended.
function Q.sceneDone(q, thisLevel)
local bonus = q.SCORESCENE - q.iScDeath * q.DEATHPENALTY
q.scene[thisLevel][q.iScene][QTE.SCENECOMPLETE] = true
if ((thisLevel == q.finalstage) and Q.beatLevel(q, q.finalstage)) or ((thisLevel == QTE.levelSecret) and Q.beatLevel(q, QTE.levelSecret)) then
if thisLevel == q.finalstage then
q.stage[thisLevel][QTE.BEATSTATUS] = true
end
if q.dip_GameType ~= 2 then
if Q.beatGame(q) then
if bonus > 0 then
Q.addPoints(q, bonus, 0)
end
Q.addPoints(q, q.SCOREGAME, 0)
if Q.beatGameWithOneLife(q) then
Q.addPoints(q, q.SCORESECRET, 0)
end
else
if bonus > 0 then
Q.addPoints(q, bonus, 0)
end
Q.addPoints(q, q.SCORELEVEL, 0)
end
if thisLevel == QTE.levelSecret then
discPause()
Q.timerON(q, 0.1)
else
Q.sound(q, "sndvictory")
discSkipToFrame(q.frameVictory)
discPause()
Q.timerON(q, 3)
end
q.lvlState = QTE.branch05
else
q.lvlState = QTE.lvlEnd
end
return true
end
if bonus > 0 then
Q.addPoints(q, bonus, 0)
q.iBonus = q.iBonus + bonus
end
q.iTotDeath = q.iTotDeath + q.iScDeath
q.iScPlayed = q.iScPlayed + 1
q.iScDeath = 0
return false
end
-- After a death clip: the death counted, the rewind setting says where play goes.
function Q.afterDeath(q, thisLevel)
local m = q.move[q.currentMove]
local after = q.move[q.currentMove + 1]
q.stage[thisLevel][QTE.DEATHCOUNT] = q.stage[thisLevel][QTE.DEATHCOUNT] + 1
if q.iLives <= 0 then
q.lvlState = QTE.lvlEnd
return
end
if q.dip_Rewind == 0 then
q.bRes, q.bPath, q.bTime = true, true, true
q.lvlState = QTE.lvlEnd
elseif q.dip_Rewind == 2 then
q.bRes, q.bPath, q.bTime, q.bSwap = true, true, true, true
q.iScene = 0
for i = 1, q.Level[thisLevel][QTE.TOTALSCENES] do
q.scene[thisLevel][i] = { i, false }
end
q.lvlState = QTE.lvlSetup
elseif (q.dip_Rewind == 3) and ((q.currentMove == q.totalMoves) or (after and (q.currentMove + 1 == q.totalMoves) and ((after[3] == QTE.CHOOSE) or (after[3] == QTE.LETGO) or (after[3] == QTE.PATH) or (after[3] == QTE.YESNO)))) then
q.bSwap = true
q.scene[q.iLevel][q.iScene][QTE.SCENECOMPLETE] = true
q.lvlState = QTE.lvlEnd
elseif q.ShowResurrect and (q.dip_GameType ~= 3) and (q.i2P == 0) then
Q.setupClip(q, q.offsetGetReady, q.offsetGetReadyEnd)
q.bShowGet, q.bShowTop = true, false
q.lvlState = QTE.branch09
else
discSkipToFrame(q.iPauseFrame)
if (q.dip_Rewind == 3) and after and (after[3] == QTE.LETGO) then
q.bSwap = true
q.currentMove = q.currentMove + 2
elseif (q.dip_Rewind == 3) and (m[3] == QTE.PATH) then
q.iPathAend = q.path[q.currentMove][4] - 1
q.iPathAjmp = q.path[q.currentMove][9]
q.currentMove = q.currentMove + 1
else
q.currentMove = q.currentMove + 1
end
q.lvlState = QTE.lvlRunning
end
end
function Q.resumeAfterGetReady(q)
local m = q.move[q.currentMove]
local after = q.move[q.currentMove + 1]
q.lvlState = QTE.lvlRunning
q.bShowGet = false
discSkipToFrame(q.iPauseFrame)
if (q.dip_Rewind == 3) and after and (after[3] == QTE.LETGO) then
q.bSwap = true
q.currentMove = q.currentMove + 2
elseif (q.dip_Rewind == 3) and m and (m[3] == QTE.PATH) then
q.iPathAend = q.path[q.currentMove][4] - 1
q.iPathAjmp = q.path[q.currentMove][9]
q.currentMove = q.currentMove + 1
else
q.currentMove = q.currentMove + 1
end
end
function Q.enterScene(q, thisLevel)
local level = q.Level[thisLevel]
if q.bSave and (q.currentMove ~= 1) then
q.currentFrame = q.move[q.currentMove - 1][QTE.inputFrmEnd] + 1
discSkipToFrame(q.currentFrame)
q.bSave = false
q.lvlState = QTE.lvlRunning
elseif (not q.bSkipIntroClip) and (q.iScene == 1) then
local flipBy = q.bFlip and level[QTE.MIRROR] or 0
Q.setupClip(q, level[QTE.INTROCLIP] + flipBy, level[QTE.INTROCLIPEND] + flipBy)
q.bShowSkip = true
q.lvlState = QTE.branch01
else
if (q.currentFrame + 2 <= q.sceneStart) or (q.currentFrame > q.sceneStart) then
discSkipToFrame(q.sceneStart)
end
q.lvlState = QTE.lvlRunning
end
end
function Q.wrong(q)
q.iPauseFrame = q.move[q.currentMove][QTE.inputFrmEnd]
q.bShowAction = false
q.lvlState = QTE.branch02
end
function Q.good(q)
q.lvlState = QTE.branch04
end
-- The move waited for, judged by its kind: the framework's tests, a branch per kind.
function Q.judge(q)
local m = q.move[q.currentMove]
local kind = m[QTE.correctMove]
local thisMove = QTE.NOMOVE
local fps = q.MovieFPS
if (kind >= QTE.HOLDUP) and (kind <= QTE.HOLDBUT) then
q.bTestHold = true
if q.bCalc then
q.bCalc = false
q.lenCounter = (m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) - (14 - q.dip_Difficulty)
end
thisMove = Q.checkHold(q, kind - 20, kind)
if thisMove == kind then
q.bTestHold, q.iLenHold, q.lastHold, q.bCalc = false, 0, 0, true
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif kind == QTE.LETGO then
thisMove = Q.checkLet(q, Q.scanInput(q), kind)
if thisMove == kind then
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif (kind == QTE.LOOPLEFT) or (kind == QTE.LOOPRIGHT) then
local first = (kind == QTE.LOOPLEFT) and QTE.LEFT or QTE.RIGHT
local third = (kind == QTE.LOOPLEFT) and QTE.RIGHT or QTE.LEFT
thisMove = Q.checkLoop(q, Q.scanInput(q), kind, first, third)
if thisMove == kind then
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif (kind >= QTE.RUN) and (kind <= QTE.RUNMAX) then
thisMove = Q.scanInput(q)
q.bTestRunL, q.bTestRunR = true, true
if q.bCalc then
q.bCalc = false
q.unMash = 0.07 + q.dip_Difficulty / 100
q.mashCounter = ((m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) / fps) * ((q.dip_MashtoRun == 1) and 6 or 9)
if kind == QTE.RUNMIN then q.unMash = q.unMash - 0.01 elseif kind == QTE.RUNMAX then q.unMash = q.unMash + 0.01 end
end
thisMove = Q.checkRun(q, thisMove, kind)
if thisMove == kind then
q.bTestRunL, q.bTestRunR, q.iMash, q.bCalc = false, false, 0, true
Q.clearInput(q)
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif (kind >= QTE.MASH2) and (kind <= QTE.MASH2MAX) then
thisMove = Q.scanInput(q)
q.bTestMashL, q.bTestMashR = true, true
if q.bCalc then
q.bCalc = false
q.unMash = 0.07 + q.dip_Difficulty / 100
q.mashCounter = ((m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) / fps) * (Q.mazescater(q) and 6 or 9)
if kind == QTE.MASH2MIN then q.unMash = q.unMash - 0.01 elseif kind == QTE.MASH2MAX then q.unMash = q.unMash + 0.01 end
end
thisMove = Q.checkMash2(q, thisMove, kind)
if thisMove == kind then
q.bTestMashL, q.bTestMashR, q.iMash, q.bCalc = false, false, 0, true
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif (kind >= QTE.MASH) and (kind <= QTE.MASHMAX) then
thisMove = Q.scanInput(q)
q.bTestMash = true
if q.bCalc then
q.bCalc = false
q.unMash = 0.07 + q.dip_Difficulty / 100
q.mashCounter = ((m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) / fps) * 2.8
if kind == QTE.MASHMIN then q.unMash = q.unMash - 0.01 elseif kind == QTE.MASHMAX then q.unMash = q.unMash + 0.01 end
end
thisMove = Q.checkMash(q, thisMove, kind)
if thisMove == kind then
q.bTestMash, q.iMash, q.bCalc = false, 0, true
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif kind == QTE.MULTI then
q.bTestMulti = true
thisMove = Q.checkMulti(q, Q.scanInput(q), kind)
if thisMove == kind then
q.bTestMulti = false
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif MAZE_SEQ[kind] then
thisMove = Q.checkSequence(q, Q.scanInput(q), kind, MAZE_SEQ[kind])
if thisMove == kind then
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif MAZE_MASH[kind] then
local spec = MAZE_MASH[kind]
thisMove = Q.scanInput(q)
q.bTestMashL, q.bTestMashR = true, true
if q.bCalc then
q.bCalc = false
q.unMash = 0.07 + q.dip_Difficulty / 100
q.mashCounter = ((m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) / fps) * spec.times
if kind == spec.min then q.unMash = q.unMash - 0.01 elseif kind == spec.max then q.unMash = q.unMash + 0.01 end
end
thisMove = Q.checkMashOf(q, thisMove, kind, spec)
if thisMove == kind then
q.bTestMashL, q.bTestMashR, q.iMash, q.bCalc = false, false, 0, true
Q.good(q)
elseif thisMove ~= QTE.MOVEPENDING then
Q.wrong(q)
end
elseif (kind <= QTE.RIGHT) or ((kind >= QTE.BUTTON1) and (kind <= QTE.BUTTON4)) then
thisMove = Q.scanInput(q)
if thisMove ~= QTE.NOMOVE then
if (thisMove == kind) or ((m[5] ~= nil) and (thisMove == m[5])) then
Q.good(q)
else
Q.wrong(q)
end
end
elseif ACT_WANTS[kind] then
-- Inputs together: every one the kind wants down at once, and none it does not.
local wants = ACT_WANTS[kind]
local other = false
local all = true
for _, name in ipairs({ "p1UP", "p1DOWN", "p1LEFT", "p1RIGHT", "p1BUTTON1", "p1BUTTON2", "p1BUTTON3" }) do
local wanted = false
for _, want in ipairs(wants) do
if want == name then
wanted = true
end
end
if q[name] and not wanted then
other = true
end
if wanted and not q[name] then
all = false
end
end
if (q.currentFrame >= m[QTE.inputFrmEnd]) or other then
Q.wrong(q)
elseif all then
Q.good(q)
end
elseif kind == QTE.PATH then
thisMove = Q.scanInput(q)
if thisMove ~= QTE.NOMOVE then
local p = q.path[q.currentMove]
local slot = nil
for _, at in ipairs({ 1, 3, 5, 7 }) do
if thisMove == p[at] then
slot = at
break
end
end
if slot == nil then
q.bShowAction = false
q.lvlState = QTE.branch02
else
q.iPath = p[slot + 1]
if q.iPath > 1000 then
m[QTE.moveDeath] = q.iPath - 1000
q.bShowAction = false
q.iPath = 0
q.lvlState = QTE.branch02
else
if slot == 1 then
q.iPathAend, q.iPathAjmp = p[4] - 1, p[9]
elseif slot == 3 then
if p[5] == 0 then
q.iPathAend = 0
q.iPathAjmp = (p[9] == QTE.OUT) and QTE.OUT or 0
else
q.iPathAend, q.iPathAjmp = p[6] - 1, p[9]
end
elseif slot == 5 then
if p[4] == 0 then
q.iPathAend = 0
q.iPathAjmp = (p[9] == QTE.OUT) and QTE.OUT or 0
else
q.iPathAend, q.iPathAjmp = p[8] - 1, p[9]
end
else
q.iPathAend = 0
q.iPathAjmp = (p[9] == QTE.OUT) and QTE.OUT or 0
end
q.bShowAction = false
Q.good(q)
end
end
end
elseif kind == QTE.YESNO then
local p = q.path[q.currentMove]
thisMove = Q.scanInput(q)
if thisMove ~= QTE.NOMOVE then
if thisMove == QTE.BUTTON1 then
q.iPath = p[1]
if q.iPath > 1000 then
m[QTE.moveDeath], q.bShowAction, q.iPath, q.lvlState = q.iPath - 1000, false, 0, QTE.branch02
else
q.iPathAend, q.iPathAjmp, q.bShowAction = p[2] - 1, p[3], false
Q.good(q)
end
else
q.bShowAction = false
q.lvlState = QTE.branch02
end
elseif q.currentFrame == m[QTE.inputFrmEnd] then
q.iPath = p[2]
if q.iPath > 1000 then
m[QTE.moveDeath], q.bShowAction, q.iPath, q.lvlState = q.iPath - 1000, false, 0, QTE.branch02
else
q.iPathAend, q.iPathAjmp, q.bShowAction = 0, 0, false
Q.good(q)
end
end
elseif kind == QTE.TIMED then
local i, j = m[5] or q.currentMove, m[6] or q.currentMove
thisMove = Q.scanInput(q)
if thisMove ~= QTE.NOMOVE then
if q.bTime then
q.bTime = false
q.Hit = q.currentFrame
end
for tcount = i, j do
local t = q.timed[tcount]
if (q.Hit >= t[2]) and (q.Hit <= t[3]) then
if t[1] == thisMove then
if t[5] ~= nil then
if t[5] == 0 then
discSkipToFrame(m[QTE.moveFrmEnd])
elseif t[5] > 0 then
q.iPath = t[5]
discSkipToFrame(m[QTE.moveFrmEnd])
end
end
Q.good(q)
else
q.iPauseFrame, q.bShowAction, m[QTE.moveDeath], q.lvlState = m[QTE.inputFrmEnd], false, t[4], QTE.branch02
end
break
elseif tcount == j then
q.iPauseFrame, m[QTE.moveDeath], q.bShowAction, q.lvlState = m[QTE.inputFrmEnd], t[4], false, QTE.branch02
end
end
end
elseif kind == QTE.SKIP then
Q.checkSkip(q, Q.scanInput(q), kind)
end
end
function Q.doLevel(q)
local thisLevel = q.iLevel
local level = q.Level[thisLevel]
if q.lvlState == QTE.lvlSetup then
q.bShuffleOrder, q.bPlayPrompt, q.bPlayRight = true, true, true
q.bShowLvl, q.bShowAction = false, false
q.bTestMash, q.bTestMashL, q.bTestMashR, q.bTestRunL, q.bTestRunR, q.bTestHold = false, false, false, false, false, false
q.iMash, q.iMulti, q.iLoopStep, q.iLenHold = 0, 0, 0, 0
q.bPath, q.bTime, q.bCalc = true, true, true
q.bShowScore = (q.dip_GameType ~= 2) and (q.dip_Display == 0)
q.bShowLives = q.bShowScore
if not q.bSave then
q.currentMove = 1
end
Q.setupLevel(q, thisLevel)
if level[QTE.INTROCLIPEND] - level[QTE.INTROCLIP] < 2 then
q.bSkipIntroClip = true
end
if q.ShowResurrect and q.bRes and (q.dip_GameType ~= 3) and (q.i2P == 0) then
q.bShowTop = false
Q.setupClip(q, q.offsetGetReady, q.offsetGetReadyEnd)
q.bShowGet = true
q.lvlState = QTE.branch08
q.bRes = false
else
Q.enterScene(q, thisLevel)
end
elseif q.lvlState == QTE.branch01 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 or q.p1BUTTON2 or q.p1BUTTON3 or q.p1UP or q.p1DOWN or q.p1LEFT or q.p1RIGHT then
Q.clearInput(q)
q.bShowSkip = false
q.bSkipIntroClip = true
if q.currentFrame ~= q.iFrameEnd then
discSkipToFrame(q.sceneStart)
end
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.branch02 then
if q.dip_Hints and (q.dip_GameType ~= 1) and (q.dip_GameType ~= 2) then
Q.sound(q, "sndwrong")
q.bShowLvl, q.bShowScore, q.bShowLives = false, false, false
discSkipToFrame(q.frameHints)
Q.timerON(q, Q.mazescater(q) and 4 or 2)
discPause()
q.lvlState = QTE.branch03
else
q.iWrongMv = q.iWrongMv + 1
q.iScDeath = q.iScDeath + 1
q.bTestMash, q.bTestMashL, q.bTestMashR, q.bTestRunL, q.bTestRunR, q.bTestHold = false, false, false, false, false, false
q.iMash, q.iMulti, q.iLoopStep, q.iLenHold = 0, 0, 0, 0
q.bCalc = true
if q.dip_GameType == 2 then
Q.sound(q, "sndwrong")
q.lvlState = QTE.lvlPlayRest
elseif q.dip_GameType == 1 then
q.iLifeBar = q.iLifeBar - q.BarMinT
q.iRightMv = 0
Q.sound(q, "sndwrong")
q.lvlState = QTE.lvlPlayRest
if q.iLifeBar <= 0 then
Q.setupDeathClip(q)
end
else
Q.setupDeathClip(q)
end
end
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) then
Q.setupDeathClip(q)
end
elseif q.lvlState == QTE.branch04 then
local worth = q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE
q.bShowAction, q.bTime = false, true
q.iMulti, q.iLoopStep = 0, 0
q.lvlState = QTE.lvlPlayRest
if q.bPlayRight then
Q.sound(q, "sndright")
q.bPlayRight = false
end
q.iRightMv = q.iRightMv + 1
Q.addPoints(q, worth, q.currentMove)
q.iScoreTemp = q.iScoreTemp + worth
elseif q.lvlState == QTE.branch05 then
if Q.timerDue(q) then
q.bGOAlt = true
if Q.beatGameWithOneLife(q) and (thisLevel ~= QTE.levelSecret) and q.AllowSecret then
Q.sound(q, "sndvictory")
discSkipToFrame(q.frameSecret)
discPause()
Q.timerON(q, 4)
q.lvlState = QTE.branch06
elseif Q.beatGameWithOneCredit(q) and (thisLevel ~= QTE.levelSecret) and q.AllowSecret then
Q.sound(q, "sndvictory")
discSkipToFrame(q.frameSecret)
discPause()
Q.timerON(q, 4)
q.lvlState = QTE.branch06
elseif Q.newScore(q, q.iScore) then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelHighScore
else
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelGameOver
end
end
elseif q.lvlState == QTE.branch06 then
if Q.timerDue(q) then
q.bExtendedPlay = true
q.iScene = 0
Q.startGame(q)
end
elseif q.lvlState == QTE.branch07 then
if q.currentFrame >= q.iFrameEnd then
Q.afterDeath(q, thisLevel)
end
elseif q.lvlState == QTE.branch08 then
if q.currentFrame >= q.iFrameEnd then
q.bShowGet = false
q.bShowScore = (q.dip_GameType ~= 2) and (q.dip_Display == 0)
q.bShowLives = q.bShowScore
Q.enterScene(q, thisLevel)
end
elseif q.lvlState == QTE.branch09 then
if q.currentFrame >= q.iFrameEnd then
Q.resumeAfterGetReady(q)
end
elseif q.lvlState == QTE.branch10 then
Q.doChoose(q)
elseif q.lvlState == QTE.branch11 then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlSetup
end
elseif q.lvlState == QTE.branch12 then
if not Q.sceneDone(q, thisLevel) then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlPlayRest then
q.bPlayRight = true
q.thisMove = Q.scanInput(q)
if q.currentFrame >= q.move[q.currentMove][QTE.moveFrmEnd] then
if q.iPathAjmp == QTE.OUT then
q.currentMove = q.iPathAjmp
q.iPathAend, q.iPathAjmp = 0, 0
q.lvlState = QTE.branch12
return
elseif (q.iPathAend ~= 0) and (q.currentMove == q.iPathAend) then
q.currentMove = q.iPathAjmp
q.iPathAend, q.iPathAjmp = 0, 0
elseif (q.iPath ~= 0) and (q.currentMove <= q.totalMoves) then
q.currentMove = q.iPath
q.iPath = 0
q.bPath = true
else
q.currentMove = q.currentMove + 1
end
if q.currentMove <= q.totalMoves then
local m = q.move[q.currentMove]
if ((q.currentFrame + 2) <= m[QTE.moveFrmStart]) or (q.currentFrame > m[QTE.moveFrmStart]) then
discSkipToFrame(m[QTE.moveFrmStart])
end
q.bShowAction = false
q.bPlayPrompt = true
if m[QTE.correctMove] == QTE.CHOOSE then
q.altState = QTE.lvlSetup
q.lvlState = QTE.branch10
else
q.lvlState = QTE.lvlRunning
end
elseif not Q.sceneDone(q, thisLevel) then
if q.currentFrame >= q.sceneEnd then
q.lvlState = QTE.lvlEnd
end
end
end
elseif q.lvlState == QTE.lvlRunning then
local m = q.move[q.currentMove]
if m == nil then
q.lvlState = QTE.lvlEnd
elseif (q.currentFrame >= m[QTE.inputFrmStart]) and (q.currentFrame <= m[QTE.inputFrmEnd]) then
q.bShowAction = true
if q.bPlayPrompt and ((q.dip_ShowAction == 1) or (q.dip_ShowAction == 5)) and (m[QTE.correctMove] < 50) then
Q.sound(q, "sndcoin")
q.bPlayPrompt = false
end
Q.judge(q)
elseif (q.currentFrame > m[QTE.inputFrmEnd]) and (q.thisMove == QTE.NOMOVE) and (m[QTE.moveDeath] == -1) then
q.bShowAction = false
q.lvlState = QTE.lvlPlayRest
elseif (q.currentFrame > m[QTE.inputFrmEnd]) and (q.thisMove == QTE.NOMOVE) and (m[QTE.moveDeath] == -2) then
q.bShowAction = false
q.lvlState = QTE.branch04
elseif (q.currentFrame > m[QTE.inputFrmEnd]) and (m[QTE.correctMove] ~= QTE.SKIP) and (m[QTE.correctMove] ~= QTE.WAY) and (m[QTE.correctMove] ~= QTE.WAYOUT) then
q.iPauseFrame = m[QTE.inputFrmEnd]
q.bShowAction = false
q.lvlState = QTE.branch02
elseif q.currentFrame > m[QTE.inputFrmEnd] then
q.bShowAction = false
Q.addPoints(q, 0, q.currentMove)
if (m[QTE.correctMove] == QTE.WAY) and (m[QTE.moveDeath] > 0) then
q.iPath = m[QTE.moveDeath]
q.lvlState = QTE.lvlPlayRest
elseif m[QTE.correctMove] == QTE.WAYOUT then
q.iPath = m[QTE.moveDeath]
q.lvlState = QTE.branch12
else
q.lvlState = QTE.lvlPlayRest
end
else
q.thisMove = Q.scanInput(q)
end
elseif q.lvlState == QTE.lvlPlayDeath then
if q.currentFrame >= q.iFrameEnd then
if q.ShowSupDeath and (q.dip_GameType ~= 3) then
Q.setupClip(q, q.offsetSupDeath, q.offsetSupDeathEnd)
q.lvlState = QTE.branch07
else
Q.afterDeath(q, thisLevel)
end
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.bPath, q.bTime = true, true
if q.iLives == 0 then
if (q.dip_GameType ~= 3) and (q.dip_LimitContinue > 0) and ((q.iContinues < q.dip_LimitContinue) or (q.dip_LimitContinue == QTE.DOPT_INFINITE_CONTINUES)) then
q.iTempLevel = q.currentLevel
q.currentLevel = QTE.levelContinue
q.iContinues = q.iContinues + 1
else
q.dip_StartLevel, q.dip_StartScene = 1, 1
q.currentLevel = Q.newScore(q, q.iScore) and QTE.levelHighScore or QTE.levelGameOver
end
else
if Q.beatLevel(q, thisLevel) then
q.stage[thisLevel][QTE.BEATSTATUS] = true
q.iScene = 0
Q.addPoints(q, q.SCORELEVEL, 0)
q.iBonus = q.iBonus + q.SCORELEVEL
if ((q.dip_GameType == 1) and (q.iTotDeath == 0)) or ((q.dip_GameType ~= 1) and (q.stage[thisLevel][QTE.DEATHCOUNT] == 0)) then
Q.addPoints(q, q.PERFECTBONUS, 0)
q.iBonus = q.iBonus + q.PERFECTBONUS
end
q.bSkipIntroClip = false
q.iLiveSave, q.iScoreSave, q.bAllowSave = q.iLives, q.iScore, true
q.bRes = true
if (q.ShowLvlClear or (q.dip_GameType == 2)) and (q.dip_GameType ~= 3) then
Q.sound(q, "sndclear")
q.currentLevel = QTE.levelFinish
else
q.iScoreTemp, q.iBonus = 0, 0
if q.dip_PlayStyle == 3 then
q.currentLevel = QTE.levelMap
else
Q.nextLevel(q, q.iLevel)
q.currentLevel = QTE.levelNormal
end
end
else
if q.iPath ~= 0 then
q.iScene = q.iPath
q.iPath = 0
end
if not q.scene[thisLevel][q.iScene][QTE.SCENECOMPLETE] then
if q.dip_Rewind == 0 then
q.bSwap = true
end
if q.iScene > 0 then
q.iScene = q.iScene - 1
end
else
q.iLiveSave, q.iScoreSave, q.bAllowSave, q.bSave = q.iLives, q.iScore, true, false
end
if (q.dip_Rewind == 0) or (q.dip_Rewind == 2) then
Q.levelReplay(q)
end
end
q.bShowLives, q.bShowLvl, q.bShowAction = false, false, false
q.bTestMash, q.bTestMashL, q.bTestMashR, q.bTestRunL, q.bTestRunR, q.bTestHold = false, false, false, false, false, false
q.iMash, q.iMulti, q.iLoopStep, q.iLenHold = 0, 0, 0, 0
end
end
end
-- The level-cleared screen: the clip, then the bonus rolled into the score.
function Q.doFinish(q)
q.bShowScore, q.bRes = false, true
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetClear, q.offsetClearend)
q.lvlState = QTE.branch01
elseif q.lvlState == QTE.branch01 then
if q.currentFrame >= q.iFrameEnd then
discPause()
Q.timerON(q, 0.1)
q.lvlState = QTE.branch02
end
elseif q.lvlState == QTE.branch02 then
if Q.timerDue(q) then
if q.iBonus > 0 then
q.iBonus = q.iBonus - 500
q.iScoreTemp = q.iScoreTemp + 500
Q.timerON(q, 0.01)
else
Q.sound(q, "sndvictory")
Q.timerON(q, 2)
q.lvlState = QTE.branch03
end
end
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) then
q.iScoreTemp, q.iBonus, q.numTrophy = 0, 0, 0
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
else
q.iWrongMv = 0
Q.nextLevel(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
end
end
function Q.doContinue(q)
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetContinue, q.offsetContinueend)
q.bShowLives, q.bShowLvl, q.bShowScore, q.bShowAction = false, false, false, false
q.bShowCredits = true
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlEnd
elseif q.p1START1 then
q.p1START1 = false
if (q.iCredits > 0) or (q.dip_CoinsPerCredit == QTE.DOPT_FREEPLAY) then
q.bOneDiff, q.bSkipIntroClip = false, true
if q.iScene > 0 then
q.iScene = q.iScene - 1
end
Q.startGame(q)
end
elseif q.p1BUTTON2 then
q.p1BUTTON2 = false
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
if Q.newScore(q, q.iScore) then
q.currentLevel = QTE.levelHighScore
q.bGOAlt = true
else
q.currentLevel = QTE.levelGameOver
end
end
end
function Q.doGameOver(q)
if q.lvlState == QTE.lvlSetup then
q.bShowLives, q.bShowLvl, q.bShowScore, q.bShowCredits, q.bShowAction = false, false, false, false, false
q.bOneDiff = true
if q.bGOAlt then
Q.setupClip(q, q.offsetGameOverAlt, q.offsetGameOverAltend)
q.bGOAlt = false
elseif q.currentFrame == q.offsetContinueend then
q.iFrameEnd = q.offsetGameOverend
else
Q.setupClip(q, q.offsetGameOver, q.offsetGameOverend)
end
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
q.bShowScore = false
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
end
end
-- The difficulty screen's stick: LEFT and RIGHT step through the four frames.
function Q.moveFrameDiff(q)
local frame = q.currentFrame
local step = nil
if (frame == q.frameEasy) and q.p1RIGHT then
step = { q.frameNormal, "p1RIGHT", 1 }
elseif (frame == q.frameNormal) and q.p1LEFT then
step = { q.frameEasy, "p1LEFT", 0 }
elseif (frame == q.frameNormal) and q.p1RIGHT then
step = { q.frameHard, "p1RIGHT", 2 }
elseif (frame == q.frameHard) and q.p1LEFT then
step = { q.frameNormal, "p1LEFT", 1 }
elseif (frame == q.frameHard) and q.p1RIGHT then
step = { q.frameExtreme, "p1RIGHT", 3 }
elseif (frame == q.frameExtreme) and q.p1LEFT then
step = { q.frameHard, "p1LEFT", 2 }
end
if step then
Q.sound(q, "sndcoin")
discSkipToFrame(step[1])
discPause()
q[step[2]] = false
q.dip_Difficulty = step[3]
end
end
-- The in-game difficulty screen: a still per difficulty, the stick to choose, the action button
-- or thirty seconds to take it.
function Q.doDiffSelect(q)
if q.altState == QTE.lvlSetup then
q.bShowScore, q.bShowLives, q.bIgnoreJoy = false, false, false
q.dip_Difficulty = 0
discSkipToFrame(q.frameEasy)
discPause()
Q.timerON(q, 30)
q.altState = QTE.lvlRunning
elseif q.altState == QTE.lvlRunning then
if Q.timerDue(q) then
q.altState = QTE.lvlEnd
elseif q.p1BUTTON1 then
Q.sound(q, "sndcredit")
q.p1BUTTON1 = false
q.altState = QTE.lvlEnd
else
Q.moveFrameDiff(q)
end
elseif q.altState == QTE.lvlEnd then
if q.dip_GameType == 1 then
q.iLives = 1
if q.dip_Difficulty == 0 then
q.BarMinT = q.BarMin
if q.BarBonus > 0 then
q.BarBonusT = q.BarBonus - 1
end
elseif q.dip_Difficulty == 1 then
q.BarMinT, q.BarBonusT = q.BarMin, q.BarBonus
elseif q.dip_Difficulty == 2 then
q.BarMinT, q.BarBonusT = q.BarMin + 1, q.BarBonus
else
q.BarMinT, q.BarBonusT = q.BarMin + 1, q.BarBonus + 1
end
elseif q.dip_GameType == 3 then
q.iLives = 1
else
q.iLives = q.dip_LivesPerCredit
end
if (q.offsetIntroGame ~= 0) and (q.dip_StartScene == 1) then
Q.setupClip(q, q.offsetIntroGame, q.offsetIntroGameend)
q.lvlState = QTE.branch11
else
q.lvlState = QTE.lvlSetup
end
if (q.dip_PlayStyle == 3) and (q.MapStart == 0) then
q.currentLevel = QTE.levelMap
else
q.currentLevel = QTE.levelNormal
end
end
end
-- The high score board: its clips, the name left unentered (what is typed is not play), and
-- on to the game over.
function Q.doHighScore(q)
if q.lvlState == QTE.lvlSetup then
q.bShowLives, q.bShowScore, q.bShowCredits, q.bShowAction = false, false, false, false
Q.setupClip(q, q.offsetNewHScore, q.offsetNewHScoreend)
q.lvlState = QTE.branch01
elseif q.lvlState == QTE.branch01 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
if q.iFrameEnd + 1 ~= q.offsetEnterHScore then
Q.setupClip(q, q.offsetEnterHScore, q.offsetEnterHScoreend)
else
q.iFrameEnd = q.offsetEnterHScoreend
end
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame == q.iFrameEnd then
if q.iFrameEnd + 1 ~= q.offsetRankings then
Q.setupClip(q, q.offsetRankings, q.offsetRankingsend)
else
q.iFrameEnd = q.offsetRankingsend
end
q.lvlState = QTE.branch02
end
elseif q.lvlState == QTE.branch02 then
if q.currentFrame == q.iFrameEnd then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.bGOAlt = true
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelGameOver
end
end
-- ----- The game's own Lua: its settings script and its add-ons, run over the loop's state ---
-- A file the game's Lua names under its MYDIR, as a name beside the description: MYDIR is
-- Forge's (the description's directory) whatever the script set it to.
function Q.gameFile(q, name)
local mine = q.MYDIR or ""
local stem = mine:match("([^/\\]+)$")
if (mine ~= "") and (name:sub(1, #mine + 1) == mine .. "/") then
return name:sub(#mine + 2)
end
-- A game that names its files by their place in the Hypseus tree (singe/<title>/...): the
-- port carries them flat beside itself.
if stem and (name:sub(1, #stem + 7) == "singe/" .. stem .. "/") then
return name:sub(#stem + 8)
end
return name
end
-- A game's picture, found where the port carries it (a sheet of so many frames across when
-- a count is given); -1 when the file is not there, as a stub gave, so a game's load never
-- ends on missing art. One handle per file for the game's life: the games load a crosshair
-- or a flash again every frame they show it (some without unloading), so a load answers with
-- the handle it gave before, and an unload is nothing.
function Q.loadSprite(q, name, frames)
local path = authorFile(Q.gameFile(q, tostring(name)))
local key = path .. "#" .. tostring(frames or 0)
q.spriteCache = q.spriteCache or {}
if q.spriteCache[key] ~= nil then
return q.spriteCache[key]
end
local probe = io.open(path, "rb")
local handle = -1
if probe ~= nil then
probe:close()
if frames then
handle = spriteLoadFrames(path, frames)
else
handle = spriteLoad(path)
end
end
q.spriteCache[key] = handle
return handle
end
-- What a game's own code draws is drawn when Forge draws: the shim queues each sprite, text,
-- box, colour, and font call as it comes (a behaviour steps before the overlay is cleared, so
-- a call made then would be wiped), and the game's look runs the queue in order.
function Q.queueDraw(q, op)
q.draws = q.draws or {}
q.draws[#q.draws + 1] = op
end
-- The queue run, then emptied; a look calls this, then draws its own.
function Q.flushDraws(q)
for _, op in ipairs(q.draws or {}) do
op()
end
q.draws = {}
end
-- The queue emptied without drawing (a frame with no look to run it).
function Q.dropDraws(q)
q.draws = {}
end
-- A handle a game holds is a sprite when the load found the file.
local function isHandle(handle)
return (type(handle) == "number") and (handle >= 0)
end
-- The framework's font colours (the Karis toolbox's setFontColor), for the loops that do not
-- load that file; a game that defines its own keeps it.
local FONT_COLOURS = { { 255, 255, 255 }, { 255, 0, 0 }, { 0, 0, 255 }, { 255, 255, 0 }, { 0, 255, 0 }, { 255, 150, 0 }, { 252, 0, 148 }, { 127, 0, 255 }, { 30, 160, 250 }, { 128, 128, 128 }, { 1, 0, 0 } }
function Q.fontColour(q, which)
local c = FONT_COLOURS[which] or ((which == 12) and { q.miscR or 255, q.miscG or 255, q.miscB or 255 }) or nil
if c then
colorForeground(c[1], c[2], c[3])
Q.queueDraw(q, function() colorForeground(c[1], c[2], c[3]) end)
end
end
-- The dips a service screen changed, kept in the engine's save store for this game and read
-- back over the description's (the shipped defaults) when the loop attaches. The names are
-- the description's dips.
function Q.configLoad(q)
local text = saveGet("forge.cfg")
if type(text) ~= "string" then
return
end
local chunk = load(text, "config", "t", {})
local saved = chunk and chunk() or nil
if type(saved) ~= "table" then
return
end
for name in pairs(q.snapshot.dips or {}) do
if saved[name] ~= nil then
q[name] = saved[name]
end
end
-- The board the game keeps in the same file as its dips (the American Laser Games editions).
if (type(saved.highscore) == "table") and (type(q.highscore) == "table") then
q.highscore = saved.highscore
end
end
function Q.configSave(q)
local saved = {}
for name in pairs(q.snapshot.dips or {}) do
saved[name] = q[name]
end
if type(q.highscore) == "table" then
saved.highscore = q.highscore
end
saveSet("forge.cfg", "return " .. sourceOf(saved))
end
-- The globals a game's script sees: the state table for what it names there, the loop's
-- helpers under the framework's names, and the engine for the rest. What it assigns lands in
-- the state.
function Q.shim(q)
local helpers = {
timerON = function(seconds) Q.timerON(q, seconds) end,
timerDue = function() return Q.timerDue(q) end,
timerOFF = function() q.timerLimit = nil end,
joyDelayON = function(seconds) Q.joyDelayON(q, seconds) end,
joyDelayDue = function() return Q.joyDelayDue(q) end,
setupClip = function(from, to) Q.setupClip(q, from, to) end,
addPoints = function(thisMuch, thisValue) Q.addPoints(q, thisMuch, thisValue) end,
scanInput = function() return Q.scanInput(q) end,
getMiddle = function(phrase)
local spr = fontToSprite(tostring(phrase))
local x = (q.OVLW or overlayGetWidth()) / 2 - spriteGetWidth(spr) * 0.5
spriteUnload(spr)
return x
end,
getMiddle2 = function() return 0 end,
getMiddle3 = function() return 0 end,
setFontColor = function(which) Q.fontColour(q, which) end,
fontSelect = function(handle)
if isHandle(handle) then
fontSelect(handle)
Q.queueDraw(q, function() fontSelect(handle) end)
end
end,
fontQuality = function(quality)
fontQuality(quality)
Q.queueDraw(q, function() fontQuality(quality) end)
end,
colorForeground = function(r, g, b, a)
colorForeground(r, g, b, a or 255)
Q.queueDraw(q, function() colorForeground(r, g, b, a or 255) end)
end,
-- The games draw with the older argument order (x, y, sprite); a sheet's frame the same
-- way (x, y, frame, sprite).
spriteDraw = function(...)
local first = select(1, ...)
if type(first) == "number" and (select("#", ...) >= 3) then
local x, y, handle = ...
if isHandle(handle) then
Q.queueDraw(q, function() spriteDraw(handle, x, y) end)
end
end
end,
spriteDrawFrame = function(x, y, frame, handle)
if isHandle(handle) and (type(frame) == "number") then
Q.queueDraw(q, function() spriteDrawFrame(handle, x, y, math.max(1, math.floor(frame))) end)
end
end,
fontPrint = function(x, y, text)
local line = tostring(text)
Q.queueDraw(q, function() fontPrint(x, y, line) end)
end,
overlayBox = function(...)
local args = { ... }
Q.queueDraw(q, function() overlayBox(table.unpack(args)) end)
end,
overlayPrint = function(...)
local args = { ... }
Q.queueDraw(q, function() overlayPrint(table.unpack(args)) end)
end,
fontToSprite = function(text) return fontToSprite(tostring(text)) end,
spriteLoad = function(name) return Q.loadSprite(q, name) end,
-- A sheet, by the engine's order (file, count) or Hypseus's (count, file).
spriteLoadFrames = function(a, b)
if type(a) == "number" then
return Q.loadSprite(q, b, a)
end
return Q.loadSprite(q, a, b)
end,
spriteUnload = function() end,
spriteResetColorKey = function() end,
spriteGetWidth = function(handle)
if isHandle(handle) then
return spriteGetWidth(handle)
end
return 0
end,
spriteGetHeight = function(handle)
if isHandle(handle) then
return spriteGetHeight(handle)
end
return 0
end,
soundPlay = function(handle)
if type(handle) == "string" then
Q.sound(q, handle)
elseif type(handle) == "number" and (handle >= 0) then
soundPlay(handle)
end
end,
soundLoad = function(file)
-- A map-mode game keeps the name: its handle is the file, played by name.
if q.namedSounds then
local name = Q.gameFile(q, file)
q.sounds[name] = name
return name
end
return -1
end,
soundStop = function(handle) if type(handle) == "number" and (handle >= 0) then soundStop(handle) end end,
soundPause = function(handle) if type(handle) == "number" and (handle >= 0) then soundPause(handle) end end,
soundResume = function(handle) if type(handle) == "number" and (handle >= 0) then soundResume(handle) end end,
soundIsPlaying = function() return false end,
soundSetVolume = function() end,
discAudio = function() end,
videoLoad = function() return -1 end,
musicLoad = function(name) return musicLoad(authorFile(Q.gameFile(q, name))) end,
-- A font named by its place in the game's tree (the typing edition's), found where the
-- port carries it.
fontLoad = function(name, size) return fontLoad(authorFile(Q.gameFile(q, name)), size) end,
videoPlay = function() end,
videoUnload = function() end,
videoSeek = function() end,
videoSetVolume = function() end,
videoDraw = function() end,
overlayClear = function() end,
colorForeground = function() end,
autoSave = function() end,
writeSave = function() end,
loadSave = function() end,
cfgReadPath = function(name) return singeGetDataPath() .. "Cfg/" .. name end,
cfgWritePath = function(name) return singeGetDataPath() .. "Cfg/" .. name end,
singeGetScriptPath = function() return q.MYDIR .. "/" .. (q.snapshot.script or "game.singe") end,
dofile = function(name)
-- A map-mode game's files are its own data and helpers: loaded into the shim.
if q.loadsFiles then
Q.runScript(q, Q.gameFile(q, name))
end
end,
-- The port keeps no saves and reads no config of its own: a game's save, config, and
-- board files come from the description, so its file calls find nothing and write
-- nothing (lfs.attributes says the folder is not there). A read is the exception: a
-- game that reads its shipped config as its script loads (the library's Mad Dog McCree
-- and Mad Dog II) would end its load on the empty answer, so a file that is there is
-- read; what it sets, the description sets again after the load.
require = function(name)
if name == "lfs" then
return { attributes = function() return nil end, mkdir = function() end }
end
return {}
end,
io = { input = function(path)
q.ioReading = (type(path) == "string") and io.open(path, "rb") or nil
return q.ioReading
end,
output = function() return nil end,
read = function(what)
if q.ioReading then
return q.ioReading:read(what or "l")
end
return nil
end,
write = function() end,
close = function(handle)
if handle and (handle == q.ioReading) then
q.ioReading:close()
q.ioReading = nil
end
end,
open = function() return nil end,
lines = function() return function() return nil end end }
}
return setmetatable({}, {
__index = function(_, key)
if key == "MYDIR" then
-- Where the game is, Forge says -- with the slash the game's own MYDIR ended in,
-- since a script may join a file name straight onto it. A game that took its
-- directory from the script's path (the later map-mode copies, whose files sit
-- in Script/ under it) has it right already.
local own = q.gameMYDIR or ""
if (own ~= "") and (q.MYDIR:sub(1, #own) == own) then
return own
end
return q.MYDIR .. (string.match(own, "[/\\]$") and "/" or "")
end
local value = rawget(q, key)
if value ~= nil then
return value
end
value = helpers[key]
if value ~= nil then
return value
end
return _G[key]
end,
__newindex = function(_, key, value)
-- A script's own MYDIR names the Hypseus layout it came from ("singe/<Title>/"), not
-- where the port has put it; it is kept apart, for its shape.
if key == "MYDIR" then
q.gameMYDIR = value
else
q[key] = value
end
end
})
end
-- Runs a file of the game's Lua in the shim. Answers whether it loaded.
function Q.runScript(q, name)
-- The game's own files are beside the description first: a built script can share a name
-- with the game's script (Timegal.singe both), and loading the built one here would be
-- loading the game inside itself.
local path = authorBeside(name)
local chunk, why = loadfile(path, "t", q.env)
if chunk == nil then
debugPrint("Author: qte could not load " .. tostring(path) .. ": " .. tostring(why))
return false
end
local ok, err = xpcall(chunk, debug.traceback)
if not ok then
debugPrint("Author: qte running " .. tostring(name) .. ": " .. tostring(err):sub(1, 1500))
return false
end
return true
end
-- ----- The behaviour -------------------------------------------------------------------------
AUTHOR.behaviours.qte = {
help = "Plays a laserdisc quick-time-event game the KarisFramework way (or the MazescaterFramework's, with its compound moves), from the framework's own tables under game.qte: attract, credits, levels of scenes with moves to answer in a window of frames, death clips, continues, the dips, and the game's own add-ons. Written by util/forgePortKaris.lua from a game's script.",
params = {},
attach = function(instance, params)
local data = AUTHOR_GAME.qte or {}
local q = { instance = instance, snapshot = data }
instance.q = q
instance.hearsAll = true
-- The framework's constants, then the description's snapshot of the game's settings,
-- under the names the game's own Lua uses.
for key, value in pairs(QTE) do
if (key ~= "FLAG_OF") and (key ~= "BRANCH") then
q[key] = value
end
end
for key, value in pairs(data.settings or {}) do
q[key] = value
end
for key, value in pairs(data.dips or {}) do
q[key] = value
end
q.dip_Hints = (q.dip_Hints == 1) or (q.dip_Hints == true)
if Q.mazescater(q) then
-- The MazescaterFramework numbers DOUBLE after MULTI.
q.DOUBLE = 34
end
q.env = Q.shim(q)
q.MYDIR = (AUTHOR_SOURCE_DIR or AUTHOR_DIR or ""):gsub("[/\\]$", "")
-- The tables the framework's globals declare before a game's script fills them.
for _, name in ipairs({ "Level", "Death", "Tiers", "PlayOrder", "LvlOrder", "LvlMap", "move", "choice", "path", "timed", "stage", "scene", "sprite", "sprArrow", "sprNUM", "Group1", "Group2", "Group3", "Group4" }) do
q[name] = q[name] or {}
end
-- The game's own script declares the tables and setupMoves; its add-ons hook the loop.
-- Both run in the shim, so what they set is the state and what they read is the state.
if data.script then
Q.runScript(q, data.script)
end
if data.addons then
Q.runScript(q, data.addons)
end
q.sounds = {}
for _, name in ipairs({ "right", "wrong", "death", "victory", "coin", "credit", "clear", "roll" }) do
q.sounds["snd" .. name] = "Sounds/" .. name .. ".wav"
q["snd" .. name] = "snd" .. name
end
q.highScores = data.highScores or {}
q.iTopN = q.highScores[1] and q.highScores[1][2] or 0
q.iTopLB, q.iTopS = q.iTopN, q.iTopN
q.iTop = q.iTopN
-- The framework lays itself out on the disc's own size, or half of it.
if discGetWidth and (discGetWidth() > 0) then
if (q.dip_Res or 0) == 0 then
overlaySetResolution(discGetWidth(), discGetHeight())
else
overlaySetResolution(discGetWidth() / 2, discGetHeight() / 2)
end
end
local extra = q.dip_Extravid or 0
q.ShowResurrect = (extra == 1) or (extra == 4) or (extra == 5) or (extra == 7)
q.ShowSupDeath = (extra == 2) or (extra == 4) or (extra == 6) or (extra == 7)
q.ShowLvlClear = (extra == 3) or (extra == 5) or (extra == 6) or (extra == 7)
q.iPenal = ({ [0] = 0, q.PenalNormal or 0, q.PenalHard or 0, q.PenalExtreme or 0 })[q.dip_Difficulty or 1] or 0
q.BarMinT, q.BarBonusT = q.BarMin or 2, q.BarBonus or 3
q.iCoins, q.iCredits, q.iScore, q.iScoreTemp, q.iBonus, q.iScene, q.iExtraLife = 0, 0, 0, 0, 0, 0, 0
q.iLives, q.iLevel, q.currentMove, q.iContinues, q.iPath, q.iPathAend, q.iPathAjmp, q.i2P = 0, 1, 0, 0, 0, 0, 0, 0
q.iMash, q.iMulti, q.iLoopStep, q.iLoopPrev, q.iLenHold, q.lastHold, q.lenCounter, q.mashCounter, q.unMash = 0, 0, 0, 0, 0, 0, 8, 5, 0.07
q.iRightMv, q.iWrongMv, q.iScPlayed, q.iScDeath, q.iTotDeath, q.iLifeBar, q.iPauseFrame, q.iTempLevel, q.numTrophy = 0, 0, 0, 0, 0, q.BarSize or 10, 0, 0, 0
q.thisMove, q.currentFrame, q.iFrameStart, q.iFrameEnd, q.thisScore, q.Hit = QTE.NOMOVE, 0, 0, 0, 0, 0
q.bOneDiff, q.bRes, q.bPath, q.bTime, q.bCalc = true, false, false, true, true
q.bSave, q.bSwap, q.bGOAlt, q.bShowCredits, q.bIgnoreJoy, q.bAllowSave, q.bExtendedPlay = false, false, false, true, false, false, false
q.gameflow = "vldp"
q.currentLevel = QTE.levelIntro
q.lvlState = QTE.lvlSetup
q.move, q.path, q.choice, q.timed, q.totalMoves, q.sceneStart, q.sceneEnd = {}, {}, {}, {}, 0, 0, 0
q.stage, q.scene, q.LvlOrder = {}, {}, {}
Q.clearInput(q)
q.p1START1, q.p1START2, q.p1COIN1, q.p1COIN2, q.p1SERVICE = false, false, false, false, false
Q.initStages(q)
if q.MovieFPS then
discSetFPS(q.MovieFPS)
end
end,
step = function(instance)
local q = instance.q
q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then
-- The framework's start: the title clip once through, a frame of init, the attract loop.
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetTitle, q.offsetTitleend)
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.gameflow = "init"
end
return
end
if q.gameflow == "init" then
q.gameflow = "running"
q.lvlState = QTE.lvlSetup
return
end
if q.currentLevel == QTE.levelIntro then
Q.doIntro(q)
elseif q.currentLevel == QTE.levelNormal then
Q.doLevel(q)
elseif q.currentLevel == QTE.levelMap then
if q.doLevelSelect then
q.doLevelSelect()
else
q.currentLevel = QTE.levelNormal
q.lvlState = QTE.lvlSetup
end
elseif q.currentLevel == QTE.levelContinue then
Q.doContinue(q)
elseif q.currentLevel == QTE.levelGameOver then
Q.doGameOver(q)
elseif q.currentLevel == QTE.levelHighScore then
Q.doHighScore(q)
elseif q.currentLevel == QTE.levelFinish then
Q.doFinish(q)
elseif q.currentLevel == QTE.levelDiffScreen then
Q.doDiffSelect(q)
end
AUTHOR_VARS.score = q.iScore
AUTHOR_VARS.lives = q.iLives
AUTHOR_VARS.credits = q.iCredits
AUTHOR_VARS.level = q.iLevel
if q.bShowAction and q.move[q.currentMove] then
AUTHOR_VARS.prompt = q.move[q.currentMove][QTE.correctMove]
else
AUTHOR_VARS.prompt = ""
end
end,
on = function(instance, name, event)
local q = instance.q
local flag = qteFlagOf(event.switch or -1)
if flag == nil then
return
end
if name == "pressed" then
q[flag] = true
if (flag == "p1BUTTON1") and q.bTestMash then
q.iMash = q.iMash + 1
end
if (flag == "p1BUTTON1") and q.bTestMashL then
q.iMash = q.iMash + 1
end
if (flag == "p1BUTTON2") and q.bTestMashR then
q.iMash = q.iMash + 1
end
if (flag == "p1LEFT") and q.bTestRunL then
q.iMash = q.iMash + 1
end
if (flag == "p1RIGHT") and q.bTestRunR then
q.iMash = q.iMash + 1
end
-- The MazescaterFramework counts the third button and the directions for either
-- side of a mash.
if Q.mazescater(q) and ((flag == "p1BUTTON3") or (flag == "p1UP") or (flag == "p1DOWN") or (flag == "p1LEFT") or (flag == "p1RIGHT")) then
if q.bTestMashR then
q.iMash = q.iMash + 1
end
if q.bTestMashL then
q.iMash = q.iMash + 1
end
end
if q.bTestMulti and (flag ~= "p1START1") and (flag ~= "p1START2") and (flag ~= "p1COIN1") and (flag ~= "p1COIN2") and (flag ~= "p1SERVICE") then
q.iMulti = q.iMulti + 1
Q.sound(q, "sndroll")
end
elseif name == "released" then
if (flag == "p1COIN1") or (flag == "p1COIN2") then
q.p1COIN1, q.p1COIN2 = false, false
if (q.currentLevel ~= QTE.levelNormal) and (q.dip_CoinsPerCredit ~= QTE.DOPT_FREEPLAY) and (q.iCredits < 9) then
q.iCoins = q.iCoins + 1
if q.iCoins >= q.dip_CoinsPerCredit then
q.iCoins = q.iCoins - q.dip_CoinsPerCredit
q.iCredits = q.iCredits + 1
Q.sound(q, "sndcredit")
else
Q.sound(q, "sndcoin")
end
end
else
q[flag] = false
end
end
end
}
-- ===== The Kimmy Script Engine game loop ======================================================
--
-- The kimmy behaviour plays a game written for Karis's 2024 successor to the framework the qte
-- behaviour plays (FORGE.md section 14.11). The state machine, the screens, and the tables are
-- the same family, so the Q helpers above serve both; what Kimmy changed lives here under K:
-- a move judged by a seven-bit mask of what is held (acombo, from the presses) against the mask
-- the move asks for (gcombo), which gives combos, diagonals, a second accepted answer, and the
-- multi and loop sequences; mash in up/down, left/right, and button-pair variants; a hold of a
-- computed length; paths that jump to a later move or a death; yes/no and timed branches; way,
-- wayout, toscene, and tolevel jumps; tilt; the life bar; rewind modes 0, 2, and 3; the "die
-- and retry" game type that counts deaths instead of points; a new-game menu; a level select.
-- The numbers are Kimmy's own, and a run of the original and a run of the port trace alike.
local K = {} -- The loop's functions.
local KIMMY = {
-- Kimmy's constants, as its globals.singe declares them; the states and screens are QTE's.
UP = 1, DOWN = 2, LEFT = 3, RIGHT = 4, BUTTON1 = 5, BUTTON2 = 6, BUTTON3 = 7, BUTTON4 = 8,
UPLEFT = 9, UPRIGHT = 10, DOWNLEFT = 11, DOWNRIGHT = 12,
COMBO = 20, MULTI = 21, LOOP = 22, MASH = 23, HOLD = 24, LETGO = 25,
CHOOSE = 50, PATH = 51, YESNO = 52, TIMED = 53, ANYTHING = 54, NOTHING = 55,
SKIP = 100, WAY = 101, WAYOUT = 102, TOLEVEL = 103, TOSCENE = 104,
ACTUP = 120, ACTDOWN = 121, ACTLEFT = 122, ACTRIGHT = 123, RUN = 124, MASH2 = 125, MASHMIN = 126, MASHMAX = 127, MASH2MIN = 128, MASH2MAX = 129,
RUNMIN = 130, RUNMAX = 131, HOLDUP = 132, HOLDDOWN = 133, HOLDLEFT = 134, HOLDRIGHT = 135, HOLDBUT = 136, LOOPLEFT = 137, LOOPRIGHT = 138,
DOUBLE = 150, UD = 151, DU = 152, LR = 153, RL = 154, B1B2 = 155,
NOMOVE = -1, MOVEPENDING = -2, MOVEFAIL = -3, OUT = 1000,
levelNG = 118, level2P = 115, level2PEnd = 116, levelSelect = 117,
branch13 = 22
}
-- The masks: one bit per input, in the order the framework keeps them.
local KIMMY_MASK = {
[1] = { 1, 0, 0, 0, 0, 0, 0 }, [2] = { 0, 1, 0, 0, 0, 0, 0 }, [3] = { 0, 0, 1, 0, 0, 0, 0 }, [4] = { 0, 0, 0, 1, 0, 0, 0 },
[5] = { 0, 0, 0, 0, 1, 0, 0 }, [6] = { 0, 0, 0, 0, 0, 1, 0 }, [7] = { 0, 0, 0, 0, 0, 0, 1 },
[9] = { 1, 0, 1, 0, 0, 0, 0 }, [10] = { 1, 0, 0, 1, 0, 0, 0 }, [11] = { 0, 1, 1, 0, 0, 0, 0 }, [12] = { 0, 1, 0, 1, 0, 0, 0 }
}
local KIMMY_BIT = { p1UP = 1, p1DOWN = 2, p1LEFT = 3, p1RIGHT = 4, p1BUTTON1 = 5, p1BUTTON2 = 6, p1BUTTON3 = 7 }
local function maskOf(kind)
local bits = { 0, 0, 0, 0, 0, 0, 0 }
for i, bit in ipairs(KIMMY_MASK[kind] or {}) do
bits[i] = bit
end
return bits
end
-- How many bits of the held mask agree with the wanted one, and whether a wanted bit is held
-- that the move does not want: the framework's countcombo loop.
local function maskAgrees(held, wanted)
local count = 0
local extra = false
for i = 1, 7 do
if held[i] == wanted[i] then
count = count + 1
elseif (held[i] == 1) and (wanted[i] == 0) then
extra = true
end
end
return count, extra
end
-- ----- The state a move leaves behind ---------------------------------------------------------
function K.resetArrows(q)
q.bShowAction = false
end
function K.resetVar(q)
K.resetArrows(q)
q.bTestMash, q.bTestMashL, q.bTestMashR = false, false, false
q.bTestRunL, q.bTestRunR, q.bTestRunU, q.bTestRunD = false, false, false, false
q.bTestHold, q.bTestCombo, q.bTestMulti = false, false, false
q.iMash, q.iLenHold, q.iMulti = 0, 0, 1
q.bCalc = true
q.acombo = { 0, 0, 0, 0, 0, 0, 0 }
end
function K.resultMove(q)
local m = q.move[q.currentMove]
if q.thisMove == m[QTE.correctMove] then
q.lvlState = QTE.branch04
elseif q.thisMove ~= KIMMY.MOVEPENDING then
q.iPauseFrame = m[QTE.inputFrmEnd]
q.bShowAction = false
q.lvlState = QTE.branch02
end
end
-- The mask a move asks for, from the column that names it; once per move (bCalc).
function K.fillMove(q, column)
local m = q.move[q.currentMove]
q.bTestCombo = true
if not q.bCalc then
return
end
q.bCalc = false
q.gcombo = maskOf(m[column])
if m[QTE.correctMove] <= KIMMY.DOWNRIGHT then
if m[5] ~= nil then
q.g2combo = maskOf(m[5])
if m[5] == KIMMY.UPRIGHT then
-- The framework's own slip: UP lands on the first mask, not the second.
q.g2combo[1] = 0
q.gcombo[1] = 1
end
end
elseif m[QTE.correctMove] == KIMMY.COMBO then
for i, bit in ipairs(maskOf(m[6])) do
if bit == 1 then
q.gcombo[i] = 1
end
end
end
end
-- ----- The tests, one per kind of move --------------------------------------------------------
function K.checkAny(q, curMove)
if q.p1UP or q.p1DOWN or q.p1LEFT or q.p1RIGHT or q.p1BUTTON1 or q.p1BUTTON2 or q.p1BUTTON3 then
return curMove
end
return KIMMY.MOVEPENDING
end
function K.checkBasic(q, curMove)
local m = q.move[q.currentMove]
local z = KIMMY.MOVEPENDING
local count, extra = maskAgrees(q.acombo, q.gcombo)
local count2 = 0
if extra then
z = KIMMY.MOVEFAIL
end
if m[5] ~= nil then
local extra2
count2, extra2 = maskAgrees(q.acombo, q.g2combo)
if extra2 then
z = KIMMY.MOVEFAIL
end
end
if (count == 7) or (count2 == 7) then
z = curMove
end
return z
end
function K.checkCombo(q, curMove)
local count, extra = maskAgrees(q.acombo, q.gcombo)
if count == 7 then
return curMove
end
return extra and KIMMY.MOVEFAIL or KIMMY.MOVEPENDING
end
function K.checkHold(q, curMove)
local m = q.move[q.currentMove]
local z = KIMMY.MOVEPENDING
local count, extra = maskAgrees(q.acombo, q.gcombo)
if extra then
z = KIMMY.MOVEFAIL
end
if (q.currentFrame == m[QTE.inputFrmStart]) and (count == 7) then
z = KIMMY.MOVEFAIL
elseif (q.currentFrame > m[QTE.inputFrmStart]) and (count == 7) then
if q.iLenHold >= q.lenCounter then
z = curMove
q.iLenHold = 0
else
z = KIMMY.MOVEPENDING
if q.bTestHold and (q.currentFrame == q.lastHold) then
q.lastHold = q.currentFrame
elseif q.bTestHold and (q.currentFrame == q.lastHold + 1) then
q.iLenHold = q.iLenHold + 1
q.lastHold = q.currentFrame
else
q.lastHold = q.currentFrame
end
end
elseif count == 5 then
if z ~= KIMMY.NOMOVE then
q.iLenHold = 0
else
if q.iLenHold > 0 then
q.iLenHold = q.iLenHold - 1
end
z = KIMMY.MOVEPENDING
end
end
return z
end
function K.checkLet(q, playerMove, curMove)
local m = q.move[q.currentMove]
if (q.currentFrame == m[QTE.inputFrmStart]) and (playerMove == KIMMY.NOMOVE) then
return KIMMY.MOVEFAIL
elseif (q.currentFrame == m[QTE.inputFrmEnd] - 1) and (playerMove == KIMMY.NOMOVE) then
return curMove
end
return KIMMY.MOVEPENDING
end
function K.checkMash(q, curMove)
local z = KIMMY.MOVEPENDING
local count, extra = maskAgrees(q.acombo, q.gcombo)
if extra then
q.iMash = 0
z = KIMMY.MOVEFAIL
end
if count == 7 then
q.bTestMash, q.bTestRunU, q.bTestRunD, q.bTestRunL, q.bTestRunR = false, false, false, false, false
if q.iMash >= q.mashCounter then
z = curMove
q.p1BUTTON1, q.p1UP, q.p1DOWN, q.p1LEFT, q.p1RIGHT = false, false, false, false, false
end
elseif q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
return z
end
function K.checkMashBB(q, playerMove, curMove)
local z = KIMMY.MOVEPENDING
if (playerMove == KIMMY.BUTTON1) and q.bTestMashL then
q.bTestMashL, q.bTestMashR = false, true
if q.iMash >= q.mashCounter then
z = curMove
q.p1BUTTON1 = false
end
elseif (playerMove == KIMMY.BUTTON2) and q.bTestMashR then
q.bTestMashR, q.bTestMashL = false, true
if q.iMash >= q.mashCounter then
z = curMove
q.p1BUTTON2 = false
end
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= KIMMY.NOMOVE then
q.iMash = 0
z = KIMMY.MOVEFAIL
end
end
return z
end
function K.checkMashLR(q, playerMove, curMove)
-- LEFT sets bTestRunL false and bTestRunR true; RIGHT the reverse; UP and DOWN are let go.
local z = KIMMY.MOVEPENDING
if playerMove == KIMMY.LEFT then
q.bTestRunL, q.bTestRunR, q.p1LEFT = false, true, false
if q.iMash >= q.mashCounter then
z = curMove
end
elseif playerMove == KIMMY.RIGHT then
q.bTestRunL, q.bTestRunR, q.p1RIGHT = true, false, false
if q.iMash >= q.mashCounter then
z = curMove
end
elseif playerMove == KIMMY.UP then
q.p1UP = false
elseif playerMove == KIMMY.DOWN then
q.p1DOWN = false
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= KIMMY.NOMOVE then
q.iMash = 0
z = KIMMY.MOVEFAIL
end
end
return z
end
function K.checkMashUD(q, playerMove, curMove)
local z = KIMMY.MOVEPENDING
if playerMove == KIMMY.UP then
q.bTestRunU, q.bTestRunD, q.p1UP = false, true, false
if q.iMash >= q.mashCounter then
z = curMove
end
elseif playerMove == KIMMY.DOWN then
q.bTestRunU, q.bTestRunD, q.p1DOWN = true, false, false
if q.iMash >= q.mashCounter then
z = curMove
end
elseif playerMove == KIMMY.LEFT then
q.p1LEFT = false
elseif playerMove == KIMMY.RIGHT then
q.p1RIGHT = false
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= KIMMY.NOMOVE then
q.iMash = 0
z = KIMMY.MOVEFAIL
end
end
return z
end
function K.checkMulti(q, curMove)
local m = q.move[q.currentMove]
local z = KIMMY.MOVEPENDING
local steps = q.multi[q.currentMove]
if (m[6] ~= nil) and (q.currentFrame < m[QTE.inputFrmStart] + m[6]) and (m[QTE.inputFrmStart] + m[6] < m[QTE.inputFrmEnd]) then
return z
end
local count, extra = maskAgrees(q.acombo, q.mcombo)
if extra then
z = KIMMY.MOVEFAIL
end
if (count == 7) and (q.iMulti < #steps) then
q.iMulti = q.iMulti + 1
q.acombo = { 0, 0, 0, 0, 0, 0, 0 }
elseif (count == 7) and (q.iMulti == #steps) then
z = curMove
end
return z
end
function K.checkNo(q, curMove)
local m = q.move[q.currentMove]
local z = KIMMY.MOVEPENDING
if q.p1UP or q.p1DOWN or q.p1LEFT or q.p1RIGHT or q.p1BUTTON1 or q.p1BUTTON2 or q.p1BUTTON3 then
z = KIMMY.MOVEFAIL
end
if (q.currentFrame == m[QTE.inputFrmEnd]) and (z == KIMMY.MOVEPENDING) then
z = curMove
end
return z
end
function K.checkSkip(q, playerMove, curMove)
if playerMove ~= KIMMY.NOMOVE then
Q.clearInput(q)
q.bShowAction = false
discSkipToFrame(q.move[q.currentMove][QTE.inputFrmEnd])
end
return curMove
end
-- A branch taken: a death when the target is past OUT, else the moves to skip and where to.
local function takeBranch(q, target, endAt, jumpTo)
local m = q.move[q.currentMove]
q.iPath = target
q.bShowAction = false
if target > KIMMY.OUT then
m[QTE.moveDeath] = target - KIMMY.OUT
q.lvlState = QTE.branch02
else
q.iPathAend = endAt
q.iPathAjmp = jumpTo
q.lvlState = QTE.branch04
end
end
-- ----- The move being waited for, judged by its kind -----------------------------------------
function K.doMove(q)
local m = q.move[q.currentMove]
local kind = m[QTE.correctMove]
local fps = q.MovieFPS
if kind == KIMMY.HOLD then
q.bTestHold = true
if q.bCalc then
q.lenCounter = (m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) - (14 * (fps / 24) - q.dip_Difficulty)
end
K.fillMove(q, 5)
q.thisMove = K.checkHold(q, kind)
K.resultMove(q)
elseif kind == KIMMY.LETGO then
q.thisMove = K.checkLet(q, Q.scanInput(q), kind)
K.resultMove(q)
elseif kind == KIMMY.MASH then
local with = m[5]
q.thisMove = Q.scanInput(q)
if (with == nil) or (with == KIMMY.BUTTON1) or (with == KIMMY.BUTTON2) or (with == KIMMY.BUTTON3) then
q.bTestMash = true
elseif with == KIMMY.UP then
q.bTestRunU = true
elseif with == KIMMY.DOWN then
q.bTestRunD = true
elseif with == KIMMY.LEFT then
q.bTestRunL = true
elseif with == KIMMY.RIGHT then
q.bTestRunR = true
elseif with == KIMMY.UPLEFT then
q.bTestRunU, q.bTestRunL = true, true
elseif with == KIMMY.UPRIGHT then
q.bTestRunU, q.bTestRunR = true, true
elseif with == KIMMY.DOWNLEFT then
q.bTestRunD, q.bTestRunL = true, true
elseif with == KIMMY.DOWNRIGHT then
q.bTestRunD, q.bTestRunR = true, true
end
if q.bCalc then
local adjust = m[6] or 0
q.unMash = (q.dip_MashRes + adjust) * (0.014 + q.dip_Difficulty / 500)
q.mashCounter = ((m[QTE.inputFrmEnd] - m[QTE.inputFrmStart]) / fps) * 3
if (with >= KIMMY.UPLEFT) and (with <= KIMMY.DOWNRIGHT) then
q.mashCounter = q.mashCounter * 2
q.unMash = q.unMash * 1.5
elseif with == KIMMY.UD then
q.bTestRunU, q.bTestRunD = true, true
q.mashCounter = q.mashCounter * ((q.dip_MashtoRun == 1) and 1.5 or 2.5)
elseif with == KIMMY.LR then
q.bTestRunL, q.bTestRunR = true, true
q.mashCounter = q.mashCounter * ((q.dip_MashtoRun == 1) and 1.5 or 2.5)
elseif with == KIMMY.B1B2 then
q.bTestMashL, q.bTestMashR = true, true
q.mashCounter = q.mashCounter * 2.5
end
end
if with == KIMMY.UD then
q.thisMove = K.checkMashUD(q, q.thisMove, kind)
elseif with == KIMMY.LR then
q.thisMove = K.checkMashLR(q, q.thisMove, kind)
elseif with == KIMMY.B1B2 then
q.thisMove = K.checkMashBB(q, q.thisMove, kind)
else
K.fillMove(q, 5)
q.thisMove = K.checkMash(q, kind)
end
K.resultMove(q)
elseif kind <= KIMMY.DOWNRIGHT then
q.thisMove = KIMMY.NOMOVE
K.fillMove(q, 3)
q.thisMove = K.checkBasic(q, kind)
K.resultMove(q)
elseif kind == KIMMY.ANYTHING then
q.thisMove = K.checkAny(q, kind)
K.resultMove(q)
elseif kind == KIMMY.NOTHING then
q.thisMove = K.checkNo(q, kind)
K.resultMove(q)
elseif kind == KIMMY.COMBO then
K.fillMove(q, 5)
q.thisMove = K.checkCombo(q, kind)
K.resultMove(q)
elseif (kind == KIMMY.MULTI) or (kind == KIMMY.LOOP) then
q.bTestCombo, q.bTestMulti = true, true
if kind == KIMMY.LOOP then
q.bCalc = false -- The framework picks a loop picture here; the port draws none.
end
q.thisMove = Q.scanInput(q)
q.mcombo = maskOf(q.multi[q.currentMove][q.iMulti])
q.thisMove = K.checkMulti(q, kind)
K.resultMove(q)
elseif kind == KIMMY.PATH then
q.thisMove = Q.scanInput(q)
if q.thisMove ~= KIMMY.NOMOVE then
local p = q.path[q.currentMove]
if q.thisMove == p[1] then
takeBranch(q, p[2], p[4] - 1, p[9])
elseif q.thisMove == p[3] then
if p[5] == 0 then
takeBranch(q, p[4], 0, (p[9] == KIMMY.OUT) and KIMMY.OUT or 0)
else
takeBranch(q, p[4], p[6] - 1, p[9])
end
elseif q.thisMove == p[5] then
if p[7] == 0 then
takeBranch(q, p[6], 0, (p[9] == KIMMY.OUT) and KIMMY.OUT or 0)
else
takeBranch(q, p[6], p[8] - 1, p[9])
end
elseif q.thisMove == p[7] then
takeBranch(q, p[8], 0, (p[9] == KIMMY.OUT) and KIMMY.OUT or 0)
else
q.bShowAction = false
q.lvlState = QTE.branch02
end
end
elseif kind == KIMMY.YESNO then
local p = q.path[q.currentMove]
q.thisMove = Q.scanInput(q)
if q.thisMove ~= KIMMY.NOMOVE then
if q.thisMove == KIMMY.BUTTON1 then
takeBranch(q, p[1], p[2] - 1, p[3])
else
q.bShowAction = false
q.lvlState = QTE.branch02
end
elseif q.currentFrame == m[QTE.inputFrmEnd] then
takeBranch(q, p[2], 0, 0)
end
elseif kind == KIMMY.TIMED then
local i, j = m[5] or q.currentMove, m[6] or q.currentMove
q.thisMove = Q.scanInput(q)
if q.thisMove ~= KIMMY.NOMOVE then
if q.bTime then
q.bTime = false
q.Hit = q.currentFrame
end
for tcount = i, j do
local t = q.timed[tcount]
if (q.Hit >= t[2]) and (q.Hit <= t[3]) then
if t[1] == q.thisMove then
if t[5] ~= nil then
if t[5] == 0 then
discSkipToFrame(m[QTE.moveFrmEnd])
elseif t[5] > 0 then
q.iPath = t[5]
discSkipToFrame(m[QTE.moveFrmEnd])
end
end
q.lvlState = QTE.branch04
else
q.iPauseFrame, q.bShowAction, m[QTE.moveDeath], q.lvlState = m[QTE.inputFrmEnd], false, t[4], QTE.branch02
end
break
elseif tcount == j then
q.iPauseFrame, m[QTE.moveDeath], q.bShowAction, q.lvlState = m[QTE.inputFrmEnd], t[4], false, QTE.branch02
end
end
end
elseif kind == KIMMY.SKIP then
q.thisMove = K.checkSkip(q, Q.scanInput(q), kind)
end
end
-- ----- A scene's moves, made ready: shortcuts, difficulty, mirroring, frames ------------------
-- A direction or button named in a move's text ("U", "B2", "dl").
local KIMMY_WORD = { U = 1, D = 2, L = 3, R = 4, B1 = 5, B2 = 6, B3 = 7, UL = 9, UR = 10, DL = 11, DR = 12 }
local function wordOf(text)
return KIMMY_WORD[string.upper(text)]
end
-- The words of a comma-separated list, as the framework splits it: the last word is taken
-- whether or not a comma follows it.
local function wordsOf(text)
local words = {}
for word in string.gmatch(text .. ",", "([^,]*),") do
words[#words + 1] = word
end
return words
end
-- The framework's shorthand kinds (ACTUP, HOLDLEFT, RUNMAX, LOOPLEFT, DOUBLE, ...) rewritten
-- as the kind they stand for, and the multi, loop, path, and yes/no texts read into tables.
function K.getShortcuts(q, at)
local m = q.move[at]
local kind = m[3]
if kind == KIMMY.ACTUP then
m[3], m[5], m[6] = KIMMY.COMBO, KIMMY.BUTTON1, KIMMY.UP
elseif kind == KIMMY.ACTDOWN then
m[3], m[5], m[6] = KIMMY.COMBO, KIMMY.BUTTON1, KIMMY.DOWN
elseif kind == KIMMY.ACTLEFT then
m[3], m[5], m[6] = KIMMY.COMBO, KIMMY.BUTTON1, KIMMY.LEFT
elseif kind == KIMMY.ACTRIGHT then
m[3], m[5], m[6] = KIMMY.COMBO, KIMMY.BUTTON1, KIMMY.RIGHT
elseif kind == KIMMY.HOLDUP then
m[3], m[5] = KIMMY.HOLD, KIMMY.UP
elseif kind == KIMMY.HOLDDOWN then
m[3], m[5] = KIMMY.HOLD, KIMMY.DOWN
elseif kind == KIMMY.HOLDLEFT then
m[3], m[5] = KIMMY.HOLD, KIMMY.LEFT
elseif kind == KIMMY.HOLDRIGHT then
m[3], m[5] = KIMMY.HOLD, KIMMY.RIGHT
elseif kind == KIMMY.HOLDBUT then
m[3], m[5] = KIMMY.HOLD, KIMMY.BUTTON1
elseif kind == KIMMY.RUN then
m[3], m[5] = KIMMY.MASH, KIMMY.LR
elseif kind == KIMMY.MASH2 then
m[3], m[5] = KIMMY.MASH, KIMMY.B1B2
elseif kind == KIMMY.MASHMIN then
m[3], m[5], m[6] = KIMMY.MASH, KIMMY.BUTTON1, -2
elseif kind == KIMMY.MASHMAX then
m[3], m[5], m[6] = KIMMY.MASH, KIMMY.BUTTON1, 2
elseif kind == KIMMY.MASH2MIN then
m[3], m[5], m[6] = KIMMY.MASH, KIMMY.B1B2, -2
elseif kind == KIMMY.MASH2MAX then
m[3], m[5], m[6] = KIMMY.MASH, KIMMY.B1B2, 2
elseif (kind == KIMMY.MASH) and (m[5] == nil) then
m[5] = KIMMY.BUTTON1
elseif (kind == KIMMY.MASH) and (m[5] == KIMMY.DU) then
m[5] = KIMMY.UD
elseif (kind == KIMMY.MASH) and (m[5] == KIMMY.RL) then
m[5] = KIMMY.LR
elseif kind == KIMMY.RUNMIN then
m[3], m[5], m[6] = KIMMY.MASH, KIMMY.LR, -2
elseif kind == KIMMY.RUNMAX then
m[3], m[5], m[6] = KIMMY.MASH, KIMMY.LR, 2
elseif (kind == KIMMY.LOOP) or (kind == KIMMY.LOOPLEFT) or (kind == KIMMY.LOOPRIGHT) then
-- "U,+,1,B2": the first direction, the way round, the turns (Q a quarter, H a half),
-- and a button to finish on.
if kind == KIMMY.LOOPLEFT then
m[3], m[5] = KIMMY.LOOP, "L,+,1"
elseif kind == KIMMY.LOOPRIGHT then
m[3], m[5] = KIMMY.LOOP, "R,-,1"
end
local text = m[5]
local turns = string.sub(text, 5, 5)
local steps = { wordOf(string.sub(text, 1, 1)) }
local count = (turns == "Q") and 2 or ((turns == "H") and 3 or (4 * tonumber(turns) + 1))
local clockwise = { [KIMMY.UP] = KIMMY.RIGHT, [KIMMY.RIGHT] = KIMMY.DOWN, [KIMMY.DOWN] = KIMMY.LEFT, [KIMMY.LEFT] = KIMMY.UP }
local widdershins = { [KIMMY.UP] = KIMMY.LEFT, [KIMMY.LEFT] = KIMMY.DOWN, [KIMMY.DOWN] = KIMMY.RIGHT, [KIMMY.RIGHT] = KIMMY.UP }
local turn = (string.sub(text, 3, 3) == "+") and clockwise or widdershins
for i = 2, count do
steps[i] = turn[steps[i - 1]]
end
local button = wordOf(string.sub(text, 7, 8))
if button and (button >= KIMMY.BUTTON1) and (button <= KIMMY.BUTTON3) then
steps[count + 1] = button
end
q.multi[at] = steps
elseif (kind == KIMMY.MULTI) and (m[6] ~= nil) and (string.find(tostring(m[5]), ",") == nil) then
-- The same input so many times.
local steps = {}
for i = 1, m[6] do
steps[i] = m[5]
end
q.multi[at] = steps
m[6] = nil
elseif kind == KIMMY.MULTI then
local steps = {}
for i, word in ipairs(wordsOf(m[5])) do
steps[i] = wordOf(word) or KIMMY.BUTTON1
end
q.multi[at] = steps
elseif (kind == KIMMY.PATH) and (m[5] ~= nil) then
-- "U,10,D,11,L,12,R,14": a direction and the move it leads to, in pairs; the ninth
-- slot is where the branch rejoins, or OUT.
local p = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }
local slot = 1
for _, word in ipairs(wordsOf(m[5])) do
if slot % 2 == 1 then
p[slot] = wordOf(word) or 0
else
p[slot] = tonumber(word)
end
slot = slot + 1
end
p[9] = m[6] or KIMMY.OUT
q.path[at] = p
elseif (kind == KIMMY.YESNO) and (m[5] ~= nil) then
local p = { 0, 0, 0 }
for i, word in ipairs(wordsOf(m[5])) do
p[i] = tonumber(word)
end
p[3] = m[6] or KIMMY.OUT
q.path[at] = p
elseif kind == KIMMY.DOUBLE then
m[3], q.multi[at] = KIMMY.MULTI, { KIMMY.BUTTON1, KIMMY.BUTTON1 }
elseif kind == KIMMY.UD then
m[3], q.multi[at] = KIMMY.MULTI, { KIMMY.UP, KIMMY.DOWN }
elseif kind == KIMMY.DU then
m[3], q.multi[at] = KIMMY.MULTI, { KIMMY.DOWN, KIMMY.UP }
elseif kind == KIMMY.LR then
m[3], q.multi[at] = KIMMY.MULTI, { KIMMY.LEFT, KIMMY.RIGHT }
elseif kind == KIMMY.RL then
m[3], q.multi[at] = KIMMY.MULTI, { KIMMY.RIGHT, KIMMY.LEFT }
end
end
local KIMMY_SIDE = { [KIMMY.LEFT] = KIMMY.RIGHT, [KIMMY.RIGHT] = KIMMY.LEFT, [KIMMY.UPLEFT] = KIMMY.UPRIGHT, [KIMMY.UPRIGHT] = KIMMY.UPLEFT, [KIMMY.DOWNLEFT] = KIMMY.DOWNRIGHT, [KIMMY.DOWNRIGHT] = KIMMY.DOWNLEFT }
local function mirrored(value)
return KIMMY_SIDE[value] or value
end
-- The framework's setupFramesMoves: game type 5 makes every move a button; otherwise the
-- shortcuts, the easy difficulty's simplifications, the mash and hold dips, mirroring, and
-- the frames each move plays from and to.
function K.setupFramesMoves(q)
local move = q.move
for at = 1, q.totalMoves do
local m = move[at]
if q.dip_GameType == 5 then
if not ((m[3] >= KIMMY.CHOOSE) and (m[3] <= KIMMY.YESNO)) then
m[3] = KIMMY.BUTTON1
end
else
K.getShortcuts(q, at)
if q.dip_Difficulty == 0 then
if m[3] == KIMMY.HOLD then
m[3] = m[5]
elseif (m[3] == KIMMY.MASH) or (m[3] == KIMMY.LETGO) then
m[3] = KIMMY.BUTTON1
elseif (m[3] == KIMMY.LOOP) or (m[3] == KIMMY.MULTI) then
m[3], m[5] = q.multi[at][1], nil
elseif m[3] == KIMMY.COMBO then
m[3] = m[5]
end
else
if q.dip_MashtoRun == 2 then
if (m[3] == KIMMY.MASH) and ((m[5] == KIMMY.UD) or (m[5] == KIMMY.LR)) then
m[5] = KIMMY.B1B2
end
elseif q.dip_MashtoRun == 3 then
if (m[3] == KIMMY.MASH) and ((m[5] == KIMMY.UD) or (m[5] == KIMMY.LR) or (m[5] == KIMMY.B1B2)) then
m[3], m[5] = KIMMY.MASH, KIMMY.BUTTON1
end
end
if (q.dip_HoldtoLoop == 1) and (m[3] == KIMMY.LOOP) then
local way = string.sub(m[5], 3, 3)
if way == "+" then
m[3], m[5] = KIMMY.HOLD, KIMMY.RIGHT
elseif way == "-" then
m[3], m[5] = KIMMY.HOLD, KIMMY.LEFT
end
end
end
end
if q.bFlip then
if KIMMY_SIDE[m[3]] then
m[3] = KIMMY_SIDE[m[3]]
elseif ((m[3] == KIMMY.HOLD) or (m[3] == KIMMY.MASH)) and KIMMY_SIDE[m[5]] then
m[5] = KIMMY_SIDE[m[5]]
elseif m[3] == KIMMY.PATH then
local p = q.path[at]
for _, slot in ipairs({ 1, 3, 5, 7 }) do
if (p[slot] == KIMMY.LEFT) or (p[slot] == KIMMY.RIGHT) then
p[slot] = KIMMY_SIDE[p[slot]]
end
end
elseif m[3] == KIMMY.TIMED then
local from, to = m[5] or at, m[6] or at
for p = from, to do
local t = q.timed[p]
if (t[1] == KIMMY.LEFT) or (t[1] == KIMMY.RIGHT) then
t[1] = KIMMY_SIDE[t[1]]
end
t[2] = t[2] + q.offsetFlip
t[3] = t[3] + q.offsetFlip
end
elseif m[3] == KIMMY.COMBO then
m[5] = mirrored(m[5])
m[6] = mirrored(m[6])
elseif m[3] == KIMMY.MULTI then
for i, step in ipairs(q.multi[at]) do
q.multi[at][i] = mirrored(step)
end
elseif m[3] == KIMMY.LOOP then
for i, step in ipairs(q.multi[at]) do
if (step == KIMMY.LEFT) or (step == KIMMY.RIGHT) then
q.multi[at][i] = KIMMY_SIDE[step]
end
end
end
end
-- The frames the move plays from and to: after the move before, unless a jump kind
-- put that one later than this begins.
local jump = (m[3] == KIMMY.WAY) or (m[3] == KIMMY.WAYOUT) or (m[3] == KIMMY.TOSCENE) or (m[3] == KIMMY.TOLEVEL)
local before = move[at - 1]
if (at == 1) or jump then
m[7] = m[1]
elseif ((before[3] == KIMMY.WAY) or (m[3] == KIMMY.WAYOUT) or (m[3] == KIMMY.TOSCENE) or (m[3] == KIMMY.TOLEVEL)) and (before[8] > m[1]) then
m[7] = move[at - 2][2] + 1
else
m[7] = before[2] + 1
end
m[8] = (at < q.totalMoves) and m[2] or q.sceneEnd
end
end
function K.setupFrames(q, thisLevel)
local level = q.Level[thisLevel]
local w = q.RelativeFrames and 1 or 0
local move = q.move
q.offsetFlip = q.bFlip and level[QTE.MIRROR] or 0
q.sceneStart = q.sceneStart + w * level[QTE.INTROCLIP] + q.offsetFlip
q.sceneEnd = q.sceneEnd + w * level[QTE.INTROCLIP] + q.offsetFlip
q.Tlimit = q.sceneStart + 50
for k = 1, q.totalMoves do
move[k][1] = move[k][1] + w * level[QTE.INTROCLIP] + q.offsetFlip + q.iPenal
move[k][2] = move[k][2] + w * level[QTE.INTROCLIP] + q.offsetFlip
end
K.setupFramesMoves(q)
end
function K.setupLevel(q, thisLevel)
local level = q.Level[thisLevel]
q.iScene = q.iScene + 1
q.iPath, q.iPathAjmp, q.iPathAend = 0, 0, 0
if q.swapScene then
q.swapScene()
end
q.bFlip = false
if q.iScene > level[QTE.TOTALSCENES] then
q.iScene = q.iScene - 1
end
if level[QTE.MIRROR] > 0 then
if math.random(100) <= 50 then
q.bFlip = true
end
end
q.multi = {}
Q.loadMoves(q, thisLevel, q.iScene)
K.setupFrames(q, thisLevel)
if q.bAllowSave and (q.dip_GameType ~= 2) and (q.dip_GameType ~= 3) and (q.i2P == 0) then
-- The framework autosaves here; a save is not play, and the port keeps none.
q.bShowDiskA = true
q.altState = QTE.branch01
q.bAllowSave = false
end
end
-- ----- Scoring, deaths, and the next level ----------------------------------------------------
function K.addPoints(q, thisMuch, thisValue)
if q.specialScore then
q.specialScore(thisValue)
end
if q.dip_GameType == 4 then
return
end
q.thisScore = 0
if q.dip_GameType ~= 3 then
local worth = (q.thisScore == 0) and thisMuch or q.thisScore
q.iScore = q.iScore + worth
q.iExtraLife = q.iExtraLife + worth
if (q.EXTRALIFE > 0) and (q.iExtraLife >= q.EXTRALIFE) and (q.dip_GameType == 0) and ((q.i2P == 0) or q.hayate) then
q.iExtraLife = 0
if q.iLives < q.dip_LivesPerCredit then
Q.sound(q, "sndvictory")
q.iLives = q.iLives + 1
end
end
if (q.dip_GameType == 1) and (q.iRightMv == q.BarBonusT) then
q.iRightMv, q.iTilt = 0, 0
q.bShowWarnTilt, q.bShowTilt = false, false
if (q.BarBonusT ~= 0) and (q.iLifeBar < q.BarSize) then
q.iLifeBar = q.iLifeBar + 1
end
end
elseif thisValue == 1 then
q.iScore = q.iScore + 1
end
if q.iScore > q.iTop then
q.iTop = q.iScore
end
if q.iScore > 99999999 then
q.iScore = 99999999
end
end
-- Whether a result makes the board: fewer deaths than the "die and retry" record, or a score
-- among the ten.
function K.newScore(q, score)
if q.dip_GameType == 4 then
return score <= q.hsDR[q.dip_Difficulty + 1]
end
return Q.newScore(q, score)
end
function K.setupDeathClip(q)
local level = q.Level[q.iLevel]
local m = q.move[q.currentMove]
q.curDeath = m[QTE.moveDeath]
if q.swapDeath then
q.swapDeath()
end
q.bShowLvl, q.bShowNext, q.bShowWarnTilt = false, false, false
K.resetVar(q)
if q.curDeath < 0 then
q.lvlState = QTE.lvlPlayRest
return
end
q.lvlState = QTE.lvlPlayDeath
if q.dip_GameType ~= 4 then
q.iLives = q.iLives - 1
end
if q.i2P == 1 then
if q.iLives == 0 then
q.b1PEnd, q.i1PScore = true, q.iScore
end
elseif q.i2P == 2 then
if q.iLives == 0 then
q.b2PEnd, q.i2PScore = true, q.iScore
end
end
if m[QTE.moveDeath] == 0 then
q.curDeath = math.random(q.totalDeath)
end
local clip = q.Death[q.curDeath]
if q.bFlip then
Q.setupClip(q, clip[1] + level[QTE.DTHMIRROR], clip[2] + level[QTE.DTHMIRROR])
else
Q.setupClip(q, clip[1], clip[2])
end
if not q.dip_Hints and (q.dip_GameType ~= 1) then
Q.sound(q, "sndwrong")
end
if q.dip_Rewind == 1 then
if q.currentMove == 1 then
q.iPauseFrame = m[QTE.inputFrmStart] - 15
q.currentMove = 0
elseif m[QTE.correctMove] == KIMMY.CHOOSE then
m[QTE.moveDeath] = q.numChoice
q.iPauseFrame = q.move[q.currentMove - 2][QTE.inputFrmEnd] + 1
q.currentMove = q.currentMove - 2
elseif m[QTE.correctMove] == KIMMY.LETGO then
q.iPauseFrame = q.move[q.currentMove - 1][QTE.inputFrmStart] - 15
q.currentMove = q.currentMove - 2
else
q.iPauseFrame = m[QTE.inputFrmStart] - 15
q.currentMove = q.currentMove - 1
end
end
end
function K.nextLevel(q, thisLevel)
q.iScPlayed = 0
q.bLvlJump = false
if q.hayate or not q.MazeGame then
q.iScDeath = 0
end
if q.dip_GameType ~= 4 then
q.iTotDeath = 0
end
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
q.iScene = 0
elseif q.iPath ~= 0 then
q.iLevel = q.iPath
q.iPath = 0
q.iScene = 0
elseif not q.MazeGame then
for k = 1, q.finalstage - 1 do
if q.LvlOrder[k] == thisLevel then
q.iLevel = q.LvlOrder[k + 1]
q.iScene = 0
break
end
end
end
if q.swapLevel then
q.swapLevel()
end
end
-- On from a level: the map, the level select (play style 4), or the next level in order.
function K.onToNextLevel(q, reordering)
q.bSkipIntroClip = false
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
q.bAllowSave, q.bRes = true, true
q.iScoreTemp, q.iBonus = 0, 0
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
elseif reordering then
Q.reOrder(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 4 then
q.dip_StartLevel = q.dip_StartLevel + 1
q.iScene = 0
q.altState = QTE.lvlSetup
q.currentLevel = KIMMY.levelSelect
else
K.nextLevel(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
function K.levelReplay(q)
local replay = q.Level[q.iLevel][QTE.LVLREPLAY]
if replay == 0 then
K.onToNextLevel(q, false)
elseif replay == 1 then
if not q.stage[q.iLevel][QTE.LEVELSTARTED] then
q.stage[q.iLevel][QTE.LEVELSTARTED] = true
else
K.onToNextLevel(q, false)
end
elseif (replay > 1) or (replay < -1) then
if not q.stage[q.iLevel][QTE.LEVELSTARTED] then
q.stage[q.iLevel][QTE.LEVELSTARTED] = true
K.onToNextLevel(q, true)
else
K.onToNextLevel(q, false)
end
end
end
-- ----- The level: scenes, moves, deaths, and what follows them --------------------------------
-- The score, lives, and level flags as the display dip and the game type set them.
local function showPlayFlags(q)
if q.dip_GameType == 2 then
q.bShowScore, q.bShowLives = false, false
elseif q.dip_Display == 0 then
q.bShowScore, q.bShowLives, q.bShowLvl = true, true, true
q.bShowTop = q.ShowTop and true or false
else
q.bShowScore, q.bShowLives, q.bShowLvl, q.bShowTop = false, false, false, false
end
end
-- Into the scene: from a save, through the level's intro clip, or straight to its start.
function K.enterScene(q, thisLevel)
local level = q.Level[thisLevel]
if q.bSave and (q.currentMove ~= 1) then
q.currentFrame = q.move[q.currentMove - 1][QTE.inputFrmEnd] + 1
discSkipToFrame(q.currentFrame)
q.bSave = false
q.lvlState = QTE.lvlRunning
elseif (not q.bSkipIntroClip) and (q.iScene == 1) then
local flipBy = q.bFlip and level[QTE.MIRROR] or 0
Q.setupClip(q, level[QTE.INTROCLIP] + flipBy, level[QTE.INTROCLIPEND] + flipBy)
q.bShowSkip = true
q.lvlState = QTE.branch01
else
if (q.currentFrame + 2 <= q.sceneStart) or (q.currentFrame > q.sceneStart) then
discSkipToFrame(q.sceneStart)
end
q.lvlState = QTE.lvlRunning
end
end
-- Whether the next move ends the scene with a kind that rewind mode 3 counts the scene done on.
local function nextEndsScene(q)
local after = q.move[q.currentMove + 1]
if q.currentMove == q.totalMoves then
return true
end
return after and (q.currentMove + 1 == q.totalMoves) and ((after[3] == KIMMY.CHOOSE) or (after[3] == KIMMY.LETGO) or (after[3] == KIMMY.PATH) or (after[3] == KIMMY.YESNO))
end
-- Back into play after a death, from the frame the move paused on.
local function resumePlay(q)
local m = q.move[q.currentMove]
local after = q.move[q.currentMove + 1]
K.resetArrows(q)
discSkipToFrame(q.iPauseFrame)
if q.dip_Display == 0 then
q.bShowScore, q.bShowLives, q.bShowLvl = true, true, true
q.bShowTop = q.ShowTop and true or false
else
q.bShowScore, q.bShowLives, q.bShowLvl, q.bShowTop = false, false, false, false
end
if (q.dip_Rewind == 3) and after and (after[3] == KIMMY.LETGO) then
q.bSwap = true
q.currentMove = q.currentMove + 2
elseif (q.dip_Rewind == 3) and (m[3] == KIMMY.PATH) then
q.iPathAend = q.path[q.currentMove][4] - 1
q.iPathAjmp = q.path[q.currentMove][9]
q.currentMove = q.currentMove + 1
else
q.currentMove = q.currentMove + 1
end
q.lvlState = QTE.lvlRunning
end
-- After a death clip (or the extra clip after it): the death counted, the rewind mode says
-- where play goes. getReady: whether the "get ready" clip may play first.
function K.afterDeath(q, thisLevel, getReady)
q.stage[thisLevel][QTE.DEATHCOUNT] = q.stage[thisLevel][QTE.DEATHCOUNT] + 1
if q.iLives <= 0 then
q.lvlState = QTE.lvlEnd
return
end
if q.dip_Rewind == 0 then
q.bRes, q.bPath, q.bTime = true, true, true
q.lvlState = QTE.lvlEnd
elseif q.dip_Rewind == 2 then
q.bRes, q.bPath, q.bTime, q.bSwap = true, true, true, true
q.iScene = 0
for i = 1, q.Level[thisLevel][QTE.TOTALSCENES] do
q.scene[thisLevel][i] = { i, false }
end
q.lvlState = QTE.lvlSetup
elseif (q.dip_Rewind == 3) and nextEndsScene(q) then
q.bSwap = true
q.scene[q.iLevel][q.iScene][QTE.SCENECOMPLETE] = true
q.lvlState = QTE.lvlEnd
elseif getReady and q.ShowResurrect and (q.dip_GameType ~= 3) and (q.i2P == 0) then
Q.setupClip(q, q.offsetGetReady, q.offsetGetReadyEnd)
q.bShowGet, q.bShowTop = true, false
if (q.dip_Display == 1) or ((q.dip_Display == 2) and getReady == "both") then
q.bShowScore, q.bShowLives, q.bShowLvl = true, true, true
else
q.bShowScore, q.bShowLives, q.bShowLvl, q.bShowTop = false, false, false, false
end
q.lvlState = QTE.branch09
else
resumePlay(q)
end
end
-- A scene completed: the last of the game's last level ends the game; any other counts its
-- bonus and moves on. Answers whether the game ended.
function K.sceneDone(q, thisLevel)
local bonus = q.SCORESCENE - q.iScDeath * q.DEATHPENALTY
q.scene[thisLevel][q.iScene][QTE.SCENECOMPLETE] = true
if ((thisLevel == q.finalstage) and Q.beatLevel(q, q.finalstage)) or ((thisLevel == QTE.levelSecret) and Q.beatLevel(q, QTE.levelSecret)) then
if thisLevel == q.finalstage then
q.stage[thisLevel][QTE.BEATSTATUS] = true
end
if q.dip_GameType ~= 2 then
if Q.beatGame(q) then
if bonus > 0 then
K.addPoints(q, bonus, 0)
end
K.addPoints(q, q.SCOREGAME, 0)
if Q.beatGameWithOneLife(q) then
K.addPoints(q, q.SCORESECRET, 0)
end
else
if bonus > 0 then
K.addPoints(q, bonus, 0)
end
K.addPoints(q, q.SCORELEVEL, 0)
end
if thisLevel == QTE.levelSecret then
discPause()
Q.timerON(q, 0.1)
else
Q.sound(q, "sndvictory")
discSkipToFrame(q.frameVictory)
discPause()
Q.timerON(q, 3)
end
q.lvlState = QTE.branch05
else
q.lvlState = QTE.lvlEnd
end
return true
end
if bonus > 0 then
K.addPoints(q, bonus, 0)
q.iBonus = q.iBonus + bonus
end
q.iTotDeath = q.iTotDeath + q.iScDeath
q.iScPlayed = q.iScPlayed + 1
q.iScDeath = 0
return false
end
-- A wrong move, or a tilt: hints first if the dip says so, else the death.
function K.failed(q)
K.resetVar(q)
if q.dip_Hints and (q.dip_GameType ~= 1) and (q.dip_GameType ~= 2) and (q.move[q.currentMove][QTE.moveDeath] >= 0) then
Q.sound(q, "sndwrong")
q.bShowLvl, q.bShowScore, q.bShowLives, q.bShowNext, q.bShowWarnTilt = false, false, false, false, false
discSkipToFrame(q.frameHints)
Q.timerON(q, 2)
discPause()
q.lvlState = QTE.branch03
else
if q.iPath > KIMMY.OUT then
q.iPath = 0
end
q.iWrongMv = q.iWrongMv + 1
q.iScDeath = q.iScDeath + 1
if q.dip_GameType == 2 then
Q.sound(q, "sndwrong")
q.lvlState = QTE.lvlPlayRest
elseif q.dip_GameType == 1 then
q.iLifeBar = q.iLifeBar - q.BarMinT
q.iRightMv = 0
Q.sound(q, "sndwrong")
q.lvlState = QTE.lvlPlayRest
if q.iLifeBar <= 0 then
K.setupDeathClip(q)
end
else
K.setupDeathClip(q)
end
end
end
function K.doLevel(q)
local thisLevel = q.iLevel
local level = q.Level[thisLevel]
if q.lvlState == QTE.lvlSetup then
q.bShuffleOrder, q.bPlayPrompt, q.bPlayRight = true, true, true
q.bShowLvl, q.bShowAction, q.bShowNext = false, false, false
q.bLvlJump, q.bScnJump = false, false
q.bPath, q.bTime, q.bCalc = true, true, true
if (q.dip_GameType ~= 1) or (q.iScene == 0) then
q.iTilt = 0
q.bShowWarnTilt, q.bShowTilt = false, false
end
if q.hayate and (q.iScene == 0) then
q.dip_Rewind = 0
end
K.resetVar(q)
showPlayFlags(q)
K.resetArrows(q)
if not q.bSave then
q.currentMove = 1
end
K.setupLevel(q, thisLevel)
if level[QTE.INTROCLIPEND] - level[QTE.INTROCLIP] < 2 then
q.bSkipIntroClip = true
end
if q.ShowResurrect and q.bRes and (q.dip_GameType ~= 3) and (q.i2P == 0) then
q.bShowTop = false
Q.setupClip(q, q.offsetGetReady, q.offsetGetReadyEnd)
if (q.dip_Display == 1) or (q.dip_Display == 2) then
q.bShowScore, q.bShowLives, q.bShowLvl = true, true, true
else
q.bShowScore, q.bShowLives, q.bShowLvl, q.bShowTop = false, false, false, false
end
q.bShowGet = true
q.lvlState = QTE.branch08
q.bRes = false
else
K.enterScene(q, thisLevel)
end
elseif q.lvlState == QTE.branch01 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 or q.p1BUTTON2 or q.p1BUTTON3 or q.p1UP or q.p1DOWN or q.p1LEFT or q.p1RIGHT then
Q.clearInput(q)
q.bShowSkip = false
q.bSkipIntroClip = true
if q.currentFrame ~= q.iFrameEnd then
discSkipToFrame(q.sceneStart)
end
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.branch02 then
K.failed(q)
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) then
q.iPath = 0
q.iWrongMv = q.iWrongMv + 1
q.iScDeath = q.iScDeath + 1
K.setupDeathClip(q)
end
elseif q.lvlState == QTE.branch04 then
q.bShowAction, q.bShowNext, q.bTime = false, false, true
K.resetVar(q)
q.lvlState = QTE.lvlPlayRest
if (q.thisMove ~= KIMMY.NOTHING) or ((not q.hayate) and (q.thisMove == KIMMY.NOTHING) and (q.move[q.currentMove][5] == nil)) then
if q.bPlayRight then
Q.sound(q, "sndright")
q.bPlayRight = false
end
q.iRightMv = q.iRightMv + 1
K.addPoints(q, q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE, q.currentMove)
q.iScoreTemp = q.iScoreTemp + (q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE)
end
elseif q.lvlState == QTE.branch05 then
if Q.timerDue(q) then
q.bGOAlt = true
if (Q.beatGameWithOneLife(q) or Q.beatGameWithOneCredit(q)) and (thisLevel ~= QTE.levelSecret) and q.AllowSecret and (q.dip_GameType ~= 4) then
Q.sound(q, "sndvictory")
discSkipToFrame(q.frameSecret)
discPause()
Q.timerON(q, 4)
q.lvlState = QTE.branch06
elseif (q.dip_GameType ~= 4) and K.newScore(q, q.iScore) then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelHighScore
elseif (q.dip_GameType == 4) and (not q.bUnlockSel) and K.newScore(q, q.iTotDeath + q.iScDeath) then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelHighScore
else
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelGameOver
end
end
elseif q.lvlState == QTE.branch06 then
if Q.timerDue(q) then
q.bExtendedPlay = true
q.iScene = 0
K.startGame(q)
end
elseif q.lvlState == QTE.branch07 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 then
K.afterDeath(q, thisLevel, "both")
end
elseif q.lvlState == QTE.branch08 then
if q.currentFrame >= q.iFrameEnd then
q.bShowGet = false
showPlayFlags(q)
K.enterScene(q, thisLevel)
end
elseif q.lvlState == QTE.branch09 then
if q.currentFrame >= q.iFrameEnd then
q.bShowGet = false
resumePlay(q)
showPlayFlags(q)
end
elseif q.lvlState == QTE.branch10 then
Q.doChoose(q)
elseif q.lvlState == QTE.branch11 then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlSetup
end
elseif q.lvlState == QTE.branch12 then
if not K.sceneDone(q, thisLevel) then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == KIMMY.branch13 then
if Q.timerDue(q) then
K.afterDeath(q, thisLevel, false)
else
q.bShowScore, q.bShowLives = true, true
end
elseif q.lvlState == QTE.lvlPlayRest then
local m = q.move[q.currentMove]
q.bPlayRight = true
if q.currentMove < q.totalMoves then
q.bCheckMove = true
end
q.thisMove = Q.scanInput(q)
if q.currentFrame >= m[QTE.moveFrmEnd] then
q.bCheckMove = false
if q.iPathAjmp == KIMMY.OUT then
q.currentMove = q.iPathAjmp
q.iPathAend, q.iPathAjmp = 0, 0
q.lvlState = QTE.branch12
elseif (q.iPathAend ~= 0) and (q.currentMove == q.iPathAend) then
q.currentMove = q.iPathAjmp
q.iPathAend, q.iPathAjmp = 0, 0
elseif (q.iPath ~= 0) and (q.currentMove <= q.totalMoves) then
q.currentMove = q.iPath
q.iPath = 0
q.bPath = true
else
q.currentMove = q.currentMove + 1
end
if q.currentMove <= q.totalMoves then
local next = q.move[q.currentMove]
if (q.currentFrame + 2 <= next[QTE.moveFrmStart]) or (q.currentFrame > next[QTE.moveFrmStart]) then
discSkipToFrame(next[QTE.moveFrmStart])
end
q.bShowAction = false
q.bPlayPrompt = true
K.resetArrows(q)
if next[QTE.correctMove] == KIMMY.CHOOSE then
q.altState = QTE.lvlSetup
q.lvlState = QTE.branch10
else
q.lvlState = QTE.lvlRunning
end
else
q.bShowNext = false
if not K.sceneDone(q, thisLevel) then
if q.currentFrame >= q.sceneEnd then
q.lvlState = QTE.lvlEnd
end
end
end
end
elseif q.lvlState == QTE.lvlRunning then
local m = q.move[q.currentMove]
local kind = m[QTE.correctMove]
if (q.dip_GameType <= 2) and q.dip_Next then
q.bShowNext = true
end
if q.currentFrame < m[QTE.inputFrmStart] then
q.thisMove = Q.scanInput(q)
if (q.thisMove ~= KIMMY.NOMOVE) and (kind < 25) and (q.dip_Tilt > 0) and (q.dip_GameType < 5) then
q.iTilt = q.iTilt + 1
q.iTrigTilt = 50 - 10 * q.dip_Tilt
if q.iTilt >= q.iTrigTilt then
q.iPauseFrame = m[QTE.inputFrmEnd]
if (q.dip_GameType == 0) or (q.dip_GameType == 3) or (q.dip_GameType == 4) then
q.lvlState = QTE.branch02
elseif q.dip_GameType == 1 then
q.iLifeBar = q.iLifeBar - q.BarMinT
q.iRightMv, q.iTilt = 0, 0
Q.sound(q, "sndwrong")
q.altState = QTE.branch01
if q.iLifeBar <= 0 then
K.setupDeathClip(q)
end
elseif q.dip_GameType == 2 then
q.iTiltMv = q.iTiltMv + 1
q.iTilt = 0
Q.sound(q, "sndwrong")
q.altState = QTE.branch01
end
q.bShowTilt = true
elseif (q.iTilt > q.iTrigTilt / 2) and (q.iTilt < q.iTrigTilt) and (q.dip_GameType ~= 5) and (q.dip_GameType ~= 6) then
q.bShowWarnTilt = true
end
end
elseif (q.currentFrame >= m[QTE.inputFrmStart]) and (q.currentFrame <= m[QTE.inputFrmEnd]) then
q.bShowAction = true
if q.bPlayPrompt and ((q.dip_ShowAction == 1) or (q.dip_ShowAction == 5)) and (kind < 50) and not q.hayate then
Q.sound(q, "sndcoin")
q.bPlayPrompt = false
end
K.doMove(q)
elseif q.currentFrame > m[QTE.inputFrmEnd] then
local pending = (q.thisMove == KIMMY.NOMOVE) or (q.thisMove == KIMMY.MOVEPENDING)
local jump = (kind == KIMMY.SKIP) or (kind == KIMMY.WAY) or (kind == KIMMY.WAYOUT) or (kind == KIMMY.TOSCENE) or (kind == KIMMY.TOLEVEL)
if pending and ((m[QTE.moveDeath] == -1) or (m[QTE.moveDeath] == -3)) then
q.bShowAction = false
q.lvlState = QTE.lvlPlayRest
elseif pending and (m[QTE.moveDeath] == -2) then
q.bShowAction = false
q.lvlState = QTE.branch04
elseif not jump then
q.iPauseFrame = m[QTE.inputFrmEnd]
q.bShowAction = false
q.lvlState = QTE.branch02
else
q.bShowAction = false
K.addPoints(q, 0, q.currentMove)
if (kind == KIMMY.WAY) and (m[QTE.moveDeath] > 0) then
q.iPath = m[QTE.moveDeath]
q.lvlState = QTE.lvlPlayRest
elseif kind == KIMMY.WAYOUT then
q.iPath = m[QTE.moveDeath]
q.lvlState = QTE.branch12
elseif kind == KIMMY.TOSCENE then
q.bScnJump = true
q.iPath = m[QTE.moveDeath]
q.lvlState = QTE.branch12
elseif kind == KIMMY.TOLEVEL then
q.bLvlJump = true
q.iPath = m[QTE.moveDeath]
q.lvlState = QTE.branch12
else
q.lvlState = QTE.lvlPlayRest
end
end
else
K.resetVar(q)
end
elseif q.lvlState == QTE.lvlPlayDeath then
if q.dip_Display == 1 then
q.bShowScore, q.bShowLives = true, true
else
q.bShowScore, q.bShowLives = false, false
end
if q.currentFrame >= q.iFrameEnd then
q.iTilt = 0
q.bShowWarnTilt, q.bShowTilt = false, false
if q.i2P > 0 then
q.i2P = (q.i2P == 1) and 2 or 1
q.lvlState = QTE.lvlSetup
q.currentLevel = (q.b1PEnd and q.b2PEnd) and KIMMY.level2PEnd or KIMMY.level2P
elseif (q.dip_Display == 2) and (q.iLives > 0) and (not q.ShowSupDeath) and (not q.ShowResurrect) then
discSkipToFrame(q.frameHints)
discPause()
Q.timerON(q, 3)
q.lvlState = KIMMY.branch13
elseif q.ShowSupDeath and (q.dip_GameType ~= 3) then
Q.setupClip(q, q.offsetSupDeath, q.offsetSupDeathEnd)
q.lvlState = QTE.branch07
else
K.afterDeath(q, thisLevel, true)
end
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.bPath, q.bTime = true, true
if q.iLives == 0 then
if (q.dip_GameType ~= 3) and (q.dip_LimitContinue > 0) and ((q.iContinues < q.dip_LimitContinue) or (q.dip_LimitContinue == QTE.DOPT_INFINITE_CONTINUES)) then
q.iTempLevel = q.currentLevel
q.currentLevel = QTE.levelContinue
q.iContinues = q.iContinues + 1
else
q.dip_StartLevel, q.dip_StartScene = 1, 1
q.currentLevel = K.newScore(q, q.iScore) and QTE.levelHighScore or QTE.levelGameOver
end
else
if Q.beatLevel(q, thisLevel) and not q.bScnJump then
q.stage[thisLevel][QTE.BEATSTATUS] = true
q.iScene = 0
K.addPoints(q, q.SCORELEVEL, 0)
q.iBonus = q.iBonus + q.SCORELEVEL
if ((q.dip_GameType == 1) and (q.iTotDeath == 0)) or ((q.dip_GameType ~= 1) and (q.stage[thisLevel][QTE.DEATHCOUNT] == 0)) then
K.addPoints(q, q.PERFECTBONUS, 0)
q.iBonus = q.iBonus + q.PERFECTBONUS
end
q.bSkipIntroClip = false
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
q.bAllowSave, q.bRes = true, true
if (q.ShowLvlClear or (q.dip_GameType == 2)) and (q.dip_GameType ~= 3) and (q.dip_GameType ~= 4) then
Q.sound(q, "sndclear")
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelFinish
else
q.iScoreTemp, q.iBonus = 0, 0
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
elseif q.dip_PlayStyle == 4 then
q.dip_StartLevel = q.dip_StartLevel + 1
q.iScene = 0
q.altState = QTE.lvlSetup
q.currentLevel = KIMMY.levelSelect
else
K.nextLevel(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
else
if (q.iPath ~= 0) and not q.bLvlJump then
q.iScene = q.iPath - 1
end
if (not q.scene[thisLevel][q.iScene][QTE.SCENECOMPLETE]) and (q.iPath == 0) then
if q.dip_Rewind == 0 then
q.bSwap = true
end
if q.iScene > 0 then
q.iScene = q.iScene - 1
end
else
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
q.bAllowSave, q.bSave = true, false
if q.bLvlJump then
K.nextLevel(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
q.iPath = 0
if (q.dip_Rewind == 0) or (q.dip_Rewind == 2) then
K.levelReplay(q)
end
end
q.bShowLives, q.bShowLvl, q.bShowAction, q.bShowNext = false, false, false, false
K.resetVar(q)
end
end
end
-- ----- The screens ---------------------------------------------------------------------------
-- The life bar's costs and gains for the difficulty, and the lives a game type starts with.
local function livesForType(q)
if q.dip_GameType == 1 then
q.iLives = 1
if q.dip_Difficulty == 0 then
q.BarMinT = q.BarMin
if q.BarBonus > 0 then
q.BarBonusT = q.BarBonus - 1
end
elseif q.dip_Difficulty == 1 then
q.BarMinT, q.BarBonusT = q.BarMin, q.BarBonus
elseif q.dip_Difficulty == 2 then
q.BarMinT, q.BarBonusT = q.BarMin + 1, q.BarBonus
else
q.BarMinT, q.BarBonusT = q.BarMin + 1, q.BarBonus + 1
end
elseif q.dip_GameType == 3 then
q.iLives = 1
else
q.iLives = q.dip_LivesPerCredit
end
end
-- The record the game type plays against.
local function topFor(q)
if q.dip_GameType == 1 then
return q.iTopLB
elseif q.dip_GameType == 3 then
return q.iTopS
elseif q.dip_GameType == 4 then
return q.hsDR[q.dip_Difficulty + 1]
end
return q.iTopN
end
local function toLevelSelect(q)
q.altState = QTE.lvlSetup
q.currentLevel = KIMMY.levelSelect
end
function K.startGame(q)
math.randomseed(KARIS_SEED or os.time())
math.random(100)
q.bLvlJump = false
if q.bExtendedPlay then
Q.initStages(q)
q.currentLevel = QTE.levelNormal
q.iLevel = QTE.levelSecret
else
if (q.iCredits > 0) and (q.i2P == 0) then
q.iCredits = q.iCredits - 1
end
q.iScore, q.iScoreTemp, q.iBonus, q.iScPlayed, q.iScDeath, q.iTotDeath = 0, 0, 0, 0, 0, 0
q.iRightMv, q.iWrongMv, q.iTiltMv, q.numTrophy = 0, 0, 0, 0
if q.currentLevel == QTE.levelContinue then
q.currentLevel = q.iTempLevel
q.iLifeBar = q.BarSize
if q.dip_Rewind == 0 then
q.bSwap = true
K.levelReplay(q)
elseif q.dip_Rewind == 1 then
q.currentMove = q.currentMove + 1
q.bSave = true
discSkipToFrame(q.iPauseFrame)
q.lvlState = QTE.lvlRunning
elseif q.dip_Rewind == 2 then
q.bRes, q.bPath, q.bTime, q.bSwap = true, true, true, true
q.iScene = 0
K.levelReplay(q)
elseif q.dip_Rewind == 3 then
if q.currentMove == q.totalMoves then
q.bSwap = true
q.scene[q.iLevel][q.iScene][QTE.SCENECOMPLETE] = true
q.iScene, q.currentMove, q.bSave = q.iScene + 1, 1, true
elseif nextEndsScene(q) then
q.bSwap = true
q.scene[q.iLevel][q.iScene][QTE.SCENECOMPLETE] = true
q.iScene, q.currentMove = q.iScene + 1, 1
else
q.currentMove, q.bSave = q.currentMove + 1, true
end
end
else
Q.initStages(q)
q.bSkipIntroClip = false
q.iPath, q.iPathAend, q.iPathAjmp = 0, 0, 0
q.iContinues, q.iScene, q.currentMove = 0, 0, 1
if (q.dip_GameType == 0) or (q.dip_GameType == 1) or (q.dip_GameType == 4) then
q.iLifeBar = q.BarSize
if q.dip_GameType == 4 then
q.dip_PlayStyle = 0
if not q.bUnlockSel then
q.dip_StartLevel, q.dip_StartScene = 1, 1
end
end
q.iTop = topFor(q)
if q.dip_PlayStyle == 0 then
Q.doMixSEQ(q)
q.iLevel = q.dip_StartLevel
q.iScene = q.dip_StartScene - 1
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 1 then
Q.doMixRND(q)
q.iLevel = q.LvlOrder[1]
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 2 then
Q.doMixTIE(q)
q.iLevel = q.LvlOrder[1]
q.currentLevel = QTE.levelNormal
elseif q.dip_PlayStyle == 3 then
q.iLevel = q.PlayOrder[1]
if q.MapStart == 1 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
else
q.currentLevel = QTE.levelMap
end
elseif q.dip_PlayStyle == 4 then
q.iLevel, q.iScene = q.dip_StartLevel, 0
toLevelSelect(q)
end
elseif q.dip_GameType == 2 then
q.bShowLives, q.bShowScore = false, false
q.iLevel, q.iScene = q.dip_StartLevel, 0
if not q.bNoScreen then
toLevelSelect(q)
else
q.currentLevel = QTE.levelNormal
end
elseif q.dip_GameType == 3 then
Q.doMixSEQ(q)
q.iLevel, q.iScene = 1, 0
q.iTop = q.iTopS
q.currentLevel = QTE.levelNormal
elseif q.dip_GameType == 5 then
q.iLevel, q.iScene = q.dip_StartLevel, 0
toLevelSelect(q)
end
if q.startConf then
q.startConf()
end
end
end
if q.IngameDiffchoice and (q.dip_Diffshow == 4) and q.bOneDiff and not q.bNoScreen then
q.altState = QTE.lvlSetup
q.currentLevel = QTE.levelDiffScreen
if q.i2P > 0 then
q.bOneDiff = false
end
end
if (q.offsetIntroGame ~= 0) and (q.iContinues == 0) and (q.dip_StartScene == 1) then
Q.setupClip(q, q.offsetIntroGame, q.offsetIntroGameend)
q.lvlState = QTE.branch11
else
q.lvlState = QTE.lvlSetup
end
q.bShowScore, q.bRes = false, true
livesForType(q)
q.bShowAction, q.bShowNext, q.bShowCredits, q.bShowLCD, q.bResetContinue, q.bExtendedPlay = false, false, false, false, false, false
end
-- The attract loop: the clips and stills in turn, the secret combination, and a start.
function K.doIntro(q)
local function still(frame, seconds, nextState)
q.p1BUTTON1 = false
discSkipToFrame(frame)
discPause()
q.bShowLCD, q.bShowCredits = false, false
Q.timerON(q, seconds)
q.lvlState = nextState
end
local function clip(from, to, nextState)
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = true, true
Q.setupClip(q, from, to)
q.lvlState = nextState
end
local function secret()
if Q.secretPressed(q) then
q.p1BUTTON2, q.p1BUTTON3, q.p1BUTTON4, q.p1UP, q.p1RIGHT = false, false, false, false, false
q.bExtendedPlay = true
K.startGame(q)
q.bShowCredits = false
return true
end
return false
end
local ended = (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetIntro01, q.offsetIntro01end)
q.lvlState = QTE.branch01
q.bShowCredits, q.bShowLCD, q.bShowLives, q.bCheckForCredits = true, true, false, true
elseif q.lvlState == QTE.branch01 then
if ended then
still(q.frameControls, 10, (q.frameSpecial ~= q.frameControls) and QTE.branch02 or QTE.branch03)
else
secret()
end
elseif q.lvlState == QTE.branch02 then
if Q.timerDue(q) or q.p1BUTTON1 then
still(q.frameSpecial, 10, QTE.branch03)
end
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) or q.p1BUTTON1 then
clip(q.offsetIntro02, q.offsetIntro02end, QTE.branch04)
end
elseif q.lvlState == QTE.branch04 then
if ended then
still(q.frameRankings, 15, QTE.branch05)
else
secret()
end
elseif q.lvlState == QTE.branch05 then
if Q.timerDue(q) or q.p1BUTTON1 then
clip(q.offsetIntro03, q.offsetIntro03end, (q.LvlTrophy3 ~= 0) and QTE.branch06 or QTE.branch09)
else
secret()
end
elseif q.lvlState == QTE.branch06 then
if ended then
still(q.frameTrophy, 10, QTE.branch07)
else
secret()
end
elseif q.lvlState == QTE.branch07 then
if Q.timerDue(q) or q.p1BUTTON1 then
still(q.frameTrophy, 10, QTE.branch08)
end
elseif q.lvlState == QTE.branch08 then
if Q.timerDue(q) or q.p1BUTTON1 then
clip(q.offsetTitle, q.offsetTitleend, QTE.branch09)
else
secret()
end
elseif q.lvlState == QTE.branch09 then
if ended then
still(q.frameRankingsAlt, 15, QTE.branch10)
else
secret()
end
elseif q.lvlState == QTE.branch10 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLCD, q.bShowCredits = false, false
q.lvlState = QTE.lvlSetup
end
end
local free = (q.dip_CoinsPerCredit == QTE.DOPT_FREEPLAY)
if free or (q.iCredits > 0) then
if q.p1START1 then
q.p1START1, q.bShowCredits, q.i2P = false, false, 0
if q.hayate and q.ArcadeMode then
q.dip_StartLevel, q.dip_StartScene = 1, 1
end
if (q.dip_GameType == 2) or (q.dip_GameType == 3) then
K.startGame(q)
elseif q.dip_GameType == 6 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMovie
elseif free and (q.dip_PlayStyle ~= 4) then
q.lvlState = QTE.lvlSetup
q.currentLevel = KIMMY.levelNG
else
q.lvlState = QTE.lvlSetup
K.startGame(q)
end
end
end
if free or (q.iCredits > 1) then
if q.p1START2 then
q.p1START2 = false
if ((q.dip_GameType == 0) or (q.dip_GameType == 3)) and not free then
q.iCredits = q.iCredits - 2
end
q.lvlState = QTE.lvlSetup
q.i2P = 1
q.b1PStart, q.b2PStart, q.b1PEnd, q.b2PEnd = false, false, false, false
q.i1PScore, q.i2PScore = 0, 0
q.bShowCredits = false
q.currentLevel = KIMMY.level2P
end
end
end
-- The saves the game shipped with, as the description snapshots them: the framework reads
-- its six slots from Cfg/s<n>.cfg, a line of eleven fields, then a line per level.
local function slotOf(q, slot)
return (q.snapshot.saves or {})[slot]
end
-- The autosave, resumed: the framework's loadSave, without the writing.
function K.loadSave(q, thisSlot)
local save = slotOf(q, thisSlot)
if save == nil then
q.lvlState = QTE.lvlEnd
return
end
local line = save.line
q.scene, q.stage = {}, {}
for k = 1, q.finalstage do
q.scene[k] = {}
q.stage[k] = { false, false, 0 }
for i = 1, q.Level[k][QTE.TOTALSCENES] do
q.scene[k][i] = { i, false }
end
end
if (thisSlot == 5) or (thisSlot == 6) then
q.iLevel, q.iScene = tonumber(line[1]), tonumber(line[2])
else
q.dip_StartLevel, q.dip_StartScene = tonumber(line[1]), tonumber(line[2])
end
q.currentMove = tonumber(line[3])
q.dip_GameType = tonumber(line[7])
if q.dip_GameType == 1 then
q.iLifeBar, q.iLives = tonumber(line[4]), 1
else
q.iLives = tonumber(line[4])
end
q.iScPlayed, q.iScDeath, q.iTotDeath = tonumber(line[8]), tonumber(line[9]), tonumber(line[10])
q.dip_PlayStyle, q.dip_Difficulty, q.iScore = tonumber(line[11]), tonumber(line[5]), tonumber(line[6])
for k, entry in ipairs(save.levels) do
q.LvlOrder[k] = entry[1]
if entry[2] then
q.stage[k][QTE.LEVELSTARTED] = true
end
if entry[3] then
q.stage[k][QTE.BEATSTATUS] = true
end
q.stage[k][QTE.DEATHCOUNT] = entry[4]
end
for k = 1, q.finalstage do
if q.stage[k][QTE.BEATSTATUS] then
for i = 1, q.Level[k][QTE.TOTALSCENES] do
q.scene[k][i] = { i, true }
end
elseif q.stage[k][QTE.LEVELSTARTED] and (q.dip_StartScene ~= 1) then
for i = 1, q.dip_StartScene - 1 do
q.scene[k][i] = { i, true }
end
end
end
if q.AllowSecret and q.Level[QTE.levelSecret] then
q.stage[QTE.levelSecret] = { false, false, 0 }
q.scene[QTE.levelSecret] = {}
for i = 1, q.Level[QTE.levelSecret][QTE.TOTALSCENES] do
q.scene[QTE.levelSecret][i] = { i, false }
end
end
if (q.dip_StartLevel == 1) and (q.dip_StartScene == 1) and (q.currentMove == 1) and (thisSlot < 5) then
q.lvlState = QTE.lvlEnd
elseif (q.dip_StartScene <= 0) and (q.dip_PlayStyle == 3) then
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
K.startMap(q)
else
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
K.startSave(q)
end
if q.loadSavePlus then
q.loadSavePlus(thisSlot)
end
end
function K.startSave(q)
if q.currentMove ~= 1 then
q.bSave = true
end
q.iContinues, q.bLvlJump = 0, false
if q.i2P == 0 then
q.iLevel = q.dip_StartLevel
q.iScene = q.dip_StartScene - 1
else
q.iScene = q.iScene - 1
end
q.iScoreTemp = q.iScore
if q.dip_GameType == 1 then
livesForType(q)
end
q.iTop = topFor(q)
q.currentLevel = QTE.levelNormal
q.lvlState = QTE.lvlSetup
if q.dip_Display == 0 then
q.bShowScore, q.bShowLives, q.bShowLvl = true, true, true
q.bShowTop = q.ShowTop and true or false
else
q.bShowScore, q.bShowLives, q.bShowLvl, q.bShowTop = false, false, false, false
end
q.bShowGet, q.bShowSkip, q.bShowAction, q.bShowNext = false, false, false, false
q.bShowCredits, q.bShowLCD, q.bResetContinue, q.bExtendedPlay = false, false, false, false
end
function K.startMap(q)
q.iContinues, q.iScene = 0, 0
q.iLevel = q.dip_StartLevel
q.iScoreTemp = q.iScore
q.iScPlayed, q.iScDeath, q.iTotDeath = 0, 0, 0
if q.dip_GameType == 1 then
livesForType(q)
else
q.iLives = q.dip_LivesPerCredit
end
q.iTop = topFor(q)
q.currentLevel = QTE.levelMap
q.lvlState = QTE.lvlSetup
q.bShowScore, q.bShowLives, q.bShowLvl, q.bShowTop, q.bShowAction, q.bShowNext = false, false, false, false, false, false
q.bShowGet, q.bShowSkip, q.bShowCredits, q.bShowLCD, q.bResetContinue, q.bExtendedPlay = false, false, false, false, false, false
end
-- Whether the autosave slot holds a game just begun, which is no game to continue.
local function autosaveIsFresh(q)
local save = slotOf(q, 4)
if save == nil then
return true
end
local line = save.line
return (tonumber(line[1]) == 1) and ((tonumber(line[2]) == 1) or (tonumber(line[2]) == 0)) and (tonumber(line[3]) == 1)
end
-- The new-game menu of a free-play game: NEW GAME, CONTINUE (the autosave), BACK.
function K.updateNG(q, index)
if index == 2 then
q.dip_StartLevel, q.dip_StartScene = 1, 1
K.startGame(q)
elseif index == 3 then
if autosaveIsFresh(q) then
Q.sound(q, "sndwrong")
else
K.loadSave(q, 4)
end
elseif index == 4 then
q.gameflow = "init"
elseif index == 10 then
K.startGame(q)
end
end
function K.doNG(q)
local optMax = 4
if q.lvlState == QTE.lvlSetup then
if autosaveIsFresh(q) then
K.startGame(q)
else
discSkipToFrame(q.frameNewGame)
discPause()
q.timerLimit = nil
q.optSel = 2
q.bShowLvl, q.bShowScore, q.bShowLives, q.bShowSkip, q.bShowCredits = false, false, false, false, false
q.bShowGet, q.bShowLCD, q.bShowAction, q.bShowNext, q.bIgnoreJoy = false, false, false, false, false
q.bShowWarnTilt, q.bShowTilt = false, false
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.lvlRunning then
if q.p1BUTTON1 then
if q.bIgnoreJoy then
if Q.joyDelayDue(q) then
q.bIgnoreJoy = false
end
else
q.p1BUTTON1 = false
Q.sound(q, "sndcredit")
K.updateNG(q, q.optSel)
q.bIgnoreJoy = true
Q.joyDelayON(q, 0.250)
end
elseif q.p1BUTTON2 then
if q.bIgnoreJoy then
if Q.joyDelayDue(q) then
q.bIgnoreJoy = false
end
else
q.p1BUTTON2 = false
Q.sound(q, "sndcredit")
if ((q.optSel == 2) and (q.dip_GameType ~= 4)) or q.bUnlockSel then
K.updateNG(q, 10)
else
K.updateNG(q, q.optSel)
end
q.bIgnoreJoy = true
Q.joyDelayON(q, 0.250)
end
elseif q.p1DOWN or q.p1UP then
if q.bIgnoreJoy then
if Q.joyDelayDue(q) then
q.bIgnoreJoy = false
end
else
if q.p1DOWN then
q.p1DOWN = false
q.optSel = q.optSel + 1
if q.optSel > optMax then
q.optSel = 2
end
else
q.p1UP = false
q.optSel = q.optSel - 1
if q.optSel < 2 then
q.optSel = optMax
end
end
Q.sound(q, "sndcoin")
q.bIgnoreJoy = true
Q.joyDelayON(q, 0.250)
end
end
end
end
-- The level select: a still per level, LEFT and RIGHT to step, the button to take one not
-- yet beaten, a minute to decide.
function K.moveFrameLevel(q)
if q.p1LEFT then
q.dip_StartLevel = q.dip_StartLevel - 1
if q.dip_StartLevel < 1 then
q.dip_StartLevel = q.finalstage
end
Q.sound(q, "sndcoin")
q.p1LEFT = false
elseif q.p1RIGHT then
q.dip_StartLevel = q.dip_StartLevel + 1
if q.dip_StartLevel > q.finalstage then
q.dip_StartLevel = 1
end
Q.sound(q, "sndcoin")
q.p1RIGHT = false
end
local level = q.Level[q.dip_StartLevel]
if level[8] ~= nil then
discSkipToFrame(q.RelativeFrames and (level[2] + level[8]) or level[8])
else
discSkipToFrame(level[2] + 100)
end
discPause()
end
function K.doLvlSelect(q)
if q.altState == QTE.lvlSetup then
q.bShowScore, q.bShowLives, q.bIgnoreJoy = false, false, false
if q.finalstage > 1 then
local level = q.Level[q.dip_StartLevel]
if level[8] ~= nil then
discSkipToFrame(q.RelativeFrames and (level[2] + level[8]) or level[8])
else
discSkipToFrame(level[2] + 100)
end
discPause()
Q.timerON(q, 60)
q.altState = QTE.lvlRunning
else
if (q.offsetIntroGame ~= 0) and (q.dip_StartScene == 1) then
Q.setupClip(q, q.offsetIntroGame, q.offsetIntroGameend)
q.lvlState = QTE.branch11
else
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
elseif q.altState == QTE.lvlRunning then
if Q.timerDue(q) then
q.altState = QTE.lvlEnd
elseif q.p1BUTTON1 then
if not q.stage[q.dip_StartLevel][QTE.BEATSTATUS] then
Q.sound(q, "sndcredit")
q.p1BUTTON1 = false
q.altState = QTE.lvlEnd
else
Q.sound(q, "sndwrong")
end
elseif q.p1LEFT or q.p1RIGHT then
K.moveFrameLevel(q)
end
elseif q.altState == QTE.lvlEnd then
q.iLevel = q.dip_StartLevel
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
-- The level cleared: the clear clip, the bonus counted into the score, and on.
function K.doClear(q)
q.bShowScore, q.bRes = false, true
q.bShowWarnTilt, q.bShowTilt = false, false
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetClear, q.offsetClearend)
q.lvlState = QTE.branch01
elseif q.lvlState == QTE.branch01 then
if q.currentFrame >= q.iFrameEnd then
discPause()
Q.timerON(q, 0.1)
q.lvlState = QTE.branch02
end
elseif q.lvlState == QTE.branch02 then
if Q.timerDue(q) then
if q.iBonus > 0 then
q.iBonus = q.iBonus - 500
q.iScoreTemp = q.iScoreTemp + 500
Q.timerON(q, 0.01)
else
Q.sound(q, "sndvictory")
Q.timerON(q, 2)
q.lvlState = QTE.branch03
end
end
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) then
q.iScoreTemp, q.iBonus, q.numTrophy, q.iTiltMv = 0, 0, 0, 0
if q.dip_PlayStyle == 3 then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMap
elseif q.dip_PlayStyle == 4 then
q.dip_StartLevel = q.dip_StartLevel + 1
q.iScene = 0
toLevelSelect(q)
else
q.iWrongMv = 0
K.nextLevel(q, q.iLevel)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
end
end
-- The percent game's finish: the clear clip, the score of the moves, and a button to quit,
-- replay, or take another level.
function K.doFinish(q)
q.bShowScore, q.bRes = false, true
q.bShowWarnTilt, q.bShowTilt = false, false
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetClear, q.offsetClearend)
q.lvlState = QTE.branch01
elseif q.lvlState == QTE.branch01 then
if q.currentFrame >= q.iFrameEnd then
discPause()
Q.timerON(q, 2)
q.lvlState = QTE.branch02
end
elseif q.lvlState == QTE.branch02 then
local percent = math.floor(100 * (q.iRightMv / (q.iRightMv + q.iWrongMv))) - 5 * q.iTiltMv
if percent < 0 then
percent = 0
end
q.percentMade = percent
if Q.timerDue(q) then
local restart = nil
if q.p1BUTTON1 then
q.bNoScreen = false
elseif q.p1BUTTON2 then
q.bNoScreen = true
restart = true
elseif q.p1BUTTON3 then
q.bNoScreen = false
q.dip_StartLevel = (q.dip_StartLevel < q.finalstage) and (q.dip_StartLevel + 1) or 1
restart = true
end
if q.p1BUTTON1 or q.p1BUTTON2 or q.p1BUTTON3 then
q.iScoreTemp, q.iBonus, q.numTrophy = 0, 0, 0
Q.sound(q, "sndcredit")
if restart then
K.startGame(q)
end
end
end
end
end
function K.doContinue(q)
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetContinue, q.offsetContinueend)
q.bShowLives, q.bShowLvl, q.bShowScore, q.bShowAction, q.bShowNext = false, false, false, false, false
q.bShowCredits = true
q.iTilt = 0
q.bShowWarnTilt, q.bShowTilt = false, false
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlEnd
elseif q.p1START1 then
q.p1START1 = false
if (q.iCredits > 0) or (q.dip_CoinsPerCredit == QTE.DOPT_FREEPLAY) then
q.bOneDiff, q.bSkipIntroClip = false, true
if q.iScene > 0 then
if q.hayate then
Q.initStages(q)
q.iScene, q.currentMove = 0, 1
else
q.iScene = q.iScene - 1
end
end
K.startGame(q)
end
elseif q.p1BUTTON2 then
q.p1BUTTON2 = false
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
if K.newScore(q, q.iScore) then
q.currentLevel = QTE.levelHighScore
q.bGOAlt = true
else
q.currentLevel = QTE.levelGameOver
end
end
end
function K.doGameOver(q)
if q.lvlState == QTE.lvlSetup then
q.bShowLives, q.bShowLvl, q.bShowScore, q.bShowCredits, q.bShowAction, q.bShowNext = false, false, false, false, false, false
q.bOneDiff = true
q.iTilt = 0
q.bShowWarnTilt, q.bShowTilt = false, false
if q.bGOAlt then
Q.setupClip(q, q.offsetGameOverAlt, q.offsetGameOverAltend)
q.bGOAlt = false
else
Q.setupClip(q, q.offsetGameOver, q.offsetGameOverend)
end
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
q.bShowScore = false
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
end
end
-- The high score board: its clips, the name left unentered (what is typed is not play), the
-- rankings, and on to the game over -- or the intro, for the percent game.
function K.doHighScore(q)
if q.lvlState == QTE.lvlSetup then
q.bShowLives, q.bShowScore, q.bShowCredits, q.bShowAction, q.bShowNext, q.bShowLCD = false, false, false, false, false, false
q.bIgnoreJoy = false
q.iTilt = 0
q.bShowWarnTilt, q.bShowTilt = false, false
Q.setupClip(q, q.offsetNewHScore, q.offsetNewHScoreend)
q.lvlState = QTE.branch01
elseif q.lvlState == QTE.branch01 then
if (q.currentFrame >= q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
if q.iFrameEnd + 1 ~= q.offsetEnterHScore then
Q.setupClip(q, q.offsetEnterHScore, q.offsetEnterHScoreend)
else
q.iFrameEnd = q.offsetEnterHScoreend
end
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
Q.setupClip(q, q.offsetRankings, q.offsetRankingsend)
q.lvlState = ((q.dip_GameType == 2) or (q.dip_GameType == 4)) and QTE.branch03 or QTE.branch02
end
elseif (q.lvlState == QTE.branch02) or (q.lvlState == QTE.branch03) then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
if q.dip_GameType == 2 then
q.currentLevel = QTE.levelIntro
else
q.bGOAlt = true
q.currentLevel = QTE.levelGameOver
end
end
end
function K.doDiffSelect(q)
if q.altState == QTE.lvlSetup then
q.bShowScore, q.bShowLives, q.bIgnoreJoy = false, false, false
q.dip_Difficulty = 0
discSkipToFrame(q.frameEasy)
discPause()
Q.timerON(q, 30)
q.altState = QTE.lvlRunning
elseif q.altState == QTE.lvlRunning then
if Q.timerDue(q) then
q.altState = QTE.lvlEnd
elseif q.p1BUTTON1 then
Q.sound(q, "sndcredit")
q.p1BUTTON1 = false
q.altState = QTE.lvlEnd
else
Q.moveFrameDiff(q)
end
elseif q.altState == QTE.lvlEnd then
livesForType(q)
if (q.offsetIntroGame ~= 0) and (q.dip_StartScene == 1) then
Q.setupClip(q, q.offsetIntroGame, q.offsetIntroGameend)
q.lvlState = QTE.branch11
else
q.lvlState = QTE.lvlSetup
end
if (q.dip_PlayStyle == 3) and (q.MapStart == 0) then
q.currentLevel = QTE.levelMap
elseif (q.dip_GameType == 2) or (q.dip_GameType == 4) or (q.dip_PlayStyle == 4) then
toLevelSelect(q)
else
q.currentLevel = QTE.levelNormal
end
end
end
-- The quit menu, reached by START with COIN held: QUIT ends the game as the original does;
-- the other rows go back to play or to the attract loop (the service menus are not played).
function K.doExit(q)
if q.lvlState == QTE.lvlSetup then
discSkipToFrame(q.frameQuit)
discPause()
q.timerLimit = nil
q.optSel = 1
q.bShowLvl, q.bShowScore, q.bShowLives, q.bShowSkip, q.bShowCredits = false, false, false, false, false
q.bShowGet, q.bShowLCD, q.bShowAction, q.bShowNext, q.bIgnoreJoy = false, false, false, false, false
q.bShowWarnTilt, q.bShowTilt = false, false
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.branch01 then
if q.currentFrame >= q.iFrameEnd then
singeQuit()
end
elseif q.lvlState == QTE.lvlRunning then
if q.p1BUTTON1 then
if q.bIgnoreJoy then
if Q.joyDelayDue(q) then
q.bIgnoreJoy = false
end
else
q.p1BUTTON1 = false
Q.sound(q, "sndcredit")
if q.optSel == 1 then
if q.offsetQuit ~= 0 then
Q.setupClip(q, q.offsetQuit, q.offsetQuitend)
q.lvlState = QTE.branch01
else
singeQuit()
end
elseif q.optSel == 2 then
q.lvlState = QTE.lvlEnd
else
q.bInPlayExit = false
q.lvlState = QTE.lvlEnd
end
q.bIgnoreJoy = true
Q.joyDelayON(q, 0.250)
end
elseif q.p1DOWN or q.p1UP then
if q.bIgnoreJoy then
if Q.joyDelayDue(q) then
q.bIgnoreJoy = false
end
else
if q.p1DOWN then
q.p1DOWN = false
q.optSel = (q.optSel % 4) + 1
else
q.p1UP = false
q.optSel = (q.optSel == 1) and 4 or (q.optSel - 1)
end
Q.sound(q, "sndcoin")
q.bIgnoreJoy = true
Q.joyDelayON(q, 0.250)
end
end
elseif q.lvlState == QTE.lvlEnd then
if q.bInPlayExit then
q.bInPlayExit = false
K.loadSave(q, 4)
else
q.gameflow = "init"
end
end
end
-- ----- The behaviour -------------------------------------------------------------------------
-- START with COIN held, or the console key: out of play to the quit menu, with an autosave
-- the port does not write.
function K.exitPlay(q)
if (q.currentLevel == QTE.levelNormal) or (q.currentLevel == QTE.levelMap) then
q.bInPlayExit = true
if (q.currentLevel == QTE.levelMap) or ((q.currentMove <= q.totalMoves) and (q.dip_GameType < 2)) then
q.altState = QTE.branch01
end
end
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelExit
end
AUTHOR.behaviours.kimmy = {
help = "Plays a laserdisc quick-time-event game the Kimmy Script Engine way (Karis's 2024 framework), from the framework's own tables under game.qte: the attract loop, a new-game menu, levels of scenes with moves judged by what is held -- combos, mashes, holds, sequences, paths, timed and yes/no branches -- tilt, the life bar, the die-and-retry count, and the game's own add-ons. Written by util/forgePortKaris.lua from a game's script.",
params = {},
attach = function(instance, params)
local data = AUTHOR_GAME.qte or {}
local q = { instance = instance, snapshot = data, kimmy = true }
instance.q = q
instance.hearsAll = true
-- The framework's constants -- Kimmy's over the older ones where the numbers differ --
-- then the description's snapshot of the game's settings, under the game's own names.
for key, value in pairs(QTE) do
if (key ~= "FLAG_OF") and (key ~= "BRANCH") then
q[key] = value
end
end
for key, value in pairs(KIMMY) do
q[key] = value
end
for key, value in pairs(data.settings or {}) do
q[key] = value
end
for key, value in pairs(data.dips or {}) do
q[key] = value
end
for _, name in ipairs({ "dip_Hints", "ShowTop", "ShowLCD", "ShowLevel" }) do
q[name] = (q[name] == 1) or (q[name] == true)
end
q.addPoints = K.addPoints
q.env = Q.shim(q)
-- Ninja Hayate 1080 carries a later copy of the engine (an LED panel, an arcade mode) that
-- differs from the others in a few things a player can see; its settings say which.
q.hayate = (q.ArcadeMode ~= nil)
q.MYDIR = (AUTHOR_SOURCE_DIR or AUTHOR_DIR or ""):gsub("[/\\]$", "")
for _, name in ipairs({ "Level", "Death", "Tiers", "PlayOrder", "LvlOrder", "LvlMap", "move", "choice", "path", "timed", "multi", "stage", "scene", "sprite", "sprArrow", "sprNUM", "Group1", "Group2", "Group3", "Group4" }) do
q[name] = q[name] or {}
end
if data.script then
Q.runScript(q, data.script)
end
if data.addons then
Q.runScript(q, data.addons)
end
q.sounds = {}
for _, name in ipairs({ "right", "wrong", "death", "victory", "coin", "credit", "clear", "roll" }) do
q.sounds["snd" .. name] = "Sounds/" .. name .. ".wav"
q["snd" .. name] = "snd" .. name
end
q.highScores = data.highScores or {}
q.hsDR = data.hsDR or { 100, 100, 100, 100 }
q.iTopN = q.highScores[1] and q.highScores[1][2] or 0
q.iTopLB, q.iTopS, q.iTop = q.iTopN, q.iTopN, q.iTopN
if discGetWidth and (discGetWidth() > 0) then
if (q.dip_Res or 0) == 0 then
overlaySetResolution(discGetWidth(), discGetHeight())
else
overlaySetResolution(discGetWidth() / 2, discGetHeight() / 2)
end
end
local extra = q.dip_Extravid or 0
q.ShowResurrect = (extra == 1) or (extra == 4) or (extra == 5) or (extra == 7)
q.ShowSupDeath = (extra == 2) or (extra == 4) or (extra == 6) or (extra == 7)
q.ShowLvlClear = (extra == 3) or (extra == 5) or (extra == 6) or (extra == 7)
q.iPenal = ({ [0] = 0, q.PenalNormal or 0, q.PenalHard or 0, q.PenalExtreme or 0 })[q.dip_Difficulty or 1] or 0
q.BarMinT, q.BarBonusT = q.BarMin or 2, q.BarBonus or 3
-- initJob's defaults for the frames a game may not name.
q.frameRankingsAlt = q.frameRankingsAlt or q.frameRankings
q.frameQuit = q.frameQuit or q.frameHints
q.frameNewGame = q.frameNewGame or q.frameHints
if q.offsetQuit == nil then
q.offsetQuit, q.offsetQuitend = 0, 1
end
q.iCoins, q.iCredits, q.iScore, q.iScoreTemp, q.iBonus, q.iScene, q.iExtraLife = 0, 0, 0, 0, 0, 0, 0
q.iLives, q.iLevel, q.currentMove, q.iContinues, q.iPath, q.iPathAend, q.iPathAjmp, q.i2P = 0, 1, 0, 0, 0, 0, 0, 0
q.iMash, q.iMulti, q.iLenHold, q.lastHold, q.lenCounter, q.mashCounter, q.unMash = 0, 1, 0, 0, 8, 5, 0
q.iRightMv, q.iWrongMv, q.iTiltMv, q.iScPlayed, q.iScDeath, q.iTotDeath, q.iLifeBar, q.iPauseFrame, q.iTempLevel, q.numTrophy = 0, 0, 0, 1, 0, 0, q.BarSize or 10, 0, 0, 0
q.iTilt, q.iTrigTilt, q.curDeath, q.optSel, q.offsetFlip, q.Tlimit, q.i1PScore, q.i2PScore = 0, 50 - 10 * (q.dip_Tilt or 0), 0, 1, 0, 0, 0, 0
q.thisMove, q.currentFrame, q.iFrameStart, q.iFrameEnd, q.thisScore, q.Hit = KIMMY.NOMOVE, 0, 0, 0, 0, 0
q.bOneDiff, q.bRes, q.bPath, q.bTime, q.bCalc = true, false, false, true, true
q.bSave, q.bSwap, q.bGOAlt, q.bShowCredits, q.bIgnoreJoy, q.bAllowSave, q.bExtendedPlay = false, false, false, true, false, false, false
q.bShowNext, q.bNoScreen, q.bUnlockSel, q.bLvlJump, q.bScnJump, q.bInPlayExit, q.bPause, q.bCheckMove = false, false, false, false, false, false, false, false
q.bTestMash, q.bTestMashL, q.bTestMashR, q.bTestRunL, q.bTestRunR, q.bTestRunU, q.bTestRunD = false, false, false, false, false, false, false
q.bTestHold, q.bTestCombo, q.bTestMulti, q.bShowWarnTilt, q.bShowTilt = false, false, false, false, false
q.b1PStart, q.b2PStart, q.b1PEnd, q.b2PEnd = false, false, false, false
q.acombo, q.gcombo, q.g2combo, q.mcombo = { 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0 }
q.gameflow = "vldp"
q.currentLevel = QTE.levelIntro
q.lvlState = QTE.lvlSetup
q.move, q.path, q.choice, q.timed, q.multi, q.totalMoves, q.sceneStart, q.sceneEnd = {}, {}, {}, {}, {}, 0, 0, 0
q.stage, q.scene, q.LvlOrder = {}, {}, {}
Q.clearInput(q)
q.p1START1, q.p1START2, q.p1COIN1, q.p1COIN2, q.p1SERVICE, q.p1BUTTON4 = false, false, false, false, false, false
Q.initStages(q)
if q.MovieFPS then
discSetFPS(q.MovieFPS)
end
end,
step = function(instance)
local q = instance.q
q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetTitle, q.offsetTitleend)
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame >= q.iFrameEnd then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.gameflow = "init"
end
return
end
if q.gameflow == "init" then
q.gameflow = "running"
q.currentLevel = QTE.levelIntro
q.lvlState = QTE.lvlSetup
q.iCoins, q.iScore, q.iScoreTemp, q.iBonus, q.iScene = 0, 0, 0, 0, 0
q.bShowCredits = true
return
end
local level = q.currentLevel
if level == QTE.levelIntro then
K.doIntro(q)
elseif level == QTE.levelNormal then
K.doLevel(q)
elseif level == QTE.levelMap then
if q.doLevelSelect then
q.doLevelSelect()
else
q.currentLevel = QTE.levelNormal
q.lvlState = QTE.lvlSetup
end
elseif level == QTE.levelDiffScreen then
K.doDiffSelect(q)
elseif level == KIMMY.levelSelect then
K.doLvlSelect(q)
elseif level == QTE.levelContinue then
K.doContinue(q)
elseif level == QTE.levelGameOver then
K.doGameOver(q)
elseif level == QTE.levelHighScore then
K.doHighScore(q)
elseif level == QTE.levelFinish then
if q.dip_GameType == 2 then
K.doFinish(q)
else
K.doClear(q)
end
elseif level == KIMMY.levelNG then
K.doNG(q)
elseif level == QTE.levelExit then
K.doExit(q)
elseif (level == QTE.levelMovie) or (level == KIMMY.level2P) or (level == KIMMY.level2PEnd) then
-- The movie player and the two-player screens are not played: back to the attract loop.
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
end
AUTHOR_VARS.score = q.iScore
AUTHOR_VARS.lives = q.iLives
AUTHOR_VARS.credits = q.iCredits
AUTHOR_VARS.level = q.iLevel
AUTHOR_VARS.deaths = q.iTotDeath + q.iScDeath
AUTHOR_VARS.lifeBar = q.iLifeBar
AUTHOR_VARS.tilt = q.iTilt
if q.bShowAction and q.move[q.currentMove] then
AUTHOR_VARS.prompt = q.move[q.currentMove][QTE.correctMove]
else
AUTHOR_VARS.prompt = ""
end
end,
on = function(instance, name, event)
local q = instance.q
local switch = event.switch or -1
local flag = qteFlagOf(switch)
if (name == "released") and (switch == SWITCH_PAUSE) then
q.bPause = not q.bPause
return
end
if flag == nil then
return
end
if name == "pressed" then
if q.bPause then
return
end
q[flag] = true
local bit = KIMMY_BIT[flag]
if bit then
if q.bTestCombo then
q.acombo[bit] = 1
end
if ((flag == "p1UP") and q.bTestRunU) or ((flag == "p1DOWN") and q.bTestRunD) or ((flag == "p1LEFT") and q.bTestRunL) or ((flag == "p1RIGHT") and q.bTestRunR) then
q.iMash = q.iMash + 1
end
if (bit >= 5) and q.bTestMash then
q.iMash = q.iMash + 1
end
if ((flag == "p1BUTTON1") and q.bTestMashL) or ((flag == "p1BUTTON2") and q.bTestMashR) then
q.iMash = q.iMash + 1
end
if q.bTestMulti then
Q.sound(q, "sndroll")
end
elseif (flag == "p1COIN1") and q.p1START1 and (q.currentLevel ~= QTE.levelContinue) then
q.p1START1 = false
K.exitPlay(q)
elseif (flag == "p1START1") and q.p1COIN1 and (q.currentLevel ~= QTE.levelContinue) then
q.p1COIN1 = false
K.exitPlay(q)
end
elseif (name == "released") and (q.gameflow == "running") and not q.bPause then
if (flag == "p1COIN1") or (flag == "p1COIN2") then
q.p1COIN1, q.p1COIN2 = false, false
if (q.currentLevel ~= QTE.levelService) and (q.currentLevel ~= QTE.levelNormal) and (q.dip_CoinsPerCredit ~= QTE.DOPT_FREEPLAY) and (q.iCredits < (q.hayate and 99 or 9)) then
q.iCoins = q.iCoins + 1
if q.iCoins >= q.dip_CoinsPerCredit then
q.iCoins = q.iCoins - q.dip_CoinsPerCredit
q.iCredits = q.iCredits + 1
Q.sound(q, "sndcredit")
else
Q.sound(q, "sndcoin")
end
if q.currentLevel == QTE.levelContinue then
q.bResetContinue = true
end
end
else
q[flag] = false
local bit = KIMMY_BIT[flag]
if bit and q.bTestCombo then
q.acombo[bit] = 0
end
end
end
end
}
-- ===== RDG's map-mode game loop =============================================================
--
-- The rdg behaviour plays a game of RDG's "LUA SINGE 1.0/1.1" lineage, the ancestor of the two
-- loops above (FORGE.md section 14.12): the same states and screens, a simpler judging (one
-- input, a mash, a button with a direction, two directions, a skip), levels made of segments,
-- and a map. A game of this lineage keeps its data as functions of its own main.singe --
-- the segments (createLevelNN), the moves (setupLevelNN, some drawn at random), the level's
-- offsets (SetupFramesLevel), the intro clip (getIntroClip), the death's clip (setupDeathClip),
-- the order of levels (NextLevel), and the map's cursor -- so the game's files are loaded into
-- the shim and those functions are called where the ancestor calls them, over the loop's
-- state; the loop itself is this. The numbers are the game's own, read from its globals.
local R = {}
function R.addPoints(q, thisMuch)
q.iScore = q.iScore + thisMuch
if q.iScore > 99999999 then
q.iScore = 99999999
end
if q.iTop and (q.iScore > q.iTop) then
q.iTop = q.iScore
end
end
-- The ancestor reads the inputs and lets them all go.
function R.scanInput(q)
local result = q.NOMOVE
if q.p1UP then result = q.UP
elseif q.p1DOWN then result = q.DOWN
elseif q.p1LEFT then result = q.LEFT
elseif q.p1RIGHT then result = q.RIGHT
elseif q.p1BUTTON1 then result = q.BUTTON1
elseif q.p1BUTTON2 then result = q.BUTTON2
elseif q.p1BUTTON3 then result = q.BUTTON3 end
Q.clearInput(q)
return result
end
-- The sound of a right move: the framework's sndright, or the one the game's tweaks name
-- (Freedom Fighter shoots).
local function rightSound(q)
return q[q.tweaks.rightSound or "sndright"]
end
function R.checkMash(q, playerMove, curMove)
local z = q.MOVEPENDING
if playerMove == q.BUTTON1 then
q.p1BUTTON1 = false
if q.iMash >= q.mashCounter + q.dip_Difficulty then
z = curMove
q.iMash = 0
Q.sound(q, rightSound(q))
q.p1BUTTON1 = false
end
elseif playerMove ~= q.NOMOVE then
q.iMash = 0
z = q.MOVEFAIL
end
return z
end
function R.checkSkip(q, playerMove, curMove)
if ((not q.later) and (q.myButton == SWITCH_BUTTON3)) or ((playerMove >= q.UP) and (playerMove <= q.BUTTON3)) then
Q.clearInput(q)
q.bShowAction = false
discSkipToFrame(q.move[q.currentMove][q.inputFrmEnd])
end
return curMove
end
-- A move that is a button with a direction, or two directions: wanted both down, failed by
-- any other input or the window's last frame.
local RDG_PAIRS = nil
local function pairFor(q, kind)
if RDG_PAIRS == nil then
RDG_PAIRS = { [q.ACTUP] = { "p1BUTTON1", "p1UP" }, [q.ACTDOWN] = { "p1BUTTON1", "p1DOWN" }, [q.ACTLEFT] = { "p1BUTTON1", "p1LEFT" }, [q.ACTRIGHT] = { "p1BUTTON1", "p1RIGHT" },
[q.UPLEFT] = { "p1UP", "p1LEFT" }, [q.UPRIGHT] = { "p1UP", "p1RIGHT" }, [q.DOWNLEFT] = { "p1DOWN", "p1LEFT" }, [q.DOWNRIGHT] = { "p1DOWN", "p1RIGHT" } }
end
return RDG_PAIRS[kind]
end
local function rightMove(q, silent)
local worth = q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE
if not silent then
Q.sound(q, rightSound(q))
end
R.addPoints(q, worth)
q.iScoreTemp = q.iScoreTemp + worth
q.lvlState = QTE.lvlPlayRest
end
local function wrongMove(q)
q.iPauseFrame = q.move[q.currentMove][q.inputFrmEnd]
q.bShowAction = false
q.setupDeathClip(q.thisMove)
end
-- The later copies (Dragon Trainer's generation, the ones with a level order) test a clip's
-- end with "at or past" where the first ones test "at".
local function clipEnded(q)
if q.later then
return q.currentFrame >= q.iFrameEnd
end
return q.currentFrame == q.iFrameEnd
end
-- The segment begins: its intro clip, or the disc put at its start (resumed a move on from a
-- continue).
local function startSegment(q, thisLevel)
if not q.stage[thisLevel][q.LEVELSTARTED] then
q.stage[thisLevel][q.LEVELSTARTED] = true
if q.bSave and (q.currentMove ~= 1) then
q.currentFrame = q.move[q.currentMove - 1][q.inputFrmEnd] + 1
discSkipToFrame(q.currentFrame)
q.bSave = false
q.lvlState = QTE.lvlRunning
elseif (not q.bSkipIntroClip) and ((q.dip_StartLevel ~= thisLevel) or (q.dip_StartSegment == 1)) then
q.bShowLvl, q.bShowSkip = true, true
q.getIntroClip(thisLevel)
q.lvlState = QTE.branch01
else
discSkipToFrame(q.segmentStart)
q.lvlState = QTE.lvlRunning
end
else
if q.currentFrame + 1 ~= q.segmentStart then
discSkipToFrame(q.segmentStart)
end
q.lvlState = QTE.lvlRunning
end
end
-- The next move is a choice (the later copies' CHOOSE kind).
local function nextIsChoice(q)
local after = q.CHOOSE and q.move[q.currentMove + 1]
return (after ~= nil) and (after[q.correctMove] == q.CHOOSE)
end
-- A death clip has ended: a life gone, and on by the rewind dip.
local function afterDeath(q, thisLevel)
q.iLives = q.iLives - 1
q.stage[thisLevel][q.DEATHCOUNT] = q.stage[thisLevel][q.DEATHCOUNT] + 1
if q.iLives <= 0 then
q.lvlState = QTE.lvlEnd
return
end
local rewind = q.dip_Rewind or 0
if rewind == 0 then
if q.later then
q.bRes = true
q.lvlState = QTE.lvlEnd
else
q.lvlState = QTE.branch07
end
elseif rewind == 3 then
q.bRes = q.later or q.bRes
q.iSegPointer = 0
q.lvlState = QTE.lvlSetup
elseif (rewind == 2) and ((q.currentMove == q.totalMoves) or (q.later and nextIsChoice(q))) then
q.segment[q.iCurPos][q.iSegPointer][q.SEGMENTCOMPLETE] = true
q.lvlState = QTE.lvlEnd
elseif q.later then
discSkipToFrame(q.iPauseFrame)
q.bShowScore, q.bShowLives = true, true
q.currentMove = q.currentMove + 1
q.lvlState = QTE.lvlRunning
else
q.lvlState = QTE.branch09
end
end
function R.doLevel(q)
local thisLevel = q.iCurPos
if q.lvlState == QTE.lvlSetup then
q.bPlayPrompt = true
q.bShuffleOrder = true
q.bShowLives, q.bShowLvl, q.bShowScene, q.bShowAction = true, false, false, false
q.bAct, q.bTestMash, q.iMash = false, false, 0
if not q.bSave then
q.currentMove = 1
end
q.setupLevel(thisLevel)
if q.later and q.ShowResurrect and q.bRes then
-- The later copies can play a "get up" clip before the retry.
q.bShowGet = true
Q.setupClip(q, q.offsetResurrect, q.offsetResurrectEnd)
q.lvlState = QTE.branch09
q.bRes = false
else
q.bShowScore = true
startSegment(q, thisLevel)
end
elseif q.lvlState == QTE.branch01 then
if clipEnded(q) or q.p1BUTTON1 or (q.myButton == SWITCH_BUTTON3) then
q.p1BUTTON1 = false
q.bShowLvl, q.bShowSkip = false, false
if q.currentFrame ~= q.iFrameEnd then
discSkipToFrame(q.segmentStart)
end
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.branch02 then
R.doChoose(q)
elseif q.lvlState == QTE.branch03 then
if q.currentFrame == q.move[q.currentMove][q.moveFrmStart] then
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.branch04 then
if q.currentFrame >= q.segmentEnd then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.branch05 then
if clipEnded(q) then
for j = 1, q.stage[thisLevel][q.SEGMENTCOUNT] do
q.segment[thisLevel][j][q.SEGMENTCOMPLETE] = true
end
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.branch06 then
if Q.timerDue(q) then
q.bGOAlt = true
local fresh = (thisLevel ~= q.levelExt) and q.bAllowSecret and (q.dip_StartLevel == 1) and (q.dip_StartSegment == 1)
-- The extended play's wait: branch07 in the later copies, branch08 in the first.
local extended = q.later and QTE.branch07 or QTE.branch08
if fresh and q.BeatGameWithOneLife() then
Q.sound(q, q.sndvictory)
R.addPoints(q, q.SCORESECRET)
q.iBonus = q.iBonus + q.SCORESECRET
discSkipToFrame(q.frameExtendedPlay)
discPause()
Q.timerON(q, 4)
q.lvlState = extended
elseif fresh and q.BeatGameWithOneCredit() then
Q.sound(q, q.sndvictory)
discSkipToFrame(q.frameExtendedPlay)
discPause()
Q.timerON(q, 4)
q.lvlState = extended
elseif q.newScore(q.iScore) then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelHighScore
else
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelGameOver
end
end
elseif (q.lvlState == QTE.branch07) and q.later then
-- The extended play begins.
if Q.timerDue(q) then
q.bExtendedPlay = true
q.iSegPointer = 0
R.startGame(q)
end
elseif q.lvlState == QTE.branch07 then
-- The death clip has ended (no rewind).
if q.currentFrame == q.iFrameEnd then
q.lvlState = QTE.lvlEnd
end
elseif (q.lvlState == QTE.branch08) and q.later then
-- The extra death clip has ended.
if clipEnded(q) then
afterDeath(q, thisLevel)
end
elseif q.lvlState == QTE.branch08 then
if Q.timerDue(q) then
q.bExtendedPlay = true
q.iSegPointer = 0
R.startGame(q)
end
elseif (q.lvlState == QTE.branch09) and q.later then
-- The "get up" clip has ended: the segment begins.
if clipEnded(q) then
q.bShowGet, q.bShowScore = false, true
startSegment(q, thisLevel)
end
elseif q.lvlState == QTE.branch09 then
-- The rewind: back to the move after the death.
if q.currentFrame == q.iFrameEnd then
discSkipToFrame(q.iPauseFrame)
q.currentMove = q.currentMove + 1
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.lvlPlayRest then
local m = q.move[q.currentMove]
if q.currentMove < q.totalMoves then
q.bCheckMove = true
end
q.thisMove = R.scanInput(q)
if q.thisMove ~= q.NOMOVE then
Q.sound(q, q.sndwrong)
end
if q.currentFrame == m[q.moveFrmEnd] then
q.bCheckMove = false
q.currentMove = q.currentMove + 1
if q.currentMove <= q.totalMoves then
local next = q.move[q.currentMove]
if q.currentFrame + 1 ~= next[q.moveFrmStart] then
discSkipToFrame(next[q.moveFrmStart])
end
q.bShowAction, q.bPlayPrompt, q.bAct, q.bTestMash, q.iMash = false, true, false, false, 0
if q.later and (q.move[q.currentMove][q.correctMove] == q.CHOOSE) then
q.altState = QTE.lvlSetup
q.lvlState = QTE.branch02
else
q.lvlState = QTE.lvlRunning
end
else
local bonus = q.SCORESCENE - q.stage[thisLevel][q.DEATHCOUNT] * q.DEATHPENALTY
q.segment[thisLevel][q.iSegPointer][q.SEGMENTCOMPLETE] = true
if ((thisLevel == q.finalstage) and q.stageBeat(q.finalstage)) or ((thisLevel == q.levelExt) and ((not q.later) or q.stageBeat(q.levelExt))) then
R.addPoints(q, bonus)
if (q.dip_StartLevel == 1) and (q.dip_StartSegment == 1) then
R.addPoints(q, q.SCOREGAME)
else
R.addPoints(q, q.SCORELEVEL)
end
if thisLevel == q.levelExt then
discPause()
Q.timerON(q, 0.1)
else
Q.sound(q, q.sndvictory)
discSkipToFrame(q.frameVictory)
discPause()
Q.timerON(q, 3)
end
q.lvlState = QTE.branch06
else
if q.stage[thisLevel][q.DEATHCOUNT] < 5 then
R.addPoints(q, bonus)
q.iBonus = q.iBonus + bonus
end
q.lvlState = QTE.branch04
end
end
end
elseif q.lvlState == QTE.lvlRunning then
local m = q.move[q.currentMove]
local kind = m[q.correctMove]
-- Easy or kid mode: the harder kinds become the button, a mash a little later.
if ((q.dip_Difficulty == 0) and (kind >= 6) and (kind <= 12)) or q.dip_Kidmode then
if kind == q.MASH then
m[q.inputFrmStart] = m[q.inputFrmStart] + 10
end
m[q.correctMove] = q.BUTTON1
kind = q.BUTTON1
end
local inWindow = (q.currentFrame >= m[q.inputFrmStart]) and (q.currentFrame <= m[q.inputFrmEnd])
local pair = pairFor(q, kind)
if (q.currentFrame > m[q.inputFrmEnd]) and (kind ~= q.SKIP) then
q.iPauseFrame = m[q.inputFrmEnd]
q.bShowAction, q.bTestMash, q.iMash = false, false, 0
q.setupDeathClip(q.thisMove)
elseif q.currentFrame > m[q.inputFrmEnd] then
q.bShowAction = false
q.lvlState = QTE.lvlPlayRest
elseif inWindow and pair then
q.bShowAction = true
q.bAct = true
local other = false
-- A down action whose row carries a zone (columns 7 to 10) is a shot at that part
-- of the picture (Freedom Fighter): the button with the pointer inside it.
local shot = (kind == q.ACTDOWN) and (m[7] ~= nil)
for _, name in ipairs({ "p1UP", "p1DOWN", "p1LEFT", "p1RIGHT", "p1BUTTON1", "p1BUTTON2", "p1BUTTON3" }) do
if q[name] and (name ~= pair[1]) and (name ~= pair[2]) then
other = true
end
end
if (m[q.inputFrmEnd] - q.currentFrame <= 0) or other then
wrongMove(q)
elseif shot then
if q.p1BUTTON1 and (q.mouseX >= m[7]) and (q.mouseX <= m[9]) and (q.mouseY >= m[8]) and (q.mouseY <= m[10]) then
q.bShowAction, q.bAct = false, false
q.p1BUTTON1, q.p1DOWN = false, false
rightMove(q, true)
discSkipForward(m[q.inputFrmEnd] - discGetFrame() - 2)
end
elseif q[pair[1]] and q[pair[2]] then
q.bShowAction, q.bAct = false, false
q[pair[1]], q[pair[2]] = false, false
rightMove(q)
end
elseif inWindow then
q.thisMove = R.scanInput(q)
q.bShowScene, q.bShowLvl, q.bShowAction = false, false, true
if q.bPlayPrompt then
Q.sound(q, q.sndprompt)
q.bPlayPrompt = false
end
if q.thisMove ~= q.NOMOVE then
if kind == q.MASH then
q.bTestMash = true
q.thisMove = R.checkMash(q, q.thisMove, kind)
elseif kind == q.SKIP then
q.thisMove = R.checkSkip(q, q.thisMove, kind)
end
if q.thisMove == kind then
q.bShowAction, q.bTestMash, q.iMash = false, false, 0
if kind ~= q.SKIP then
rightMove(q)
else
q.lvlState = QTE.lvlPlayRest
end
elseif q.thisMove ~= q.MOVEPENDING then
wrongMove(q)
end
end
else
R.scanInput(q)
end
elseif q.lvlState == QTE.lvlPlayDeath then
q.bShowScene = false
if q.later then
q.bShowScore = false
end
if clipEnded(q) then
if q.later and q.ShowSupDeath then
-- The later copies can play a second clip after the death.
Q.setupClip(q, q.offsetSupDeath, q.offsetSupDeathEnd)
q.lvlState = QTE.branch08
else
afterDeath(q, thisLevel)
end
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
if q.iLives == 0 then
if q.dip_AllowContinue and ((q.iContinues < q.dip_LimitContinue) or (q.dip_LimitContinue == q.DOPT_INFINITE_CONTINUES)) then
q.iTempLevel = q.currentLevel
q.currentLevel = QTE.levelContinue
q.iContinues = q.iContinues + 1
else
q.currentLevel = q.newScore(q.iScore) and QTE.levelHighScore or QTE.levelGameOver
end
else
if q.stageBeat(thisLevel) then
q.stage[thisLevel][q.BEATSTATUS] = true
q.levelMap[thisLevel] = true
if (q.later and (q.dip_GameType == 3)) or ((not q.later) and q.bAllowMap) then
-- The ancestor autosaves here; the port keeps no saves.
q.bShowDiskA = true
q.altState = QTE.branch01
end
q.iSegPointer = 0
if q.choiceMode and (q.iCurPos == q.choiceLevel) then
q.lvlState = QTE.lvlSetup
q.currentLevel = q.levelFinish or QTE.levelFinish
else
R.addPoints(q, q.SCORELEVEL)
q.iBonus = q.iBonus + q.SCORELEVEL
q.bSkipIntroClip = false
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
q.bAllowSave = true
Q.sound(q, q.sndclear)
q.lvlState = QTE.lvlSetup
q.currentLevel = q.levelFinish or QTE.levelFinish
end
elseif not q.segment[thisLevel][q.iSegPointer][q.SEGMENTCOMPLETE] then
if q.iSegPointer > 0 then
q.iSegPointer = q.iSegPointer - 1
end
else
q.iLiveSave, q.iScoreSave = q.iLives, q.iScore
q.bAllowSave = true
end
q.bShowLives, q.bShowLvl, q.bShowScene, q.bShowAction, q.bTestMash, q.iMash = false, false, false, false, false, 0
end
end
end
-- A choice: the options walked with the stick and taken with the button, the right one a
-- move made, a wrong one the death it names (the later copies' CHOOSE kind).
function R.doChoose(q)
local m = q.move[q.currentMove]
local numChoice = m[q.moveDeath]
if q.altState == QTE.lvlSetup then
q.altState = QTE.lvlRunning
q.iChoice = 1
q.bIgnoreJoy = false
q.bShowChoices = true
-- The ancestor shuffles the options on their first drawing, the same frame, from a
-- reseeded stream.
if q.bShuffleOrder then
q.bShuffleOrder = false
singeRandomize()
if numChoice == 2 then
q.optorder = ({ { 1, 2 }, { 2, 1 } })[math.random(2)]
elseif numChoice == 3 then
q.optorder = ({ { 1, 2, 3 }, { 2, 3, 1 }, { 3, 1, 2 }, { 1, 3, 2 }, { 2, 1, 3 }, { 3, 2, 1 } })[math.random(6)]
elseif numChoice == 4 then
q.optorder = ({ { 1, 2, 3, 4 }, { 4, 2, 3, 1 }, { 3, 4, 1, 2 }, { 1, 3, 2, 4 }, { 2, 1, 4, 3 }, { 4, 3, 2, 1 } })[math.random(6)]
end
end
elseif q.altState == QTE.lvlRunning then
local function take()
q.bShowChoices = false
if q.choice[q.optorder[q.iChoice]][2] == true then
Q.sound(q, rightSound(q))
q.lvlState = QTE.lvlPlayRest
else
Q.sound(q, q.sndwrong)
m[q.moveDeath] = q.choice[q.optorder[q.iChoice]][3]
q.setupDeathClip(q.thisMove)
q.lvlState = QTE.lvlPlayDeath
end
end
if q.currentFrame > m[q.inputFrmEnd] then
take()
elseif (q.currentFrame >= m[q.inputFrmStart]) and (q.currentFrame <= m[q.inputFrmEnd]) then
local thisMove = q.NOMOVE
if q.bIgnoreJoy then
if Q.timerDue(q) then
q.bIgnoreJoy = false
end
else
thisMove = R.scanInput(q)
end
if thisMove == q.UP then
if q.iChoice > 1 then
q.iChoice = q.iChoice - 1
Q.sound(q, q.sndcoin)
end
elseif thisMove == q.DOWN then
if q.iChoice < numChoice then
q.iChoice = q.iChoice + 1
Q.sound(q, q.sndcoin)
end
elseif thisMove == q.BUTTON1 then
if q.choice[q.optorder[q.iChoice]][2] == true then
discSkipToFrame(m[q.inputFrmEnd])
end
take()
end
end
end
end
-- ----- The screens ---------------------------------------------------------------------------
function R.startGame(q)
q.initStages()
if q.bExtendedPlay then
q.currentLevel = QTE.levelNormal
q.iCurPos = q.levelExt
else
if q.iCredits > 0 then
q.iCredits = q.iCredits - 1
end
q.iScore, q.iScoreTemp, q.iBonus = 0, 0, 0
if q.currentLevel == QTE.levelContinue then
q.currentLevel = q.iTempLevel
if q.dip_Rewind == 1 then
q.currentMove, q.bSave = q.currentMove + 1, true
elseif q.dip_Rewind == 2 then
q.currentMove = (q.currentMove == q.totalMoves) and (q.currentMove - 1) or (q.currentMove + 1)
q.bSave = true
elseif q.dip_Rewind == 3 then
q.iSegPointer = 0
end
elseif q.later then
-- The later copies order the levels by a game type: in sequence, at random, by
-- tiers, from the map, or from the dips' level and segment.
q.iContinues, q.iSegPointer = 0, 0
if q.dip_GameType == 0 then
q.doMixSEQ()
q.iCurPos, q.currentLevel = q.LvlOrder[1], QTE.levelNormal
elseif q.dip_GameType == 1 then
q.doMixRND()
q.iCurPos, q.currentLevel = q.LvlOrder[1], QTE.levelNormal
elseif q.dip_GameType == 2 then
q.doMixTIE()
q.iCurPos, q.currentLevel = q.LvlOrder[1], QTE.levelNormal
elseif q.dip_GameType == 4 then
q.iCurPos = q.dip_StartLevel
q.iSegPointer = q.dip_StartSegment - 1
q.currentLevel = QTE.levelNormal
elseif q.dip_GameType == 3 then
q.iCurPos, q.currentLevel = q.PlayOrder[1], q.levelMenuScreen
end
else
q.iContinues, q.iSegPointer = 0, 0
q.iCurPos = q.dip_StartLevel
q.iSegPointer = q.dip_StartSegment - 1
-- The ancestor draws from its random stream here (a seed it never uses).
q.rndegg = math.random(math.floor(os.clock() * 100000))
if q.bAllowMap and (q.MapStart == 0) then
q.currentLevel = q.levelMenuScreen
else
q.currentLevel = QTE.levelNormal
end
end
end
q.lvlState = QTE.lvlSetup
q.bShowLives, q.bShowScore = true, not q.later
q.iLives = q.dip_LivesPerCredit
q.bShowAction, q.bTestMash, q.iMash = false, false, 0
q.bShowCredits, q.bShowLCD, q.bResetContinue, q.bExtendedPlay = false, false, false, false
end
function R.doIntro(q)
-- The first copies quit on start with coin; the secret's combination ends in DOWN there
-- and RIGHT in the later ones.
local fourth = q.later and "p1RIGHT" or "p1DOWN"
local function quitting()
if (not q.later) and q.p1START1 and q.p1COIN1 then
singeQuit()
return true
end
return false
end
local function secret()
if q.p1BUTTON2 and q.p1BUTTON3 and q.p1UP and q[fourth] and q.bAllowSecret then
q.p1BUTTON2, q.p1BUTTON3, q.p1UP, q[fourth] = false, false, false, false
q.bExtendedPlay = true
R.startGame(q)
q.bShowCredits = false
return true
end
return false
end
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetIntro01, q.offsetIntro01end)
q.lvlState = QTE.branch01
q.bShowCredits, q.bShowLCD, q.bShowLives, q.bCheckForCredits = true, true, false, true
elseif q.lvlState == QTE.branch01 then
if clipEnded(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
discSkipToFrame(q.frameCommands)
discPause()
q.bShowLCD = false
if q.later then
q.bShowCredits = false
end
Q.timerON(q, q.tweaks.introHold or (q.later and 12 or 5))
q.lvlState = QTE.branch02
else
quitting()
end
elseif q.lvlState == QTE.branch02 then
if Q.timerDue(q) then
discSkipToFrame(q.frameRankings)
Q.timerON(q, q.tweaks.rankingsHold or (q.later and 8 or 5))
discPause()
q.lvlState = QTE.branch03
elseif not secret() then
quitting()
end
elseif (q.lvlState == QTE.branch03) and q.later then
-- The later copies show a filler clip or still after the rankings.
if Q.timerDue(q) then
q.bShowLCD, q.bShowCredits = true, true
q.doFillerFrame()
q.lvlState = QTE.branch04
end
elseif q.lvlState == QTE.branch03 then
if Q.timerDue(q) then
q.lvlState = QTE.lvlSetup
else
quitting()
end
elseif q.lvlState == QTE.branch04 then
if clipEnded(q) then
q.lvlState = QTE.lvlSetup
elseif q.later and q.p1BUTTON1 then
q.p1BUTTON1 = false
q.lvlState = QTE.branch01
end
elseif q.lvlState == QTE.branch05 then
if Q.timerDue(q) then
q.gameflow = "init"
elseif not quitting() and q.p1START1 then
q.p1START1 = false
if ((q.iCredits > 0) or (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY)) and not q.dip_Movie then
R.startGame(q)
q.bShowCredits = false
elseif q.dip_Movie then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMovie
q.bShowCredits = false
end
end
end
if (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY) or (q.bShowCredits and (q.iCredits > 0)) then
if q.p1START1 and not q.dip_Movie then
q.p1START1, q.bShowCredits = false, false
R.startGame(q)
elseif q.p1START1 and q.dip_Movie then
q.p1START1, q.bShowCredits = false, false
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelMovie
end
end
end
function R.doContinue(q)
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetContinue, q.offsetContinueend)
q.bShowLives, q.bShowLvl, q.bShowScene, q.bShowScore, q.bShowAction = false, false, false, false, false
q.bShowCredits, q.bTestMash, q.iMash = true, false, 0
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if clipEnded(q) then
q.lvlState = QTE.lvlEnd
elseif q.p1START1 then
q.p1START1 = false
if (q.iCredits > 0) or (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY) then
q.bSkipIntroClip = true
if q.iSegPointer > 0 then
q.iSegPointer = q.iSegPointer - 1
end
R.startGame(q)
end
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.bSkipIntroClip = false
if q.newScore(q.iScore) then
q.currentLevel = QTE.levelHighScore
q.bGOAlt = true
else
q.currentLevel = QTE.levelGameOver
end
end
end
function R.doGameOver(q)
if q.lvlState == QTE.lvlSetup then
q.bShowLives, q.bShowLvl, q.bShowScene, q.bShowScore, q.bShowCredits, q.bShowAction = false, false, false, false, false, false
q.bTestMash, q.iMash = false, 0
if q.bGOAlt then
Q.setupClip(q, q.offsetGameOverAlt, q.offsetGameOverAltend)
q.bGOAlt = false
else
Q.setupClip(q, q.offsetGameOver, q.offsetGameOverend)
end
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if clipEnded(q) then
q.bShowScore = false
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
end
end
-- The level cleared: the clear clip, then the score and the bonus shown to a timer, and on.
function R.doFinish(q)
-- The first copies count the steps from branch02, the later from branch01.
local step = q.later and { QTE.branch01, QTE.branch02, QTE.branch03, QTE.branch04, QTE.branch05 } or { QTE.branch02, QTE.branch03, QTE.branch04, QTE.branch05, QTE.branch06 }
if q.later then
q.bShowScore, q.bRes = false, true
end
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetClear, q.offsetClearend)
q.lvlState = step[1]
elseif q.lvlState == step[1] then
if clipEnded(q) then
Q.timerON(q, 1)
discPause()
q.lvlState = step[2]
end
elseif q.lvlState == step[2] then
if Q.timerDue(q) then
Q.timerON(q, 2)
discPause()
Q.sound(q, q.sndcredit)
q.lvlState = step[3]
end
elseif q.lvlState == step[3] then
if Q.timerDue(q) then
Q.timerON(q, 2)
discPause()
Q.sound(q, q.sndcredit)
q.lvlState = step[4]
end
elseif q.lvlState == step[4] then
if Q.timerDue(q) then
Q.timerON(q, 1)
discPause()
Q.sound(q, q.sndvictory)
q.lvlState = step[5]
end
elseif q.lvlState == step[5] then
if Q.timerDue(q) then
q.iScoreTemp, q.iBonus = 0, 0
if ((q.later and (q.dip_GameType == 3)) or ((not q.later) and q.bAllowMap)) and (q.MapStart == 0) then
q.lvlState = QTE.lvlSetup
q.currentLevel = q.levelMenuScreen
else
q.NextLevel(q.iCurPos)
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelNormal
end
end
end
end
-- The high score board: the clips, the name left unentered (what is typed is not play), the
-- board updated, and on to the game over.
function R.doHighScore(q)
if q.lvlState == QTE.lvlSetup then
q.bShowLives, q.bShowScore, q.bShowCredits, q.bShowAction, q.bShowLCD, q.bIgnoreJoy = false, false, false, false, false, false
q.sName = ""
Q.setupClip(q, q.offsetNewHScore, q.offsetNewHScoreend)
q.lvlState = QTE.branch01
elseif q.lvlState == QTE.branch01 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
Q.setupClip(q, q.offsetEnterHScore, q.offsetEnterHScoreend)
q.lvlState = QTE.lvlRunning
end
elseif q.lvlState == QTE.branch02 then
if Q.timerDue(q) then
q.lvlState = QTE.lvlEnd
end
elseif (q.lvlState == QTE.branch03) or (q.lvlState == QTE.branch04) then
if q.lvlState == QTE.branch03 then
if q.currentFrame == q.iFrameEnd then
if q.sdq then
-- Super Don Quixote's board ends without the two-second hold.
q.lvlState = QTE.lvlEnd
else
discPause()
Q.timerON(q, 2)
q.lvlState = QTE.branch04
end
end
elseif Q.timerDue(q) then
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlRunning then
if q.currentFrame == q.iFrameEnd then
q.updateHS(q.sName, q.iScore)
discPause()
Q.timerON(q, 0.5)
q.lvlState = QTE.branch02
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelGameOver
end
end
-- The movie player: the levels' clips in turn, START back to the attract loop.
function R.startMovie(q)
if q.lvlState == QTE.lvlSetup then
q.iScore, q.iScoreTemp, q.iBonus, q.iContinues, q.iSegPointer, q.iMovie = 0, 0, 0, 0, 0, 1
Q.setupClip(q, q.offsetLevel01, q.offsetLevel02 - 2)
q.bShowLives, q.bShowAction, q.bShowScore, q.bShowLvl, q.bShowCredits, q.bShowLCD = false, false, false, false, false, false
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
local function clipOf(index)
local from = q["offsetLevel" .. string.format("%02d", index)]
local to = (index < q.finalstage) and (q["offsetLevel" .. string.format("%02d", index + 1)] - 2) or (q.offsetMenus - 2)
Q.setupClip(q, from, to)
end
if q.currentFrame == q.iFrameEnd then
q.p1RIGHT = true
elseif q.p1START1 then
q.p1START1 = false
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
elseif q.p1RIGHT then
q.p1RIGHT = false
q.iMovie = (q.iMovie < q.finalstage) and (q.iMovie + 1) or 1
clipOf(q.iMovie)
elseif q.p1LEFT then
q.p1LEFT = false
q.iMovie = (q.iMovie > 1) and (q.iMovie - 1) or q.finalstage
clipOf(q.iMovie)
end
elseif q.lvlState == QTE.lvlEnd then
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
end
end
-- ----- The behaviour -------------------------------------------------------------------------
AUTHOR.behaviours.rdg = {
help = "Plays a laserdisc quick-time-event game of RDG's map-mode lineage (the ancestor of the Karis framework): levels of segments, a move an input or a mash or a pair of inputs in a window, a map between levels. The game's own files are loaded and its level, move, death-clip, order, and map functions run as they are; the loop is this. Written by util/forgePortKaris.lua from a game's script.",
params = {},
attach = function(instance, params)
local data = AUTHOR_GAME.qte or {}
local q = { instance = instance, snapshot = data, rdg = true, namedSounds = true, loadsFiles = true }
instance.q = q
instance.hearsAll = true
for key, value in pairs(QTE) do
if (key ~= "FLAG_OF") and (key ~= "BRANCH") then
q[key] = value
end
end
q.env = Q.shim(q)
q.MYDIR = (AUTHOR_SOURCE_DIR or AUTHOR_DIR or ""):gsub("[/\\]$", "")
q.sounds = {}
-- The game's script loads its globals, its levels, its map, its board, and its
-- helpers through the shim: they declare the constants, the tables, and the
-- functions the loop calls.
if data.script then
Q.runScript(q, data.script)
end
-- The port keeps no saves and reads no config: the game's own file functions (the
-- later copies autosave in setupLevel) do nothing here.
for _, name in ipairs({ "autoSave", "readSave", "writeSave", "readConfig", "writeConfig" }) do
q[name] = function() end
end
-- The map screen (the game's own doLevelSelect) draws from the sprite table its
-- initJob fills; the port draws its own, so the table is there and empty.
q.sprite = q.sprite or {}
for key, value in pairs(data.dips or {}) do
q[key] = value
end
for _, name in ipairs({ "dip_AllowContinue", "dip_ShowAction", "dip_Kidmode", "dip_Movie", "dip_Debug" }) do
q[name] = (q[name] == 1) or (q[name] == true)
end
-- What this title does differently from the lineage, named by the converter.
q.tweaks = data.tweaks or {}
-- The later copies (Dragon Trainer's generation) declare a level order; they number
-- some states differently, test clip ends with "at or past", and add a game type.
q.later = (q.LvlOrder ~= nil)
if q.later then
q.iPenal = ({ [0] = 0, [1] = q.PenalNorm, [2] = q.PenalHard })[q.dip_Difficulty] or 0
end
-- The pointer in the game's coordinates, as the ancestor's onMouseMoved keeps it.
q.mouseX, q.mouseY = q.mouseX or 0, q.mouseY or 0
q.highscore = {}
for k, entry in ipairs(data.highScores or {}) do
q.highscore[k] = { entry[1], entry[2] }
end
q.levelMap = {}
for k = 1, 16 do
q.levelMap[k] = false
end
q.iCoins, q.iCredits, q.iScore, q.iScoreTemp, q.iBonus, q.iSegPointer, q.iMash, q.iContinues = 0, 0, 0, 0, 0, 0, 0, 0
q.iLives, q.iCurPos, q.currentMove, q.iPauseFrame, q.iTempLevel, q.iMovie, q.totalMoves = 0, 1, 0, 0, 0, 1, 0
q.move, q.stage, q.segment = q.move or {}, q.stage or {}, q.segment or {}
q.thisMove, q.currentFrame, q.iFrameStart, q.iFrameEnd, q.myButton = q.NOMOVE, 0, 0, 0, 0
q.bPause, q.bAct, q.bTestMash, q.bSave, q.bSkipIntroClip, q.bGOAlt, q.bExtendedPlay, q.bAllowSave, q.bCheckMove = false, false, false, false, false, false, false, false, false
q.bShowCredits, q.bShowScore, q.bShowLives, q.bShowAction, q.bShowLvl, q.bShowScene, q.bShowSkip, q.bShowLCD = true, false, false, false, false, false, false, true
q.gameflow = "vldp"
q.currentLevel = QTE.levelIntro
q.lvlState = QTE.lvlSetup
Q.clearInput(q)
q.p1START1, q.p1START2, q.p1COIN1, q.p1COIN2, q.p1SERVICE = false, false, false, false, false
if q.MovieFPS then
discSetFPS(q.MovieFPS)
end
end,
step = function(instance)
local q = instance.q
q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then
-- The ancestor's start: the title clip with the inputs paused, then the init.
if q.later then
q.saveOffset() -- The later copies copy their offsets under short names here.
end
if q.lvlState == QTE.lvlSetup then
Q.setupClip(q, q.offsetTitle, q.offsetTitleend)
q.bPause = true
q.lvlState = QTE.lvlRunning
elseif q.lvlState == QTE.lvlRunning then
if clipEnded(q) then
discPause()
q.lvlState = QTE.lvlEnd
end
elseif q.lvlState == QTE.lvlEnd then
q.bPause = false
q.gameflow = "init"
q.lvlState = QTE.lvlSetup
end
return
end
if q.gameflow == "init" then
q.gameflow = "running"
q.currentLevel = QTE.levelIntro
q.lvlState = QTE.lvlSetup
q.iCoins, q.iScore, q.iScoreTemp, q.iBonus, q.iSegPointer = 0, 0, 0, 0, 0
q.rndegg = math.random(math.floor(os.clock() * 100000))
for k = 1, 16 do
q.levelMap[k] = false
end
q.bShowCredits = true
q.bShowScore, q.bShowLives, q.bShowAction, q.bAct, q.bTestMash, q.iMash = false, false, false, false, false, 0
q.bShowLvl, q.bShowScene, q.bShowSkip = false, false, false
return
end
local level = q.currentLevel
if level == QTE.levelIntro then
R.doIntro(q)
elseif level == QTE.levelNormal then
R.doLevel(q)
elseif level == q.levelMenuScreen then
q.doLevelSelect()
elseif level == QTE.levelContinue then
R.doContinue(q)
elseif level == QTE.levelGameOver then
R.doGameOver(q)
elseif level == QTE.levelHighScore then
R.doHighScore(q)
elseif level == QTE.levelMovie then
R.startMovie(q)
elseif level == (q.levelFinish or QTE.levelFinish) then
R.doFinish(q)
elseif (level == QTE.levelService) or (level == QTE.levelSave) then
-- The service and save menus are not played: back to the attract loop.
q.lvlState = QTE.lvlSetup
q.currentLevel = QTE.levelIntro
end
AUTHOR_VARS.score = q.iScore
AUTHOR_VARS.lives = q.iLives
AUTHOR_VARS.credits = q.iCredits
AUTHOR_VARS.level = q.iCurPos
if q.bShowAction and q.move[q.currentMove] then
AUTHOR_VARS.prompt = q.move[q.currentMove][q.correctMove]
else
AUTHOR_VARS.prompt = ""
end
end,
on = function(instance, name, event)
local q = instance.q
local switch = event.switch or -1
local flag = qteFlagOf(switch)
if name == "pointer" then
if q.ratiox then
q.mouseX = event.x * q.ratiox - q.ratioxOffset
q.mouseY = event.y * q.ratioy - q.ratioyOffset
end
return
end
if name == "pressed" then
q.myButton = switch
if q.bPause or (flag == nil) then
return
end
if flag == "p1COIN1" then
q.p1COIN1 = true
if q.p1START1 then
q.p1START1 = false
singeQuit()
end
elseif flag == "p1START1" then
q.p1START1 = true
if q.p1COIN1 then
q.p1COIN1 = false
singeQuit()
end
else
q[flag] = true
if (flag == "p1BUTTON1") and q.bTestMash then
q.iMash = q.iMash + 1
end
end
elseif name == "released" then
q.myButton = 0
if switch == SWITCH_PAUSE then
q.bPause = not q.bPause
return
end
if (q.gameflow ~= "running") or q.bPause or (flag == nil) then
return
end
if (flag == "p1COIN1") or (flag == "p1COIN2") then
if (q.currentLevel ~= QTE.levelService) and (q.currentLevel ~= QTE.levelNormal) then
q.p1COIN1, q.p1COIN2 = false, false
if (q.dip_CoinsPerCredit ~= q.DOPT_FREEPLAY) and (q.iCredits < 9) then
q.iCoins = q.iCoins + 1
if q.iCoins >= q.dip_CoinsPerCredit then
q.iCoins = q.iCoins - q.dip_CoinsPerCredit
q.iCredits = q.iCredits + 1
Q.sound(q, q.sndcredit)
else
Q.sound(q, q.sndcoin)
end
if q.currentLevel == QTE.levelContinue then
q.bResetContinue = true
end
end
end
elseif (flag ~= "p1SERVICE") then
q[flag] = false
end
end
end
}
-- A vocabulary behaviour that plays a game over the game's own files (a per-game port of a
-- hand-written program) reaches the shim through these: the state table's environment, and
-- a file of the game's run in it, beside the description.
-- The one-player American Laser Games editions' crosshair and recoil, as their frame loops
-- draw them after the level ran: the crosshair where the pointer is, and for the frames of
-- the recoil the flash where the shot went; an edition that swaps the crosshair's sprite for
-- the flash's puts it back by the crosshair dip when the recoil ends.
-- opts.highScore the recoil is left alone on the board
-- opts.cannon no crosshair while the cannon fires (Space Pirates)
-- opts.reload the folder of the crosshair pictures, for an edition that swaps them
function authorDrawGun(q, opts)
local env = q.env
if not (q.bShowMouse and (not (opts.cannon and q.bCannonFired)) and singeWantsCrosshairs()) then
return
end
env.spriteDraw(q.cursorx, q.cursory, q.sprCursor)
if q.bReversePointer and not (opts.highScore and (q.currentLevel == q.levelHighScore)) then
q.iRevFrames = q.iRevFrames + 1
if q.iRevFrames == q.REV_DELAY then
q.bReversePointer = false
if opts.reload then
local name = ALG_CROSSHAIRS[q.dip_Crosshair]
if name then
q.sprCursor = env.spriteLoad(opts.reload .. name)
end
end
else
if opts.reload then
q.sprCursor = env.spriteLoad(opts.reload .. "crosshaire.png")
end
env.spriteDraw(q.revsetx, q.revsety, q.sprRev)
end
end
end
ALG_CROSSHAIRS = { "crosshaira.png", "crosshairb.png", "crosshairc.png", "crosshaird.png", "crosshaire.png" }
-- The Hypseus multiplayer editions' two guns: for each, the crosshair, the recoil as a sheet
-- of frames counted after the level ran and frozen where the shot went, and the hand's
-- animation the same way. The second gun's names end in 2.
-- opts.cannon no crosshair while the cannon fires (Space Pirates)
-- opts.highScore the recoil is left alone on the boards (Johnny Rock)
-- opts.resets the counts go back to zero when done (Johnny Rock)
-- opts.reload the folder of the crosshair pictures, swapped in by the dip through the
-- recoil (Johnny Rock; the second gun's pictures end in p2)
function authorDrawGuns(q, opts)
local env = q.env
for player = 1, 2 do
local s = (player == 2) and "2" or ""
if q["bShowMouse" .. s] and not (opts.cannon and q.bCannonFired) then
if singeWantsCrosshairs() then
env.spriteDraw(q["cursor" .. player .. "x"], q["cursor" .. player .. "y"], q["sprCursor" .. s])
end
if q["bReversePointer" .. s] and not (opts.highScore and ((q.currentLevel == q.levelHighScore) or (q.currentLevel == q.levelHighScore2))) then
if q["iRevFrames" .. s] == 0 then
q["freeze" .. player .. "x"], q["freeze" .. player .. "y"] = q["revset" .. player .. "x"], q["revset" .. player .. "y"]
end
q["iRevFrames" .. s] = q["iRevFrames" .. s] + 1
if q["iRevFrames" .. s] <= q.sprRevFrames * q.sprRevFrameReps then
env.spriteDrawFrame(q["freeze" .. player .. "x"], q["freeze" .. player .. "y"], q["iRevFrames" .. s] / q.sprRevFrameReps, q["sprRev" .. s])
else
if opts.resets then
q["iRevFrames" .. s] = 0
end
q["bReversePointer" .. s] = false
end
if opts.reload then
local name = ALG_CROSSHAIRS[q.dip_Crosshair]
if name then
q["sprCursor" .. s] = env.spriteLoad(opts.reload .. ((player == 2) and name:gsub("%.png$", "p2.png") or name))
end
end
end
if q["bGunAnim" .. s] then
q["iHandFrames" .. s] = q["iHandFrames" .. s] + 1
if q["iHandFrames" .. s] <= q.sprHandFrames * q.sprHandFrameReps then
env.spriteDrawFrame(q["cursor" .. player .. "x"], q["cursor" .. player .. "y"], math.ceil(q["iHandFrames" .. s] / q.sprHandFrameReps), q["sprHand" .. s])
else
if opts.resets then
q["iHandFrames" .. s] = 0
end
q["bGunAnim" .. s] = false
end
end
end
end
end
function authorFlushDraws(q)
Q.flushDraws(q)
fontSelect(AUTHOR_FONT)
end
function authorDropDraws(q)
Q.dropDraws(q)
end
function authorConfigLoad(q)
Q.configLoad(q)
end
function authorConfigSave(q)
Q.configSave(q)
end
function authorShim(q)
return Q.shim(q)
end
function authorRunScript(q, name)
return Q.runScript(q, name)
end
-- ===== The Time Gal (Singe Edition) game loop ================================================
--
-- RDG2010's HD remake of the Singe 1 Time Gal keeps a compact loop of its own, not the map-mode
-- one: a level's moves come from arrayEasy, arrayNormal, or arrayHard by difficulty (mirrored
-- by a random draw), play has checkpoints resumed after a death, a time-stop is a choice of
-- three, a tally pays a bonus by deaths, a quota gives a life, and chooseLevel picks the next
-- level by tiers, at random, or in sequence. As with the map-mode loop, the game's own files
-- are loaded through the shim and its data and order functions (setupLevel, chooseLevel, the
-- doMixLevels three, fetchIntroVideo, doFillerFrame, the board) run as they are; the loop is
-- this, under the game's own names and numbers.
local T = {}
-- The ancestor's setupClip: a skip only when the disc is not already a frame short of the
-- clip, and the disc let go if it was paused there.
function T.setupClip(q, from, to)
q.iFrameStart, q.iFrameEnd = from, to
if q.currentFrame + 1 ~= from then
discSkipToFrame(from)
discChangeSpeed(1, 1)
elseif discGetState() == q.VLDP_PAUSED then
discPlay()
end
end
-- The inputs read and let go; any input holds the stick off for a fifth of a second.
function T.scanInput(q)
local result = q.NOMOVE
if q.p1UP then result = q.UP
elseif q.p1DOWN then result = q.DOWN
elseif q.p1LEFT then result = q.LEFT
elseif q.p1RIGHT then result = q.RIGHT
elseif q.p1BUTTON1 then result = q.ACTION end
if q.p1UP or q.p1DOWN or q.p1LEFT or q.p1RIGHT or q.p1BUTTON1 then
q.bIgnoreJoy = true
Q.timerON(q, 0.2)
end
q.p1UP, q.p1DOWN, q.p1LEFT, q.p1RIGHT, q.p1BUTTON1 = false, false, false, false, false
return result
end
-- Points, and a life at every quota.
function T.addPoints(q, amount)
q.iScore = q.iScore + amount
q.i1up = q.i1up + amount
if q.i1up >= q.iQuota then
q.i1up = q.i1up - q.iQuota
q.iQuota = 300000
q.iLives = q.iLives + 1
Q.sound(q, q.snd1up)
if q.i1up < 0 then
q.i1up = 0
end
end
end
function T.startGame(q)
q.iCredits = q.iCredits - 1
q.iLives = q.dip_LivesPerCredit
q.iScore, q.i1up, q.iQuota = 0, 0, 200000
q.rndegg = random.new(math.floor(os.clock() * 100000))
-- The ancestor renders the top score to a sprite here for its attract screen.
q.finalShuffle = { false, false, false }
if q.currentLevel == q.levelContinue then
q.currentLevel = q.iTempLevel
q.bSection01Reached, q.bSection02Reached, q.bSection03Reached = false, false, false
else
q.iContinues = 0
q.rndegg = random.new(math.floor(os.clock() * 100000))
q.currentLevel = q.levelChoose
q.bLevelInit = false
if q.dip_SortStyle == q.DOPT_RANDOM then
q.iStageIndex = 0
q.doMixLevelsRandom()
elseif q.dip_SortStyle == q.DOPT_DL then
q.iStageIndex = 1
q.doMixLevels()
elseif q.dip_SortStyle == q.DOPT_SEQUENTIAL then
q.iStageIndex = 0
q.doMixLevelSequential()
end
end
q.lvlState = q.lvlSetup
q.bShowLives, q.bShowScore, q.bShowCredits, q.bResetContinue = true, true, false, false
q.bShowResurrect, q.bShowTimeChoices = true, false
q.resetChannels()
end
-- The attract loop: an intro clip, a filler, the credits clip, the rankings still, and round;
-- a coin brings the "press start" still for half a minute.
function T.doIntro(q)
if q.lvlState == q.lvlSetup then
q.fetchIntroVideo()
q.lvlState = q.branch01
q.resetChannels()
q.bMuteAttract = false
q.bShowCredits, q.bShowLives, q.bCheckForCredits = true, false, true
elseif q.lvlState == q.branch01 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.doFillerFrame()
Q.timerON(q, 6)
q.bMuteAttract = not q.bMuteAttract
q.lvlState = q.branch02
end
elseif q.lvlState == q.branch02 then
if Q.timerDue(q) then
T.setupClip(q, 60400, 60680)
q.lvlState = q.branch03
q.bShowCredits = false
end
elseif q.lvlState == q.branch03 then
if q.currentFrame == q.iFrameEnd then
discSkipToFrame(59496)
discPause()
Q.timerON(q, 12)
q.lvlState = q.branch04
end
elseif q.lvlState == q.branch04 then
if Q.timerDue(q) then
q.fetchIntroVideo()
q.lvlState = q.branch01
q.bShowCredits = true
if q.bMuteAttract then
q.muteAudio()
else
q.resetChannels()
end
end
elseif q.lvlState == q.branch05 then
if Q.timerDue(q) then
q.gameflow = "init"
elseif q.p1START1 then
q.p1START1 = false
if (q.iCredits > 0) or (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY) then
T.startGame(q)
q.bShowCredits = false
end
end
end
if q.dip_CoinsPerCredit == q.DOPT_FREEPLAY then
if q.p1START1 then
q.p1START1 = false
T.startGame(q)
end
elseif q.bCheckForCredits and (q.iCredits > 0) then
q.bCheckForCredits = false
q.bShowCredits = true
discSkipToFrame(59500)
discPause()
Q.timerON(q, 30)
q.lvlState = q.branch05
end
end
-- A time-stop: three options walked with the stick and taken with the action button, the
-- right one a move made, a wrong one its own death clip. The ancestor shuffles the options on
-- their first drawing, the same frame.
function T.doTimeStop(q)
local m = q.move[q.currentMove]
if q.altState == q.lvlSetup then
q.altState = q.lvlRunning
q.iChoice = 1
q.bIgnoreJoy = false
q.bShowTimeChoices = true
if q.bShuffleOrder then
q.bShuffleOrder = false
q.iShuffle = math.modf(math.random(1000, 6999) / 1000)
q.optorder = ({ { 1, 2, 3 }, { 2, 3, 1 }, { 3, 1, 2 }, { 1, 3, 2 }, { 2, 1, 3 }, { 3, 2, 1 } })[q.iShuffle]
end
elseif q.altState == q.lvlRunning then
local picked = q.opt[q.optorder[q.iChoice]]
if q.currentFrame > m[q.inputFrmEnd] then
if picked[2] == true then
q.lvlState = q.lvlPlayRest
Q.sound(q, q.sndteedo)
else
Q.sound(q, q.sndwrong)
T.setupClip(q, picked[3], picked[4])
q.lvlState = q.lvlPlayDeath
end
q.bShowTimeChoices = false
elseif (q.currentFrame >= m[q.inputFrmStart]) and (q.currentFrame <= m[q.inputFrmEnd]) then
local thisMove = q.NOMOVE
if q.bIgnoreJoy then
if Q.timerDue(q) then
q.bIgnoreJoy = false
end
else
thisMove = T.scanInput(q)
end
if thisMove == q.UP then
if q.iChoice > 1 then
q.iChoice = q.iChoice - 1
Q.sound(q, q.sndsel)
end
elseif thisMove == q.DOWN then
if q.iChoice < 3 then
q.iChoice = q.iChoice + 1
Q.sound(q, q.sndsel)
end
elseif thisMove == q.ACTION then
if picked[2] == true then
Q.sound(q, q.sndteedo)
discSkipToFrame(m[q.inputFrmEnd])
q.lvlState = q.lvlPlayRest
else
Q.sound(q, q.sndwrong)
T.setupClip(q, picked[3], picked[4])
q.lvlState = q.lvlPlayDeath
end
q.bShowTimeChoices = false
end
end
end
end
-- A level: its moves from the checkpoint reached, a death clip and a life for a wrong or
-- missed one, a time-stop where the move says so, a tally at the end.
function T.doLevel(q)
local thisLevel = q.stage[q.iStageIndex][q.THISLEVEL]
if q.lvlState == q.lvlSetup then
q.lvlState = q.branch02
q.bShuffleOrder = true
q.bLevelInit = true
q.setupLevel(thisLevel, q.dip_Difficulty)
if q.bShowResurrect then
T.setupClip(q, 59602, 59746)
q.lvlState = q.branch01
q.bShowResurrect = false
end
q.bSpecialCase, q.bPlayPrompt = false, true
elseif q.lvlState == q.branch01 then
if q.currentFrame == q.iFrameEnd then
q.lvlState = q.branch02
end
elseif q.lvlState == q.branch02 then
if q.checkpoint == nil then
q.currentMove = 1
else
local section = 1
if q.bSection01Reached and not q.bSection02Reached then
section = 2
elseif q.bSection02Reached and not q.bSection03Reached then
section = 3
elseif q.bSection03Reached then
section = 4
end
q.currentMove = (section > 1) and q.checkpoint[section] or 1
end
if (q.iDeathCount == 0) and (q.currentMove == 1) then
discSkipToFrame(q.iLevelStart)
else
discSkipToFrame(q.move[q.currentMove][q.moveFrmStart])
end
q.lvlState = q.lvlRunning
elseif q.lvlState == q.branch03 then
T.doTimeStop(q)
elseif q.lvlState == q.lvlShowTally then
if q.currentFrame == q.iLevelEnd then
Q.timerON(q, 1)
if thisLevel == q.finalstage then
discSkipToFrame(3)
q.lvlState = q.branch08
else
q.bLevelComplete = true
q.lvlState = q.branch05
end
discPause()
end
elseif q.lvlState == q.lvlPlayRest then
if q.currentFrame == q.move[q.currentMove][q.moveFrmEnd] then
q.currentMove = q.currentMove + 1
if q.currentMove <= q.totalMoves then
if q.checkpoint ~= nil then
for k = 2, q.checkpoint[q.checkpointmax] do
if q.currentMove == q.checkpoint[k] then
if k - 1 == 1 then
q.bSection01Reached = true
elseif k - 1 == 2 then
q.bSection02Reached = true
elseif k - 1 == 3 then
q.bSection03Reached = true
end
break
end
end
end
q.bShowAction, q.bPlayPrompt = false, true
T.setupClip(q, q.move[q.currentMove][q.moveFrmStart], q.move[q.currentMove][q.moveFrmEnd])
if q.move[q.currentMove][q.correctMove] == q.TIMESTOP then
q.altState = q.lvlSetup
q.lvlState = q.branch03
else
q.lvlState = q.lvlRunning
end
else
q.iBonus = (q.iDeathCount >= 4) and 0 or ((4 - q.iDeathCount) * 10000)
if thisLevel == q.finalstage then
q.bMuteAttract = false
q.bShowCredits, q.bShowLives, q.bShowScore, q.bCheckForCredits = false, false, false, false
q.iBonus = q.iBonus + 40000
end
T.addPoints(q, q.iBonus)
q.lvlState = q.lvlShowTally
end
end
elseif q.lvlState == q.lvlRunning then
local m = q.move[q.currentMove]
if q.currentFrame > m[q.inputFrmEnd] then
Q.sound(q, q.sndwrong)
q.bShowAction = false
T.setupClip(q, m[q.deathFrmStart], m[q.deathFrmEnd])
q.lvlState = q.lvlPlayDeath
elseif (q.currentFrame >= m[q.inputFrmStart]) and (q.currentFrame <= m[q.inputFrmEnd]) then
local thisMove = T.scanInput(q)
if q.dip_showAction then
q.bShowAction = true
end
q.bPlayPrompt = false
if thisMove ~= q.NOMOVE then
q.bShowAction = false
if thisMove == m[q.correctMove] then
T.addPoints(q, q.POINTS_MOVE)
Q.sound(q, q.sndright)
q.lvlState = q.lvlPlayRest
else
Q.sound(q, q.sndwrong)
T.setupClip(q, m[q.deathFrmStart], m[q.deathFrmEnd])
q.lvlState = q.lvlPlayDeath
end
end
else
T.scanInput(q)
end
elseif q.lvlState == q.lvlPlayDeath then
if q.currentFrame == q.iFrameEnd then
q.iLives = q.iLives - 1
q.iDeathCount = q.iDeathCount + 1
if q.iLives > 0 then
T.setupClip(q, 59752, 59825)
q.lvlState = q.branch06
elseif q.iLives == 0 then
T.setupClip(q, 59828, 59885)
q.bShowLives, q.bShowScore, q.bShowAction = false, false, false
q.lvlState = q.branch07
end
end
elseif q.lvlState == q.branch04 then
if Q.timerDue(q) then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.branch05 then
if Q.timerDue(q) then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.branch06 then
if q.currentFrame == q.iFrameEnd then
discPause()
Q.timerON(q, 4)
q.lvlState = q.branch04
end
elseif q.lvlState == q.branch07 then
if q.currentFrame == q.iFrameEnd then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.branch08 then
if Q.timerDue(q) then
if q.dip_IntroType == q.DOPT_ORIGINAL then
T.setupClip(q, 58630, 59320)
elseif q.dip_IntroType == q.DOPT_REMIX then
T.setupClip(q, 62120, 65203)
elseif math.random(1, 1000) > 500 then
T.setupClip(q, 62120, 65203)
else
T.setupClip(q, 58630, 59320)
end
q.bLevelComplete = true
q.lvlState = q.branch07
end
elseif q.lvlState == q.lvlEnd then
q.lvlState = q.lvlSetup
if q.iLives == 0 then
if q.dip_AllowContinue and ((q.iContinues < q.dip_LimitContinue) or (q.dip_LimitContinue == q.DOPT_INFINITE_CONTINUES)) then
q.iTempLevel = q.currentLevel
q.currentLevel = q.levelContinue
q.iContinues = q.iContinues + 1
else
q.currentLevel = q.newScore(q.iScore) and q.levelHighScore or q.levelGameOver
end
elseif q.bLevelComplete then
q.stage[q.iStageIndex][q.BEATSTATUS] = true
q.currentLevel = q.levelChoose
elseif not q.dip_MustBeatLevel then
q.currentLevel = q.levelChoose
end
end
end
function T.doContinue(q)
if q.lvlState == q.lvlSetup then
T.setupClip(q, 59900, 60390)
q.lvlState = q.lvlRunning
q.bShowLives, q.bShowCredits = false, true
elseif q.lvlState == q.lvlRunning then
if q.bResetContinue then
q.bResetContinue = false
discSkipToFrame(59920)
end
if q.currentFrame == q.iFrameEnd then
q.lvlState = q.lvlEnd
elseif q.p1START1 then
q.p1START1 = false
if (q.iCredits > 0) or (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY) then
T.startGame(q)
q.bShowLives, q.bShowCredits = true, false
end
end
elseif q.lvlState == q.lvlEnd then
q.lvlState = q.lvlSetup
q.currentLevel = q.newScore(q.iScore) and q.levelHighScore or q.levelIntro
end
end
function T.doGameOver(q)
if q.lvlState == q.lvlSetup then
q.bShowLives, q.bShowScore, q.bShowCredits = false, false, false
T.setupClip(q, 59320, 59487)
q.lvlState = q.lvlRunning
elseif q.lvlState == q.lvlRunning then
if q.currentFrame == q.iFrameEnd then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.lvlEnd then
q.lvlState = q.lvlSetup
q.currentLevel = q.levelIntro
end
end
-- The board: the clip, the name left unentered (what is typed is not play; the ancestor gives
-- an empty name one at random), the board updated, and on to the game over.
function T.doHighScore(q)
if q.lvlState == q.lvlSetup then
q.bShowLives, q.bShowScore, q.bShowCredits, q.bShowAction, q.bIgnoreJoy = false, false, false, false, false
q.iCursor, q.sName = 1, ""
q.initArray()
T.setupClip(q, 60745, 62122)
q.lvlState = q.lvlRunning
elseif q.lvlState == q.lvlRunning then
if q.currentFrame == q.iFrameEnd then
discPause()
Q.timerON(q, 0.5)
q.lvlState = q.branch01
end
elseif q.lvlState == q.branch01 then
if Q.timerDue(q) then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.lvlEnd then
q.updateHS(q.sName, q.iScore)
q.lvlState = q.lvlSetup
q.currentLevel = q.levelGameOver
end
end
AUTHOR.behaviours.timegal = {
help = "Plays RDG2010's Time Gal (Singe Edition): levels of moves from a difficulty array (mirrored by a random draw), checkpoints, a time-stop choice of three, a tally by deaths, a life per quota, and a level order by tiers, at random, or in sequence. The game's own files are loaded and its data and order functions run as they are; the loop is this. Written by util/forgePortKaris.lua from the game's script.",
params = {},
attach = function(instance, params)
local data = AUTHOR_GAME.qte or {}
local q = { instance = instance, snapshot = data, rdg = true, timegal = true, namedSounds = true, loadsFiles = true }
instance.q = q
instance.hearsAll = true
for key, value in pairs(QTE) do
if (key ~= "FLAG_OF") and (key ~= "BRANCH") then
q[key] = value
end
end
q.env = Q.shim(q)
q.MYDIR = (AUTHOR_SOURCE_DIR or AUTHOR_DIR or ""):gsub("[/\\]$", "")
q.sounds = {}
if data.script then
Q.runScript(q, data.script)
end
-- The port keeps no config of its own: the dips and the board come from the description.
for _, name in ipairs({ "readConfig", "writeConfig" }) do
q[name] = function() end
end
for key, value in pairs(data.dips or {}) do
q[key] = value
end
for _, name in ipairs({ "dip_showAction", "dip_AllowContinue", "dip_MustBeatLevel" }) do
q[name] = (q[name] == 1) or (q[name] == true)
end
q.highscore = {}
for k, entry in ipairs(data.highScores or {}) do
q.highscore[k] = { entry[1], entry[2] }
end
q.move, q.stage, q.opt = q.move or {}, q.stage or {}, q.opt or {}
q.iCoins, q.iCredits, q.iScore, q.iLives, q.i1up, q.iQuota, q.iStageIndex, q.iContinues, q.iDeathCount, q.iBonus = 0, 0, 0, 0, 0, 200000, 0, 0, 0, 0
q.currentMove, q.totalMoves, q.currentFrame, q.iFrameStart, q.iFrameEnd, q.iTempLevel, q.iChoice = 0, 0, 0, 0, 0, 0, 0
q.bPause, q.bIgnoreJoy, q.bShuffleOrder, q.bShowTimeChoices, q.bShowResurrect, q.bLevelComplete, q.bLevelInit = false, false, false, false, false, false, false
q.bShowCredits, q.bShowScore, q.bShowLives, q.bShowAction, q.bCheckForCredits, q.bResetContinue, q.bMuteAttract = false, false, false, false, false, false, false
q.bSection01Reached, q.bSection02Reached, q.bSection03Reached, q.bSpecialCase, q.bPlayPrompt = false, false, false, false, false
q.gameflow = "vldp"
q.currentLevel = q.levelIntro
q.lvlState = q.lvlSetup
Q.clearInput(q)
q.p1START1, q.p1COIN1, q.p1COIN2, q.p1SERVICE = false, false, false, false
end,
step = function(instance)
local q = instance.q
q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then
-- The ancestor's start: the title clip, then the init.
if q.lvlState == q.lvlSetup then
discSetFPS(29.97)
T.setupClip(q, 59536, 59598)
discPlay()
q.lvlState = q.lvlRunning
elseif q.lvlState == q.lvlRunning then
if q.currentFrame == q.iFrameEnd then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.lvlEnd then
q.gameflow = "init"
q.lvlState = q.lvlSetup
end
return
end
if q.gameflow == "init" then
q.gameflow = "running"
q.currentLevel = q.levelIntro
q.lvlState = q.lvlSetup
q.iCoins, q.iCredits, q.iScore = 0, 0, 0
q.rndegg = random.new(math.floor(os.clock() * 100000))
q.bShowCredits = true
q.bShowScore, q.bShowLives, q.bShowAction = false, false, false
return
end
local level = q.currentLevel
if level == q.levelIntro then
T.doIntro(q)
elseif level == q.levelNormal then
T.doLevel(q)
elseif level == q.levelChoose then
q.chooseLevel()
elseif level == q.levelContinue then
T.doContinue(q)
elseif level == q.levelGameOver then
T.doGameOver(q)
elseif level == q.levelHighScore then
T.doHighScore(q)
elseif level == q.levelService then
-- The service menu is not played: back to the attract loop.
q.lvlState = q.lvlSetup
q.currentLevel = q.levelIntro
end
AUTHOR_VARS.score = q.iScore
AUTHOR_VARS.lives = q.iLives
AUTHOR_VARS.credits = q.iCredits
AUTHOR_VARS.level = q.iStageIndex
if q.bShowAction and q.move[q.currentMove] then
AUTHOR_VARS.prompt = q.move[q.currentMove][q.correctMove]
else
AUTHOR_VARS.prompt = ""
end
end,
on = function(instance, name, event)
local q = instance.q
local switch = event.switch or -1
local flag = qteFlagOf(switch)
if name == "pressed" then
if q.bPause or (flag == nil) then
return
end
if (flag == "p1BUTTON1") or (flag == "p1SERVICE") or (flag == "p1START1") or (flag == "p1COIN1") or (flag == "p1COIN2") or (flag == "p1UP") or (flag == "p1DOWN") or (flag == "p1LEFT") or (flag == "p1RIGHT") then
q[flag] = true
end
elseif name == "released" then
if switch == SWITCH_PAUSE then
q.bPause = not q.bPause
return
end
if (q.gameflow ~= "running") or q.bPause or (flag == nil) then
return
end
if ((flag == "p1COIN1") or (flag == "p1COIN2")) and (q.currentLevel ~= q.levelService) then
q.p1COIN1, q.p1COIN2 = false, false
if (q.dip_CoinsPerCredit ~= q.DOPT_FREEPLAY) and (q.iCredits < 9) then
q.iCoins = q.iCoins + 1
q.resetChannels()
if q.iCoins >= q.dip_CoinsPerCredit then
q.iCoins = q.iCoins - q.dip_CoinsPerCredit
q.iCredits = q.iCredits + 1
Q.sound(q, q.sndcredit)
else
Q.sound(q, q.sndsel)
end
if q.currentLevel == q.levelContinue then
q.bResetContinue = true
end
end
elseif flag ~= "p1SERVICE" then
q[flag] = false
end
end
end
}
-- ===== The Super Don Quixote game loop ========================================================
--
-- One hybrid: map-mode data (the level, scene, death-clip, order, and map functions of its
-- main.singe, run as they are through the shim as the rdg loop runs them) under a level loop
-- of the Karis 3.31c shape (branch11 the right move, branch02 a wrong one with the hints
-- frame in branch03, holds, mashes, doubles, paths, and choices, though its rows use only the
-- five plain kinds), with screens of its own: a difficulty select, a level clear with a
-- rolling bonus, a finish with a rolling percent, trophies, and two hardcoded level orders
-- (after a beaten level, after an unfinished one) the converter reads out as data.
local S = {}
function S.scanInput(q)
if q.p1UP then return q.UP
elseif q.p1DOWN then return q.DOWN
elseif q.p1LEFT then return q.LEFT
elseif q.p1RIGHT then return q.RIGHT
elseif q.p1BUTTON1 then return q.BUTTON1
elseif q.p1BUTTON2 then return q.BUTTON2
elseif q.p1BUTTON3 then return q.BUTTON3 end
return q.NOMOVE
end
function S.addPoints(q, thisMuch)
q.iScore = q.iScore + thisMuch
q.iExtraLife = q.iExtraLife + thisMuch
if (q.EXTRALIFE > 0) and (q.iExtraLife >= q.EXTRALIFE) then
q.iExtraLife = 0
if q.iLives < q.dip_LivesPerCredit then
Q.sound(q, q.sndvictory)
q.iLives = q.iLives + 1
end
end
if q.iScore > q.iTop then
q.iTop = q.iScore
end
if q.iScore > 99999999 then
q.iScore = 99999999
end
end
function S.checkHold(q, playerMove, curMove)
local z = S.scanInput(q)
local m = q.move[q.currentMove]
if (q.currentFrame == m[q.inputFrmStart]) and (z == playerMove) then
z = q.MOVEFAIL
elseif z == playerMove then
if q.iLenHold >= q.lenCounter then
z = curMove
q.iLenHold = 0
else
z = q.MOVEPENDING
if q.bTestHold and (q.currentFrame == q.lastHold) then
q.lastHold = q.currentFrame
elseif q.bTestHold and (q.currentFrame == q.lastHold + 1) then
q.iLenHold = q.iLenHold + 1
q.lastHold = q.currentFrame
else
q.lastHold = q.currentFrame
end
end
elseif z ~= q.NOMOVE then
q.iLenHold = 0
z = q.MOVEFAIL
else
if q.iLenHold > 0 then
q.iLenHold = q.iLenHold - 1
end
z = q.MOVEPENDING
end
return z
end
function S.checkLet(q, playerMove, curMove)
local m = q.move[q.currentMove]
if (q.currentFrame == m[q.inputFrmStart]) and (playerMove == q.NOMOVE) then
return q.MOVEFAIL
elseif (q.currentFrame == m[q.inputFrmEnd] - 1) and (playerMove == q.NOMOVE) then
return curMove
end
return q.MOVEPENDING
end
function S.checkMash(q, playerMove, curMove)
local z = q.MOVEPENDING
if playerMove == q.BUTTON1 then
q.bTestMash = false
if q.iMash >= q.mashCounter then
z = curMove
end
else
if q.iMash > 0 then
q.iMash = q.iMash - q.unMash
end
if playerMove ~= q.NOMOVE then
q.iMash = 0
z = q.MOVEFAIL
end
end
return z
end
function S.checkDouble(q, playerMove, curMove)
local z = q.MOVEPENDING
if playerMove == q.BUTTON1 then
q.bTestMash = false
if q.iMash >= 2 then
z = curMove
end
elseif playerMove ~= q.NOMOVE then
q.iMash = 0
z = q.MOVEFAIL
end
return z
end
function S.checkSkip(q, playerMove, curMove)
if (playerMove >= q.UP) and (playerMove <= q.BUTTON3) then
Q.clearInput(q)
discSkipToFrame(q.move[q.currentMove][q.inputFrmEnd])
end
return curMove
end
-- A wrong move: the hints frame first when the dip says so, else the death (nothing in the
-- practice game type).
local function sdqWrong(q)
if q.dip_Hints then
Q.sound(q, q.sndwrong)
discSkipToFrame(q.frameHints)
Q.timerON(q, 2)
discPause()
q.lvlState = q.branch03
else
q.iWrongMv = q.iWrongMv + 1
if q.dip_GameType == 4 then
q.bTestMash, q.bTestHold, q.iMash, q.iLenHold, q.bCalc = false, false, 0, 0, true
Q.sound(q, q.sndwrong)
q.lvlState = q.lvlPlayRest
else
q.setupDeathClip(q.thisMove)
end
end
end
-- A path's option taken: a jump to a move, or a death when the target is over a thousand.
local function sdqPath(q, target, aEnd, aJump)
q.iPath = target
if q.iPath > 1000 then
q.move[q.currentMove][q.moveDeath] = q.iPath - 1000
q.bShowAction = false
q.iPath = 0
q.iWrongMv = q.iWrongMv + 1
q.setupDeathClip(q.thisMove)
else
q.iPathAend, q.iPathAjmp = aEnd, aJump
q.bShowAction = false
q.lvlState = q.branch11
end
end
-- A death clip has ended: a life was already taken by setupDeathClip; on by the rewind dip.
local function sdqAfterDeath(q, thisLevel)
q.stage[thisLevel][q.DEATHCOUNT] = q.stage[thisLevel][q.DEATHCOUNT] + 1
if q.iLives <= 0 then
q.lvlState = q.lvlEnd
return
end
local after = q.move[q.currentMove + 1]
local last = (q.currentMove == q.totalMoves) or (after and (q.currentMove + 1 == q.totalMoves) and ((after[q.correctMove] == q.CHOOSE) or (after[q.correctMove] == q.LETGO) or (after[q.correctMove] == q.PATH) or (after[q.correctMove] == q.YESNO)))
if q.dip_Rewind == 0 then
q.bRes, q.curPath, q.bPath = true, 0, true
q.lvlState = q.lvlEnd
elseif q.dip_Rewind == 2 then
q.bRes, q.iSegPointer, q.curPath, q.bPath = true, 0, 0, true
q.lvlState = q.lvlSetup
elseif (q.dip_Rewind == 3) and last then
q.scene[q.iCurPos][q.iSegPointer][q.SCENECOMPLETE] = true
q.lvlState = q.lvlEnd
elseif q.ShowResurrect then
Q.setupClip(q, q.offsetGetReady, q.offsetGetReady)
q.lvlState = q.branch09
else
discSkipToFrame(q.iPauseFrame)
if (q.dip_Rewind == 3) and after and (after[q.correctMove] == q.LETGO) then
q.currentMove = q.currentMove + 2
else
q.currentMove = q.currentMove + 1
end
q.lvlState = q.lvlRunning
end
end
-- The scene begins: its intro clip, or the disc put at its start.
local function sdqStartScene(q, thisLevel)
if not q.stage[thisLevel][q.LEVELSTARTED] then
q.stage[thisLevel][q.LEVELSTARTED] = true
if q.bSave and (q.currentMove ~= 1) then
q.currentFrame = q.move[q.currentMove - 1][q.inputFrmEnd] + 1
discSkipToFrame(q.currentFrame)
q.bSave = false
q.lvlState = q.lvlRunning
elseif (not q.bSkipIntroClip) and ((q.dip_StartLevel ~= thisLevel) or (q.dip_StartScene == 1)) then
q.bShowSkip = true
q.getIntroClip(thisLevel)
q.lvlState = q.branch01
else
discSkipToFrame(q.sceneStart)
q.lvlState = q.lvlRunning
end
else
if q.currentFrame + 1 ~= q.sceneStart then
discSkipToFrame(q.sceneStart)
end
q.lvlState = q.lvlRunning
end
end
function S.doLevel(q)
local thisLevel = q.iCurPos
if q.lvlState == q.lvlSetup then
q.bShuffleOrder, q.bPlayPrompt, q.bPlayRight = true, true, true
q.bShowLvl, q.bShowAction, q.bAct, q.bTestMash, q.bTestHold = false, false, false, false, false
q.iMash, q.iLenHold, q.bPath, q.bCalc = 0, 0, true, true
q.bShowScore, q.bShowLives = (q.dip_GameType ~= 4), (q.dip_GameType ~= 4)
if not q.bSave then
q.currentMove = 1
end
q.setupLevel(thisLevel)
if q.ShowResurrect and q.bRes then
Q.setupClip(q, q.offsetGetReady, q.offsetGetReadyEnd)
q.bShowGet = true
q.lvlState = q.branch08
q.bRes = false
elseif not q.stage[thisLevel][q.LEVELSTARTED] then
q.stage[thisLevel][q.LEVELSTARTED] = true
if q.bSave and (q.currentMove ~= 1) then
q.currentFrame = q.move[q.currentMove - 1][q.inputFrmEnd] + 1
discSkipToFrame(q.currentFrame)
q.bSave = false
q.lvlState = q.lvlRunning
elseif (not q.bSkipIntroClip) and ((q.dip_StartLevel ~= thisLevel) or (q.dip_StartScene == 1)) then
q.bShowSkip = true
q.getIntroClip(thisLevel)
q.lvlState = q.branch01
else
discSkipToFrame(q.sceneStart)
q.lvlState = q.lvlRunning
end
else
if not ((q.currentFrame == q.sceneStart) or (q.currentFrame + 1 == q.sceneStart) or (q.currentFrame - 1 == q.sceneStart)) then
discSkipToFrame(q.sceneStart)
end
q.lvlState = q.lvlRunning
end
elseif q.lvlState == q.branch01 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.bShowLvl, q.bShowSkip = false, false
if q.currentFrame ~= q.iFrameEnd then
discSkipToFrame(q.sceneStart)
end
q.lvlState = q.lvlRunning
end
elseif q.lvlState == q.branch02 then
sdqWrong(q)
elseif q.lvlState == q.branch03 then
if Q.timerDue(q) then
q.setupDeathClip(q.thisMove)
end
elseif q.lvlState == q.branch04 then
if q.currentFrame >= q.sceneEnd then
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.branch05 then
if Q.timerDue(q) then
q.bGOAlt = true
local fresh = (thisLevel ~= q.levelExt) and q.AllowSecret
if fresh and (q.BeatGameWithOneLife() or q.BeatGameWithOneCredit()) then
Q.sound(q, q.sndvictory)
discSkipToFrame(q.frameSecret)
discPause()
Q.timerON(q, 4)
q.lvlState = q.branch06
elseif q.newScore(q.iScore) then
q.lvlState, q.currentLevel = q.lvlSetup, q.levelHighScore
else
q.lvlState, q.currentLevel = q.lvlSetup, q.levelGameOver
end
end
elseif q.lvlState == q.branch06 then
if Q.timerDue(q) then
q.bExtendedPlay = true
q.iSegPointer = 0
S.startGame(q)
end
elseif q.lvlState == q.branch07 then
-- The extra death clip has ended.
if q.currentFrame == q.iFrameEnd then
sdqAfterDeath(q, thisLevel)
end
elseif q.lvlState == q.branch08 then
-- The "get ready" clip has ended: the scene begins.
if q.currentFrame == q.iFrameEnd then
sdqStartScene(q, thisLevel)
end
elseif q.lvlState == q.branch09 then
-- The "get ready" clip after a death has ended: back to the move after it.
if q.currentFrame == q.iFrameEnd then
local after = q.move[q.currentMove + 1]
q.lvlState = q.lvlRunning
discSkipToFrame(q.iPauseFrame)
if (q.dip_Rewind == 3) and after and (after[q.correctMove] == q.LETGO) then
q.currentMove = q.currentMove + 2
else
q.currentMove = q.currentMove + 1
end
end
elseif q.lvlState == q.branch10 then
S.doChoose(q)
elseif q.lvlState == q.branch11 then
Q.clearInput(q)
q.lvlState = q.lvlPlayRest
if q.bPlayRight then
Q.sound(q, q.sndright)
q.bPlayRight = false
end
local worth = q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE
S.addPoints(q, worth)
q.iScoreTemp = q.iScoreTemp + worth
q.iRightMv = q.iRightMv + 1
elseif q.lvlState == q.lvlPlayRest then
q.bPlayRight = true
if q.currentMove < q.totalMoves then
q.bCheckMove = true
end
q.thisMove = S.scanInput(q)
if q.currentFrame >= q.move[q.currentMove][q.moveFrmEnd] then
q.bCheckMove = false
if (q.iPath ~= 0) and (q.currentMove < q.totalMoves) then
q.currentMove = q.iPath
q.iPath, q.bPath = 0, true
elseif (q.iPathAend ~= 0) and (q.currentMove == q.iPathAend) then
q.currentMove = q.iPathAjmp
q.iPathAend, q.iPathAjmp = 0, 0
else
q.currentMove = q.currentMove + 1
end
if q.currentMove <= q.totalMoves then
if q.currentFrame + 1 ~= q.move[q.currentMove][q.moveFrmStart] then
discSkipToFrame(q.move[q.currentMove][q.moveFrmStart])
end
q.bPlayPrompt, q.bAct = true, false
if q.move[q.currentMove][q.correctMove] == q.CHOOSE then
q.altState = q.lvlSetup
q.lvlState = q.branch10
else
q.lvlState = q.lvlRunning
end
else
local bonus = q.SCORESCENE - q.stage[thisLevel][q.DEATHCOUNT] * q.DEATHPENALTY
q.scene[thisLevel][q.iSegPointer][q.SCENECOMPLETE] = true
if ((thisLevel == q.finalstage) and q.BeatLevel(q.finalstage)) or ((thisLevel == q.levelExt) and q.BeatLevel(q.levelExt)) then
if thisLevel == q.finalstage then
q.stage[thisLevel][q.BEATSTATUS] = true
end
if q.dip_GameType ~= 4 then
q.doTrophy()
end
if q.BeatGame() then
if bonus > 0 then
S.addPoints(q, bonus)
end
S.addPoints(q, q.SCOREGAME)
if q.BeatGameWithOneLife() then
S.addPoints(q, q.SCORESECRET)
end
else
if bonus > 0 then
S.addPoints(q, bonus)
end
S.addPoints(q, q.SCORELEVEL)
end
if thisLevel == q.levelExt then
discPause()
Q.timerON(q, 0.1)
else
Q.sound(q, q.sndvictory)
discSkipToFrame(q.frameVictory)
discPause()
Q.timerON(q, 3)
end
q.lvlState = q.branch05
else
if bonus > 0 then
S.addPoints(q, bonus)
q.iBonus = q.iBonus + bonus
end
q.lvlState = q.branch04
end
end
end
elseif q.lvlState == q.lvlRunning then
local m = q.move[q.currentMove]
local kind = m[q.correctMove]
if (q.currentFrame >= m[q.inputFrmStart]) and (q.currentFrame <= m[q.inputFrmEnd]) then
q.bShowAction, q.bShowLvl = true, false
if q.bPlayPrompt and (q.dip_ShowAction == 1) and (kind <= q.HOLDRIGHT) then
Q.sound(q, q.sndcoin)
q.bPlayPrompt = false
end
local function judged(thisMove)
if thisMove == kind then
return true
elseif thisMove ~= q.MOVEPENDING then
q.iPauseFrame = m[q.inputFrmEnd]
q.bShowAction = false
q.lvlState = q.branch02
end
return false
end
if (kind >= q.HOLDUP) and (kind <= q.HOLDBUT) then
q.bTestHold = true
if q.bCalc then
q.bCalc = false
q.lenCounter = (m[q.inputFrmEnd] - m[q.inputFrmStart]) - (13 - q.dip_Difficulty)
end
q.thisMove = S.checkHold(q, kind - 20, kind)
if judged(q.thisMove) then
q.thisMove = S.scanInput(q)
q.bTestHold, q.iLenHold, q.lastHold, q.bCalc = false, 0, 0, true
q.lvlState = q.branch11
end
elseif kind == q.LETGO then
q.thisMove = S.checkLet(q, S.scanInput(q), kind)
if judged(q.thisMove) then
q.lvlState = q.branch11
end
elseif (kind >= q.MASH) and (kind <= q.MASHMAX) then
q.thisMove = S.scanInput(q)
q.bTestMash = true
if q.bCalc then
q.bCalc = false
q.unMash = ({ [q.MASH] = 0.15, [q.MASHMIN] = 0.12, [q.MASHMAX] = 0.18 })[kind]
q.mashCounter = q.dip_Difficulty + (m[q.inputFrmEnd] - m[q.inputFrmStart]) / 10
end
q.thisMove = S.checkMash(q, q.thisMove, kind)
if judged(q.thisMove) then
q.bTestMash, q.iMash, q.bCalc = false, 0, true
q.lvlState = q.branch11
end
elseif kind == q.DOUBLE then
q.thisMove = S.scanInput(q)
q.bTestMash = true
q.thisMove = S.checkDouble(q, q.thisMove, kind)
if judged(q.thisMove) then
q.bTestMash, q.iMash = false, 0
q.lvlState = q.branch11
end
elseif kind <= q.BUTTON4 then
q.thisMove = S.scanInput(q)
if (q.thisMove ~= q.NOMOVE) and judged(q.thisMove) then
q.lvlState = q.branch11
end
elseif (kind >= q.UPLEFT) and (kind <= q.ACTRIGHT) then
local pair = pairFor(q, kind)
local other = false
Q.timerON(q, (m[q.inputFrmEnd] - q.currentFrame) / q.MovieFPS)
for _, name in ipairs({ "p1UP", "p1DOWN", "p1LEFT", "p1RIGHT", "p1BUTTON1", "p1BUTTON2", "p1BUTTON3" }) do
if q[name] and (name ~= pair[1]) and (name ~= pair[2]) then
other = true
end
end
if Q.timerDue(q) or other then
q.iPauseFrame = m[q.inputFrmEnd]
q.bShowAction = false
q.lvlState = q.branch02
elseif q[pair[1]] and q[pair[2]] then
q.bAct = false
q.lvlState = q.branch11
end
elseif kind == q.PATH then
q.thisMove = S.scanInput(q)
if q.bPath then
q.curPath = q.curPath + 1
q.bPath = false
end
if q.thisMove ~= q.NOMOVE then
local p = q.path[q.curPath]
if q.thisMove == p[1] then
sdqPath(q, p[4], p[5] - 1, p[7])
elseif q.thisMove == p[2] then
if p[3] == 0 then
sdqPath(q, p[5], 0, 0)
else
sdqPath(q, p[5], p[6] - 1, p[7])
end
elseif q.thisMove == p[3] then
sdqPath(q, p[6], 0, 0)
else
q.setupDeathClip(q.thisMove)
end
end
elseif kind == q.YESNO then
q.thisMove = S.scanInput(q)
if q.bPath then
q.curPath = q.curPath + 1
q.bPath = false
end
local p = q.path[q.curPath]
if q.thisMove ~= q.NOMOVE then
if q.thisMove == q.BUTTON1 then
sdqPath(q, p[4], p[5] - 1, p[7])
else
q.setupDeathClip(q.thisMove)
end
elseif q.currentFrame >= m[q.inputFrmEnd] then
sdqPath(q, p[5], 0, 0)
end
elseif kind == q.SKIP then
q.thisMove = S.checkSkip(q, S.scanInput(q), kind)
end
elseif (q.currentFrame > m[q.inputFrmEnd]) and (kind ~= q.SKIP) then
q.iPauseFrame = m[q.inputFrmEnd]
q.bShowAction = false
q.lvlState = q.branch02
elseif q.currentFrame > m[q.inputFrmEnd] then
q.bShowAction = false
q.lvlState = q.lvlPlayRest
else
S.scanInput(q)
end
elseif q.lvlState == q.lvlPlayDeath then
if q.currentFrame == q.iFrameEnd then
if q.ShowSupDeath then
Q.setupClip(q, q.offsetSupDeath, q.offsetSupDeathEnd)
q.lvlState = q.branch07
else
sdqAfterDeath(q, thisLevel)
end
end
elseif q.lvlState == q.lvlEnd then
q.lvlState = q.lvlSetup
if q.iLives == 0 then
if (q.dip_LimitContinue > 0) and ((q.iContinues < q.dip_LimitContinue) or (q.dip_LimitContinue == q.DOPT_INFINITE_CONTINUES)) then
q.iTempLevel = q.currentLevel
q.currentLevel = q.levelContinue
q.iContinues = q.iContinues + 1
else
q.currentLevel = q.newScore(q.iScore) and q.levelHighScore or q.levelGameOver
end
else
local order = q.snapshot.sdq or {}
if q.BeatLevel(thisLevel) then
if q.dip_GameType ~= 4 then
q.doTrophy()
end
q.stage[thisLevel][q.BEATSTATUS] = true
q.levelMap[thisLevel] = true
if q.dip_GameType == 3 then
-- The ancestor autosaves here; the port keeps no saves.
q.bShowDiskA = true
q.altState = q.branch01
end
q.iSegPointer = 0
S.addPoints(q, q.SCORELEVEL)
q.iBonus = q.iBonus + q.SCORELEVEL
if q.stage[thisLevel][q.DEATHCOUNT] == 0 then
S.addPoints(q, q.PERFECTBONUS)
q.iBonus = q.iBonus + q.PERFECTBONUS
end
q.bSkipIntroClip = false
q.iLiveSave, q.iScoreSave, q.bAllowSave, q.bRes = q.iLives, q.iScore, true, true
if q.ShowLvlClear then
Q.sound(q, q.sndclear)
q.lvlState, q.currentLevel = q.lvlSetup, q.levelFinish
else
q.iScoreTemp, q.iBonus = 0, 0
if q.dip_GameType == 3 then
q.lvlState, q.currentLevel = q.lvlSetup, q.levelMenuScreen
elseif (order.beaten or {})[thisLevel] then
q.iCurPos = order.beaten[thisLevel]
end
end
elseif q.CompleteLevel then
if q.iPath ~= 0 then
q.iSegPointer, q.iPath = q.iPath, 0
end
if not q.scene[thisLevel][q.iSegPointer][q.SCENECOMPLETE] then
if q.iSegPointer > 0 then
q.iSegPointer = q.iSegPointer - 1
end
else
q.iLiveSave, q.iScoreSave, q.bAllowSave = q.iLives, q.iScore, true
end
elseif (order.unfinished or {})[thisLevel] then
q.iCurPos = order.unfinished[thisLevel]
end
q.bTestMash, q.bTestHold, q.iMash, q.iLenHold = false, false, 0, 0
end
end
end
-- A choice (none of the game's rows make one): the Karis shape, a wrong option a death.
function S.doChoose(q)
local m = q.move[q.currentMove]
if q.altState == q.lvlSetup then
q.altState, q.iChoice, q.bIgnoreJoy, q.bShowChoices = q.lvlRunning, 1, false, true
elseif q.altState == q.lvlRunning then
local worth = q.SCOREMOVE + q.dip_Difficulty * q.BUFFMOVE
local function take(skip)
q.bShowChoices = false
if q.choice[q.optorder[q.iChoice]][2] == true then
Q.sound(q, q.sndright)
S.addPoints(q, worth)
q.iScoreTemp = q.iScoreTemp + worth
q.iRightMv = q.iRightMv + 1
if skip then
discSkipToFrame(m[q.inputFrmEnd])
end
q.lvlState = q.lvlPlayRest
else
m[q.moveDeath] = q.choice[q.optorder[q.iChoice]][3]
q.iWrongMv = q.iWrongMv + 1
q.setupDeathClip(q.thisMove)
q.lvlState = q.lvlPlayDeath
end
end
if q.currentFrame > m[q.inputFrmEnd] then
take(false)
elseif (q.currentFrame >= m[q.inputFrmStart]) and (q.currentFrame <= m[q.inputFrmEnd]) then
local thisMove = q.NOMOVE
if q.bIgnoreJoy then
if Q.timerDue(q) then
q.bIgnoreJoy = false
end
else
thisMove = S.scanInput(q)
end
if thisMove == q.UP then
q.p1UP = false
if q.iChoice > 1 then
q.iChoice = q.iChoice - 1
Q.sound(q, q.sndcoin)
end
elseif thisMove == q.DOWN then
q.p1DOWN = false
if q.iChoice < m[q.moveDeath] then
q.iChoice = q.iChoice + 1
Q.sound(q, q.sndcoin)
end
elseif thisMove == q.BUTTON1 then
q.p1BUTTON1 = false
take(true)
end
end
end
end
function S.startGame(q)
q.initStages()
q.curPath = 0
if q.bExtendedPlay then
q.currentLevel, q.iCurPos = q.levelNormal, q.levelExt
else
if q.iCredits > 0 then
q.iCredits = q.iCredits - 1
end
q.iScore, q.iScoreTemp, q.iBonus = 0, 0, 0
if q.currentLevel == q.levelContinue then
q.currentLevel = q.iTempLevel
if q.dip_Rewind == 1 then
q.currentMove, q.bSave = q.currentMove + 1, true
elseif q.dip_Rewind == 3 then
q.currentMove = (q.currentMove == q.totalMoves) and (q.currentMove - 1) or (q.currentMove + 1)
q.bSave = true
elseif q.dip_Rewind == 2 then
q.iSegPointer = 0
end
else
q.iContinues, q.iSegPointer = 0, 0
if q.dip_GameType == 0 then
q.doMixSEQ()
q.iCurPos, q.iSegPointer, q.currentLevel = q.dip_StartLevel, q.dip_StartScene - 1, q.levelNormal
elseif q.dip_GameType == 1 then
q.doMixRND()
q.iCurPos, q.currentLevel = q.LvlOrder[1], q.levelNormal
elseif q.dip_GameType == 2 then
q.doMixTIE()
q.iCurPos, q.currentLevel = q.LvlOrder[1], q.levelNormal
elseif q.dip_GameType == 3 then
q.iCurPos, q.lvlState = q.PlayOrder[1], q.lvlSetup
q.currentLevel = (q.MapStart == 0) and q.levelMenuScreen or q.levelNormal
elseif q.dip_GameType == 4 then
q.iRightMv, q.iWrongMv = 0, 0
q.iCurPos, q.iSegPointer, q.currentLevel = q.dip_StartLevel, q.dip_StartScene - 1, q.levelNormal
end
end
end
if q.IngameDiffchoice and (q.dip_Diffshow == 4) and q.bOneDiff then
q.altState = q.lvlSetup
q.currentLevel = q.levelDiffScreen
end
q.lvlState = q.lvlSetup
q.bRes = true
q.iLives = q.dip_LivesPerCredit
q.bResetContinue, q.bExtendedPlay = false, false
end
-- The attract loop: the intro clip, the controls and the specials stills, a filler, the
-- rankings, the percents, the trophies, another filler, and round; the secret's combination
-- at three of its stops.
function S.doIntro(q)
local function secret()
if q.p1BUTTON2 and q.p1BUTTON3 and q.p1UP and q.p1RIGHT and q.AllowSecret then
q.p1BUTTON2, q.p1BUTTON3, q.p1UP, q.p1RIGHT = false, false, false, false
q.bExtendedPlay = true
S.startGame(q)
return true
end
return false
end
local function still(frame, hold, next)
q.p1BUTTON1 = false
discSkipToFrame(frame)
discPause()
Q.timerON(q, hold)
q.lvlState = next
end
if q.lvlState == q.lvlSetup then
Q.setupClip(q, q.offsetIntro01, q.offsetIntro01end)
q.initLCD()
q.lvlState = q.branch01
q.bCheckForCredits = true
elseif q.lvlState == q.branch01 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
still(q.frameControls, 10, q.branch02)
else
secret()
end
elseif q.lvlState == q.branch02 then
if Q.timerDue(q) or q.p1BUTTON1 then
still(q.frameSpecial, 10, q.branch03)
end
elseif q.lvlState == q.branch03 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.initLCD()
q.doFillerFrame()
q.lvlState = q.branch04
end
elseif q.lvlState == q.branch04 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
still(q.frameRankings, 6, q.branch05)
else
secret()
end
elseif q.lvlState == q.branch05 then
if Q.timerDue(q) or q.p1BUTTON1 then
still(q.frameRankings, 6, q.branch06)
end
elseif q.lvlState == q.branch06 then
if Q.timerDue(q) or q.p1BUTTON1 then
still(q.frameTrophy, 6, q.branch07)
end
elseif q.lvlState == q.branch07 then
if Q.timerDue(q) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.initLCD()
q.doFillerFrame()
q.lvlState = q.branch08
else
secret()
end
elseif q.lvlState == q.branch08 then
if (q.currentFrame == q.iFrameEnd) or q.p1BUTTON1 then
q.p1BUTTON1 = false
q.lvlState = q.lvlSetup
end
end
if q.dip_CoinsPerCredit == q.DOPT_FREEPLAY then
-- The ancestor compares its movie dip, a number, with booleans: neither ever holds.
if q.p1START1 and (q.dip_Movie == false) then
q.p1START1 = false
S.startGame(q)
elseif q.p1START1 and (q.dip_Movie == true) then
q.p1START1 = false
q.lvlState = q.lvlSetup
q.currentLevel = q.levelMovie
end
elseif q.bShowCredits and ((q.iCoins > 0) or (q.iCredits > 0)) then
discSkipToFrame(1500)
discPause()
if q.p1START1 and (q.iCredits > 0) then
q.p1START1 = false
S.startGame(q)
end
end
end
-- The difficulty select: a still per difficulty, the stick moving between them.
function S.moveFrameDiff(q)
local function step(frame, level)
Q.sound(q, q.sndcoin)
discSkipToFrame(frame)
discPause()
q.p1LEFT, q.p1RIGHT = false, false
q.dip_Difficulty = level
end
if q.currentFrame == q.frameEasy then
if q.p1RIGHT then step(q.frameNormal, 1) end
elseif q.currentFrame == q.frameNormal then
if q.p1LEFT then step(q.frameEasy, 0) elseif q.p1RIGHT then step(q.frameHard, 2) end
elseif q.currentFrame == q.frameHard then
if q.p1LEFT then step(q.frameNormal, 1) elseif q.p1RIGHT then step(q.frameExtreme, 3) end
elseif q.currentFrame == q.frameExtreme then
if q.p1LEFT then step(q.frameHard, 2) end
end
end
function S.doDiffSelect(q)
if q.altState == q.lvlSetup then
q.bIgnoreJoy = false
discSkipToFrame(q.frameEasy)
discPause()
Q.timerON(q, 30)
q.altState = q.lvlRunning
elseif q.altState == q.lvlRunning then
if Q.timerDue(q) then
q.altState = q.lvlEnd
elseif q.p1BUTTON1 then
Q.sound(q, q.sndcredit)
q.p1BUTTON1 = false
q.altState = q.lvlEnd
else
S.moveFrameDiff(q)
end
elseif q.altState == q.lvlEnd then
q.lvlState, q.currentLevel = q.lvlSetup, q.levelNormal
end
end
-- The level's end: the ancestor runs doFinish and then doClear every frame of it, over one
-- state; so does this, in that order.
function S.doFinish(q)
local iTemp = math.floor(100 * (q.iRightMv / (q.iRightMv + q.iWrongMv)))
q.bShowScore, q.bRes = false, true
if q.lvlState == q.lvlSetup then
Q.setupClip(q, q.offsetClear, q.offsetClearend)
q.lvlState = q.branch01
elseif q.lvlState == q.branch01 then
if q.currentFrame == q.iFrameEnd then
discPause()
Q.timerON(q, 0.1)
q.lvlState = q.branch02
end
elseif q.lvlState == q.branch02 then
if Q.timerDue(q) then
if q.numTrophy < iTemp then
if Q.timerDue(q) then
q.numTrophy = q.numTrophy + 1
Q.timerON(q, 0.01)
Q.sound(q, q.sndroll)
end
else
Q.sound(q, q.sndvictory)
Q.timerON(q, 5)
q.lvlState = q.branch03
end
end
elseif q.lvlState == q.branch03 then
if Q.timerDue(q) then
q.iScoreTemp, q.iBonus, q.numTrophy = 0, 0, 0
if q.newPercent(iTemp) and (q.dip_Difficulty > 0) then
q.lvlState, q.currentLevel = q.lvlSetup, q.levelHighScore
else
q.lvlState, q.currentLevel = q.lvlSetup, q.levelIntro
end
end
end
end
function S.doClear(q)
q.bRes = true
if q.lvlState == q.lvlSetup then
Q.setupClip(q, q.offsetClear, q.offsetClearend)
q.lvlState = q.branch01
elseif q.lvlState == q.branch01 then
if q.currentFrame == q.iFrameEnd then
discPause()
Q.timerON(q, 2)
q.lvlState = q.branch02
end
elseif q.lvlState == q.branch02 then
if Q.timerDue(q) then
if q.iBonus > 0 then
if Q.timerDue(q) then
q.iBonus = q.iBonus - 1000
q.iScoreTemp = q.iScoreTemp + 1000
Q.timerON(q, 0.01)
Q.sound(q, q.sndroll)
end
else
Q.sound(q, q.sndvictory)
Q.timerON(q, 3)
q.lvlState = q.branch03
end
end
elseif q.lvlState == q.branch03 then
if Q.timerDue(q) then
q.iScoreTemp, q.iBonus, q.numTrophy = 0, 0, 0
if q.dip_GameType == 3 then
q.lvlState, q.currentLevel = q.lvlSetup, q.levelMenuScreen
else
q.NextLevel(q.iCurPos)
q.lvlState, q.currentLevel = q.lvlSetup, q.levelNormal
end
end
end
end
function S.doContinue(q)
if q.lvlState == q.lvlSetup then
Q.setupClip(q, q.offsetContinue, q.offsetContinueend)
q.lvlState = q.lvlRunning
elseif q.lvlState == q.lvlRunning then
if q.currentFrame == q.iFrameEnd then
q.lvlState = q.lvlEnd
elseif q.p1START1 then
q.p1START1 = false
if (q.iCredits > 0) or (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY) then
q.bOneDiff, q.bSkipIntroClip = false, true
if q.iSegPointer > 0 then
q.iSegPointer = q.iSegPointer - 1
end
S.startGame(q)
end
elseif q.p1BUTTON2 then
q.p1BUTTON2 = false
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.lvlEnd then
q.lvlState, q.bSkipIntroClip = q.lvlSetup, false
if q.newScore(q.iScore) then
q.currentLevel, q.bGOAlt = q.levelHighScore, true
else
q.currentLevel = q.levelGameOver
end
end
end
AUTHOR.behaviours.sdq = {
help = "Plays Super Don Quixote: map-mode levels, scenes, death clips, order, and map from the game's own files, judged as the Karis 3.31c framework judges, with its difficulty select, level clear, finish, and trophies. Written by util/forgePortKaris.lua from the game's script.",
params = {},
attach = function(instance, params)
-- The state the rdg loop keeps, and then this game's own.
AUTHOR.behaviours.rdg.attach(instance, params)
local q = instance.q
q.sdq, q.later = true, false
q.percent = {}
for k, entry in ipairs(q.snapshot.percents or {}) do
q.percent[k] = { entry[1], entry[2] }
end
-- The dips as the game's readConfig leaves them: the extra clips by a table of
-- eight, hints a boolean, the action prompt and the movie flag numbers.
local extra = ({ [0] = {}, [1] = { r = true }, [2] = { s = true }, [3] = { c = true }, [4] = { r = true, s = true }, [5] = { r = true, c = true }, [6] = { s = true, c = true }, [7] = { r = true, s = true, c = true } })[q.dip_Extravid or 0] or {}
q.ShowResurrect, q.ShowSupDeath, q.ShowLvlClear = extra.r or false, extra.s or false, extra.c or false
q.dip_Hints = ((q.snapshot.dips or {}).dip_Hints == 1)
q.dip_ShowAction = (q.snapshot.dips or {}).dip_ShowAction or 0
q.dip_Movie = (q.snapshot.dips or {}).dip_Movie or 0
q.iPenal = ({ [0] = 0, [1] = q.PenalNormal, [2] = q.PenalHard, [3] = q.PenalExtreme })[q.dip_Difficulty] or 0
q.iRightMv, q.iWrongMv, q.iExtraLife, q.iTop, q.numTrophy = 0, 0, 0, 0, 0
q.iPath, q.iPathAend, q.iPathAjmp, q.curPath, q.bPath, q.bCalc = 0, 0, 0, 0, false, true
q.iLenHold, q.lastHold, q.lenCounter, q.unMash, q.mashCounter = 0, 0, q.lenCounter or 8, q.unMash or 0.14, q.mashCounter or 5
q.bOneDiff, q.bGOAlt, q.bPlayRight, q.bTestHold, q.bShowGet = true, false, true, false, false
q.thisMove = q.NOMOVE
end,
step = function(instance)
local q = instance.q
q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then
if q.lvlState == q.lvlSetup then
Q.setupClip(q, q.offsetTitle, q.offsetTitleend)
q.bPause = true
q.lvlState = q.lvlRunning
elseif q.lvlState == q.lvlRunning then
if q.currentFrame == q.iFrameEnd then
discPause()
q.lvlState = q.lvlEnd
end
elseif q.lvlState == q.lvlEnd then
q.bPause = false
q.gameflow = "init"
q.lvlState = q.lvlSetup
end
return
end
if q.gameflow == "init" then
q.gameflow = "running"
q.currentLevel = q.levelIntro
q.lvlState = q.lvlSetup
q.iCoins, q.iScore, q.iScoreTemp, q.iBonus, q.iSegPointer = 0, 0, 0, 0, 0
for k = 1, 16 do
q.levelMap[k] = false
end
q.bAct = false
q.initLCD()
return
end
local level = q.currentLevel
if level == q.levelIntro then
S.doIntro(q)
elseif level == q.levelNormal then
S.doLevel(q)
elseif level == q.levelMenuScreen then
q.doLevelSelect()
elseif level == q.levelDiffScreen then
S.doDiffSelect(q)
elseif level == q.levelContinue then
S.doContinue(q)
elseif level == q.levelGameOver then
R.doGameOver(q)
elseif level == q.levelHighScore then
R.doHighScore(q)
elseif level == q.levelFinish then
S.doFinish(q)
S.doClear(q)
elseif (level == q.levelService) or (level == q.levelSave) or (level == q.levelMovie) then
-- The service and save menus and the movie player are not played.
q.lvlState, q.currentLevel = q.lvlSetup, q.levelIntro
end
AUTHOR_VARS.score = q.iScore
AUTHOR_VARS.lives = q.iLives
AUTHOR_VARS.credits = q.iCredits
AUTHOR_VARS.level = q.iCurPos
if q.bShowAction and q.move[q.currentMove] then
AUTHOR_VARS.prompt = q.move[q.currentMove][q.correctMove]
else
AUTHOR_VARS.prompt = ""
end
end,
on = function(instance, name, event)
-- The map-mode handler: the same switches, the mash count, the coins, and the quit.
AUTHOR.behaviours.rdg.on(instance, name, event)
end
}