-- 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