-- 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//<Title>.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