-- An American Laser Games title as it ships, driven by algDrive.singe and traced. PORT_GAME
-- names the title's folder under the library and PORT_SCRIPT its script under it (the Hypseus
-- layout: singe/
/.singe); run with -v the title's framefile.
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/hypseus")
local game = os.getenv("PORT_GAME") or "maddog-hd"
local script = library .. "/" .. game .. "/" .. (os.getenv("PORT_SCRIPT") or ("singe/" .. game .. "/" .. game .. ".singe"))
dofile(here .. "algDrive.singe")
SINGE_LEGACY_SPRITE_ARGS = true
local realTime = os.time
os.time = function(...) return 12345 end
local realScriptPath = singeGetScriptPath
singeGetScriptPath = function() return script end
local realSkip = discSkipToFrame
discSkipToFrame = function(frame)
algTraceSeek(frame)
return realSkip(frame)
end
local realSearch = discSearch
discSearch = function(frame)
algTraceSeek(frame)
return realSearch(frame)
end
-- One mouse: a single gun joins by START, as the game does with one mouse.
local realMice = mouseHowMany
mouseHowMany = function() return 1 end
dofile(script)
singeGetScriptPath = realScriptPath
local realUpdate = onOverlayUpdate
-- PORT_HOOK=1 names the line the script is on every ten million instructions: where a run
-- that never ends is spinning.
if os.getenv("PORT_HOOK") == "1" then
debug.sethook(function()
local where = debug.getinfo(2, "Sl")
debugPrint("HOOK " .. tostring(where and where.short_src) .. ":" .. tostring(where and where.currentline))
end, "", 10000000)
end
function algPress(switch, device, down)
if down then
onInputPressed(switch, device)
else
onInputReleased(switch, device)
end
end
-- A key released, as the typing edition reads it (its keysym, in MODE_FULL).
function algKey(keysym)
onInputReleased(keysym)
end
-- The pointer as the game's onMouseMoved computes it: a raw position that lands on the point
-- asked for, in the game's own coordinates.
function algPointer(device, x, y)
local rx = (x + (ratioxOffset or 0)) / ((ratiox or 1) * (gunscale or 1))
local ry = (y + (ratioyOffset or 0)) / ((ratioy or 1) * (gunscale or 1))
onMouseMoved(math.floor(rx), math.floor(ry), 0, 0, device)
end
-- The hitmap's coordinates scaled as the game scales them, where it does (the multiplayer
-- editions); the one-player editions aim in the hitmap's own.
local function hmx(v)
if ResHMx then
return ResHMx(v)
end
return v
end
local function hmy(v)
if ResHMy then
return ResHMy(v)
end
return v
end
-- The hitboxes of a frame in the current move's hitmap range, in pointer coordinates.
local function boxesAt(rel)
local boxes = {}
for k = (move[currentMove][hitmapStart or hitboxStart] or 1), (hitmapTotal or 0) do
local row = hitmap[k]
if row and (row[hitmapFrame] == rel) and (row[hitmapCount] > 0) then
for j = row[hitmapIndex], row[hitmapIndex] + row[hitmapCount] - 1 do
local b = hitbox[j]
-- (A box given back to front -- Platoon's hitbox lines are, often -- can never be
-- hit by the game's own test, so it is no aim.)
if b and (b[bbx1] <= b[bbx2]) and (b[bby1] <= b[bby2]) then
boxes[#boxes + 1] = { hmx(b[bbx1]), hmy(b[bby1]), hmx(b[bbx2]), hmy(b[bby2]) }
end
end
break
end
end
return boxes
end
local function firstBox()
-- The first hitbox of this frame, aimed at a point that also lies inside a box of the next
-- frame when the two overlap: the press the drive makes now is judged on this frame or the
-- next, as the clock falls. Without an overlap (a fast target) the box of this frame stands.
if (move == nil) or (move[currentMove] == nil) or (hitmap == nil) then
return nil
end
local rel = currentFrame - (thisOffset or 0)
local now = boxesAt(rel)
local b = now[1]
if b == nil then
return nil
end
for _, n in ipairs(boxesAt(rel + 1)) do
local x1, y1, x2, y2 = math.max(b[1], n[1]), math.max(b[2], n[2]), math.min(b[3], n[3]), math.min(b[4], n[4])
if (x1 <= x2) and (y1 <= y2) then
return { x1, y1, x2, y2 }
end
end
return b
end
local function view()
local window = nil
-- A level of Mad Dog's (101 to 121) or of Mad Dog II's (its trails, and levelStart); Mad Dog II's
-- move rows have no shoot window of their own, so the move's frames stand in.
local inLevel = (currentLevel ~= nil) and (((frameShootStart ~= nil) and (currentLevel >= 101) and (currentLevel <= 121)) or ((frameShootStart == nil) and (((currentLevel >= 1) and (currentLevel <= 30)) or (currentLevel == levelStart))))
if ((practiceHit ~= nil) or (fRndPopOrder ~= nil)) and (currentLevel == levelPractice) then
inLevel = false
end
if (levelSigns ~= nil) and (currentLevel == levelSigns) then
inLevel = false
end
if (levelTieIntro ~= nil) and ((currentLevel == levelTie) or (currentLevel == levelTieIntro)) then
inLevel = false
end
-- Space Pirates's puzzles and crystals are targets, not windows: the band and Ursula's colour
-- at the doors, Fellina's skull and arrow, the sockets of the crystal build.
if (levelCrystalBuild ~= nil) and ((currentLevel == levelCrystalBuild) or ((currentLevel == levelDoor2) and (lvlState == lvlPlayClip4)) or ((currentLevel == levelDoor3) and (lvlState == lvlPlayClip3)) or ((currentLevel == levelFellina1) and ((lvlState == lvlPlayPuzzle1) or (lvlState == lvlPlayPuzzle2)))) then
inLevel = false
end
-- Who Shot Johnny Rock's store, clue items, and safe are targets, not windows.
if (GetMapArray01 ~= nil) and ((currentLevel == levelStore) or (currentLevel == levelMansion2) or (currentLevel == levelMansion4)) then
inLevel = false
end
-- Space Pirates's Reaper wants exactly as many hits as he laughed: once the drive has them,
-- the moves left are let pass.
if (levelReaper2 ~= nil) and (currentLevel == levelReaper2) and (currentMove > 1) and (iReaperHits >= iReaperLaughs) then
inLevel = false
end
-- The typing edition of Mad Dog II plays on the keyboard: the key the game waits for now (the
-- next letter of the word, a sprinkled letter, the return key at a menu), and no gun at all.
local typing = nil
if validKey ~= nil then
inLevel = false
local function nextOf(word)
if word and (iKeyIndex or 1) <= string.len(word) then
return string.byte(word, iKeyIndex or 1)
end
return nil
end
-- (The game starts with the key slot at zero, not at none, and never clears it.)
local waiting = (iKey == SWITCH_NONE) or (iKey == 0)
if not waiting then
typing = nil
elseif (currentLevel == levelTitle) and (lvlState == lvlRunning) then
typing = SWITCH_RETURN
elseif (currentLevel == levelMenu) and (lvlState == lvlRunning) then
typing = SWITCH_RETURN
elseif (currentLevel == levelHighScore) and (lvlState == lvlRunning) then
-- (Once, before the name's frames run out; the return key seeks to the entry's end.)
if discGetFrame() < offsetMenus + 3275 then
typing = SWITCH_RETURN
end
elseif bShowSprinkles and sprinkles then
for k = 1, (iSprinkleMax or 0) do
if sprinkles[k] and (sprinkles[k][4] == true) then
typing = string.byte(sprinkles[k][3])
break
end
end
elseif (currentLevel == levelItem1) and (lvlState == lvlPlayClip3) then
typing = nextOf(sMagicWord)
elseif move and move[currentMove] and ((lvlState == lvlPauseAction) or ((lvlState == lvlRunning) and (discGetFrame() >= move[currentMove][moveFrameStart]) and (discGetFrame() < move[currentMove][moveFrameEnd]))) then
typing = nextOf(move[currentMove][(currentLevel == levelItem2) and moveType or wordval])
end
end
-- Platoon's patrols open with a clip the first press of the trigger jumps past, whatever the
-- state: no window until that press is in, and a target anywhere to make it.
local skipping = (levelCharlie ~= nil) and bOnce and (currentLevel ~= nil) and (currentLevel >= 1) and (currentLevel <= 6) and (lvlState == lvlRunning)
if skipping then
inLevel = false
end
-- A good guy's move (the Crime Patrol family marks the row) is no window: shooting him is a
-- death, and the drive would take it at every level that has one.
if inLevel and move and move[currentMove] and (rndMoveType ~= nil) and (GOODGUY ~= nil) and (move[currentMove][rndMoveType] == GOODGUY) then
inLevel = false
end
if inLevel and move and move[currentMove] then
window = { move[currentMove][frameShootStart or moveFrameStart], move[currentMove][frameShootEnd or moveFrameEnd] }
end
local quadrants = nil
if (currentLevel == levelTownSelect) and stage then
quadrants = {}
local w, h = resX or 360, resY or 240
-- In the order the game itself takes when the select times out: the saloon before the
-- sheriff, whose level needs the key from it.
if not stage[STAGE_CORRAL] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.25 } end
if not stage[STAGE_SALOON] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.25 } end
if not stage[STAGE_SHERIFF] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.75 } end
if not stage[STAGE_BANK] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.75 } end
end
-- The signpost's sign and the mine's lantern and item, as a list of points to shoot in
-- order (the middle line of each, scaled as the game scales them).
local targets = nil
local targetStep = nil
local function lineMiddle(rows, count)
local row = rows and rows[math.floor((count + 1) / 2)]
if row then
return { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy(row[lineY]) }
end
return nil
end
if skipping then
targets = { { 180, 120 } }
targetStep = lvlRunning
elseif (currentLevel == levelSignSelect) and sign then
targets = {}
if stage[STAGE_FUSE] == false then
targets[1] = lineMiddle(sign[SIGN_PROSPECTOR], 48)
elseif stage[STAGE_MINE] == false then
targets[1] = lineMiddle(sign[SIGN_MINE], 28)
else
targets[1] = lineMiddle(sign[SIGN_HIDEOUT], 58)
end
elseif (currentLevel == levelMine) and mine then
targets = {}
local counts = { [MINE_SIGN] = 21, [MINE_LANTERN] = 20, [MINE_PAN] = 12, [MINE_PITCHFORK] = 23, [MINE_SKULL] = 25 }
if not bLanternHit then targets[#targets + 1] = lineMiddle(mine[MINE_LANTERN], counts[MINE_LANTERN]) end
if not bItemHit then targets[#targets + 1] = lineMiddle(mine[iWhatToHit], counts[iWhatToHit]) end
elseif (currentLevel == levelMaze) and maze and maze[curMazePos] and (maze[curMazePos][mazeType] > 0) and sign then
-- A maze sign: the sign for the way to go.
local frame, dir = maze[curMazePos][signType], maze[curMazePos][signDirection]
local counts = { [32] = { 51, 49 }, [175] = { 59, 17 }, [340] = { 67, 90 }, [500] = { 24, 42 }, [630] = { 11, 30 }, [446] = { 59, 64 } }
local pair = counts[frame]
if pair and sign[dir] then
targets = { lineMiddle(sign[dir], pair[dir]) }
end
elseif (levelItem1 ~= nil) and (ResHMx ~= nil) and (currentLevel == levelItem1) and (lvlState == lvlPlayClip3) then
-- Mad Dog II's first item level: the item the guide asked for, from the game's own
-- rectangles.
local boxes = { [ITEM_LANTERN] = { 57, 66, 117, 129 }, [ITEM_AMMOBOX] = { 120, 148, 111, 132 }, [ITEM_MOONSHINE] = { 260, 272, 60, 72 } }
local b = boxes[iThisItem]
if b then
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
targetStep = lvlPlayClip3
end
elseif (levelProspector1 ~= nil) and (ResHMx ~= nil) and (currentLevel == levelProspector1) and (lvlState == lvlPlayClip4) then
-- Mad Dog II's prospector: the skull he chose, from the game's own rectangles.
local boxes = { { 305, 315, 59, 76 }, { 68, 75, 73, 82 }, { 147, 154, 58, 67 }, { 119, 127, 101, 115 } }
local b = boxes[iSkull] or boxes[4]
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
targetStep = lvlPlayClip4
elseif (panelHit ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and area then
-- Crime Patrol's mission menus: the first panel whose stage is open, by the middle line
-- of its area (the count of lines is the game's own).
local MENUS = {}
if MENU_SIERRA ~= nil then
MENUS[MENU_SIERRA] = { { PANEL_PRACTICE, STAGE_PRACTICE, 99 }, { PANEL_BAR, STAGE_BAR, 99 }, { PANEL_CARCHASE, STAGE_CARCHASE, 99 }, { PANEL_HIDEOUT, STAGE_HIDEOUT, 99 } }
MENUS[MENU_CHICAGO] = { { PANEL_HELI, STAGE_HELI, 111 }, { PANEL_COURTROOM, STAGE_COURTROOM, 111 }, { PANEL_BUSCHASE, STAGE_BUSCHASE, 111 } }
MENUS[MENU_BORDER] = { { PANEL_SHIPYARD, STAGE_SHIPYARD, 111 }, { PANEL_BOATHOUSE, STAGE_BOATHOUSE, 111 }, { PANEL_BEACH, STAGE_BEACH, 111 } }
MENUS[MENU_SA] = { { PANEL_BASE, STAGE_BASE, 111 }, { PANEL_NIGHTVISION, STAGE_NIGHTVISION, 111 }, { PANEL_TOWN, STAGE_TOWN, 111 } }
end
if MENU_ROOKIE ~= nil then
MENUS = { [MENU_ROOKIE] = { { PANEL_PRACTICE, STAGE_PRACTICE, 98 }, { PANEL_STORE, STAGE_STORE, 99 }, { PANEL_GANG, STAGE_GANG, 99 }, { PANEL_WAREHOUSE, STAGE_WAREHOUSE, 97 } },
[MENU_UNDERCOVER] = { { PANEL_AIRPORT, STAGE_AIRPORT, 104 }, { PANEL_GARAGE, STAGE_GARAGE, 104 }, { PANEL_STRIPCLUB, STAGE_STRIPCLUB, 104 } },
[MENU_SWAT] = { { PANEL_DRUGS1, STAGE_DRUGS1, 111 }, { PANEL_DRUGS2, STAGE_DRUGS2, 111 }, { PANEL_BANK, STAGE_ROBBERY, 103 } },
[MENU_DELTA] = { { PANEL_HANGAR, STAGE_HANGAR, 108 }, { PANEL_AMBUSH, STAGE_AMBUSH, 103 }, { PANEL_AIRTERRORISTS, STAGE_AIRTERRORISTS, 109 } } }
end
for _, entry in ipairs(MENUS[iCurrentMenu] or {}) do
if not panel[entry[2]] then
targets = { lineMiddle(area[entry[1]], entry[3]) }
break
end
end
elseif (GetArrayPanels ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and area then
-- The Last Bounty Hunter's wanted posters: the first outlaw still at large, by the middle of
-- his panel's rectangle.
for _, stage in ipairs({ STAGE_HARRY, STAGE_DAN, STAGE_LOCO, STAGE_CACTUS }) do
local r = (panel[stage] == ATLARGE) and area[stage] and area[stage][1]
if r then
targets = { { hmx((r[lineStartX] + r[lineEndX]) / 2), hmy((r[lineStartY] + r[lineEndY]) / 2) } }
break
end
end
elseif (levelSigns ~= nil) and (currentLevel == levelSigns) and (lvlState == lvlEnd) then
-- The Last Bounty Hunter's signs: the practice range, in the game's own pointer coordinates.
targets = { { 175, 101 } }
targetStep = lvlEnd
elseif (iLastMove ~= nil) and (levelFinal ~= nil) and (currentLevel == levelFinal) and (lvlState == lvlPlayClip2) then
-- Drug Wars's last stand: the tank, the blanket, or the gas cans, in the game's own
-- pointer coordinates.
local STAND = { { 262, 329, 133, 172 }, { 101, 111, 160, 188 }, { 209, 243, 201, 215 } }
local b = STAND[iLastMove] or STAND[3]
targets = { { (b[1] + b[2]) / 2, (b[3] + b[4]) / 2 } }
targetStep = lvlPlayClip2
elseif (practiceHit ~= nil) and (currentLevel == levelPractice) and (lvlState == lvlRunning) then
-- Crime Patrol's practice range: the first standing target of the still, in the game's
-- own pointer coordinates, or the cutout of the move (the middle line of its outline).
if currentMove == 0 then
local STILL = { { "bTopRight", 261, 300, 9, 60 }, { "bTopLeft", 78, 119, 70, 120 }, { "bMiddle", 205, 243, 70, 123 }, { "bBottomLeft", 120, 158, 124, 181 }, { "bBottomRight", 268, 297, 132, 183 } }
for _, t in ipairs(STILL) do
if _G[t[1]] then
targets = { { (t[2] + t[3]) / 2, (t[4] + t[5]) / 2 } }
break
end
end
elseif (currentMove >= 1) and move and move[currentMove] and target then
local kind = move[currentMove][rndMoveType]
local counts = { [BADGUY01] = 98, [CIV01] = 82, [BADGUY02] = 97, [CIV02] = 107, [BADGUY03] = 79, [CIV03] = 148 }
if kind and counts[kind] and target[kind] then
targets = { lineMiddle(target[kind], counts[kind]) }
end
end
elseif (levelFirePractice ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and panel then
-- Space Pirates's planet menu: the first panel not yet done, in the order the game takes
-- when the clip runs out, by the middle of its quadrant (pointer coordinates scaled as the
-- game scales them).
local PANELS = { { STAGE_FELLINA, 807, 1280, 565, 916 }, { STAGE_REAPER, 245, 720, 565, 916 }, { STAGE_SCRAPYARD, 807, 1280, 100, 450 }, { STAGE_MOUNTAIN, 245, 720, 100, 450 } }
for _, p in ipairs(PANELS) do
if panel[p[1]] == false then
targets = { { UnX * (p[2] + p[3]) / 2, UnY * (p[4] + p[5]) / 2 } }
break
end
end
elseif (levelDoor2 ~= nil) and (currentLevel == levelDoor2) and (lvlState == lvlPlayClip4) and band then
-- Space Pirates's band of colour: the right one, within its frames, by the middle of its
-- line.
local row = band[iBand] and band[iBand][1]
if row and (discGetFrame() >= bandFrameStart + 3) and (discGetFrame() < bandFrameEnd) then
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
targetStep = lvlPlayClip4
end
elseif (levelDoor3 ~= nil) and (currentLevel == levelDoor3) and (lvlState == lvlPlayClip3) and (ursie or (ursulaHit ~= nil)) then
-- Space Pirates's commander: Ursula shot within her colour's frames.
local WINDOWS = { [COLOR_RED] = { 2243, 2306 }, [COLOR_GREEN] = { 2312, 2371 }, [COLOR_BLUE] = { 2377, 2439 } }
local w = WINDOWS[iUrsulaColor]
local rel = discGetFrame() - thisOffset
if w and (rel >= w[1] + 3) and (rel <= w[2] - 3) then
-- (The one-player edition builds her rectangles inside its hit test, so before the first
-- shot the game's own numbers stand in.)
local box = (ursie and ursie[1]) or { 152, 137, 210, 238 }
targets = { { hmx((box[1] + box[3]) / 2), hmy((box[2] + box[4]) / 2) } }
targetStep = lvlPlayClip3
end
elseif (levelFellina1 ~= nil) and (currentLevel == levelFellina1) and ((lvlState == lvlPlayPuzzle1) or (lvlState == lvlPlayPuzzle2)) and thingie then
-- Space Pirates's Fellina puzzles: the skull she chose, then the arrow, by the middle of
-- the game's own line.
local which = (lvlState == lvlPlayPuzzle1) and iFellinaSkull or iFellinaArrow
local row = thingie[which] and thingie[which][1]
if row and ((lvlState == lvlPlayPuzzle2) or (discGetFrame() >= iSkullStart + 3)) then
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
targetStep = lvlState
end
elseif (levelCrystalBuild ~= nil) and (currentLevel == levelCrystalBuild) and (lvlState == lvlRunning) and hole and socket then
-- Space Pirates's crystal build: the next socket in the order shown.
local ORDERS = { [ORDER_RGB] = { SOCKET_RED, SOCKET_GREEN, SOCKET_BLUE }, [ORDER_GBR] = { SOCKET_GREEN, SOCKET_BLUE, SOCKET_RED }, [ORDER_BRG] = { SOCKET_BLUE, SOCKET_RED, SOCKET_GREEN } }
for _, which in ipairs(ORDERS[iCrystalOrder] or {}) do
if not socket[which] then
local row = hole[which] and hole[which][1]
if row then
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
end
break
end
end
elseif (GetMapArray01 ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and stage then
-- Who Shot Johnny Rock's city map: the killer's place once revealed, the mansion once the
-- clues are all had, else the first place still open (the casino, the pool hall, the
-- warehouse, the garage), by the middle of its panel.
local which = nil
if bKillerRevealed then
which = ({ [JILL] = PLACE_CASINO, [MUMPS] = PLACE_POOL, [MEASLES] = PLACE_WAREHOUSE, [POX] = PLACE_GARAGE })[iKiller]
elseif bGotAllClues then
which = PLACE_MANSION
elseif not stage[levelCasino] then
which = PLACE_CASINO
elseif not stage[levelPool] then
which = PLACE_POOL
elseif not (stage[levelWarehouse1] and stage[levelWarehouse2] and stage[levelWarehouse3]) then
which = PLACE_WAREHOUSE
elseif not stage[levelGarage] then
which = PLACE_GARAGE
end
if which then
GetMapArray01()
local row = place[which] and place[which][1]
if row then
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
end
end
elseif (GetMapArray01 ~= nil) and (currentLevel == levelMansion2) and (lvlState == lvlRunning) and clue and thisItem then
-- Who Shot Johnny Rock's clue items: the item of the clue asked for, in the game's second
-- scale.
GetArrayItems()
local row = item[clue[thisItem]] and item[clue[thisItem]][1]
if row then
targets = { { ResHMx2((row[lineStartX] + row[lineEndX]) / 2), ResHMy2((row[lineStartY] + row[lineEndY]) / 2) } }
end
elseif (GetMapArray01 ~= nil) and (currentLevel == levelMansion4) and (lvlState == lvlRunning) and combination and iSafeNumber then
-- Who Shot Johnny Rock's safe: the dial shot while it shows the combination's next digit
-- (on this frame and the next, since the press is judged on the next).
local digit = tonumber(string.sub(combination, iSafeNumber, iSafeNumber))
local rel = discGetFrame() - thisOffset
if (getSafeNumber(rel) == digit) and (getSafeNumber(rel + 1) == digit) then
targets = { { hmx((151 + 207) / 2), hmy((64 + 88) / 2) } }
end
elseif (GUIDE_BEAVER ~= nil) and (ResHMx ~= nil) and (currentLevel == levelMenu) and panel then
-- Mad Dog II's guide menu: the first guide whose trail is not done, by its panel.
targets = {}
local x = (panel[GUIDE_BEAVER] == false) and 90 or ((panel[GUIDE_BONNIE] == false) and 186 or ((panel[GUIDE_PROF] == false) and 282 or nil))
if x then
targets[1] = { hmx(x), hmy(121) }
end
end
-- At the attract loop: Mad Dog's state machine names it currentState, Mad Dog II's gameflow.
local playing = ((currentState or gameflow) == statePlaying)
local intro = playing and (currentLevel == (lvlIntro or levelIntro))
-- The service screen: the option under the cursor and every dip, for the trace; a screen
-- the stick drives rather than the pointer (Hologram Time Traveler).
local dips = {}
for key, value in pairs(_G) do
if (type(key) == "string") and (key:sub(1, 4) == "dip_") and (type(value) ~= "function") then
dips[#dips + 1] = key:sub(5) .. "=" .. tostring(value)
end
end
table.sort(dips)
return { screen = currentLevel, step = lvlState, move = currentMove, frame = discGetFrame(), frames = frames, score = iScore, lives = iLives or iMoney, ammo = iBullets, active = p1Active, intro = intro,
service = (levelService ~= nil) and (currentLevel == levelService), serviceKeys = (TURNRIGHTSHOOT ~= nil), opt = optSel, dips = table.concat(dips, ","), width = overlayGetWidth(), height = overlayGetHeight(),
board = (levelHighScore ~= nil) and (currentLevel == levelHighScore),
window = window, box = window and firstBox() or nil, quadrants = quadrants, targets = targets, targetStep = targetStep, continuing = (currentLevel == levelContinue), noSelect = (mouseIndexSelect == nil), typing = typing, typed = (validKey ~= nil), offscreen = { -5, -5 }, free = bAmmo, paused = bPause,
-- The one-player editions (no player select, lives rather than money) spend a shot
-- before the level tests for ammo, so their last shot is never judged: a spare to keep.
spare = ((mouseIndexSelect == nil) and (iMoney == nil) and (iBullets ~= nil) and ((panelHit == nil) or (resX ~= nil)) and (levelCharlie == nil)) and 1 or 0,
-- The library's one-player Crime Patrol family judges a shot before the release takes its
-- bullet (its HD twins take it on the press, as the others do), and reloads by its second
-- button (immediately, as its config ships); so does Platoon.
reloadSwitch = (((panelHit ~= nil) and (mouseIndexSelect == nil)) or (levelCharlie ~= nil)) and SWITCH_BUTTON1 or SWITCH_BUTTON3 }
end
onOverlayUpdate = function(...)
local result = realUpdate(...)
algDrive(view())
return result
end