diff --git a/CHANGELOG b/CHANGELOG
index fc726113f..0bcc0874c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,6 +7,10 @@ SINGE 3.00
API Changes
-----------
+- onInputPressed and onInputReleased receive a second argument when the
+ switch came from a mouse or a light gun: the device's index, as
+ Hypseus passes it, so a two-gun game can tell its players apart. A
+ switch from the keyboard or a pad passes nothing there.
- singeScreenshot takes an optional file name, so a script that makes
pictures on purpose can name them.
@@ -21,6 +25,22 @@ API Changes
- In MODE_FULL a pad or mouse button reaches the script as the switch it
is mapped to, as the manual always said; it arrived as a keysym of 0.
+- A sprite scaled to nothing (a bullet shrinking to zero) draws as a
+ pixel instead of ending the engine with "Unable to transform sprite".
+- --deterministic stepped the disc one video frame per engine frame,
+ whatever the step, and the overlay throttle's strict comparison ran
+ the script every other iteration of the virtual clock, so a game that
+ reads the same frame on two consecutive updates (as a game paced for
+ a faster overlay does) never saw it twice; the disc now follows the
+ virtual clock like everything else, each frame lasting the time it is
+ worth, and the script runs every step. A bare os.time() answers
+ with the virtual clock too, so a game that seeds its generator from
+ the wall clock when a game starts (the one-player Space Pirates)
+ repeats; os.time with a date table is Lua's own.
+- fontSelect given no font (nil, or an index that was never loaded)
+ keeps the current font and warns instead of ending the script, as
+ Hypseus lets it pass; The Last Bounty Hunter names a font it never
+ loaded at its first board.
- guiSetHandler set again after a document's elements were rebuilt (a
list through inner_rml) now attaches to the new element with that id.
@@ -58,7 +78,42 @@ API Changes
by count and interval at a moment, a disc frame, or a rail camera's
stop, from an entity or a list of them in turn, or at a point; and
soundDone says when a clip playSound or a sound behaviour started has
- ended. Flat
+ ended. Ports of the test library began (FORGE.md section 14): the
+ five ActionMax games are Forge descriptions written by
+ util/forgePortActionMax.py, and play the same as the emulator under
+ the same inputs (testScripts/ports/). On the way: a lightSensor
+ behaviour, a pointer behaviour, fonts and anchors on the text look,
+ sprites drawn in copies, discPlay and discPause, pointerX and pointerY
+ in expressions, an event belonging to the room it happened in, and a
+ built game finding files beside its description. A game can bring a
+ vocabulary of its own: a description names a file of Lua
+ (vocabulary = "Vocabulary.singe") that adds looks, behaviours,
+ conditions, actions, layers, or events to the manifest for that game
+ alone; the editor loads it when the game opens and forgets it when
+ the game closes, the compiler checks against it, and the built game
+ carries it into a release. The ActionMax light sensor lives in one
+ now, beside those five games, and not in Forge. The Hypseus library's
+ eleven Karis Framework 3.32b titles (Arcade Xperience 1 to 3, Astroboy,
+ Danmachi, Freddy, Starship Troopers, Sugar Rush, Survival, Esh's
+ Aurunmilla, and Badlands Lite) are ported by the same converter, which
+ finds the framework where a game keeps it and notes its version; the
+ rest of that library is surveyed in FORGE.md 14.9. A branch on the
+ branching track can want its move mashed (mash = 6) or held (hold =
+ 20). The seven Kimmy Script Engine titles (Brain Dead 13, Future Boy
+ Conan, Mazinga Z, Fuma Conspiracy, Sonic 1996, Sintel, Ninja Hayate
+ 1080) are ported by the same converter and a kimmy behaviour that
+ plays that engine's loop, and play as their originals do (FORGE.md
+ 14.11).
+ util/forgePorts.py
+ assembles the ported games into a project folder of their own
+ (~/claude/singePorts), one game to a folder with its description, the
+ compiled port, the runtime, its files, and a games.dat. A description is a
+ .forge file now (it was .game, which is the packed release's, and the
+ chooser had to sniff which was which). The seventeen KarisFramework
+ games are ported too: util/forgePortKaris.lua reads a game's own
+ script, and the qte behaviour plays the framework's loop over the
+ framework's tables, running the game's script and add-ons in a shim;
+ every one plays as its original under the same driver. Flat
bodies are the size their looks are drawn at (they were half), and a
platformer stands on its point.
@@ -494,8 +549,12 @@ API Changes
- Compressed textures. KTX2 files (Basis Universal, from
util/compressTextures.py) stay compressed on the GPU in the block
- format the machine has (BC7, ASTC, BC3, or plain RGBA where nothing
- else exists), a quarter of the memory of PNGs or less. A model whose
+ format the machine has (BC7, ASTC, ETC2, BC3, or plain RGBA where
+ nothing else exists), a quarter of the memory of PNGs or less. ETC2
+ is the one block format OpenGL ES 3 guarantees, so a handheld's Mali
+ without ASTC keeps its textures compressed rather than falling to RGBA
+ on the machine with the least memory; the GLES backend takes ETC2
+ always and ASTC 4x4 where the driver offers it. A model whose
file uses KHR_texture_basisu loads them on its own; materialSetTexture
and the map calls take a .ktx2 name in place of a sprite; sceneGetStats
reports the texture memory in use. The Sponza test scene went from
@@ -1113,6 +1172,25 @@ API Changes
Fixes
-----
+- The menu's grid, horizon and sun were drawn black and unlit through the
+ intro, which kept the lights off them but not the blast's glow behind
+ them: a black line across a flash is a line, and the whole grid showed
+ through the explosion as its outline. They are not drawn at all until
+ the grid comes up out of the dark. The recording of the backdrop is
+ re-made with it.
+
+- A PNG with 16-bit samples and no alpha channel (crate.png in the test
+ scripts is one) drew with coloured fringes: SDL_image put libpng's
+ big-endian 48-bit pixels into a surface the machine reads
+ little-endian, swapping only the 64-bit kind, so every sample's low
+ byte was taken for its colour. The vendored loader swaps both now.
+
+- musicPlay answers the handle it was given, as Hypseus's does. Its
+ games keep that (sndHandle = musicPlay(music)) to stop the piece, and
+ three of them (Freddy, Starship Troopers, Sugar Rush) died on the map
+ screen with "musicStop: Argument 1 must be a number" because Singe's
+ answered nothing. musicStop takes a nil handle as none.
+
- A machine with no enumerable mouse keeps its pointer. ManyMouse reads
/dev/input/event* directly where there is no X server, and those nodes
are root:input, so an account that was never added to the "input" group
@@ -1151,13 +1229,40 @@ Fixes
clips the old video was cut from are gone, one of which nobody could
account for.
-- The menu has a sound. util/makeMenuSound.py writes it -- every noise in
- it is an oscillator, a burst of noise or an envelope over one of the two,
- in a room that is a convolution with a tail of decaying noise, so there is
- nothing in it that cannot be accounted for either. The charge, the rush
- of the logo arriving, the wingbeats (on the same curve the animation flaps
- to), the flame, and a riser as the grid comes up, over a bed whose four
- bars land exactly where the menu takes the screen.
+- util/renderMenuAd.py records the menu's intro with its sound as one
+ H.264 file at any size and rate (1920x1080 at 50 fps by default), for
+ use outside the engine; the backdrop's tagline is laid out in step with
+ the overlay it is drawn on, so a larger recording gets a larger tagline
+ rather than the menu's scaled up.
+
+- The service tools are dressed like the rest of the menu. Each tool was
+ a sheet of white text on black; it is now a card over the moving
+ backdrop in the game page's own colours -- the panel gradient, the
+ blue-grey border, the title in gold on the selection's gradient bar, a
+ chosen row on the same bar, and the key hints in a footer strip. The
+ list of tools says what the chosen tool is for in a column beside the
+ list rather than under it, and the two tools that draw their own
+ picture (Light Gun and Display) keep a strip of the card along the
+ bottom, out of the way of the targets. Both renderers draw it the same
+ way: the RmlUi document, and the plain overlay a machine without a GPU
+ falls back to. Rows line up: a tool no longer pads columns into its
+ text with spaces, which a proportional font never honoured; a pair is
+ a label column and a value column, and a list item carries its detail
+ (a size, a state, a field's value) against the right edge of the row.
+ util/shootMenu.py runs the menu headless through both renderers and
+ shoots every page into screenshots/menu, with a contact sheet each.
+
+- The menu has a sound. util/makeMenuSound.py writes it. The three things
+ the picture shows are recordings now: the charge is Mark DiAngelo's
+ "Explosion Ultra Bass", the logo bursting out of it Mike Koenig's "Large
+ Fireball", and the dragon Daniel Simon's "Dragon Fire Breath and Roar",
+ laid so the moment its roar turns to fire is the moment the animation
+ breathes -- three sounds from SoundBible under Creative Commons
+ Attribution 3.0, kept under assets/samples and credited in LICENSES.
+ The rest is still made in the script -- the wingbeats (on the same curve
+ the animation flaps to), a riser as the grid comes up, the logo leaving,
+ and a bed whose four bars land exactly where the menu takes the screen --
+ from oscillators and noise, in a room that is a convolution.
The charge and the flame are built the way the things themselves are
heard rather than the way they are drawn. Both were made of smooth
envelopes over steady noise -- the flame of a band of noise wobbled by two
diff --git a/LICENSES b/LICENSES
index edea0cbeb..c959b4288 100644
--- a/LICENSES
+++ b/LICENSES
@@ -69,6 +69,18 @@ Fonts
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
+Sounds
+------
+
+The menu's intro (assets/menuIntro.flac, written by util/makeMenuSound.py) is built on three
+recordings from SoundBible, each under the Creative Commons Attribution 3.0 licence
+(https://creativecommons.org/licenses/by/3.0/) and kept as FLAC under assets/samples:
+
+Explosion Ultra Bass Mark DiAngelo CC BY 3.0 https://soundbible.com/1807-Explosion-Ultra-Bass.html
+Large Fireball Mike Koenig CC BY 3.0 https://soundbible.com/1348-Large-Fireball.html
+Dragon Fire Breath and Roar Daniel Simon CC BY 3.0 https://soundbible.com/2127-Dragon-Fire-Breath-and-Roar.html
+
+
Data
----
diff --git a/assets/Backdrop.singe b/assets/Backdrop.singe
index 7c29b552e..cf2b7b567 100644
--- a/assets/Backdrop.singe
+++ b/assets/Backdrop.singe
@@ -135,6 +135,8 @@ local backdropTagX = 0
local backdropTagY = 0
local blown = false -- Whether the charge has gone off.
local gridLit = false -- Whether the grid has finished coming up and its colours are final.
+local gridShown = false -- Whether the grid, the horizon and the sun are drawn at all yet.
+local backdropParts = {} -- Those nodes: hidden through the intro, shown as the grid comes up.
local backdropFont = nil
-- ===== The backdrop ==========================================================================
@@ -425,12 +427,14 @@ local grid = nodeNew()
nodeSetMesh(grid, gridMesh(), gridMat)
nodeSetParent(grid, backdrop)
+backdropParts[#backdropParts + 1] = grid
local horizon = nodeNew()
nodeSetMesh(horizon, meshPlane(HORIZ_W * 2, HORIZ_H), horizMat)
nodeSetRotation(horizon, 90, 0, 0)
nodeSetPosition(horizon, 0, horizonY(HORIZ_Z), HORIZ_Z)
nodeSetParent(horizon, backdrop)
+backdropParts[#backdropParts + 1] = horizon
local sunCut = horizonY(SUN_Z) - SUN_Y
local backSun = nodeNew()
@@ -438,6 +442,7 @@ local backSun = nodeNew()
nodeSetMesh(backSun, sunMesh(sunCut), sunMat2)
nodeSetPosition(backSun, 0, SUN_Y, SUN_Z)
nodeSetParent(backSun, backdrop)
+backdropParts[#backdropParts + 1] = backSun
for n = 1, SUN_SLICES do
local slice = nodeNew()
@@ -448,6 +453,7 @@ for n = 1, SUN_SLICES do
nodeSetMesh(slice, meshPlane(w * 2, height), sliceMat)
nodeSetRotation(slice, 90, 0, 0)
nodeSetPosition(slice, 0, SUN_Y + y, SUN_Z + 0.4)
+ backdropParts[#backdropParts + 1] = slice
nodeSetParent(slice, backdrop)
end
@@ -483,6 +489,15 @@ function backdropFrame()
-- there is no moment where one thing has ended and the next has not begun.
local lit = span(t, GRID_UP, GRID_LIT)
+ -- Until then the grid, the horizon and the sun are not drawn at all. Black and unlit, they
+ -- were still there: a black line across the blast's glow is a line, and the whole grid showed
+ -- through the explosion as its outline.
+ if (lit > 0) and not gridShown then
+ gridShown = true
+ for _, part in ipairs(backdropParts) do
+ nodeSetVisible(part, true)
+ end
+ end
if (lit > 0) and not gridLit then
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
@@ -566,7 +581,10 @@ end
function backdropBegin(showIntro)
-- A font of its own. The document renderer draws everything else through RmlUi and never
-- selects one, so the tagline -- the only thing put straight into the overlay -- brings it.
- backdropFont = fontLoad("Singe/FreeSansBold.ttf", 30)
+ -- Thirty points on the menu's 480 rows, and in step with the overlay elsewhere: a recording
+ -- made at a larger size (util/renderMenuAd.py) lays the tagline out at that size rather than
+ -- scaling the menu's up.
+ backdropFont = fontLoad("Singe/FreeSansBold.ttf", math.floor(30 * overlayGetHeight() / 480 + 0.5))
fontSelect(backdropFont)
fontQuality(FONT_QUALITY_BLENDED)
@@ -582,6 +600,10 @@ function backdropBegin(showIntro)
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
blown = not showIntro
gridLit = false
+ gridShown = false
+ for _, part in ipairs(backdropParts) do
+ nodeSetVisible(part, false)
+ end
if not showIntro then
nodeSetVisible(logo, false)
end
diff --git a/assets/Forge/Author.singe b/assets/Forge/Author.singe
index c0468037a..c4c6b1327 100644
--- a/assets/Forge/Author.singe
+++ b/assets/Forge/Author.singe
@@ -123,6 +123,7 @@ 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.
@@ -351,8 +352,8 @@ 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.
-local function keyValue(name)
+-- 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
@@ -362,7 +363,7 @@ end
-- A switch named the same way.
-local function switchValue(name)
+function authorSwitchValue(name)
if type(name) == "string" then
return _G[name]
end
@@ -371,19 +372,20 @@ local function switchValue(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.
--- 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 (AUTHOR_DIR or "") ~= "" and not name:match("^[/\\]") and not name:match("^%a:") then
- local probe = io.open(AUTHOR_DIR .. name, "rb")
+-- 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 AUTHOR_DIR .. name
+ if probe ~= nil then
+ probe:close()
+ return folder .. name
+ end
+ end
end
end
@@ -391,6 +393,82 @@ function authorFile(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)
@@ -415,6 +493,17 @@ local function soundFor(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.
@@ -559,8 +648,8 @@ AUTHOR.looks.box = {
}
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.",
- params = { file = "file", frames = "number", anchor = "string", faces = "string" },
+ 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)
@@ -611,7 +700,12 @@ AUTHOR.looks.sprite = {
elseif (frames > 1) or (scale ~= 1) then
spriteDrawFrame(instance.sprite, x - w * scale / 2, top, frame, scale)
else
- spriteDraw(instance.sprite, x - w / 2, top)
+ 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
}
@@ -649,30 +743,56 @@ AUTHOR.looks.particles = {
}
AUTHOR.looks.text = {
- help = "A line of text, its top left at the instance. The var text is what it says.",
- params = { text = "string", r = "number", g = "number", b = "number" },
+ 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 = fontToSprite(instance.vars.text ~= "" and instance.vars.text or " ")
+ 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 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
}
@@ -1171,11 +1291,11 @@ AUTHOR.behaviours.keys = {
params = { player = "number", left = "scancode", right = "scancode", up = "scancode", down = "scancode", fire = "scancode" },
attach = function(instance, params)
instance.keys = {
- left = keyValue(params.left),
- right = keyValue(params.right),
- up = keyValue(params.up),
- down = keyValue(params.down),
- fire = keyValue(params.fire)
+ 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
@@ -1345,8 +1465,8 @@ AUTHOR.behaviours.spawner = {
}
AUTHOR.behaviours.sound = {
- help = "A clip per event: spawn, hit, death, or any event the instance sees. Values are file names. volume is the clips' own, 0 to 100, under the game's.",
- params = { spawn = "file", hit = "file", death = "file", pressed = "file", volume = "number" },
+ 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
@@ -1368,12 +1488,26 @@ AUTHOR.behaviours.gun = {
choices = { reload = { "offscreen" }, aim = { "centre" } },
attach = function(instance, params)
instance.gun = params
- instance.trigger = switchValue(params.trigger)
+ 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" },
@@ -1686,7 +1820,7 @@ AUTHOR.behaviours.turret = {
}
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. Raises branchTaken and branchMissed.",
+ 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
@@ -1704,13 +1838,28 @@ AUTHOR.behaviours.branching = {
if instance.branchAt ~= branch then
instance.branchAt = branch
instance.decided = false
- authorEvent("branchOpen", { self = instance, move = branch.move })
+ 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 = keyValue(branch.move)
- local switch = switchValue(branch.switch)
+ 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 (key and authorKeyPressed(key)) or (switch and pressedNow[switch]) then
+ 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)
@@ -1832,7 +1981,7 @@ AUTHOR.behaviours.hotspot = {
}
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.",
+ 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
@@ -1866,7 +2015,7 @@ AUTHOR.events.stopped = { help = "A rail camera reached a stop; event.name
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.", filter = {} }
+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" } }
@@ -2156,6 +2305,18 @@ AUTHOR.actions.discTo = {
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 = {},
@@ -3022,9 +3183,12 @@ function authorMidi(status, data1, data2)
end
-function authorMouseMoved(x, y, xr, yr)
+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
@@ -3848,7 +4012,9 @@ local function matches(rule, event)
if rule.each and (event.self ~= nil) and (event.self.type ~= rule.each) then
return false
end
- if rule.room and ((AUTHOR_ROOM == nil) or (AUTHOR_ROOM.name ~= rule.room)) then
+ -- 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
@@ -3859,6 +4025,17 @@ 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]
@@ -3866,8 +4043,11 @@ function authorEvent(name, event)
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 }
@@ -3929,12 +4109,15 @@ 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 (AUTHOR_ROOM and AUTHOR_ROOM.name == rule.room) then
+ 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, {})
@@ -4138,6 +4321,21 @@ 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
@@ -4315,6 +4513,16 @@ 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
@@ -4492,13 +4700,14 @@ local function updateDisc()
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) then
- runRule(rule, nil, nil, { frame = 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
@@ -4812,7 +5021,11 @@ function authorSwitchIsKey(what)
end
-function authorSwitchDown(what)
+-- 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
@@ -4822,7 +5035,7 @@ function authorSwitchDown(what)
return
end
for _, instance in ipairs(AUTHOR_LIVE) do
- if instance.alive and instance.gun and (what == (instance.trigger or MOUSE_LEFT)) then
+ 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
@@ -4838,16 +5051,8578 @@ function authorSwitchDown(what)
if AUTHOR_GAME.verbs and (what == MOUSE_LEFT) and verbPress(1) then
return
end
- local px, py = authorPointer(1)
+ local px, py = authorPointer(player or 1)
- authorEvent("pressed", { switch = what, x = px, y = py })
+ authorEvent("pressed", { switch = what, x = px, y = py, player = player })
end
-function authorSwitchUp(what)
+function authorSwitchUp(what, device)
if authorSwitchIsKey(what) then
return
end
AUTHOR_SWITCHES[what] = nil
- authorEvent("released", { switch = what })
+ 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//"), 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.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
+}
diff --git a/assets/Forge/AuthorCompile.singe b/assets/Forge/AuthorCompile.singe
index 4da9cea0e..3693d1d59 100644
--- a/assets/Forge/AuthorCompile.singe
+++ b/assets/Forge/AuthorCompile.singe
@@ -63,7 +63,9 @@ local FUNCTIONS = {
max = "math.max",
floor = "math.floor",
time = "authorTime",
- frame = "discGetFrame"
+ frame = "discGetFrame",
+ pointerX = "authorPointerX",
+ pointerY = "authorPointerY"
}
local NAMES = {
@@ -781,6 +783,21 @@ function authorCompile(game)
data[key] = value
end
end
+ -- A type's empty vars table says nothing the runtime does not assume; left out, so a
+ -- description that has one and one that has none compile the same.
+ if game.types then
+ data.types = {}
+ for name, entityType in pairs(game.types) do
+ local copy = {}
+
+ for key, value in pairs(entityType) do
+ if not ((key == "vars") and (type(value) == "table") and (next(value) == nil)) then
+ copy[key] = value
+ end
+ end
+ data.types[name] = copy
+ end
+ end
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
out[#out + 1] = "-- " .. (game.title or "Untitled")
out[#out + 1] = "--"
@@ -796,6 +813,17 @@ function authorCompile(game)
out[#out + 1] = 'local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\\\])") or "")'
out[#out + 1] = 'dofile(here .. "Author.singe")'
out[#out + 1] = "AUTHOR_DIR = here -- Files the description names are looked for here first."
+ -- A description built where it lives -- a port beside its game's own files, a test -- names
+ -- files beside itself, which is not where the built script sits; they are looked for there
+ -- second. A released game has them beside the script, and a packed one inside the pack.
+ local sourceDir = game.source and string.match(game.source, "^(.*[/\\])") or ""
+
+ if sourceDir ~= "" then
+ out[#out + 1] = "AUTHOR_SOURCE_DIR = " .. string.format("%q", sourceDir) .. " -- Then beside the description this was built from."
+ end
+ if game.vocabulary then
+ out[#out + 1] = "authorVocabulary(" .. string.format("%q", game.vocabulary) .. ") -- The game's own looks, behaviours, conditions, and actions."
+ end
out[#out + 1] = ""
out[#out + 1] = "-- The description, as the runtime wants it: everything but the rules, which follow as code."
out[#out + 1] = "authorBegin(" .. valueSource(data, 0) .. ")"
@@ -852,6 +880,10 @@ function authorBuild(descriptionFile, outputFile)
local file
game.source = descriptionFile
+ -- The game's own vocabulary, before the checker reads the manifest.
+ if game.vocabulary then
+ authorVocabulary(game.vocabulary, string.match(descriptionFile, "^(.*[/\\])") or "")
+ end
for _, problem in ipairs(authorCheck(game)) do
debugPrint("Author: " .. problem)
end
diff --git a/assets/Forge/Forge.singe b/assets/Forge/Forge.singe
index b505f468f..009c32ee7 100644
--- a/assets/Forge/Forge.singe
+++ b/assets/Forge/Forge.singe
@@ -102,7 +102,7 @@ local NEW_H = 40
local STRIP_H = 34 -- The timeline along the bottom of the canvas.
local STRIP_PAD = 8
local MODES = { "entities", "types", "rules", "tracks", "dialogues" }
-local FILE_KINDS = { png = true, jpg = true, jpeg = true, svg = true, glb = true, wav = true, ogg = true, mp3 = true, flac = true, mkv = true, mp4 = true, mpg = true, rml = true, hdr = true, ttf = true }
+local FILE_KINDS = { png = true, jpg = true, jpeg = true, svg = true, glb = true, wav = true, ogg = true, mp3 = true, flac = true, mkv = true, mp4 = true, mpg = true, rml = true, hdr = true, ttf = true, singe = true }
local FILE_DEPTH = 2 -- How far under the game's directory the file picker looks.
local FILE_NONE = "(none)" -- The file picker's first entry: no file.
local FRAME_STEP = 1
@@ -1024,6 +1024,44 @@ end
-- The disc layer's file, if the description has one, loaded and paused so the timeline can show
-- the frame under the cursor.
+-- A frame file names several videos, each from a disc frame: the one with the longest run of
+-- frames is the game's own, and is what the editor scrubs, its first frame remembered so the
+-- cursor's disc frame lands on the right picture. Answers the video's path and its first frame.
+local function frameFileVideo(path)
+ local folder = path:match("^(.*[/\\])") or ""
+ local entries = {}
+ local best = nil
+
+ for line in io.lines(path) do
+ local start, name = line:match("^%s*(%d+)%s+(.-)%s*$")
+
+ if start then
+ entries[#entries + 1] = { start = tonumber(start), name = name }
+ elseif (#entries == 0) and (line:match("%S")) and not line:match("^%s*%d") then
+ -- The first line is the directory the videos are in; its own is the usual.
+ local where = line:gsub("%s+$", ""):gsub("\\", "/")
+
+ if (where ~= ".") and (where ~= "./") then
+ folder = folder .. where:gsub("/?$", "/")
+ end
+ end
+ end
+ for index, entry in ipairs(entries) do
+ local following = entries[index + 1]
+
+ entry.span = following and (following.start - entry.start) or 0
+ if (best == nil) or (entry.span > best.span) then
+ best = entry
+ end
+ end
+ if best == nil then
+ return nil, 0
+ end
+
+ return folder .. best.name, best.start
+end
+
+
local function loadVideo()
local file
@@ -1037,11 +1075,19 @@ local function loadVideo()
videoUnload(FORGE.video)
FORGE.video = nil
end
- FORGE.videoFile = file
+ FORGE.videoFile = file
+ FORGE.videoStart = 0
if file and (lfs.attributes(forgeFile(file), "mode") == "file") then
- FORGE.video = videoLoad(forgeFile(file))
- videoPause(FORGE.video)
- videoSeek(FORGE.video, FORGE.cursor)
+ local path = forgeFile(file)
+
+ if path:lower():match("%.txt$") then
+ path, FORGE.videoStart = frameFileVideo(path)
+ end
+ if path and (lfs.attributes(path, "mode") == "file") then
+ FORGE.video = videoLoad(path)
+ videoPause(FORGE.video)
+ videoSeek(FORGE.video, math.max(0, FORGE.cursor - FORGE.videoStart))
+ end
end
end
end
@@ -1053,6 +1099,11 @@ function forgeBegin(path)
if FORGE.game == nil then
return false
end
+ -- The game's own vocabulary, in place of the last game's, before the panels read the manifest.
+ authorVocabularyForget()
+ if FORGE.game.vocabulary then
+ authorVocabulary(FORGE.game.vocabulary, string.match(path, "^(.*[/\\])") or "")
+ end
FORGE.game.types = FORGE.game.types or {}
FORGE.game.rules = FORGE.game.rules or {}
FORGE.game.layers = FORGE.game.layers or {}
@@ -1117,6 +1168,7 @@ function forgeClose()
if FORGE.gui ~= nil then
guiDelete(FORGE.gui)
end
+ authorVocabularyForget()
for _, cache in ipairs({ FORGE.sprites, FORGE.sheets }) do
for _, sprite in pairs(cache) do
if sprite then
@@ -1239,7 +1291,7 @@ function forgeExport(folder, name)
end
-- The description travels with the game, so the release can be opened and edited again.
- authorSave(FORGE.game, folder .. "/" .. script .. ".game")
+ authorSave(FORGE.game, folder .. "/" .. script .. ".forge")
-- A games.dat so the menu lists it, written from what the description already knows. Names
-- in it are as the engine sees them from the games directory, the folder's own name first,
@@ -1304,6 +1356,7 @@ function forgeFilesNamed(game)
end
end
+ take(game.vocabulary)
for _, layer in ipairs(game.layers or {}) do
fromParams(layer, (AUTHOR.layers[layer.kind] or {}).params)
end
@@ -3323,7 +3376,7 @@ end
function forgeCursorTo(at)
FORGE.cursor = math.max(0, math.floor(at))
if FORGE.video then
- videoSeek(FORGE.video, FORGE.cursor)
+ videoSeek(FORGE.video, math.max(0, FORGE.cursor - (FORGE.videoStart or 0)))
end
forgeRefresh()
end
@@ -3578,6 +3631,8 @@ function forgeFieldKind(name)
param = key
elseif (name == "reset") or (name == "depthSort") then
return "flag"
+ elseif name == "vocabulary" then
+ return "file"
end
elseif FORGE.mode == "tracks" then
local track = forgeRoom().tracks[FORGE.track]
@@ -4220,7 +4275,7 @@ end
-- The fields with nothing selected: the game's -- its title, players, verbs, and layers with
-- what each layer takes, named layer.parameter as a behaviour's are -- and then the room's.
function forgeRoomFieldNames()
- local names = { "title", "players", "vars", "verbs", "layers" }
+ local names = { "title", "players", "vars", "verbs", "layers", "vocabulary" }
for _, layer in ipairs(FORGE.game.layers or {}) do
local entry = AUTHOR.layers[layer.kind]
@@ -4263,6 +4318,8 @@ function forgeRoomGet(room, key)
return table.concat(FORGE.game.verbs or {}, ", ")
elseif key == "layers" then
return listText(FORGE.game.layers)
+ elseif key == "vocabulary" then
+ return FORGE.game.vocabulary
elseif group ~= nil then
local layer = layerOf(group)
@@ -4301,6 +4358,13 @@ function forgeRoomSet(room, key, value)
if key == "title" then
FORGE.game.title = tostring(value)
+ elseif key == "vocabulary" then
+ -- The game's own vocabulary, loaded as it is named so the panels offer what it adds.
+ FORGE.game.vocabulary = ((value ~= nil) and (value ~= "")) and tostring(value) or nil
+ authorVocabularyForget()
+ if FORGE.game.vocabulary then
+ authorVocabulary(FORGE.game.vocabulary, string.match(FORGE.path or "", "^(.*[/\\])") or "")
+ end
elseif key == "players" then
if type(value) == "number" then
FORGE.game.players = (value ~= 1) and math.floor(value) or nil
@@ -4435,7 +4499,11 @@ function forgeEditCancel()
if FORGE.editing == nil then
return false
end
- FORGE.editing.set(FORGE.editing.key, FORGE.editing.was)
+ -- Only a value the typing changed is put back: a field passed over is left exactly as it
+ -- was, not written again (writing "" to a type's vars used to make it an empty table).
+ if FORGE.editing.changed then
+ FORGE.editing.set(FORGE.editing.key, FORGE.editing.was)
+ end
FORGE.editing = nil
FORGE.lastField = nil -- The next ENTER starts the form over rather than moving on.
FORGE.message = "unchanged"
@@ -4602,9 +4670,10 @@ end
-- ===== Files ==================================================================================
--
--- Descriptions live in Forge's data directory, which is the one place a packed Forge can write.
--- Ones dropped into Forge's own directory are offered too, and opened as a copy in the data
--- directory, since inside a .game they are read only.
+-- Descriptions are .forge files -- Lua text; .game is a packed release, which the engine's menu
+-- lists and Forge does not. They live in Forge's data directory, which is the one place a packed
+-- Forge can write. Ones dropped into Forge's own directory are offered too, and opened as a copy
+-- in the data directory, since inside a .game they are read only.
-- A game to start from: ground, a hero that runs and jumps, and a readout. Enough to press play
-- on, which is the point; everything in it can be changed or deleted.
@@ -4637,22 +4706,7 @@ function forgeTemplate()
end
--- Whether a .game file is a packed game rather than a description: the database's own header.
-local function packedGame(path)
- local file = io.open(path, "rb")
- local head
-
- if file == nil then
- return false
- end
- head = file:read(15)
- file:close()
-
- return head == "SQLite format 3"
-end
-
-
--- Every .game description in a directory, sorted. A directory that is not there lists nothing.
+-- Every .forge description in a directory, sorted. A directory that is not there lists nothing.
local function descriptionsIn(directory)
local found = {}
@@ -4660,10 +4714,8 @@ local function descriptionsIn(directory)
return found
end
for name in lfs.dir(directory) do
- -- A description is Lua text; a packed game is a database with the same extension, and one
- -- released beside the descriptions is not offered as one.
- if (string.sub(name, -5) == ".game") and not packedGame(directory .. name) then
- found[#found + 1] = { name = string.sub(name, 1, -6), path = directory .. name }
+ if string.sub(name, -6) == ".forge" then
+ found[#found + 1] = { name = string.sub(name, 1, -7), path = directory .. name }
end
end
table.sort(found, function(a, b) return a.name < b.name end)
@@ -4696,14 +4748,14 @@ local function freshPath(stem)
local base = singeGetDataPath() .. stem
local n = 1
- if lfs.attributes(base .. ".game") == nil then
- return base .. ".game"
+ if lfs.attributes(base .. ".forge") == nil then
+ return base .. ".forge"
end
- while lfs.attributes(base .. n .. ".game") ~= nil do
+ while lfs.attributes(base .. n .. ".forge") ~= nil do
n = n + 1
end
- return base .. n .. ".game"
+ return base .. n .. ".forge"
end
diff --git a/assets/Forge/tutorials/01-tenMinutes.game b/assets/Forge/tutorials/01-tenMinutes.forge
similarity index 100%
rename from assets/Forge/tutorials/01-tenMinutes.game
rename to assets/Forge/tutorials/01-tenMinutes.forge
diff --git a/assets/Forge/tutorials/02-platformer.game b/assets/Forge/tutorials/02-platformer.forge
similarity index 100%
rename from assets/Forge/tutorials/02-platformer.game
rename to assets/Forge/tutorials/02-platformer.forge
diff --git a/assets/Forge/tutorials/03-sprites.game b/assets/Forge/tutorials/03-sprites.forge
similarity index 100%
rename from assets/Forge/tutorials/03-sprites.game
rename to assets/Forge/tutorials/03-sprites.forge
diff --git a/assets/Forge/tutorials/04-shmup.game b/assets/Forge/tutorials/04-shmup.forge
similarity index 100%
rename from assets/Forge/tutorials/04-shmup.game
rename to assets/Forge/tutorials/04-shmup.forge
diff --git a/assets/Forge/tutorials/05-qte.game b/assets/Forge/tutorials/05-qte.forge
similarity index 100%
rename from assets/Forge/tutorials/05-qte.game
rename to assets/Forge/tutorials/05-qte.forge
diff --git a/assets/Forge/tutorials/06-gun.game b/assets/Forge/tutorials/06-gun.forge
similarity index 100%
rename from assets/Forge/tutorials/06-gun.game
rename to assets/Forge/tutorials/06-gun.forge
diff --git a/assets/Forge/tutorials/07-adventure.game b/assets/Forge/tutorials/07-adventure.forge
similarity index 100%
rename from assets/Forge/tutorials/07-adventure.game
rename to assets/Forge/tutorials/07-adventure.forge
diff --git a/assets/Forge/tutorials/08-scene.game b/assets/Forge/tutorials/08-scene.forge
similarity index 100%
rename from assets/Forge/tutorials/08-scene.game
rename to assets/Forge/tutorials/08-scene.forge
diff --git a/assets/Forge/tutorials/09-rail.game b/assets/Forge/tutorials/09-rail.forge
similarity index 99%
rename from assets/Forge/tutorials/09-rail.game
rename to assets/Forge/tutorials/09-rail.forge
index 0a33baf7f..feab26fb1 100644
--- a/assets/Forge/tutorials/09-rail.game
+++ b/assets/Forge/tutorials/09-rail.forge
@@ -1,5 +1,167 @@
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
return {
+ layers = {
+ { fov = 60, kind = "scene3d" },
+ { kind = "overlay" }
+ },
+ rooms = {
+ {
+ entities = {
+ { id = "readout", type = "readout", x = 12, y = 12 },
+ { id = "floor", type = "floor", x = 0.0, y = -0.1, z = -20.0 },
+ { id = "painting", type = "painting", x = 0.0, y = 6.0, z = -30.0 },
+ { id = "pillar", type = "pillar", x = 0.0, y = 4.0, z = -22.0 },
+ { id = "camera", type = "camera", x = 0.0, y = 1.6, z = 0.0 },
+ { id = "gun", type = "gun", x = 0.0, y = 0.0, z = 0.0 }
+ },
+ name = "start",
+ tracks = {
+ {
+ key = "time",
+ name = "rail",
+ points = {
+ {
+ at = 0,
+ look = { 0, 1.2, -10 },
+ x = 0,
+ y = 1.6,
+ z = 0
+ },
+ {
+ at = 4,
+ look = { 0, 1.2, -16 },
+ stop = "gate",
+ x = 0,
+ y = 1.6,
+ z = -6
+ },
+ {
+ at = 10,
+ look = { 2, 1.2, -24 },
+ stop = "pillar",
+ x = 2,
+ y = 1.6,
+ z = -14
+ },
+ {
+ at = 14,
+ look = { 2, 1.2, -28 },
+ x = 2,
+ y = 1.6,
+ z = -18
+ }
+ }
+ }
+ }
+ }
+ },
+ rules = {
+ {
+ act = {
+ { "setText", entity = "readout", text = "\"score \" .. score .. \" health \" .. health .. \" ammo \" .. gun.ammo" }
+ },
+ note = "The readout, every frame",
+ on = "frame"
+ },
+ {
+ act = {
+ { "spawn", type = "zombie", x = -2, y = 0, z = -14 },
+ { "spawn", type = "zombie", x = 2, y = 0, z = -15 }
+ },
+ name = "gate",
+ note = "The gate: two come out of the dark",
+ on = "stopped",
+ when = {}
+ },
+ {
+ act = {
+ { "spawn", type = "zombie", x = -1, y = 0, z = -23 },
+ { "spawn", type = "zombie", x = 0, y = 0, z = -26 },
+ { "spawn", type = "zombie", x = 4, y = 0, z = -24 }
+ },
+ name = "pillar",
+ note = "The pillar: three more, one from behind it",
+ on = "stopped",
+ when = {}
+ },
+ {
+ act = {
+ { "setState", entity = "self", state = "walk" }
+ },
+ note = "A zombie walks the moment it is made",
+ on = "spawn",
+ type = "zombie",
+ when = {}
+ },
+ {
+ act = {
+ { "damage", amount = 1, entity = "self" },
+ { "addScore", amount = 100 },
+ { "playSound", file = "Forge/kit/hit.wav" }
+ },
+ note = "A shot lands",
+ on = "hit",
+ type = "zombie",
+ when = {}
+ },
+ {
+ act = {
+ { "setState", entity = "self", state = "attack" },
+ { "timerStart", after = 1.5, entity = "self", name = "bite" }
+ },
+ note = "It reaches the camera and rears up",
+ on = "arrived",
+ type = "zombie",
+ when = {}
+ },
+ {
+ act = {
+ { "addVar", amount = -1, name = "health" },
+ { "flash", b = 40, g = 40, r = 255, seconds = 0.3 },
+ { "shake", amount = 0.3, seconds = 0.4 },
+ { "timerStart", after = 1.5, entity = "self", name = "bite" }
+ },
+ name = "bite",
+ note = "The bite lands unless it was killed in time",
+ on = "timer",
+ type = "zombie",
+ when = {
+ { "inState", entity = "self", state = "attack" }
+ }
+ },
+ {
+ act = {
+ { "pathNext", entity = "camera" }
+ },
+ note = "The stop is clear: move on",
+ on = "frame",
+ when = {
+ { "waiting", entity = "camera" },
+ { "test", expr = "count(\"zombie\") == 0" }
+ }
+ },
+ {
+ act = {
+ { "say", seconds = 2, text = "\"YOU ARE DEAD\"" },
+ { "gameOver" }
+ },
+ note = "Out of health",
+ on = "frame",
+ when = {
+ { "test", expr = "health <= 0" },
+ { "once", scope = "game", tag = "over" }
+ }
+ },
+ {
+ act = {
+ { "say", seconds = 2, text = "\"STAGE CLEAR\"" }
+ },
+ note = "The end of the rail",
+ on = "railEnd",
+ when = {}
+ }
+ },
+ title = "The Dead Yard",
types = {
camera = {
behaviours = {
@@ -47,169 +209,5 @@ return {
vars = { health = 2 }
}
},
- layers = {
- { fov = 60, kind = "scene3d" },
- { kind = "overlay" }
- },
- rooms = {
- {
- entities = {
- { id = "readout", type = "readout", x = 12, y = 12 },
- { id = "floor", type = "floor", x = 0.0, y = -0.1, z = -20.0 },
- { id = "painting", type = "painting", x = 0.0, y = 6.0, z = -30.0 },
- { id = "pillar", type = "pillar", x = 0.0, y = 4.0, z = -22.0 },
- { id = "camera", type = "camera", x = 0.0, y = 1.6, z = 0.0 },
- { id = "gun", type = "gun", x = 0.0, y = 0.0, z = 0.0 }
- },
- name = "start",
- tracks = {
- {
- key = "time",
- name = "rail",
- points = {
- {
- at = 0,
- look = { 0, 1.2, -10 },
- stop = "nil",
- x = 0,
- y = 1.6,
- z = 0
- },
- {
- at = 4,
- look = { 0, 1.2, -16 },
- stop = "gate",
- x = 0,
- y = 1.6,
- z = -6
- },
- {
- at = 10,
- look = { 2, 1.2, -24 },
- stop = "pillar",
- x = 2,
- y = 1.6,
- z = -14
- },
- {
- at = 14,
- look = { 2, 1.2, -28 },
- stop = "nil",
- x = 2,
- y = 1.6,
- z = -18
- }
- }
- }
- }
- }
- },
- rules = {
- {
- act = {
- { "setText", entity = "readout", text = "\"score \" .. score .. \" health \" .. health .. \" ammo \" .. gun.ammo" }
- },
- note = "The readout, every frame",
- on = "frame"
- },
- {
- act = {
- { "spawn", type = "zombie", x = -2, y = 0, z = -14 },
- { "spawn", type = "zombie", x = 2, y = 0, z = -15 }
- },
- name = "gate",
- note = "The gate: two come out of the dark",
- on = "stopped",
- when = {}
- },
- {
- act = {
- { "spawn", type = "zombie", x = -1, y = 0, z = -23 },
- { "spawn", type = "zombie", x = 0, y = 0, z = -26 },
- { "spawn", type = "zombie", x = 4, y = 0, z = -24 }
- },
- name = "pillar",
- note = "The pillar: three more, one from behind it",
- on = "stopped",
- when = {}
- },
- {
- act = {
- { "setState", entity = "self", state = "walk" }
- },
- type = "zombie",
- note = "A zombie walks the moment it is made",
- on = "spawn",
- when = {}
- },
- {
- act = {
- { "damage", amount = 1, entity = "self" },
- { "addScore", amount = 100 },
- { "playSound", file = "Forge/kit/hit.wav" }
- },
- type = "zombie",
- note = "A shot lands",
- on = "hit",
- when = {}
- },
- {
- act = {
- { "setState", entity = "self", state = "attack" },
- { "timerStart", after = 1.5, entity = "self", name = "bite" }
- },
- type = "zombie",
- note = "It reaches the camera and rears up",
- on = "arrived",
- when = {}
- },
- {
- act = {
- { "addVar", amount = -1, name = "health" },
- { "flash", b = 40, g = 40, r = 255, seconds = 0.3 },
- { "shake", amount = 0.3, seconds = 0.4 },
- { "timerStart", after = 1.5, entity = "self", name = "bite" }
- },
- type = "zombie",
- name = "bite",
- note = "The bite lands unless it was killed in time",
- on = "timer",
- when = {
- { "inState", entity = "self", state = "attack" }
- }
- },
- {
- act = {
- { "pathNext", entity = "camera" }
- },
- note = "The stop is clear: move on",
- on = "frame",
- when = {
- { "waiting", entity = "camera" },
- { "test", expr = "count(\"zombie\") == 0" }
- }
- },
- {
- act = {
- { "say", seconds = 2, text = "\"YOU ARE DEAD\"" },
- { "gameOver" }
- },
- note = "Out of health",
- on = "frame",
- when = {
- { "test", expr = "health <= 0" },
- { "once", scope = "game", tag = "over" }
- }
- },
- {
- act = {
- { "say", seconds = 2, text = "\"STAGE CLEAR\"" }
- },
- note = "The end of the rail",
- on = "railEnd",
- when = {}
- }
- },
- title = "The Dead Yard",
vars = { health = 3, score = 0 }
}
diff --git a/assets/Forge/tutorials/10-release.game b/assets/Forge/tutorials/10-release.forge
similarity index 100%
rename from assets/Forge/tutorials/10-release.game
rename to assets/Forge/tutorials/10-release.forge
diff --git a/assets/Menu.rml b/assets/Menu.rml
index 2c811eb97..7bf84d350 100644
--- a/assets/Menu.rml
+++ b/assets/Menu.rml
@@ -32,10 +32,18 @@
+ rows and Singe/MenuDocument.singe turns them into the divs below it -- the rows into the
+ body, the key hints into the footer, and on the list of tools the chosen tool's note into
+ the column beside the list. -->