--[[ * * Singe 3 * Copyright (C) 2006-2026 Scott Duensing * * 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//...): 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 }