mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
feat: surfing minigame overhaul + authentic Game Corner rendering
Surfing minigame: - Add title screen (ROUTINE_TITLE) with Pikachu intro, logo banner, instructions - Add GLSL HBlank wave distortion shader, OAM water spray/splash sprites - Add multi-path asset loader, isMinigame/isFixedSpeed flags, crash recovery fixes - Extract SurfingPikachu graphics + composite title_bg.png from ROM Game Corner (built visual layer from ROM assets; logic existed, rendering was placeholder): - SlotMachine: GBC tilemap background, authentic reel symbol sprites, Golem/Chansey near-miss animations, lit/unlit lights, payout panel - CardFlip: GBC tilemap board, hardware-accurate card flip sequence, OAM cursor frame - RomExtractorGen2: extract slots + card_flip sprite sheets and tilemaps Tests: SurfingMinigame units 8-13; 500-spin SlotMachine and 500-hand CardFlip stress tests
This commit is contained in:
+535
-80
@@ -219,10 +219,16 @@ local TWO_LINES = {
|
||||
|
||||
function SlotMachine.matchFirstTwo(bet, r1, r2)
|
||||
local building = SlotMachine.NO_MATCH
|
||||
local matchingSevens = false
|
||||
for _, line in ipairs(TWO_LINES[(bet or 0) % 4] or {}) do
|
||||
if r1[line[1]] == r2[line[2]] then building = r1[line[1]] end
|
||||
if r1[line[1]] == r2[line[2]] then
|
||||
building = r1[line[1]]
|
||||
if building == SlotMachine.SEVEN then
|
||||
matchingSevens = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return building, building == SlotMachine.SEVEN
|
||||
return building, matchingSevens
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------- bias
|
||||
@@ -403,13 +409,14 @@ end
|
||||
-- matching SEVENs on a line this bet buys.
|
||||
function SlotMachine.spinReel2ToSevens(position, bet, stopped1)
|
||||
local strip = SlotMachine.REELS[2]
|
||||
local pos = SlotMachine.advance(position)
|
||||
for _ = 1, SEARCH_LIMIT do
|
||||
local window = { SlotMachine.window(strip, position) }
|
||||
local window = { SlotMachine.window(strip, pos) }
|
||||
local building, sevens = SlotMachine.matchFirstTwo(bet, stopped1, window)
|
||||
if building ~= SlotMachine.NO_MATCH and sevens then return position end
|
||||
position = SlotMachine.advance(position)
|
||||
if building ~= SlotMachine.NO_MATCH and sevens then return pos end
|
||||
pos = SlotMachine.advance(pos)
|
||||
end
|
||||
return position
|
||||
return pos
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------- reel 3's theatre
|
||||
@@ -580,8 +587,8 @@ end
|
||||
-- ------------------------------------------------------------------ layout
|
||||
local COINS_X, COINS_Y = 5, 1
|
||||
local PAYOUT_X, PAYOUT_Y = 11, 1
|
||||
-- REEL_X_COORD / TILE_WIDTH.
|
||||
local REEL_X = { 6, 10, 14 }
|
||||
-- REEL_X_COORD / TILE_WIDTH (5, 9, 13) for the 3 reel apertures (columns 5-6, 9-10, 13-14)
|
||||
local REEL_X = { 5, 9, 13 }
|
||||
-- Slots_UpdateReelPositionAndOAM's y ladder, converted from OAM space (which
|
||||
-- sits 16px above the screen) to tile rows: bottom, middle, top, and the
|
||||
-- fourth symbol that only half shows.
|
||||
@@ -617,7 +624,7 @@ SlotMachine.TEXTS = TEXTS
|
||||
-- _SlotsLinedUpText: "lined up!" / "Won @<wStringBuffer2> coins!", with the
|
||||
-- matched symbol's 2x2 tiles printed to its left by .Text_PrintPayout.
|
||||
local function linedUpLines(payout)
|
||||
return { "lined up!", ("Won %d coins!"):format(payout) }
|
||||
return { " lined up!", ("Won %d coins!"):format(payout) }
|
||||
end
|
||||
|
||||
-- Slots_PlaySFX's labels, spelled the pokegold way (Sound.GEN2_ALIASES is what
|
||||
@@ -729,30 +736,41 @@ function SlotMachine:startSpin()
|
||||
self.reel = 1
|
||||
self.delay = 32
|
||||
self.message = TEXTS.start
|
||||
self.stopped = nil
|
||||
self.matched = SlotMachine.NO_MATCH
|
||||
self.matchingSevens = false
|
||||
self.reel3Action = nil
|
||||
self.golemAnim = nil
|
||||
self.chanseyAnim = nil
|
||||
self.reel2Pause = nil
|
||||
for i = 1, 3 do
|
||||
self.rate[i] = 4 -- ReelAction_NormalRate
|
||||
self.stops[i] = nil
|
||||
self.distance[i] = 0
|
||||
end
|
||||
self:sfx(SFX_START)
|
||||
end
|
||||
|
||||
-- Slots_SpinReel, per reel per frame: the action jumptable runs only on a slot
|
||||
-- boundary, and the position advances when the distance's low nibble wraps.
|
||||
-- boundary, and the position advances whenever 16 pixels are traversed.
|
||||
function SlotMachine:spinReels()
|
||||
for i = 1, 3 do
|
||||
local rate = self.rate[i]
|
||||
if rate > 0 then
|
||||
self.distance[i] = (self.distance[i] + rate) % 256
|
||||
if self.distance[i] % 16 == 0 then
|
||||
self.distance[i] = (self.distance[i] or 0) + rate
|
||||
while self.distance[i] >= 16 do
|
||||
self.distance[i] = self.distance[i] - 16
|
||||
self.positions[i] = SlotMachine.advance(self.positions[i])
|
||||
-- A reel with a resting slot chosen stops the moment it reaches it.
|
||||
if self.stops[i] and self.positions[i] == self.stops[i] then
|
||||
self.rate[i] = 0
|
||||
self.distance[i] = 0
|
||||
self.stopped = self.stopped or {}
|
||||
self.stopped[i] = { SlotMachine.window(SlotMachine.REELS[i],
|
||||
self.positions[i]) }
|
||||
self:sfx(SFX_STOP)
|
||||
self:reelStopped(i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -775,11 +793,17 @@ function SlotMachine:pressStop()
|
||||
self.stops[1] = SlotMachine.stopReel1(here, self.bias)
|
||||
elseif i == 2 then
|
||||
local r1 = self.stopped[1]
|
||||
if SlotMachine.reel2SkipsToSeven(self.bet, self.bias, r1, self.random) then
|
||||
local hereWindow = { SlotMachine.window(SlotMachine.REELS[2], here) }
|
||||
local _, hereSevens = SlotMachine.matchFirstTwo(self.bet, r1, hereWindow)
|
||||
local doSkip = SlotMachine.reel2SkipsToSeven(self.bet, self.bias, r1, self.random)
|
||||
if hereSevens and not doSkip then
|
||||
self.stops[2] = here
|
||||
elseif doSkip then
|
||||
-- ReelAction_SetUpReel2SkipTo7 pauses the reel for 32 frames and then
|
||||
-- fast-spins it at double rate; the pause is the tell.
|
||||
self.stops[2] = SlotMachine.spinReel2ToSevens(here, self.bet, r1)
|
||||
self.rate[2] = 8
|
||||
self.reel2Pause = 32
|
||||
self.rate[2] = 0 -- paused during the 32-frame tell
|
||||
else
|
||||
self.stops[2] = SlotMachine.stopReel2(here, self.bias, self.bet, r1)
|
||||
end
|
||||
@@ -789,32 +813,54 @@ function SlotMachine:pressStop()
|
||||
self.matchingSevens = sevens
|
||||
local action = SlotMachine.reel3Action(sevens, self.bias, self.random)
|
||||
self.reel3Action = action
|
||||
if action == SlotMachine.REEL3_STOP then
|
||||
if action == SlotMachine.REEL3_STOP or action == "stop" then
|
||||
self.stops[3] = SlotMachine.stopReel3(here, self.bias, self.bet, r1, r2)
|
||||
elseif action == SlotMachine.REEL3_SLOW then
|
||||
elseif action == SlotMachine.REEL3_SLOW or action == "slowAdvance" then
|
||||
self.stops[3] = SlotMachine.slowAdvance(here, self.bias, self.bet, r1, r2)
|
||||
self.rate[3] = 1 -- ReelAction_QuarterRate
|
||||
elseif action == SlotMachine.REEL3_GOLEM then
|
||||
self.rate[3] = 1 -- ReelAction_QuarterRate slow crawl
|
||||
elseif action == SlotMachine.REEL3_GOLEM or action == "golem" then
|
||||
local count = SlotMachine.golemCount(here, self.bias, self.bet, r1, r2,
|
||||
self.random)
|
||||
if count == 0 then count = 3 end
|
||||
local target = here
|
||||
for _ = 1, count do target = SlotMachine.advance(target) end
|
||||
self.stops[3] = target
|
||||
self.golems = count
|
||||
self.rate[3] = 8
|
||||
self.golemAnim = {
|
||||
count = count,
|
||||
state = "falling",
|
||||
var1 = 48,
|
||||
x = 100,
|
||||
y = 44 - 112,
|
||||
animFrame = 0,
|
||||
animTimer = 0,
|
||||
}
|
||||
self.rate[3] = 0 -- reel 3 stepped by each golem impact
|
||||
else
|
||||
local target = SlotMachine.eggDrops(here, self.bet, r1, r2)
|
||||
self.stops[3] = target
|
||||
self.rate[3] = 16 -- ReelAction_QuadrupleRate, the egg drop
|
||||
self.chanseyAnim = {
|
||||
state = "walking",
|
||||
xcoord = 0,
|
||||
x = -24,
|
||||
y = 44,
|
||||
animTimer = 0,
|
||||
animPose = 0,
|
||||
}
|
||||
self.rate[3] = 0 -- paused until Chansey drops egg
|
||||
end
|
||||
end
|
||||
-- A reel already sitting on its resting slot has nowhere to turn.
|
||||
if self.stops[i] == self.positions[i] then
|
||||
self.rate[i] = 0
|
||||
self.stopped = self.stopped or {}
|
||||
self.stopped[i] = self:reelWindow(i)
|
||||
self:sfx(SFX_STOP)
|
||||
self:reelStopped(i)
|
||||
-- Only trigger immediate halt if no active pause tell or special animation is running.
|
||||
if not self.reel2Pause and not self.golemAnim and not self.chanseyAnim then
|
||||
if self.stops[i] == self.positions[i] then
|
||||
self.rate[i] = 0
|
||||
self.distance[i] = 0
|
||||
self.stopped = self.stopped or {}
|
||||
self.stopped[i] = self:reelWindow(i)
|
||||
self:sfx(SFX_STOP)
|
||||
self:reelStopped(i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -823,6 +869,9 @@ function SlotMachine:reelStopped(i)
|
||||
self.reel = i + 1
|
||||
return
|
||||
end
|
||||
self.golemAnim = nil
|
||||
self.chanseyAnim = nil
|
||||
self.reel2Pause = nil
|
||||
-- SlotsAction_FlashIfWin: a win flashes the object palette for 16 frames
|
||||
-- before the payout is counted out; a loss skips straight past it.
|
||||
local r1, r2, r3 = self.stopped[1], self.stopped[2], self.stopped[3]
|
||||
@@ -906,13 +955,134 @@ function SlotMachine:update(_dt)
|
||||
if phase == "spinning" then
|
||||
-- SlotsAction_WaitStart clears hJoypadSum first, so a press held from the
|
||||
-- bet menu cannot stop reel one.
|
||||
if self.delay > 0 then
|
||||
if (self.delay or 0) > 0 then
|
||||
self.delay = self.delay - 1
|
||||
self:spinReels()
|
||||
return
|
||||
end
|
||||
if self.reel2Pause and self.reel2Pause > 0 then
|
||||
self.reel2Pause = self.reel2Pause - 1
|
||||
if self.reel2Pause <= 0 then
|
||||
self.reel2Pause = nil
|
||||
self.rate[2] = 8 -- resume fast-spin after the 32-frame tell
|
||||
end
|
||||
end
|
||||
|
||||
if self.golemAnim then
|
||||
local g = self.golemAnim
|
||||
-- Cycle animation frames every 8 ticks (~7.5 fps) matching the original pacing
|
||||
g.animTimer = (g.animTimer or 0) + 1
|
||||
if g.animTimer >= 8 then
|
||||
g.animTimer = 0
|
||||
g.animFrame = ((g.animFrame or 0) + 1) % 4
|
||||
end
|
||||
|
||||
if g.state == "falling" then
|
||||
if g.var1 > 32 then
|
||||
g.var1 = g.var1 - 1
|
||||
local angle = (g.var1 * math.pi) / 32
|
||||
local yOffset = math.floor(112 * math.sin(angle) + 0.5)
|
||||
g.y = 44 + yOffset
|
||||
g.x = 100
|
||||
else
|
||||
-- Landed on Reel 3!
|
||||
g.y = 44
|
||||
g.x = 100
|
||||
g.state = "rolling"
|
||||
g.xoffset = 0
|
||||
g.animTimer = 0
|
||||
g.animFrame = 0
|
||||
self:sfx("Sfx_PlacePuzzlePieceDown")
|
||||
-- Advance reel 3 by 1 slot per Golem impact
|
||||
self.positions[3] = SlotMachine.advance(self.positions[3])
|
||||
self.distance[3] = 0
|
||||
end
|
||||
elseif g.state == "rolling" then
|
||||
g.xoffset = (g.xoffset or 0) + 1
|
||||
g.x = 100 - g.xoffset
|
||||
|
||||
if g.xoffset >= 88 then
|
||||
-- Rolled past reel 1 (100 - 88 = 12px) off the screen -> restart or end
|
||||
g.count = g.count - 1
|
||||
if g.count > 0 then
|
||||
g.state = "falling"
|
||||
g.var1 = 48
|
||||
g.x = 100
|
||||
g.y = 44 - 112
|
||||
else
|
||||
-- All Golems finished; halt reel 3 at target
|
||||
self.golemAnim = nil
|
||||
self.rate[3] = 0
|
||||
self.distance[3] = 0
|
||||
self.stopped = self.stopped or {}
|
||||
self.stopped[3] = self:reelWindow(3)
|
||||
self:sfx(SFX_STOP)
|
||||
self:reelStopped(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.chanseyAnim then
|
||||
local c = self.chanseyAnim
|
||||
if c.state == "walking" then
|
||||
c.xcoord = (c.xcoord or 0) + 1
|
||||
c.x = c.xcoord - 24
|
||||
c.y = 44
|
||||
|
||||
-- Cycle walking poses 0->1->2->3 (maps to Chansey 1->2->3->4) every 6 frames
|
||||
c.animTimer = (c.animTimer or 0) + 1
|
||||
if c.animTimer >= 6 then
|
||||
c.animTimer = 0
|
||||
c.animPose = ((c.animPose or 0) + 1) % 4
|
||||
end
|
||||
|
||||
if c.xcoord % 16 == 0 then self:sfx("Sfx_JumpOverLedge") end
|
||||
|
||||
if c.x >= 88 then
|
||||
-- Reached reel 3! Switch to tell pause (pose 4 = arm raised)
|
||||
c.x = 88
|
||||
c.state = "egg_pause"
|
||||
c.delay = 14
|
||||
c.animPose = 3
|
||||
end
|
||||
elseif c.state == "egg_pause" then
|
||||
c.delay = (c.delay or 14) - 1
|
||||
if c.delay <= 0 then
|
||||
-- Switch to Chansey 5 (egg drop pose) and spawn egg
|
||||
c.animPose = 4
|
||||
c.state = "egg_drop"
|
||||
c.eggTimer = 0
|
||||
c.eggStartX = c.x + 14
|
||||
c.eggStartY = c.y + 8
|
||||
c.eggX = c.eggStartX
|
||||
c.eggY = c.eggStartY
|
||||
c.eggVisible = true
|
||||
self:sfx("Sfx_Present")
|
||||
end
|
||||
elseif c.state == "egg_drop" then
|
||||
c.eggTimer = (c.eggTimer or 0) + 1
|
||||
local t = c.eggTimer / 16
|
||||
if t > 1 then t = 1 end
|
||||
c.eggX = c.eggStartX + t * (108 - c.eggStartX)
|
||||
c.eggY = c.eggStartY + t * (56 - c.eggStartY) - math.sin(t * math.pi) * 8
|
||||
|
||||
if c.eggTimer >= 16 then
|
||||
-- Egg landed on Reel 3!
|
||||
c.eggVisible = false
|
||||
c.state = "spinning"
|
||||
self:sfx("Sfx_PlacePuzzlePieceDown")
|
||||
self.rate[3] = 16 -- fast drop reel 3 to jackpot
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.message = nil
|
||||
if input:wasPressed("a") then self:pressStop() end
|
||||
if input:wasPressed("a") then
|
||||
if not self.stops[self.reel] and not self.reel2Pause and not self.golemAnim and not self.chanseyAnim then
|
||||
self:pressStop()
|
||||
end
|
||||
end
|
||||
self:spinReels()
|
||||
return
|
||||
end
|
||||
@@ -959,57 +1129,302 @@ end
|
||||
|
||||
-- ------------------------------------------------------------------- draw
|
||||
--
|
||||
-- The cart's reel art is unextracted (see the header), so a symbol draws as its
|
||||
-- two-letter label inside a 2x2 cell. A `slots` entry in menu_gfx.lua switches
|
||||
-- this to the real tiles without any other change.
|
||||
function SlotMachine:sheet()
|
||||
if self.sheetCache == nil then
|
||||
local data = self.game and self.game.data
|
||||
local gfx = data and data.gen2MenuGfx and data.gen2MenuGfx.slots
|
||||
if gfx and gfx.image then
|
||||
local TileSheet = require("src.ui.gen2.TileSheet")
|
||||
self.sheetCache = TileSheet.new({ path = gfx.image, wide = gfx.wide or 16,
|
||||
firstTile = gfx.firstTile or 0 })
|
||||
-- Authentic Color Game Boy palettes and tile graphics matching pret/pokegold
|
||||
local TileSheet = require("src.ui.gen2.TileSheet")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
|
||||
local GBC_PALS = {
|
||||
bg = {
|
||||
[0] = { { 255, 255, 255 }, { 198, 206, 231 }, { 198, 198, 74 }, { 0, 0, 0 } }, -- 0: Base Frame
|
||||
[1] = { { 255, 255, 255 }, { 247, 82, 49 }, { 198, 198, 74 }, { 0, 0, 0 } }, -- 1: Vileplume / Active Lights
|
||||
[2] = { { 255, 255, 255 }, { 123, 255, 0 }, { 198, 198, 74 }, { 0, 0, 0 } }, -- 2: Bet 3 Indicators
|
||||
[3] = { { 255, 255, 255 }, { 255, 123, 255 }, { 198, 198, 74 }, { 0, 0, 0 } }, -- 3: Bet 2 Indicators
|
||||
[4] = { { 255, 255, 255 }, { 123, 173, 255 }, { 198, 198, 74 }, { 0, 0, 0 } }, -- 4: Bet 1 Indicators
|
||||
[5] = { { 255, 255, 90 }, { 255, 255, 49 }, { 198, 198, 74 }, { 0, 0, 0 } }, -- 5: Yellow Highlights
|
||||
[6] = { { 255, 255, 255 }, { 132, 156, 239 }, { 206, 181, 0 }, { 0, 0, 0 } }, -- 6: Textbox frame
|
||||
[7] = { { 255, 255, 255 }, { 173, 173, 173 }, { 107, 107, 107 }, { 0, 0, 0 } }, -- 7: Inactive / Gray
|
||||
},
|
||||
obj = {
|
||||
[0] = { { 255, 255, 255 }, { 247, 82, 49 }, { 255, 0, 0 }, { 0, 0, 0 } }, -- 0: Seven (Red)
|
||||
[1] = { { 255, 255, 255 }, { 99, 206, 8 }, { 41, 115, 0 }, { 0, 0, 0 } }, -- 1: Pokeball (Green/Red)
|
||||
[2] = { { 255, 255, 255 }, { 99, 206, 8 }, { 247, 82, 49 }, { 0, 0, 0 } }, -- 2: Cherry
|
||||
[3] = { { 255, 255, 255 }, { 255, 255, 49 }, { 165, 123, 24 }, { 0, 0, 0 } }, -- 3: Pikachu (Yellow)
|
||||
[4] = { { 255, 255, 255 }, { 255, 255, 49 }, { 123, 173, 255 }, { 0, 0, 0 } }, -- 4: Squirtle (Blue/Yellow)
|
||||
[5] = { { 255, 255, 255 }, { 255, 255, 49 }, { 165, 123, 24 }, { 0, 0, 0 } }, -- 5: Staryu / Golem (Rock)
|
||||
[6] = { { 255, 255, 255 }, { 255, 198, 173 }, { 255, 107, 255 }, { 0, 0, 0 } }, -- 6: Chansey (Pink)
|
||||
[7] = { { 255, 255, 255 }, { 255, 255, 255 }, { 0, 0, 0 }, { 0, 0, 0 } }, -- 7: Flashing
|
||||
}
|
||||
}
|
||||
|
||||
local TILEMAP = nil
|
||||
local function getTilemap()
|
||||
if TILEMAP == nil then
|
||||
local path = "assets/generated/slots/gold_slots.tilemap"
|
||||
local f = io.open(path, "rb")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
TILEMAP = {}
|
||||
for i = 1, #data do
|
||||
TILEMAP[i] = string.byte(data, i)
|
||||
end
|
||||
else
|
||||
self.sheetCache = false
|
||||
TILEMAP = false
|
||||
end
|
||||
end
|
||||
return self.sheetCache or nil
|
||||
return TILEMAP or nil
|
||||
end
|
||||
|
||||
local function cell(tx, ty, label)
|
||||
local G = love.graphics
|
||||
G.setColor(0, 0, 0, 1)
|
||||
G.rectangle("line", tx * 8, ty * 8, 16, 16)
|
||||
Chrome.print(label, tx, ty + 1)
|
||||
function SlotMachine:sheets()
|
||||
if self.sheet1 == nil then
|
||||
self.sheet1 = TileSheet.new({ path = "assets/generated/slots/gold_slots_1.png", wide = 2, firstTile = 0 })
|
||||
self.sheet2 = TileSheet.new({ path = "assets/generated/slots/gold_slots_2.png", wide = 2, firstTile = 0 })
|
||||
self.sheet3 = TileSheet.new({ path = "assets/generated/slots/gold_slots_3.png", wide = 3, firstTile = 0 })
|
||||
end
|
||||
return self.sheet1, self.sheet2, self.sheet3
|
||||
end
|
||||
|
||||
function SlotMachine:drawBackground()
|
||||
local s1, s2 = self:sheets()
|
||||
local tm = getTilemap()
|
||||
if not tm or not s1:available() then
|
||||
-- Fallback simple background if assets unavailable
|
||||
Chrome.clear()
|
||||
return
|
||||
end
|
||||
|
||||
local bet = self.bet or 0
|
||||
|
||||
for ty = 0, 11 do
|
||||
for tx = 0, 19 do
|
||||
local idx = ty * 20 + tx + 1
|
||||
local tileId = tm[idx]
|
||||
|
||||
-- Palette attribution matching _CGB_SlotMachine
|
||||
local pal = 0
|
||||
if (tx <= 2 or tx >= 17) and ty >= 2 and ty <= 11 then
|
||||
if ty >= 6 and ty <= 7 then pal = 4
|
||||
elseif ty >= 4 and ty <= 9 then pal = 3
|
||||
else pal = 2 end
|
||||
elseif tx >= 4 and tx <= 15 and ty >= 2 and ty <= 3 then
|
||||
pal = 1 -- Vileplume
|
||||
elseif (tx == 3 or tx == 16) and ty >= 2 and ty <= 11 then
|
||||
local isLit = false
|
||||
if ty == 6 or ty == 7 then isLit = (bet >= 1)
|
||||
elseif ty == 4 or ty == 5 or ty == 8 or ty == 9 then isLit = (bet >= 2)
|
||||
elseif ty == 2 or ty == 3 or ty == 10 or ty == 11 then isLit = (bet >= 3)
|
||||
end
|
||||
if isLit then
|
||||
pal = 1
|
||||
-- Use lit lights tile
|
||||
if tileId == 0x23 then tileId = 0x14
|
||||
elseif tileId == 0x24 then tileId = 0x15 end
|
||||
else
|
||||
pal = 0
|
||||
end
|
||||
end
|
||||
|
||||
local colors = GBC_PALS.bg[pal]
|
||||
s1.palette = colors
|
||||
s2.palette = colors
|
||||
|
||||
if tileId < 0x25 then
|
||||
s1:draw(tileId, tx, ty)
|
||||
else
|
||||
s2:draw(tileId - 0x25, tx, ty)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SlotMachine:drawReels()
|
||||
local _, s2 = self:sheets()
|
||||
local G = love.graphics
|
||||
|
||||
for i = 1, 3 do
|
||||
local strip = SlotMachine.REELS[i]
|
||||
local position = self.positions[i]
|
||||
-- .LoadOAM reads FOUR consecutive strip entries from REEL_POSITION and lays
|
||||
-- them bottom upward, which is what the three repeated entries at the end
|
||||
-- of each strip are for: position 14 reads indices 14, 15, 16 and 17
|
||||
-- without wrapping. Only the lower three are on a pay line; the fourth
|
||||
-- half-shows at the top of the window.
|
||||
for row = 1, 4 do
|
||||
local symbol = strip[position + row]
|
||||
cell(REEL_X[i], REEL_ROW[row], SlotMachine.LABELS[symbol] or "?")
|
||||
local pos = self.positions[i]
|
||||
local a = pos
|
||||
if a == 0 then a = 0x0f end
|
||||
a = (a - 1) % 16
|
||||
local rx = REEL_X[i] * 8
|
||||
local dy = math.floor(self.distance[i] or 0)
|
||||
|
||||
-- Draw 4 consecutive 2x2 symbols from bottom to top, exactly matching SlotMachine.window
|
||||
for row = 0, 3 do
|
||||
local sym = strip[a + row + 1]
|
||||
local py = 64 - (row * 16) + dy
|
||||
local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0]
|
||||
s2.palette = pal
|
||||
|
||||
-- 2x2 tiles in 2-wide sheet:
|
||||
-- sym + 0 = top-left (col 0, row 0)
|
||||
-- sym + 1 = top-right (col 1, row 0)
|
||||
-- sym + 2 = bottom-left (col 0, row 1)
|
||||
-- sym + 3 = bottom-right (col 1, row 1)
|
||||
local t0 = s2:quad(sym + 0)
|
||||
local t1 = s2:quad(sym + 1)
|
||||
local t2 = s2:quad(sym + 2)
|
||||
local t3 = s2:quad(sym + 3)
|
||||
local img = s2:image()
|
||||
|
||||
if img and t0 and t1 and t2 and t3 then
|
||||
local function drawSym()
|
||||
G.draw(img, t0, rx, py)
|
||||
G.draw(img, t1, rx + 8, py)
|
||||
G.draw(img, t2, rx, py + 8)
|
||||
G.draw(img, t3, rx + 8, py + 8)
|
||||
end
|
||||
if GbcPalette.available() then
|
||||
GbcPalette.with(pal, drawSym)
|
||||
else
|
||||
drawSym()
|
||||
end
|
||||
else
|
||||
-- Fallback label
|
||||
cell(REEL_X[i], REEL_ROW[row + 1], SlotMachine.LABELS[sym] or "?")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SlotMachine:drawLights()
|
||||
local lit = {}
|
||||
-- Slots_IlluminateBetLights lights the rows for THIS bet and every smaller
|
||||
-- one: `dec a / jr z` falls through from three to two to one.
|
||||
for bet = 1, (self.bet or 0) do
|
||||
for _, row in ipairs(LIGHT_ROWS[bet] or {}) do lit[row] = true end
|
||||
function SlotMachine:actorsImage()
|
||||
if self.actorsLoaded == nil then
|
||||
local Assets = require("src.render.Assets")
|
||||
local ok, img = pcall(Assets.image, "assets/generated/slots/gold_slots_actors.png")
|
||||
if not (ok and img) then
|
||||
ok, img = pcall(Assets.image, "assets/generated/slots/gold_slots_3.png")
|
||||
end
|
||||
self.actorsLoaded = (ok and img) or false
|
||||
if self.actorsLoaded then
|
||||
local G = love.graphics
|
||||
-- 24x240 sheet:
|
||||
-- Y=0: Golem 1 (Standing, 24x32)
|
||||
-- Y=32: Golem 2 (Ball, 24x32)
|
||||
-- Y=64: Chansey 1 (Standing / Step 1, 24x32)
|
||||
-- Y=96: Chansey 2 (Step 2, 24x32)
|
||||
-- Y=128: Chansey 3 (Step 3, 24x32)
|
||||
-- Y=160: Chansey 4 (Arm raised / Step 4, 24x32)
|
||||
-- Y=192: Chansey 5 (Egg Drop pose, 24x32)
|
||||
-- Y=224: Egg (8x16 at X=0)
|
||||
self.quadGolemStand = G.newQuad(0, 0, 24, 32, 24, 240)
|
||||
self.quadGolemBall = G.newQuad(0, 32, 24, 32, 24, 240)
|
||||
self.quadChansey1 = G.newQuad(0, 64, 24, 32, 24, 240)
|
||||
self.quadChansey2 = G.newQuad(0, 96, 24, 32, 24, 240)
|
||||
self.quadChansey3 = G.newQuad(0, 128, 24, 32, 24, 240)
|
||||
self.quadChansey4 = G.newQuad(0, 160, 24, 32, 24, 240)
|
||||
self.quadChanseyDrop = G.newQuad(0, 192, 24, 32, 24, 240)
|
||||
self.quadEgg = G.newQuad(0, 224, 8, 16, 24, 240)
|
||||
end
|
||||
end
|
||||
for _, row in ipairs({ 2, 4, 6, 8, 10 }) do
|
||||
for _, col in ipairs(LIGHT_COLS) do
|
||||
Chrome.print(lit[row] and "*" or "-", col, row)
|
||||
return self.actorsLoaded or nil
|
||||
end
|
||||
|
||||
-- Redraw the top Vileplume row (rows 2..3) and bottom frame brackets (rows 10..11)
|
||||
-- over the reels with solid backdrop to naturally mask any sprite overhang like the Game Boy hardware does.
|
||||
function SlotMachine:drawOverlays()
|
||||
local s1, s2 = self:sheets()
|
||||
local tm = getTilemap()
|
||||
if not tm or not s1:available() then return end
|
||||
|
||||
local G = love.graphics
|
||||
-- Solid backdrop over header (rows 0..3) and footer (rows 10..11) between columns 4..15
|
||||
G.setColor(198 / 255, 198 / 255, 74 / 255, 1)
|
||||
G.rectangle("fill", 4 * 8, 2 * 8, 12 * 8, 2 * 8)
|
||||
G.rectangle("fill", 4 * 8, 10 * 8, 12 * 8, 2 * 8)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
|
||||
for _, ty in ipairs({ 2, 3, 10, 11 }) do
|
||||
for tx = 4, 15 do
|
||||
local idx = ty * 20 + tx + 1
|
||||
local tileId = tm[idx]
|
||||
local pal = (ty <= 3) and 1 or 0
|
||||
local colors = GBC_PALS.bg[pal]
|
||||
s1.palette = colors
|
||||
s2.palette = colors
|
||||
|
||||
if tileId < 0x25 then
|
||||
s1:draw(tileId, tx, ty)
|
||||
else
|
||||
s2:draw(tileId - 0x25, tx, ty)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw Golem sprite animation
|
||||
if self.golemAnim then
|
||||
local actors = self:actorsImage()
|
||||
local g = self.golemAnim
|
||||
if actors then
|
||||
local quad = self.quadGolemBall
|
||||
local scaleX = 1
|
||||
local scaleY = 1
|
||||
if g.state == "falling" then
|
||||
quad = self.quadGolemBall
|
||||
elseif g.state == "rolling" then
|
||||
-- Frameset_SlotsGolem: 0=Standing, 1=Ball, 2=StandingYFlip, 3=BallXFlip
|
||||
local rotFrame = (g.animFrame or 0) % 4
|
||||
if rotFrame == 0 then
|
||||
quad = self.quadGolemStand
|
||||
scaleX = 1
|
||||
scaleY = 1
|
||||
elseif rotFrame == 1 then
|
||||
quad = self.quadGolemBall
|
||||
scaleX = 1
|
||||
scaleY = 1
|
||||
elseif rotFrame == 2 then
|
||||
quad = self.quadGolemStand
|
||||
scaleX = 1
|
||||
scaleY = -1
|
||||
elseif rotFrame == 3 then
|
||||
quad = self.quadGolemBall
|
||||
scaleX = -1
|
||||
scaleY = 1
|
||||
end
|
||||
end
|
||||
G.setColor(1, 1, 1, 1)
|
||||
local function drawGolem()
|
||||
-- Draw rotated around center (ox=12, oy=16)
|
||||
G.draw(actors, quad,
|
||||
math.floor(g.x + 12),
|
||||
math.floor(g.y + 16),
|
||||
0, scaleX, scaleY, 12, 16)
|
||||
end
|
||||
if GbcPalette.available() then
|
||||
GbcPalette.with(GBC_PALS.obj[5], drawGolem)
|
||||
else
|
||||
drawGolem()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw Chansey & Egg sprite animation
|
||||
if self.chanseyAnim then
|
||||
local actors = self:actorsImage()
|
||||
local c = self.chanseyAnim
|
||||
if actors then
|
||||
local quad = self.quadChansey1
|
||||
if c.state == "walking" then
|
||||
local walkCycle = { self.quadChansey1, self.quadChansey2, self.quadChansey3, self.quadChansey4 }
|
||||
quad = walkCycle[((c.animPose or 0) % 4) + 1] or self.quadChansey1
|
||||
elseif c.state == "egg_pause" then
|
||||
quad = self.quadChansey4
|
||||
elseif c.state == "egg_drop" or c.state == "spinning" then
|
||||
quad = self.quadChanseyDrop
|
||||
end
|
||||
|
||||
G.setColor(1, 1, 1, 1)
|
||||
local function drawChansey()
|
||||
G.draw(actors, quad, math.floor(c.x), math.floor(c.y or 44))
|
||||
if c.eggVisible and c.eggX and c.eggY then
|
||||
G.draw(actors, self.quadEgg, math.floor(c.eggX), math.floor(c.eggY))
|
||||
end
|
||||
end
|
||||
if GbcPalette.available() then
|
||||
GbcPalette.with(GBC_PALS.obj[6], drawChansey)
|
||||
else
|
||||
drawChansey()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1022,39 +1437,79 @@ function SlotMachine:drawMessage()
|
||||
end
|
||||
if self.matched and self.matched ~= SlotMachine.NO_MATCH
|
||||
and self.phase == "payoutText" then
|
||||
cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y,
|
||||
SlotMachine.LABELS[self.matched] or "?")
|
||||
local _, s2 = self:sheets()
|
||||
local sym = self.matched
|
||||
local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0]
|
||||
s2.palette = pal
|
||||
local t0 = s2:quad(sym + 0)
|
||||
local t1 = s2:quad(sym + 1)
|
||||
local t2 = s2:quad(sym + 2)
|
||||
local t3 = s2:quad(sym + 3)
|
||||
local img = s2:image()
|
||||
local G = love.graphics
|
||||
local px, py = PAYOUT_SYMBOL_X * 8, PAYOUT_SYMBOL_Y * 8
|
||||
if img and t0 and t1 and t2 and t3 then
|
||||
local function drawWin()
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.draw(img, t0, px, py)
|
||||
G.draw(img, t1, px + 8, py)
|
||||
G.draw(img, t2, px, py + 8)
|
||||
G.draw(img, t3, px + 8, py + 8)
|
||||
end
|
||||
G.setColor(1, 1, 1, 1)
|
||||
if GbcPalette.available() then
|
||||
GbcPalette.with(pal, drawWin)
|
||||
else
|
||||
drawWin()
|
||||
end
|
||||
else
|
||||
cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, SlotMachine.LABELS[self.matched] or "?")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SlotMachine:drawPanel()
|
||||
Chrome.clear()
|
||||
self:drawLights()
|
||||
self:drawBackground()
|
||||
self:drawReels()
|
||||
self:drawOverlays()
|
||||
|
||||
-- PRINTNUM_LEADINGZEROS | 2 bytes, 4 digits, for both counters.
|
||||
Chrome.print(Chrome.number(self:coins(), 4, true), COINS_X, COINS_Y)
|
||||
Chrome.print(Chrome.number(self.payoutLeft or 0, 4, true), PAYOUT_X, PAYOUT_Y)
|
||||
self:drawReels()
|
||||
|
||||
if self.phase == "bet" then
|
||||
Chrome.textbox(BET_BOX_X, BET_BOX_Y, BET_BOX_W - 2, BET_BOX_H - 2)
|
||||
-- Left speech textbox for "Bet how many coins?"
|
||||
Chrome.textbox(0, 12, 12, 4)
|
||||
Chrome.print(TEXTS.betHowMany[1], 1, 14)
|
||||
Chrome.print(TEXTS.betHowMany[2], 1, 16)
|
||||
|
||||
-- Right menu for bet choices (14, 10 to 19, 17)
|
||||
Chrome.textbox(14, 10, 4, 6)
|
||||
for i, label in ipairs(BET_ROWS) do
|
||||
local ty = BET_LABEL_Y + (i - 1) * BET_SPACING
|
||||
if i == self.betIndex then Chrome.cursor(BET_LABEL_X - 1, ty) end
|
||||
Chrome.print(label, BET_LABEL_X, ty)
|
||||
local ty = 12 + (i - 1) * 2
|
||||
if i == self.betIndex then Chrome.cursor(15, ty) end
|
||||
Chrome.print(label, 16, ty)
|
||||
end
|
||||
if not self.message then
|
||||
Chrome.textbox(TEXT_BOX_X, TEXT_BOX_Y, TEXT_BOX_W - 2, TEXT_BOX_H - 2)
|
||||
for i, line in ipairs(TEXTS.betHowMany) do
|
||||
|
||||
if self.message then
|
||||
-- If "Not enough coins." message is shown, overlay full speech box
|
||||
Chrome.textbox(0, 12, 18, 4)
|
||||
for i, line in ipairs(self.message) do
|
||||
Chrome.print(line, TEXT_X, TEXT_Y + (i - 1) * TEXT_LINE)
|
||||
end
|
||||
end
|
||||
end
|
||||
self:drawMessage()
|
||||
if self.phase == "again" then
|
||||
-- PlaceYesNoBox `lb bc, 14, 12`: a 6x5 box at (14,12) with YES at (16,13).
|
||||
elseif self.phase == "again" then
|
||||
-- Speech box: "Play again?"
|
||||
Chrome.textbox(0, 12, 18, 4)
|
||||
Chrome.print(TEXTS.playAgain[1], TEXT_X, TEXT_Y)
|
||||
|
||||
-- PlaceYesNoBox at (14, 12): 6x5 box with YES at (16,13), NO at (16,15)
|
||||
Chrome.textbox(14, 12, 4, 3)
|
||||
Chrome.print("YES", 16, 13)
|
||||
Chrome.print("NO", 16, 15)
|
||||
Chrome.cursor(15, 13 + (self.againChoice - 1) * 2)
|
||||
else
|
||||
self:drawMessage()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user