singe/testScripts/ports/algPort.singe
2026-09-22 18:32:07 -05:00

503 lines
24 KiB
Text

-- The Forge port of an American Laser Games title, driven and traced as the original is.
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 name = (os.getenv("PORT_SCRIPT") or ("singe/" .. game .. "/" .. game .. ".singe")):gsub("%.singe$", "")
local stem = name:match("([^/\\]+)$")
dofile("Forge/AuthorCompile.singe")
dofile(here .. "algDrive.singe")
-- The game seeds its random draws from os.time at load; the original's driver pins it, and so
-- does this, to the same number.
os.time = function(...) return 12345 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
dofile(authorBuild(library .. "/" .. game .. "/" .. name .. ".forge", singeGetDataPath() .. stem .. ".port.singe"))
local realUpdate = onOverlayUpdate
function algPress(switch, device, down)
if down then
authorSwitchDown(switch, device)
else
authorSwitchUp(switch, device)
end
end
-- A key released, as the typing edition reads it: the runtime hands a keysym over as a switch.
function algKey(keysym)
authorSwitchUp(keysym)
end
function algPointer(device, x, y)
local q = authorEntity("game").q
local rx = (x + (q.ratioxOffset or 0)) / ((q.ratiox or 1) * (q.gunscale or 1))
local ry = (y + (q.ratioyOffset or 0)) / ((q.ratioy or 1) * (q.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)
local q = authorEntity("game").q
if q.ResHMx then
return q.ResHMx(v)
end
return v
end
local function hmy(v)
local q = authorEntity("game").q
if q.ResHMy then
return q.ResHMy(v)
end
return v
end
-- The hitboxes of a frame in the current move's hitmap range, in pointer coordinates.
local function boxesAt(q, rel)
local boxes = {}
for k = (q.move[q.currentMove][q.hitmapStart or q.hitboxStart] or 1), (q.hitmapTotal or 0) do
local row = q.hitmap[k]
if row and (row[q.hitmapFrame] == rel) and (row[q.hitmapCount] > 0) then
for j = row[q.hitmapIndex], row[q.hitmapIndex] + row[q.hitmapCount] - 1 do
local b = q.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[q.bbx1] <= b[q.bbx2]) and (b[q.bby1] <= b[q.bby2]) then
boxes[#boxes + 1] = { hmx(b[q.bbx1]), hmy(b[q.bby1]), hmx(b[q.bbx2]), hmy(b[q.bby2]) }
end
end
break
end
end
return boxes
end
local function firstBox(q)
-- 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 (q.move == nil) or (q.move[q.currentMove] == nil) or (q.hitmap == nil) then
return nil
end
local rel = q.currentFrame - (q.thisOffset or 0)
local now = boxesAt(q, rel)
local b = now[1]
if b == nil then
return nil
end
for _, n in ipairs(boxesAt(q, 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 q = authorEntity("game").q
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 = (q.currentLevel ~= nil) and (((q.frameShootStart ~= nil) and (q.currentLevel >= 101) and (q.currentLevel <= 121)) or ((q.frameShootStart == nil) and (((q.currentLevel >= 1) and (q.currentLevel <= 30)) or (q.currentLevel == q.levelStart))))
if ((q.practiceHit ~= nil) or (q.fRndPopOrder ~= nil)) and (q.currentLevel == q.levelPractice) then
inLevel = false
end
if (q.levelSigns ~= nil) and (q.currentLevel == q.levelSigns) then
inLevel = false
end
if (q.levelTieIntro ~= nil) and ((q.currentLevel == q.levelTie) or (q.currentLevel == q.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 (q.levelCrystalBuild ~= nil) and ((q.currentLevel == q.levelCrystalBuild) or ((q.currentLevel == q.levelDoor2) and (q.lvlState == q.lvlPlayClip4)) or ((q.currentLevel == q.levelDoor3) and (q.lvlState == q.lvlPlayClip3)) or ((q.currentLevel == q.levelFellina1) and ((q.lvlState == q.lvlPlayPuzzle1) or (q.lvlState == q.lvlPlayPuzzle2)))) then
inLevel = false
end
-- Who Shot Johnny Rock's store, clue items, and safe are targets, not windows.
if (q.GetMapArray01 ~= nil) and ((q.currentLevel == q.levelStore) or (q.currentLevel == q.levelMansion2) or (q.currentLevel == q.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 (q.levelReaper2 ~= nil) and (q.currentLevel == q.levelReaper2) and (q.currentMove > 1) and (q.iReaperHits >= q.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 q.validKey ~= nil then
inLevel = false
local function nextOf(word)
if word and (q.iKeyIndex or 1) <= string.len(word) then
return string.byte(word, q.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 = (q.iKey == q.SWITCH_NONE) or (q.iKey == 0)
if not waiting then
typing = nil
elseif (q.currentLevel == q.levelTitle) and (q.lvlState == q.lvlRunning) then
typing = q.SWITCH_RETURN
elseif (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) then
typing = q.SWITCH_RETURN
elseif (q.currentLevel == q.levelHighScore) and (q.lvlState == q.lvlRunning) then
-- (Once, before the name's frames run out; the return key seeks to the entry's end.)
if discGetFrame() < q.offsetMenus + 3275 then
typing = q.SWITCH_RETURN
end
elseif q.bShowSprinkles and q.sprinkles then
for k = 1, (q.iSprinkleMax or 0) do
if q.sprinkles[k] and (q.sprinkles[k][4] == true) then
typing = string.byte(q.sprinkles[k][3])
break
end
end
elseif (q.currentLevel == q.levelItem1) and (q.lvlState == q.lvlPlayClip3) then
typing = nextOf(q.sMagicWord)
elseif q.move and q.move[q.currentMove] and ((q.lvlState == q.lvlPauseAction) or ((q.lvlState == q.lvlRunning) and (discGetFrame() >= q.move[q.currentMove][q.moveFrameStart]) and (discGetFrame() < q.move[q.currentMove][q.moveFrameEnd]))) then
typing = nextOf(q.move[q.currentMove][(q.currentLevel == q.levelItem2) and q.moveType or q.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 = (q.levelCharlie ~= nil) and q.bOnce and (q.currentLevel ~= nil) and (q.currentLevel >= 1) and (q.currentLevel <= 6) and (q.lvlState == q.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 q.move and q.move[q.currentMove] and (q.rndMoveType ~= nil) and (q.GOODGUY ~= nil) and (q.move[q.currentMove][q.rndMoveType] == q.GOODGUY) then
inLevel = false
end
if inLevel and q.move and q.move[q.currentMove] then
window = { q.move[q.currentMove][q.frameShootStart or q.moveFrameStart], q.move[q.currentMove][q.frameShootEnd or q.moveFrameEnd] }
end
local quadrants = nil
if (q.currentLevel == q.levelTownSelect) and q.stage then
quadrants = {}
local w, h = q.resX or 360, q.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 q.stage[q.STAGE_CORRAL] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.25 } end
if not q.stage[q.STAGE_SALOON] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.25 } end
if not q.stage[q.STAGE_SHERIFF] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.75 } end
if not q.stage[q.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[q.lineStartX] + row[q.lineEndX]) / 2), hmy(row[q.lineY]) }
end
return nil
end
if skipping then
targets = { { 180, 120 } }
targetStep = q.lvlRunning
elseif (q.currentLevel == q.levelSignSelect) and q.sign then
targets = {}
if q.stage[q.STAGE_FUSE] == false then
targets[1] = lineMiddle(q.sign[q.SIGN_PROSPECTOR], 48)
elseif q.stage[q.STAGE_MINE] == false then
targets[1] = lineMiddle(q.sign[q.SIGN_MINE], 28)
else
targets[1] = lineMiddle(q.sign[q.SIGN_HIDEOUT], 58)
end
elseif (q.currentLevel == q.levelMine) and q.mine then
targets = {}
local counts = { [q.MINE_SIGN] = 21, [q.MINE_LANTERN] = 20, [q.MINE_PAN] = 12, [q.MINE_PITCHFORK] = 23, [q.MINE_SKULL] = 25 }
if not q.bLanternHit then targets[#targets + 1] = lineMiddle(q.mine[q.MINE_LANTERN], counts[q.MINE_LANTERN]) end
if not q.bItemHit then targets[#targets + 1] = lineMiddle(q.mine[q.iWhatToHit], counts[q.iWhatToHit]) end
elseif (q.currentLevel == q.levelMaze) and q.maze and q.maze[q.curMazePos] and (q.maze[q.curMazePos][q.mazeType] > 0) and q.sign then
-- A maze sign: the sign for the way to go.
local frame, dir = q.maze[q.curMazePos][q.signType], q.maze[q.curMazePos][q.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 q.sign[dir] then
targets = { lineMiddle(q.sign[dir], pair[dir]) }
end
elseif (q.levelItem1 ~= nil) and (q.ResHMx ~= nil) and (q.currentLevel == q.levelItem1) and (q.lvlState == q.lvlPlayClip3) then
-- Mad Dog II's first item level: the item the guide asked for, from the game's own
-- rectangles.
local boxes = { [q.ITEM_LANTERN] = { 57, 66, 117, 129 }, [q.ITEM_AMMOBOX] = { 120, 148, 111, 132 }, [q.ITEM_MOONSHINE] = { 260, 272, 60, 72 } }
local b = boxes[q.iThisItem]
if b then
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
targetStep = q.lvlPlayClip3
end
elseif (q.levelProspector1 ~= nil) and (q.ResHMx ~= nil) and (q.currentLevel == q.levelProspector1) and (q.lvlState == q.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[q.iSkull] or boxes[4]
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
targetStep = q.lvlPlayClip4
elseif (q.panelHit ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.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 q.MENU_SIERRA ~= nil then
MENUS[q.MENU_SIERRA] = { { q.PANEL_PRACTICE, q.STAGE_PRACTICE, 99 }, { q.PANEL_BAR, q.STAGE_BAR, 99 }, { q.PANEL_CARCHASE, q.STAGE_CARCHASE, 99 }, { q.PANEL_HIDEOUT, q.STAGE_HIDEOUT, 99 } }
MENUS[q.MENU_CHICAGO] = { { q.PANEL_HELI, q.STAGE_HELI, 111 }, { q.PANEL_COURTROOM, q.STAGE_COURTROOM, 111 }, { q.PANEL_BUSCHASE, q.STAGE_BUSCHASE, 111 } }
MENUS[q.MENU_BORDER] = { { q.PANEL_SHIPYARD, q.STAGE_SHIPYARD, 111 }, { q.PANEL_BOATHOUSE, q.STAGE_BOATHOUSE, 111 }, { q.PANEL_BEACH, q.STAGE_BEACH, 111 } }
MENUS[q.MENU_SA] = { { q.PANEL_BASE, q.STAGE_BASE, 111 }, { q.PANEL_NIGHTVISION, q.STAGE_NIGHTVISION, 111 }, { q.PANEL_TOWN, q.STAGE_TOWN, 111 } }
end
if q.MENU_ROOKIE ~= nil then
MENUS = { [q.MENU_ROOKIE] = { { q.PANEL_PRACTICE, q.STAGE_PRACTICE, 98 }, { q.PANEL_STORE, q.STAGE_STORE, 99 }, { q.PANEL_GANG, q.STAGE_GANG, 99 }, { q.PANEL_WAREHOUSE, q.STAGE_WAREHOUSE, 97 } },
[q.MENU_UNDERCOVER] = { { q.PANEL_AIRPORT, q.STAGE_AIRPORT, 104 }, { q.PANEL_GARAGE, q.STAGE_GARAGE, 104 }, { q.PANEL_STRIPCLUB, q.STAGE_STRIPCLUB, 104 } },
[q.MENU_SWAT] = { { q.PANEL_DRUGS1, q.STAGE_DRUGS1, 111 }, { q.PANEL_DRUGS2, q.STAGE_DRUGS2, 111 }, { q.PANEL_BANK, q.STAGE_ROBBERY, 103 } },
[q.MENU_DELTA] = { { q.PANEL_HANGAR, q.STAGE_HANGAR, 108 }, { q.PANEL_AMBUSH, q.STAGE_AMBUSH, 103 }, { q.PANEL_AIRTERRORISTS, q.STAGE_AIRTERRORISTS, 109 } } }
end
for _, entry in ipairs(MENUS[q.iCurrentMenu] or {}) do
if not q.panel[entry[2]] then
targets = { lineMiddle(q.area[entry[1]], entry[3]) }
break
end
end
elseif (q.GetArrayPanels ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.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({ q.STAGE_HARRY, q.STAGE_DAN, q.STAGE_LOCO, q.STAGE_CACTUS }) do
local r = (q.panel[stage] == q.ATLARGE) and q.area[stage] and q.area[stage][1]
if r then
targets = { { hmx((r[q.lineStartX] + r[q.lineEndX]) / 2), hmy((r[q.lineStartY] + r[q.lineEndY]) / 2) } }
break
end
end
elseif (q.levelSigns ~= nil) and (q.currentLevel == q.levelSigns) and (q.lvlState == q.lvlEnd) then
-- The Last Bounty Hunter's signs: the practice range, in the game's own pointer coordinates.
targets = { { 175, 101 } }
targetStep = q.lvlEnd
elseif (q.iLastMove ~= nil) and (q.levelFinal ~= nil) and (q.currentLevel == q.levelFinal) and (q.lvlState == q.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[q.iLastMove] or STAND[3]
targets = { { (b[1] + b[2]) / 2, (b[3] + b[4]) / 2 } }
targetStep = q.lvlPlayClip2
elseif (q.practiceHit ~= nil) and (q.currentLevel == q.levelPractice) and (q.lvlState == q.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 q.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 q[t[1]] then
targets = { { (t[2] + t[3]) / 2, (t[4] + t[5]) / 2 } }
break
end
end
elseif (q.currentMove >= 1) and q.move and q.move[q.currentMove] and q.target then
local kind = q.move[q.currentMove][q.rndMoveType]
local counts = { [q.BADGUY01] = 98, [q.CIV01] = 82, [q.BADGUY02] = 97, [q.CIV02] = 107, [q.BADGUY03] = 79, [q.CIV03] = 148 }
if kind and counts[kind] and q.target[kind] then
targets = { lineMiddle(q.target[kind], counts[kind]) }
end
end
elseif (q.levelFirePractice ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.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 = { { q.STAGE_FELLINA, 807, 1280, 565, 916 }, { q.STAGE_REAPER, 245, 720, 565, 916 }, { q.STAGE_SCRAPYARD, 807, 1280, 100, 450 }, { q.STAGE_MOUNTAIN, 245, 720, 100, 450 } }
for _, p in ipairs(PANELS) do
if q.panel[p[1]] == false then
targets = { { q.UnX * (p[2] + p[3]) / 2, q.UnY * (p[4] + p[5]) / 2 } }
break
end
end
elseif (q.levelDoor2 ~= nil) and (q.currentLevel == q.levelDoor2) and (q.lvlState == q.lvlPlayClip4) and q.band then
-- Space Pirates's band of colour: the right one, within its frames, by the middle of its
-- line.
local row = q.band[q.iBand] and q.band[q.iBand][1]
if row and (discGetFrame() >= q.bandFrameStart + 3) and (discGetFrame() < q.bandFrameEnd) then
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
targetStep = q.lvlPlayClip4
end
elseif (q.levelDoor3 ~= nil) and (q.currentLevel == q.levelDoor3) and (q.lvlState == q.lvlPlayClip3) and (q.ursie or (q.ursulaHit ~= nil)) then
-- Space Pirates's commander: Ursula shot within her colour's frames.
local WINDOWS = { [q.COLOR_RED] = { 2243, 2306 }, [q.COLOR_GREEN] = { 2312, 2371 }, [q.COLOR_BLUE] = { 2377, 2439 } }
local w = WINDOWS[q.iUrsulaColor]
local rel = discGetFrame() - q.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 = (q.ursie and q.ursie[1]) or { 152, 137, 210, 238 }
targets = { { hmx((box[1] + box[3]) / 2), hmy((box[2] + box[4]) / 2) } }
targetStep = q.lvlPlayClip3
end
elseif (q.levelFellina1 ~= nil) and (q.currentLevel == q.levelFellina1) and ((q.lvlState == q.lvlPlayPuzzle1) or (q.lvlState == q.lvlPlayPuzzle2)) and q.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 = (q.lvlState == q.lvlPlayPuzzle1) and q.iFellinaSkull or q.iFellinaArrow
local row = q.thingie[which] and q.thingie[which][1]
if row and ((q.lvlState == q.lvlPlayPuzzle2) or (discGetFrame() >= q.iSkullStart + 3)) then
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
targetStep = q.lvlState
end
elseif (q.levelCrystalBuild ~= nil) and (q.currentLevel == q.levelCrystalBuild) and (q.lvlState == q.lvlRunning) and q.hole and q.socket then
-- Space Pirates's crystal build: the next socket in the order shown.
local ORDERS = { [q.ORDER_RGB] = { q.SOCKET_RED, q.SOCKET_GREEN, q.SOCKET_BLUE }, [q.ORDER_GBR] = { q.SOCKET_GREEN, q.SOCKET_BLUE, q.SOCKET_RED }, [q.ORDER_BRG] = { q.SOCKET_BLUE, q.SOCKET_RED, q.SOCKET_GREEN } }
for _, which in ipairs(ORDERS[q.iCrystalOrder] or {}) do
if not q.socket[which] then
local row = q.hole[which] and q.hole[which][1]
if row then
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
end
break
end
end
elseif (q.GetMapArray01 ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.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 q.bKillerRevealed then
which = ({ [q.JILL] = q.PLACE_CASINO, [q.MUMPS] = q.PLACE_POOL, [q.MEASLES] = q.PLACE_WAREHOUSE, [q.POX] = q.PLACE_GARAGE })[q.iKiller]
elseif q.bGotAllClues then
which = q.PLACE_MANSION
elseif not q.stage[q.levelCasino] then
which = q.PLACE_CASINO
elseif not q.stage[q.levelPool] then
which = q.PLACE_POOL
elseif not (q.stage[q.levelWarehouse1] and q.stage[q.levelWarehouse2] and q.stage[q.levelWarehouse3]) then
which = q.PLACE_WAREHOUSE
elseif not q.stage[q.levelGarage] then
which = q.PLACE_GARAGE
end
if which then
q.GetMapArray01()
local row = q.place[which] and q.place[which][1]
if row then
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
end
end
elseif (q.GetMapArray01 ~= nil) and (q.currentLevel == q.levelMansion2) and (q.lvlState == q.lvlRunning) and q.clue and q.thisItem then
-- Who Shot Johnny Rock's clue items: the item of the clue asked for, in the game's second
-- scale.
q.GetArrayItems()
local row = q.item[q.clue[q.thisItem]] and q.item[q.clue[q.thisItem]][1]
if row then
targets = { { q.ResHMx2((row[q.lineStartX] + row[q.lineEndX]) / 2), q.ResHMy2((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
end
elseif (q.GetMapArray01 ~= nil) and (q.currentLevel == q.levelMansion4) and (q.lvlState == q.lvlRunning) and q.combination and q.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(q.combination, q.iSafeNumber, q.iSafeNumber))
local rel = discGetFrame() - q.thisOffset
if (q.getSafeNumber(rel) == digit) and (q.getSafeNumber(rel + 1) == digit) then
targets = { { hmx((151 + 207) / 2), hmy((64 + 88) / 2) } }
end
elseif (q.GUIDE_BEAVER ~= nil) and (q.ResHMx ~= nil) and (q.currentLevel == q.levelMenu) and q.panel then
-- Mad Dog II's guide menu: the first guide whose trail is not done, by its panel.
targets = {}
local x = (q.panel[q.GUIDE_BEAVER] == false) and 90 or ((q.panel[q.GUIDE_BONNIE] == false) and 186 or ((q.panel[q.GUIDE_PROF] == false) and 282 or nil))
if x then
targets[1] = { hmx(x), hmy(121) }
end
end
local playing = ((q.currentState or q.gameflow) == q.statePlaying)
local intro = playing and (q.currentLevel == (q.lvlIntro or q.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(q) 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 = q.currentLevel, step = q.lvlState, move = q.currentMove, frame = discGetFrame(), frames = frames, score = q.iScore, lives = q.iLives or q.iMoney, ammo = q.iBullets, active = q.p1Active, intro = intro,
service = (q.levelService ~= nil) and (q.currentLevel == q.levelService), serviceKeys = (q.TURNRIGHTSHOOT ~= nil), opt = q.optSel, dips = table.concat(dips, ","), width = overlayGetWidth(), height = overlayGetHeight(),
board = (q.levelHighScore ~= nil) and (q.currentLevel == q.levelHighScore),
window = window, box = window and firstBox(q) or nil, quadrants = quadrants, targets = targets, targetStep = targetStep, continuing = (q.currentLevel == q.levelContinue), noSelect = (q.mouseIndexSelect == nil), typing = typing, typed = (q.validKey ~= nil), offscreen = { -5, -5 }, free = q.bAmmo, paused = q.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 = ((q.mouseIndexSelect == nil) and (q.iMoney == nil) and (q.iBullets ~= nil) and ((q.panelHit == nil) or (q.resX ~= nil)) and (q.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 = (((q.panelHit ~= nil) and (q.mouseIndexSelect == nil)) or (q.levelCharlie ~= nil)) and SWITCH_BUTTON1 or SWITCH_BUTTON3 }
end
onOverlayUpdate = function(...)
local result = realUpdate(...)
algDrive(view())
return result
end