mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 15:51:17 +02:00
f0a88ea473
* audio timing stuff * bug fixes and translation additions * translation stuff * Update modkit.py * better asset resolution
738 lines
26 KiB
Lua
738 lines
26 KiB
Lua
-- Game Corner slot machine minigame.
|
||
--
|
||
-- Wheels are the real symbol sequences (data/events/slot_machine_wheels.asm
|
||
-- via field.slotWheels: 15 symbols per wheel plus 3 wraparound entries,
|
||
-- read exactly like SlotMachine_GetWheelTiles). Wheel positions are kept
|
||
-- in pokered's half-symbol offsets (wSlotMachineWheelXOffset, 0..29): a
|
||
-- wheel may only stop when its offset is odd (a symbol is centred), and
|
||
-- every animation step advances the offset by one (SlotMachine_AnimWheel),
|
||
-- so slips scroll on screen tile-by-tile like the original.
|
||
--
|
||
-- Per-wheel stop rules (engine/slots/slot_machine.asm):
|
||
-- * wheel 1 (SlotMachine_StopWheel1Early): at each centred position it
|
||
-- spends one of 4 slip charges (wSlotMachineWheel1SlipCounter); it stops
|
||
-- unless the centred middle symbol is a cherry, which it slips past. In
|
||
-- seven-and-bar mode the early-stop test is pokered's bug (`cp
|
||
-- HIGH(SLOTS7)` / `jr c`, never true), so it always slips all 4.
|
||
-- * wheel 2 (SlotMachine_StopWheel2Early): stops as soon as wheels 1 and 2
|
||
-- line up any potential match (SlotMachine_FindWheel1Wheel2Matches); in
|
||
-- seven-and-bar mode it instead stops when the matched (or, with no
|
||
-- match, bottom) wheel-2 symbol is a 7 or BAR. Up to 4 slips.
|
||
-- * wheel 3 (SlotMachine_StopOrAnimWheel3): stops at the next centred
|
||
-- position; SlotMachine_CheckForMatches then rerolls it one symbol at a
|
||
-- time -- past any match the luck flags forbid (without consuming the
|
||
-- counter), or toward a match while wSlotMachineRerollCounter (4) lasts.
|
||
--
|
||
-- Payouts/paylines follow SlotMachine_CheckForMatches: bet 1 plays the
|
||
-- middle row, bet 2 adds top and bottom, bet 3 adds both diagonals,
|
||
-- checked in pokered's order with the FIRST match taken; 7-7-7 pays 300,
|
||
-- BAR 100, CHERRY 8, anything else 15.
|
||
--
|
||
-- Hidden luck (SlotMachine_SetFlags + game_corner_slots.asm): one machine
|
||
-- per Game Corner visit is "lucky" (seven-and-bar mode chance 5/256 vs
|
||
-- 2/256). Each spin: 1/256 arms a 60-charge allow-matches counter,
|
||
-- r > chance arms seven-and-bar mode (sticky until a BAR win clears it, or
|
||
-- a 300 win does so half the time), r in 211..chance allows a match, the
|
||
-- rest can't win.
|
||
--
|
||
-- Presentation follows pokered's flow: PromptUserToPlaySlots asks "Want to
|
||
-- play?" first; MainSlotMachineLoop shows the static SlotMachineMap frame
|
||
-- (gfx/slots/slots.tilemap, via field.slotSymbols.tilemap), a "Bet how many
|
||
-- coins?" prompt with the ×3/×2/×1 menu (cursor defaults to ×3), flashes the
|
||
-- screen on a win (SlotReward*Func b flips of rBGP, 5 frames each) and drips
|
||
-- the payout one coin per 8 frames (4 for a 7/BAR) with a jingle and a
|
||
-- symbol-palette flicker, then asks "One more go?".
|
||
|
||
local Font = require("src.render.Font")
|
||
local Sound = require("src.core.Sound")
|
||
local Strings = require("src.core.Strings")
|
||
|
||
local SlotMachine = {}
|
||
SlotMachine.__index = SlotMachine
|
||
SlotMachine.isOpaque = true
|
||
|
||
-- rBGP/rOBP0 `xor $40` from the default $e4 shows the darkest shade (3) one
|
||
-- step lighter (shade 2): the win-screen and payout flash
|
||
-- (SlotMachine_CheckForMatches .flashScreenLoop / SlotMachine_PayCoinsToPlayer).
|
||
local FLASH_MAP = { [0] = 0, [1] = 1, [2] = 2, [3] = 2 }
|
||
|
||
-- SGB: PalPacket_Slots + BlkPacket_Slots row bands. While self.flash is set
|
||
-- the bands are permuted like pokered's rBGP flip so the machine flashes.
|
||
function SlotMachine:sgbPalettes(game)
|
||
local P = require("src.render.PaletteFX")
|
||
local s1 = P.pal(game.data, "SLOTS1")
|
||
if not s1 then return nil end
|
||
-- self.flash "all" flips every band (the win-screen rBGP flash); "reels"
|
||
-- flips only the symbol window (the payout-time rOBP0 symbol flicker, which
|
||
-- the s1 zone at cols 4-15 / rows 4-9 covers).
|
||
local function fx(c, reel)
|
||
if c and self.flash and (self.flash == "all" or reel) then
|
||
return P.permute(c, FLASH_MAP)
|
||
end
|
||
return c
|
||
end
|
||
return {
|
||
P.zone(fx(P.pal(game.data, "SLOTS2")), 0, 0, 19, 11),
|
||
P.zone(fx(P.pal(game.data, "SLOTS3")), 0, 4, 19, 9),
|
||
P.zone(fx(P.pal(game.data, "SLOTS4")), 0, 6, 19, 7),
|
||
P.zone(fx(s1, true), 4, 4, 15, 9),
|
||
P.zone(fx(s1), 0, 12, 19, 17),
|
||
}
|
||
end
|
||
|
||
local PAYOUT = { ["7"] = 300, BAR = 100, CHERRY = 8,
|
||
MOUSE = 15, FISH = 15, BIRD = 15 }
|
||
local SHORT = { ["7"] = " 7 ", BAR = "BAR", CHERRY = "CHR",
|
||
MOUSE = "MSE", FISH = "FSH", BIRD = "BRD" }
|
||
|
||
-- MainSlotMachineLoop timing: one animation step every other frame
|
||
-- (DelayFrame in SlotMachine_HandleInputWhileWheelsSpin plus DelayFrames(1)
|
||
-- on SGB, which this port colorizes as). The initial free spin is 20
|
||
-- steps at the same cadence (SlotMachine_SpinWheels .loop1).
|
||
local STEP_FRAMES = 2
|
||
local SPINUP_STEPS = 20
|
||
|
||
local function at(wheel, pos, off)
|
||
return wheel[((pos + off - 1) % #wheel) + 1]
|
||
end
|
||
|
||
-- The three visible symbols at a centred position: wheel[pos] (bottom),
|
||
-- wheel[pos+1] (middle), wheel[pos+2] (top) -- SlotMachine_GetWheelTiles.
|
||
local function rows(wheel, pos)
|
||
return at(wheel, pos, 0), at(wheel, pos, 1), at(wheel, pos, 2)
|
||
end
|
||
|
||
-- Paylines in pokered's check order (SlotMachine_CheckForMatches): a
|
||
-- 3-coin bet tries both diagonals first, then falls into the 2-coin
|
||
-- checks (top row, bottom row), then the 1-coin middle row. The FIRST
|
||
-- matching line wins. Entries are row offsets from the bottom.
|
||
local LINES = {
|
||
{ 0, 1, 2, bet = 3 }, -- wheel1 bottom / wheel2 middle / wheel3 top
|
||
{ 2, 1, 0, bet = 3 }, -- wheel1 top / wheel2 middle / wheel3 bottom
|
||
{ 2, 2, 2, bet = 2 }, -- top row
|
||
{ 0, 0, 0, bet = 2 }, -- bottom row
|
||
{ 1, 1, 1, bet = 1 }, -- middle row
|
||
}
|
||
|
||
-- stops = {pos1, pos2, pos3} (1-based bottom-row positions); returns the
|
||
-- first matching line's payout+symbol, like SlotMachine_CheckForMatches.
|
||
function SlotMachine.evaluate(wheels, stops, bet)
|
||
for _, line in ipairs(LINES) do
|
||
if bet >= line.bet then
|
||
local a = at(wheels[1], stops[1], line[1])
|
||
local b = at(wheels[2], stops[2], line[2])
|
||
local c = at(wheels[3], stops[3], line[3])
|
||
if a == b and b == c then
|
||
return { payout = PAYOUT[a] or 15, symbol = a }
|
||
end
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- SlotMachine_StopWheel1Early: true = stop at this centred position.
|
||
-- Normally wheel 1 stops unless the centred middle symbol is a cherry.
|
||
-- In seven-and-bar mode pokered compares each visible tile with
|
||
-- `cp HIGH(SLOTS7)` / `jr c` -- never true, so it never stops early
|
||
-- (the wheel always slips through all four charges).
|
||
function SlotMachine.stopWheel1Early(wheels, pos1, sevenBar)
|
||
if sevenBar then return false end
|
||
local _, middle = rows(wheels[1], pos1)
|
||
return middle ~= "CHERRY"
|
||
end
|
||
|
||
-- SlotMachine_FindWheel1Wheel2Matches: can wheels 1 and 2, as placed,
|
||
-- still line up a payline given a good wheel 3? Pairs are checked in
|
||
-- pokered's order: bottom/bottom, bottom/middle, middle/middle,
|
||
-- top/middle, top/top (wheel 1 row first). Returns matched plus the
|
||
-- wheel-2 tile DE points at afterwards (the matched tile, or wheel 2's
|
||
-- bottom tile when nothing matched).
|
||
function SlotMachine.findWheel1Wheel2Matches(wheels, pos1, pos2)
|
||
local b1, m1, t1 = rows(wheels[1], pos1)
|
||
local b2, m2, t2 = rows(wheels[2], pos2)
|
||
if b2 == b1 then return true, b2 end
|
||
if m2 == b1 then return true, m2 end
|
||
if m2 == m1 then return true, m2 end
|
||
if m2 == t1 then return true, m2 end
|
||
if t2 == t1 then return true, t2 end
|
||
return false, b2
|
||
end
|
||
|
||
-- SlotMachine_StopWheel2Early: true = stop at this centred position.
|
||
-- Normally wheel 2 stops as soon as any wheel-1/2 match is lined up; in
|
||
-- seven-and-bar mode it stops when the matched (or bottom, when nothing
|
||
-- matched) wheel-2 symbol is a 7 or BAR.
|
||
function SlotMachine.stopWheel2Early(wheels, pos1, pos2, sevenBar)
|
||
local matched, tile = SlotMachine.findWheel1Wheel2Matches(wheels, pos1, pos2)
|
||
if sevenBar then
|
||
return tile == "7" or tile == "BAR"
|
||
end
|
||
return matched
|
||
end
|
||
|
||
-- One SlotMachine_CheckForMatches decision at the current stops:
|
||
-- "accept" -- pay out `win`
|
||
-- "roll" -- a match the flags forbid: roll wheel 3 down one symbol
|
||
-- and try again (does NOT consume the reroll counter)
|
||
-- "nomatch" -- nothing lined up (the caller consumes
|
||
-- wSlotMachineRerollCounter to keep rolling toward a match
|
||
-- when the flags allow a win)
|
||
function SlotMachine.checkForMatch(wheels, stops, bet, canWin, sevenBar)
|
||
local win = SlotMachine.evaluate(wheels, stops, bet)
|
||
if not win then return "nomatch" end
|
||
if not (canWin or sevenBar) then return "roll", win end
|
||
if not sevenBar and (win.symbol == "7" or win.symbol == "BAR") then
|
||
return "roll", win
|
||
end
|
||
return "accept", win
|
||
end
|
||
|
||
function SlotMachine.new(game, lucky)
|
||
local self = setmetatable({}, SlotMachine)
|
||
self.game = game
|
||
self.wheels = game.data.field.slotWheels
|
||
-- intro | bet | spinup | spin | reroll | flash | message | payout | onemore
|
||
-- PromptUserToPlaySlots asks "Want to play?" before the session starts.
|
||
self.stage = "intro"
|
||
self.yesno = 1 -- YES/NO cursor (1 = YES); wCurrentMenuItem
|
||
-- CoinMultiplierSlotMachineText lists ×3/×2/×1 with the cursor defaulting to
|
||
-- the top (wCurrentMenuItem 0), i.e. bet = 3 - menuItem.
|
||
self.betIndex = 0
|
||
self.bet = 3
|
||
self.payoutDisplay = 0 -- wPayoutCoins (shown in the top payout box)
|
||
self.flash = false
|
||
-- wSlotMachineWheelXOffset: 29 matches pokered after LoadSlotMachineTiles
|
||
-- draws offset $1c (wheel[15] centred on the bottom row).
|
||
self.offset = { 29, 29, 29 }
|
||
self.stopping = 0 -- wStoppingWhichSlotMachineWheel
|
||
self.slip = { 4, 4 } -- wSlotMachineWheel{1,2}SlipCounter
|
||
self.reroll = 4 -- wSlotMachineRerollCounter
|
||
self.frame = 0
|
||
self.message = nil
|
||
-- the per-visit lucky machine gets better seven-and-bar odds
|
||
-- (wSlotMachineSevenAndBarModeChance 250 vs 253)
|
||
self.sevenBarChance = lucky and 250 or 253
|
||
self.allowMatchesCounter = 0 -- wSlotMachineAllowMatchesCounter
|
||
-- wSlotMachineFlags bits (BIT_SLOTS_CAN_WIN / _WITH_7_OR_BAR)
|
||
self.canWin, self.sevenBar = false, false
|
||
return self
|
||
end
|
||
|
||
local function coins(self) return self.game.save.coins or 0 end
|
||
|
||
-- SlotMachine_SetFlags, rolled as each spin starts. Seven-and-bar mode,
|
||
-- once armed, is sticky (the asm returns early while the bit is set).
|
||
function SlotMachine:setFlags()
|
||
if self.sevenBar then return end
|
||
if self.allowMatchesCounter > 0 then
|
||
self.canWin = true
|
||
return
|
||
end
|
||
local r = love.math.random(0, 255)
|
||
if r == 0 then
|
||
-- 1/256: arm 60 guaranteed-winnable spins. This spin's flags are
|
||
-- left untouched (the asm returns before writing them).
|
||
self.allowMatchesCounter = 60
|
||
return
|
||
end
|
||
if r > self.sevenBarChance then
|
||
self.sevenBar = true
|
||
return
|
||
end
|
||
if r > 210 then
|
||
self.canWin = true
|
||
return
|
||
end
|
||
self.canWin = false
|
||
end
|
||
|
||
-- SlotMachine_AnimWheel: one half-symbol step; the offset wraps at 30.
|
||
function SlotMachine:animWheel(w)
|
||
self.offset[w] = (self.offset[w] + 1) % 30
|
||
end
|
||
|
||
local function posOf(offset) return (offset + 1) / 2 end
|
||
|
||
function SlotMachine:stops()
|
||
return { posOf(self.offset[1]), posOf(self.offset[2]), posOf(self.offset[3]) }
|
||
end
|
||
|
||
-- SlotMachine_StopOrAnimWheel1/2: a stopping wheel may halt only at odd
|
||
-- offsets; each centred position spends one slip charge on the wheel's
|
||
-- early-stop check, freezing the wheel when the check passes or (at the
|
||
-- next centred position) when the charges run out.
|
||
function SlotMachine:stopOrAnimWheel(w)
|
||
if self.stopping < w then
|
||
self:animWheel(w)
|
||
return
|
||
end
|
||
local o = self.offset[w]
|
||
if o % 2 == 0 then
|
||
self:animWheel(w)
|
||
return
|
||
end
|
||
if self.slip[w] == 0 then return end -- stopped
|
||
self.slip[w] = self.slip[w] - 1
|
||
local stop
|
||
if w == 1 then
|
||
stop = SlotMachine.stopWheel1Early(self.wheels, posOf(o), self.sevenBar)
|
||
else
|
||
stop = SlotMachine.stopWheel2Early(self.wheels, posOf(self.offset[1]),
|
||
posOf(o), self.sevenBar)
|
||
end
|
||
if stop then
|
||
self.slip[w] = 0
|
||
return
|
||
end
|
||
self:animWheel(w)
|
||
end
|
||
|
||
-- SlotMachine_StopOrAnimWheel3: no slip charges; stops at the next
|
||
-- centred position. Returns true when the spin is over.
|
||
function SlotMachine:stopOrAnimWheel3()
|
||
if self.stopping < 3 then
|
||
self:animWheel(3)
|
||
return false
|
||
end
|
||
if self.offset[3] % 2 == 1 then return true end
|
||
self:animWheel(3)
|
||
return false
|
||
end
|
||
|
||
-- SlotMachine_CheckForMatches at the current stops; either resolves the
|
||
-- spin or starts a one-symbol wheel-3 roll (stage "reroll").
|
||
function SlotMachine:checkForMatches()
|
||
local action, win = SlotMachine.checkForMatch(self.wheels, self:stops(),
|
||
self.bet, self.canWin,
|
||
self.sevenBar)
|
||
if action == "accept" then
|
||
self:resolveWin(win)
|
||
return
|
||
end
|
||
if action == "nomatch" then
|
||
if not (self.canWin or self.sevenBar) then
|
||
self:resolveLose()
|
||
return
|
||
end
|
||
self.reroll = self.reroll - 1
|
||
if self.reroll == 0 then
|
||
self:resolveLose()
|
||
return
|
||
end
|
||
end
|
||
-- .rollWheel3DownByOneSymbol: two half-steps, one per frame
|
||
self.stage = "reroll"
|
||
self.rerollSteps = 2
|
||
end
|
||
|
||
function SlotMachine:resolveWin(win)
|
||
local sym, pay = win.symbol, win.payout
|
||
-- SlotReward{300,100,8,15}Func side effects run first (before the flash),
|
||
-- and set b = the number of screen flashes.
|
||
local flashes
|
||
if sym == "7" then
|
||
Sound.play(self.game.data, "Get_Item2")
|
||
-- SlotReward300Func: "Yeah!", the jackpot always ends an
|
||
-- allow-matches streak, and half the time resets the luck flags
|
||
if love.math.random(0, 255) >= 128 then
|
||
self.canWin, self.sevenBar = false, false
|
||
end
|
||
self.allowMatchesCounter = 0
|
||
flashes = 20 -- b = $14
|
||
elseif sym == "BAR" then
|
||
Sound.play(self.game.data, "Get_Key_Item")
|
||
-- SlotReward100Func always clears the luck flags
|
||
self.canWin, self.sevenBar = false, false
|
||
flashes = 8 -- b = $8
|
||
else
|
||
-- SlotReward8Func/SlotReward15Func burn one allow-matches charge
|
||
if self.allowMatchesCounter > 0 then
|
||
self.allowMatchesCounter = self.allowMatchesCounter - 1
|
||
end
|
||
flashes = (pay == 8) and 2 or 4 -- b = $2 (cherry) / $4 (15)
|
||
end
|
||
self.win = win
|
||
self.payoutRemaining = pay
|
||
self.payoutDisplay = pay
|
||
-- SlotReward300Func prints "Yeah!" (text_pause) before the flash; the port
|
||
-- shows it in the box while the screen flashes. LinedUpText follows.
|
||
self.yeah = (sym == "7")
|
||
self.message = Strings("%s lined up!\nScored %d coins!", sym, pay)
|
||
-- .flashScreenLoop: flip rBGP, wait 5 frames, b times. The coins are not
|
||
-- credited until the player dismisses the "lined up" text (see startPayout).
|
||
self.stage = "flash"
|
||
self.flashLeft = flashes
|
||
self.flashTimer = 0
|
||
self.flash = false
|
||
end
|
||
|
||
function SlotMachine:resolveLose()
|
||
-- NotThisTimeText, then MainSlotMachineLoop asks "One more go?"
|
||
self.message = "Not this time!"
|
||
self.stage = "message"
|
||
self.afterMessage = "onemore"
|
||
end
|
||
|
||
-- MainSlotMachineLoop restart: reset the ×3/×2/×1 menu (wCurrentMenuItem 0
|
||
-- defaults the cursor to ×3) and clear the payout box.
|
||
function SlotMachine:enterBet()
|
||
self.stage = "bet"
|
||
self.betIndex = 0
|
||
self.bet = 3
|
||
self.message = nil
|
||
self.payoutDisplay = 0
|
||
end
|
||
|
||
-- OneMoreGoSlotMachineText + its YES/NO menu.
|
||
function SlotMachine:enterOneMore()
|
||
self.stage = "onemore"
|
||
self.yesno = 1
|
||
self.message = nil
|
||
self.payoutDisplay = 0
|
||
end
|
||
|
||
-- After a spin resolves: running out of coins ends the session (a 60-frame
|
||
-- delay then CloseTextDisplay), otherwise ask "One more go?".
|
||
function SlotMachine:afterSpin()
|
||
if coins(self) == 0 then
|
||
self.message = Strings("Darn!\nRan out of coins!")
|
||
self.stage = "message"
|
||
self.afterMessage = nil
|
||
self.exitTimer = 60
|
||
else
|
||
self:enterOneMore()
|
||
end
|
||
end
|
||
|
||
-- SlotMachine_PayCoinsToPlayer: credit one coin every 8 frames (4 for a
|
||
-- 7/BAR), a jingle per coin, and flip the object palette every 5 coins.
|
||
function SlotMachine:startPayout()
|
||
self.stage = "payout"
|
||
local sym = self.win and self.win.symbol
|
||
self.dripFrames = (sym == "7" or sym == "BAR") and 4 or 8
|
||
self.dripTimer = 0
|
||
self.dripFlash = 5 -- wAnimCounter
|
||
self.flash = false
|
||
end
|
||
|
||
-- YES/NO prompt shared by the intro ("Want to play?") and "One more go?".
|
||
function SlotMachine:updateYesNo(onYes)
|
||
local input = self.game.input
|
||
if input:wasPressed("up") or input:wasPressed("down") then
|
||
self.yesno = self.yesno == 1 and 2 or 1
|
||
elseif input:wasPressed("a") then
|
||
Sound.play(self.game.data, "Press_AB")
|
||
if self.yesno == 1 then onYes() else self.game.stack:pop() end
|
||
elseif input:wasPressed("b") then
|
||
Sound.play(self.game.data, "Press_AB")
|
||
self.game.stack:pop()
|
||
end
|
||
end
|
||
|
||
function SlotMachine:update(dt)
|
||
local input = self.game.input
|
||
local save = self.game.save
|
||
|
||
if self.stage == "intro" then
|
||
-- PromptUserToPlaySlots: "A slot machine! Want to play?"
|
||
self:updateYesNo(function() self:enterBet() end)
|
||
return
|
||
end
|
||
|
||
if self.stage == "message" then
|
||
if self.exitTimer then
|
||
-- OutOfCoinsSlotMachineText: DelayFrames 60, then leave
|
||
self.exitTimer = self.exitTimer - 1
|
||
if self.exitTimer <= 0 then self.game.stack:pop() end
|
||
return
|
||
end
|
||
if input:wasPressed("a") or input:wasPressed("b") then
|
||
Sound.play(self.game.data, "Press_AB")
|
||
local after = self.afterMessage
|
||
self.afterMessage = nil
|
||
if after == "payout" then
|
||
self:startPayout() -- WaitForTextScrollButtonPress -> pay
|
||
elseif after == "onemore" then
|
||
self:afterSpin()
|
||
else
|
||
self:enterBet() -- NotEnoughCoinsSlotMachineText -> menu
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
if self.stage == "onemore" then
|
||
self:updateYesNo(function() self:enterBet() end)
|
||
return
|
||
end
|
||
|
||
if self.stage == "flash" then
|
||
-- .flashScreenLoop: toggle the palette every 5 frames, b times
|
||
self.flashTimer = self.flashTimer + 1
|
||
if self.flashTimer >= 5 then
|
||
self.flashTimer = 0
|
||
self.flash = self.flash and false or "all"
|
||
self.flashLeft = self.flashLeft - 1
|
||
if self.flashLeft <= 0 then
|
||
self.flash = false
|
||
self.stage = "message"
|
||
self.afterMessage = "payout"
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
if self.stage == "payout" then
|
||
if (self.payoutRemaining or 0) <= 0 then
|
||
self.flash = false
|
||
self.payoutDisplay = 0
|
||
self:afterSpin()
|
||
return
|
||
end
|
||
self.dripTimer = self.dripTimer + 1
|
||
if self.dripTimer >= self.dripFrames then
|
||
self.dripTimer = 0
|
||
save.coins = math.min(9999, coins(self) + 1)
|
||
self.payoutRemaining = self.payoutRemaining - 1
|
||
self.payoutDisplay = self.payoutRemaining
|
||
Sound.play(self.game.data, "Slots_Reward")
|
||
self.dripFlash = self.dripFlash - 1
|
||
if self.dripFlash <= 0 then
|
||
self.dripFlash = 5
|
||
self.flash = self.flash and false or "reels" -- rOBP0 xor $40 flicker
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
if self.stage == "bet" then
|
||
if input:wasPressed("b") then
|
||
self.game.stack:pop()
|
||
return
|
||
end
|
||
-- vertical ×3/×2/×1 menu: UP toward ×3 (betIndex 0), DOWN toward ×1
|
||
if input:wasPressed("up") then self.betIndex = math.max(0, self.betIndex - 1) end
|
||
if input:wasPressed("down") then self.betIndex = math.min(2, self.betIndex + 1) end
|
||
self.bet = 3 - self.betIndex
|
||
if input:wasPressed("a") then
|
||
if coins(self) < self.bet then
|
||
self.message = Strings("Not enough\ncoins!")
|
||
self.afterMessage = "bet"
|
||
self.stage = "message"
|
||
return
|
||
end
|
||
save.coins = coins(self) - self.bet
|
||
self:setFlags()
|
||
self.stopping = 0
|
||
self.slip = { 4, 4 }
|
||
self.reroll = 4
|
||
self.frame = 0
|
||
self.spinupSteps = SPINUP_STEPS
|
||
self.stage = "spinup"
|
||
Sound.play(self.game.data, "Slots_New_Spin")
|
||
end
|
||
return
|
||
end
|
||
|
||
if self.stage == "spinup" then
|
||
-- SlotMachine_SpinWheels .loop1: 20 free steps before input is read
|
||
self.frame = self.frame + 1
|
||
if self.frame % STEP_FRAMES == 0 then
|
||
for w = 1, 3 do self:animWheel(w) end
|
||
self.spinupSteps = self.spinupSteps - 1
|
||
if self.spinupSteps == 0 then self.stage = "spin" end
|
||
end
|
||
return
|
||
end
|
||
|
||
if self.stage == "spin" then
|
||
-- SlotMachine_HandleInputWhileWheelsSpin: A stops the next wheel,
|
||
-- but is ignored while the previous wheel is still slipping
|
||
if input:wasPressed("a") then
|
||
local held = (self.stopping == 1 and self.slip[1] > 0)
|
||
or (self.stopping == 2 and self.slip[2] > 0)
|
||
if not held then
|
||
self.stopping = self.stopping + 1
|
||
Sound.play(self.game.data, "Slots_Stop_Wheel")
|
||
end
|
||
end
|
||
self.frame = self.frame + 1
|
||
if self.frame % STEP_FRAMES == 0 then
|
||
self:stopOrAnimWheel(1)
|
||
self:stopOrAnimWheel(2)
|
||
if self:stopOrAnimWheel3() then
|
||
self:checkForMatches()
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
if self.stage == "reroll" then
|
||
self:animWheel(3)
|
||
self.rerollSteps = self.rerollSteps - 1
|
||
if self.rerollSteps == 0 then
|
||
self:checkForMatches()
|
||
end
|
||
return
|
||
end
|
||
end
|
||
|
||
-- reel symbol screen x (wBaseCoordX $30/$50/$70 minus the OAM 8px offset)
|
||
-- and the reel window's vertical clip (rows 4-9 of the machine frame).
|
||
local SYM_X = { 40, 72, 104 }
|
||
local WIN_TOP, WIN_BOT = 32, 80
|
||
|
||
-- Lazily load the symbol sheet (symbols.png, OAM wheel tiles) and the static
|
||
-- machine frame sheet (red_slots_1.png, a tileCols-wide tile atlas).
|
||
function SlotMachine:loadArt()
|
||
local art = self.game.data.field.slotSymbols
|
||
if not art then return nil end
|
||
if not self.symbolImg and not self.symbolImgFailed then
|
||
local ok, img = pcall(love.graphics.newImage, art.sheet)
|
||
if ok then self.symbolImg = img else self.symbolImgFailed = true end
|
||
end
|
||
if art.tilemap and not self.bgImg and not self.bgImgFailed then
|
||
local ok, img = pcall(love.graphics.newImage, art.tilemap.sheet)
|
||
if ok then self.bgImg = img else self.bgImgFailed = true end
|
||
end
|
||
return art
|
||
end
|
||
|
||
-- The three spinning strips over the reel windows. Each strip scrolls in
|
||
-- half-symbol (8px) steps like SlotMachine_AnimWheel; even drawn offsets show
|
||
-- three full symbols with wheel[(o+1)/2] on the bottom row.
|
||
function SlotMachine:drawReels(art)
|
||
for w = 1, 3 do
|
||
local x = SYM_X[w]
|
||
local wheel = self.wheels[w]
|
||
local period = math.max(#wheel - 3, 1) -- 15 real symbols + 3 wrap entries
|
||
local d = (self.offset[w] - 1) % 30 -- drawn strip offset
|
||
local k = math.floor(d / 2)
|
||
for j = k - 1, k + 3 do
|
||
local yTop = (WIN_BOT - 16) - 16 * j + 8 * d
|
||
local clipTop = math.max(yTop, WIN_TOP)
|
||
local clipBot = math.min(yTop + 16, WIN_BOT)
|
||
if clipBot > clipTop then
|
||
local sym = wheel[(j % period) + 1]
|
||
local rect = self.symbolImg and art.symbols[sym]
|
||
if rect then
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.draw(self.symbolImg,
|
||
love.graphics.newQuad(rect.x, rect.y + (clipTop - yTop),
|
||
rect.w, clipBot - clipTop,
|
||
self.symbolImg:getDimensions()),
|
||
x, clipTop)
|
||
elseif yTop >= WIN_TOP and yTop + 16 <= WIN_BOT then
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw(SHORT[sym] or sym, x, yTop)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
-- The lower dialogue box and, when a prompt is up, the ×3/×2/×1 or YES/NO
|
||
-- menu on the right (like MainSlotMachineLoop's TextBoxBorder + menus).
|
||
function SlotMachine:drawBottom()
|
||
local lines
|
||
if self.stage == "intro" then
|
||
lines = { "A slot machine!", "Want to play?" }
|
||
elseif self.stage == "bet" then
|
||
lines = { "Bet how many", "coins?" }
|
||
elseif self.stage == "onemore" then
|
||
lines = { "One more", "go?" }
|
||
elseif self.stage == "flash" then
|
||
lines = self.yeah and { "Yeah!" } or { "Start!" }
|
||
elseif self.stage == "spinup" or self.stage == "spin"
|
||
or self.stage == "reroll" then
|
||
lines = { "Start!" }
|
||
elseif self.message then -- message / payout: the wrapped prompt text
|
||
lines = {}
|
||
for line in (self.message .. "\n"):gmatch("(.-)\n") do
|
||
if line ~= "" then lines[#lines + 1] = line end
|
||
end
|
||
end
|
||
if not lines then return end
|
||
Font.drawBox(0, 12, 20, 6)
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw(lines[1] or "", 8, 14 * 8)
|
||
Font.draw(lines[2] or "", 8, 16 * 8)
|
||
if self.stage == "bet" then
|
||
Font.drawBox(14, 11, 6, 5)
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw("×3", 16 * 8, 12 * 8)
|
||
Font.draw("×2", 16 * 8, 13 * 8)
|
||
Font.draw("×1", 16 * 8, 14 * 8)
|
||
Font.drawCode(0xED, 15 * 8, (12 + self.betIndex) * 8)
|
||
elseif self.stage == "intro" or self.stage == "onemore" then
|
||
-- "One more go?" sits at the right of the box (hlcoord 14,12); the longer
|
||
-- "A slot machine!" prompt would clip against it, so the intro's YES/NO
|
||
-- floats above the reels instead.
|
||
local by = self.stage == "intro" and 6 or 11
|
||
Font.drawBox(13, by, 6, 5)
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw(Strings("YES"), 15 * 8, (by + 1) * 8)
|
||
Font.draw(Strings("NO"), 15 * 8, (by + 2) * 8)
|
||
Font.drawCode(0xED, 14 * 8, (by + 1 + (self.yesno == 1 and 0 or 1)) * 8)
|
||
end
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
function SlotMachine:draw()
|
||
local art = self:loadArt()
|
||
local tm = art and art.tilemap
|
||
if not (tm and self.bgImg) then return self:drawPlain(art) end
|
||
|
||
-- static machine frame (SlotMachineMap): blit each tile id from the
|
||
-- red_slots_1.png tile atlas; below it stays white for the dialogue box
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||
local iw, ih = self.bgImg:getDimensions()
|
||
self.bgQuads = self.bgQuads or {}
|
||
for row = 1, tm.rows do
|
||
local cells = tm.tiles[row]
|
||
for col = 1, tm.cols do
|
||
local id = cells[col]
|
||
local q = self.bgQuads[id]
|
||
if not q then
|
||
q = love.graphics.newQuad((id % tm.tileCols) * 8,
|
||
math.floor(id / tm.tileCols) * 8, 8, 8, iw, ih)
|
||
self.bgQuads[id] = q
|
||
end
|
||
love.graphics.draw(self.bgImg, q, (col - 1) * 8, (row - 1) * 8)
|
||
end
|
||
end
|
||
|
||
self:drawReels(art)
|
||
|
||
-- credit / payout numbers (SlotMachine_PrintCreditCoins @5,1 as BCD, and
|
||
-- SlotMachine_PrintPayoutCoins @11,1 with leading zeroes)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 40, 8, 32, 8)
|
||
love.graphics.rectangle("fill", 88, 8, 32, 8)
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw(("%4d"):format(math.min(9999, coins(self))), 40, 8)
|
||
Font.draw(("%04d"):format(self.payoutDisplay or 0), 88, 8)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
|
||
self:drawBottom()
|
||
end
|
||
|
||
-- Fallback layout for stale builds without the extracted machine frame.
|
||
function SlotMachine:drawPlain(art)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw(Strings("SLOT MACHINE"), 32, 4)
|
||
Font.draw(Strings("COINS %4d", coins(self)), 8, 16)
|
||
if art then self:drawReels(art) end
|
||
love.graphics.setColor(0, 0, 0, 1)
|
||
Font.draw(">", 12, 56)
|
||
Font.draw("<", 140, 56)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
self:drawBottom()
|
||
end
|
||
|
||
return SlotMachine
|