mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
also fixes feet layering of gold with grass. Unifies gen1/gen2 grass layering into a single system
This commit is contained in:
@@ -78,17 +78,31 @@ end
|
|||||||
|
|
||||||
-- Fishing: a rod's list is (cumulative chance, species, level) rows out of 256,
|
-- Fishing: a rod's list is (cumulative chance, species, level) rows out of 256,
|
||||||
-- ending at 100%. A roll past the group's own `chance` is a bite of nothing.
|
-- ending at 100%. A roll past the group's own `chance` is a bite of nothing.
|
||||||
function Encounter.fish(encounters, fishGroup, rod, random)
|
-- Rows with `day` and `nite` sub-slots (from TimeFishGroups) resolve based on
|
||||||
|
-- `daytime` ("MORN"/"DAY" vs "NITE"/"DARK").
|
||||||
|
function Encounter.fish(encounters, fishGroup, rod, daytime, random)
|
||||||
|
if type(daytime) == "function" and random == nil then
|
||||||
|
random = daytime
|
||||||
|
daytime = nil
|
||||||
|
end
|
||||||
local group = encounters and encounters.fishGroups
|
local group = encounters and encounters.fishGroups
|
||||||
and encounters.fishGroups[fishGroup]
|
and encounters.fishGroups[fishGroup]
|
||||||
if not group then return nil end
|
if not group then return nil end
|
||||||
local list = group[rod or "old"]
|
local list = group[rod or "old"]
|
||||||
if not list or #list == 0 then return nil end
|
if not list or #list == 0 then return nil end
|
||||||
local value = roll(random, 256)
|
local value = roll(random, 256)
|
||||||
|
local isNight = (daytime == "DARK" or daytime == "NITE")
|
||||||
|
local todKey = isNight and "nite" or "day"
|
||||||
for _, row in ipairs(list) do
|
for _, row in ipairs(list) do
|
||||||
if value < (row.chance or 0) then
|
if value < (row.chance or 0) then
|
||||||
if not row.species or row.species == "NO_ITEM" then return nil end
|
local slot = row[todKey]
|
||||||
return { species = row.species, level = row.level }
|
if not slot and row.timeGroup and encounters and encounters.timeFishGroups then
|
||||||
|
local tg = encounters.timeFishGroups[row.timeGroup]
|
||||||
|
slot = tg and tg[todKey]
|
||||||
|
end
|
||||||
|
slot = slot or row
|
||||||
|
if not slot.species or slot.species == 0 or slot.species == "NO_ITEM" then return nil end
|
||||||
|
return { species = slot.species, level = slot.level }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
@@ -129,7 +143,7 @@ end
|
|||||||
|
|
||||||
-- Which fish group a MAP belongs to lives on the map record, so a caller with
|
-- Which fish group a MAP belongs to lives on the map record, so a caller with
|
||||||
-- a map id and a rod does not have to know about groups at all.
|
-- a map id and a rod does not have to know about groups at all.
|
||||||
function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm)
|
function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm, daytime)
|
||||||
local map = maps and maps[mapId]
|
local map = maps and maps[mapId]
|
||||||
local group = map and map.fishGroup
|
local group = map and map.fishGroup
|
||||||
if not group then
|
if not group then
|
||||||
@@ -142,7 +156,7 @@ function Encounter.fishSlot(encounters, mapId, rod, random, maps, fishSwarm)
|
|||||||
if rod == "OLD_ROD" then key = "old"
|
if rod == "OLD_ROD" then key = "old"
|
||||||
elseif rod == "GOOD_ROD" then key = "good"
|
elseif rod == "GOOD_ROD" then key = "good"
|
||||||
elseif rod == "SUPER_ROD" then key = "super" end
|
elseif rod == "SUPER_ROD" then key = "super" end
|
||||||
return Encounter.fish(encounters, group, key or "old", random)
|
return Encounter.fish(encounters, group, key or "old", daytime, random)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Headbutt trees: TreeMonMaps says which set a map uses and TreeMons holds
|
-- Headbutt trees: TreeMonMaps says which set a map uses and TreeMons holds
|
||||||
|
|||||||
@@ -501,6 +501,12 @@ function Music.setFilterLevel(level)
|
|||||||
applyFilter(state.loopSource)
|
applyFilter(state.loopSource)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Music.setPitch(pitch)
|
||||||
|
pitch = pitch or 1.0
|
||||||
|
if state.source then pcall(state.source.setPitch, state.source, pitch) end
|
||||||
|
if state.loopSource then pcall(state.loopSource.setPitch, state.loopSource, pitch) end
|
||||||
|
end
|
||||||
|
|
||||||
-- re-apply persisted audio options (Game calls this on boot and after
|
-- re-apply persisted audio options (Game calls this on boot and after
|
||||||
-- loading a save)
|
-- loading a save)
|
||||||
function Music.applyOptions(opts)
|
function Music.applyOptions(opts)
|
||||||
|
|||||||
@@ -4102,8 +4102,30 @@ function RomExtractorGen2:extractEncounters()
|
|||||||
|
|
||||||
-- FishGroups rows: chance byte then old/good/super rod pointers, each a
|
-- FishGroups rows: chance byte then old/good/super rod pointers, each a
|
||||||
-- list of (cumulative chance, species, level) triples ending at 100%.
|
-- list of (cumulative chance, species, level) triples ending at 100%.
|
||||||
|
-- Rows with species == 0 (time_group in pokegold data/wild/fish.asm) index
|
||||||
|
-- TimeFishGroups [day_species, day_level, nite_species, nite_level].
|
||||||
self:trace("fish groups")
|
self:trace("fish groups")
|
||||||
local fish = self:symbol("FishGroups")
|
local fish = self:symbol("FishGroups")
|
||||||
|
local timeFishSym = self.symbols.TimeFishGroups and self:symbol("TimeFishGroups")
|
||||||
|
local timeFishBank = timeFishSym and timeFishSym.bank or (fish and fish.bank)
|
||||||
|
local timeFishAddr = timeFishSym and timeFishSym.address or (fish and 0x6BDE)
|
||||||
|
local timeFishGroups = {}
|
||||||
|
if timeFishBank and timeFishAddr then
|
||||||
|
for idx = 0, 31 do
|
||||||
|
local base = timeFishAddr + idx * 4
|
||||||
|
if not romAddrOk(timeFishBank, base + 3) then break end
|
||||||
|
local daySp = self.rom:byte(timeFishBank, base)
|
||||||
|
local dayLv = self.rom:byte(timeFishBank, base + 1)
|
||||||
|
local niteSp = self.rom:byte(timeFishBank, base + 2)
|
||||||
|
local niteLv = self.rom:byte(timeFishBank, base + 3)
|
||||||
|
if daySp == 0 or daySp > 251 or niteSp == 0 or niteSp > 251 then break end
|
||||||
|
timeFishGroups[idx] = {
|
||||||
|
day = { species = self:speciesName(daySp), level = dayLv },
|
||||||
|
nite = { species = self:speciesName(niteSp), level = niteLv },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local fishGroups = {}
|
local fishGroups = {}
|
||||||
local function readRod(address)
|
local function readRod(address)
|
||||||
local list = {}
|
local list = {}
|
||||||
@@ -4115,11 +4137,24 @@ function RomExtractorGen2:extractEncounters()
|
|||||||
local chance = self.rom:byte(fish.bank, address + i * 3)
|
local chance = self.rom:byte(fish.bank, address + i * 3)
|
||||||
local species = self.rom:byte(fish.bank, address + i * 3 + 1)
|
local species = self.rom:byte(fish.bank, address + i * 3 + 1)
|
||||||
local level = self.rom:byte(fish.bank, address + i * 3 + 2)
|
local level = self.rom:byte(fish.bank, address + i * 3 + 2)
|
||||||
list[#list + 1] = {
|
local entry = { chance = chance }
|
||||||
chance = chance,
|
if species == 0 then
|
||||||
species = self:speciesName(species),
|
entry.timeGroup = level
|
||||||
level = level,
|
local tg = timeFishGroups[level]
|
||||||
}
|
if tg then
|
||||||
|
entry.day = tg.day
|
||||||
|
entry.nite = tg.nite
|
||||||
|
entry.species = tg.day.species
|
||||||
|
entry.level = tg.day.level
|
||||||
|
else
|
||||||
|
entry.species = 0
|
||||||
|
entry.level = level
|
||||||
|
end
|
||||||
|
else
|
||||||
|
entry.species = self:speciesName(species)
|
||||||
|
entry.level = level
|
||||||
|
end
|
||||||
|
list[#list + 1] = entry
|
||||||
-- Rows are cumulative and the last one is 100% ($ff after `percent`).
|
-- Rows are cumulative and the last one is 100% ($ff after `percent`).
|
||||||
if chance >= 0xfe then break end
|
if chance >= 0xfe then break end
|
||||||
end
|
end
|
||||||
@@ -4217,6 +4252,7 @@ function RomExtractorGen2:extractEncounters()
|
|||||||
grass = grass,
|
grass = grass,
|
||||||
water = water,
|
water = water,
|
||||||
fishGroups = fishGroups,
|
fishGroups = fishGroups,
|
||||||
|
timeFishGroups = timeFishGroups,
|
||||||
trees = trees,
|
trees = trees,
|
||||||
rocks = rocks,
|
rocks = rocks,
|
||||||
treeSets = treeSets,
|
treeSets = treeSets,
|
||||||
@@ -5020,6 +5056,10 @@ function RomExtractorGen2:extractMenuGfx()
|
|||||||
question = "QuestionEmote",
|
question = "QuestionEmote",
|
||||||
happy = "HappyEmote",
|
happy = "HappyEmote",
|
||||||
sad = "SadEmote",
|
sad = "SadEmote",
|
||||||
|
heart = "HeartEmote",
|
||||||
|
bolt = "BoltEmote",
|
||||||
|
sleep = "SleepEmote",
|
||||||
|
fish = "FishEmote",
|
||||||
}) do
|
}) do
|
||||||
local symbol = self.symbols[label]
|
local symbol = self.symbols[label]
|
||||||
if symbol then
|
if symbol then
|
||||||
|
|||||||
@@ -987,9 +987,14 @@ R.tilesets = {
|
|||||||
-- for "no fish here, roll the map's water table instead" (pokegold
|
-- for "no fish here, roll the map's water table instead" (pokegold
|
||||||
-- data/wild/fish.asm), which is why species is a union rather than a bare id.
|
-- data/wild/fish.asm), which is why species is a union rather than a bare id.
|
||||||
local gen2Slot = f.rec{ level = f.int(1), species = f.id("pokemon") }
|
local gen2Slot = f.rec{ level = f.int(1), species = f.id("pokemon") }
|
||||||
-- the sentinel row carries level 0 as well as species 0, so both floors drop
|
-- the sentinel row carries level 0 as well as species 0, so both floors drop.
|
||||||
|
-- Time-dependent rows (TimeFishGroups) carry day/nite sub-slots.
|
||||||
|
local gen2FishSubSlot = f.rec{ level = f.int(0), species = f.union{ f.id("pokemon"), f.int(0, 0) } }
|
||||||
local gen2FishSlot = f.rec{ chance = f.int(0, 255), level = f.int(0),
|
local gen2FishSlot = f.rec{ chance = f.int(0, 255), level = f.int(0),
|
||||||
species = f.union{ f.id("pokemon"), f.int(0, 0) } }
|
species = f.union{ f.id("pokemon"), f.int(0, 0) },
|
||||||
|
timeGroup = f.opt(f.int(0, 255)),
|
||||||
|
day = f.opt(gen2FishSubSlot),
|
||||||
|
nite = f.opt(gen2FishSubSlot) }
|
||||||
-- Headbutt/Rock Smash slots. species is optional and the level floor is 0
|
-- Headbutt/Rock Smash slots. species is optional and the level floor is 0
|
||||||
-- because TreeMonSet_Rock has no `rare` half in the ROM (pokegold
|
-- because TreeMonSet_Rock has no `rare` half in the ROM (pokegold
|
||||||
-- data/wild/treemons.asm ends the table after the common rows), so the four
|
-- data/wild/treemons.asm ends the table after the common rows), so the four
|
||||||
@@ -1046,6 +1051,8 @@ R.encounters = {
|
|||||||
chance = f.int(0, 255),
|
chance = f.int(0, 255),
|
||||||
old = f.list(gen2FishSlot), good = f.list(gen2FishSlot),
|
old = f.list(gen2FishSlot), good = f.list(gen2FishSlot),
|
||||||
super = f.list(gen2FishSlot) }),
|
super = f.list(gen2FishSlot) }),
|
||||||
|
timeFishGroups = f.opt(f.map(f.union{ f.str, f.int(0, 255) },
|
||||||
|
f.rec{ day = gen2FishSubSlot, nite = gen2FishSubSlot })),
|
||||||
-- headbutt: map -> tree set id, and the set's common/rare tables. rocks
|
-- headbutt: map -> tree set id, and the set's common/rare tables. rocks
|
||||||
-- is the same indirection for Rock Smash.
|
-- is the same indirection for Rock Smash.
|
||||||
trees = f.map(f.str, f.str),
|
trees = f.map(f.str, f.str),
|
||||||
|
|||||||
@@ -360,10 +360,12 @@ end
|
|||||||
-- overwrites the sheet's own tiles in VRAM in the original, so it has to be
|
-- overwrites the sheet's own tiles in VRAM in the original, so it has to be
|
||||||
-- recolored and OG-RED-redrawn exactly like the sheet rather than blitted as
|
-- recolored and OG-RED-redrawn exactly like the sheet rather than blitted as
|
||||||
-- raw DMG shades (#384).
|
-- raw DMG shades (#384).
|
||||||
function SpriteRenderer:drawTile(path, x, y, flip)
|
function SpriteRenderer:drawTile(path, x, y, flip, quad)
|
||||||
local image, redraw = getImage(path), false
|
local image, redraw = getImage(path), false
|
||||||
if self.def.trueColor then
|
if self.def.trueColor then
|
||||||
PaletteFX.markTrueColor(x, y, 16, 8)
|
PaletteFX.markTrueColor(x, y, 16, 8)
|
||||||
|
elseif self.objColors then
|
||||||
|
image = getObpImage(path, self:gen2Obp())
|
||||||
elseif PaletteFX.usesGbcPack() then
|
elseif PaletteFX.usesGbcPack() then
|
||||||
local colors, group = PaletteFX.spriteObp(self.def, self.seed)
|
local colors, group = PaletteFX.spriteObp(self.def, self.seed)
|
||||||
if colors then image = getObpImage(path, colors, group) end
|
if colors then image = getObpImage(path, colors, group) end
|
||||||
@@ -373,10 +375,23 @@ function SpriteRenderer:drawTile(path, x, y, flip)
|
|||||||
image = getObpImage(path, PaletteFX.dmgObj())
|
image = getObpImage(path, PaletteFX.dmgObj())
|
||||||
end
|
end
|
||||||
local iw, ih = image:getDimensions()
|
local iw, ih = image:getDimensions()
|
||||||
|
local q = quad
|
||||||
|
if not q then
|
||||||
self.tileQuads = self.tileQuads or {}
|
self.tileQuads = self.tileQuads or {}
|
||||||
self.tileQuads[path] = self.tileQuads[path]
|
self.tileQuads[path] = self.tileQuads[path]
|
||||||
or love.graphics.newQuad(0, 0, iw, ih, iw, ih)
|
or love.graphics.newQuad(0, 0, iw, ih, iw, ih)
|
||||||
blitFrame(image, self.tileQuads[path], x, y, flip, redraw, iw)
|
q = self.tileQuads[path]
|
||||||
|
end
|
||||||
|
local qw = iw
|
||||||
|
if q then
|
||||||
|
if q.getViewport then
|
||||||
|
local _, _, w = q:getViewport()
|
||||||
|
qw = w
|
||||||
|
elseif q.w then
|
||||||
|
qw = q.w
|
||||||
|
end
|
||||||
|
end
|
||||||
|
blitFrame(image, q, x, y, flip, redraw, qw)
|
||||||
end
|
end
|
||||||
|
|
||||||
return SpriteRenderer
|
return SpriteRenderer
|
||||||
|
|||||||
@@ -760,35 +760,7 @@ function TileRenderer:markCellBottomRedraw(cx, cy, camX, camY, colors)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw the bottom tile row of every visible grass cell after sprites.
|
|
||||||
-- DMG/SGB: one SpriteBatch draw under the color-0-key shader.
|
|
||||||
-- GBC: per-cell draws using pre-keyed images (can't share one batch).
|
|
||||||
function TileRenderer:drawGrassOverdraw(camX, camY)
|
|
||||||
local ox, oy = -math.floor(camX), -math.floor(camY)
|
|
||||||
if self.gbcCtx then
|
|
||||||
if self.grassCells then
|
|
||||||
for _, c in ipairs(self.grassCells) do
|
|
||||||
self:drawCellBottomRaw(c[1], c[2], camX, camY)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local shader = getColor0KeyShader()
|
|
||||||
if shader then love.graphics.setShader(shader) end
|
|
||||||
if self.grassBatch then
|
|
||||||
love.graphics.draw(self.grassBatch, ox, oy)
|
|
||||||
end
|
|
||||||
if shader then love.graphics.setShader() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Queue every visible grass cell's bottom row for the post-zone
|
|
||||||
-- sprite-redraw pass (GBC OBP replay; mirrors markCellBottomRedraw).
|
|
||||||
function TileRenderer:markGrassOverdrawRedraw(camX, camY, colors)
|
|
||||||
if not self.grassCells then return end
|
|
||||||
for _, c in ipairs(self.grassCells) do
|
|
||||||
self:markCellBottomRedraw(c[1], c[2], camX, camY, colors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills
|
local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills
|
||||||
|
|
||||||
@@ -815,16 +787,6 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
|||||||
self.winBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
|
self.winBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
|
||||||
end
|
end
|
||||||
self.winBatch:clear()
|
self.winBatch:clear()
|
||||||
-- grass-overdraw pass structures: a SpriteBatch for the DMG/SGB shader
|
|
||||||
-- path (one atlas, one shader draw call) and a plain list for the GBC
|
|
||||||
-- path (per-cell pre-keyed images can't share a single SpriteBatch).
|
|
||||||
if not self.gbcCtx then
|
|
||||||
if not self.grassBatch then
|
|
||||||
self.grassBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
|
|
||||||
end
|
|
||||||
self.grassBatch:clear()
|
|
||||||
end
|
|
||||||
self.grassCells = {}
|
|
||||||
local anims = self.anims
|
local anims = self.anims
|
||||||
for _, anim in ipairs(anims) do
|
for _, anim in ipairs(anims) do
|
||||||
if not anim.batch then
|
if not anim.batch then
|
||||||
@@ -834,9 +796,6 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
|||||||
end
|
end
|
||||||
local map, quads = self.map, self.quads
|
local map, quads = self.map, self.quads
|
||||||
local claimedBy, aliasMap = self.claimedBy, self.aliasMap
|
local claimedBy, aliasMap = self.claimedBy, self.aliasMap
|
||||||
-- track which grass cells we've already added so each cx/cy pair is only
|
|
||||||
-- recorded once even though its two bottom-row tiles share the same cell.
|
|
||||||
local grassSeen = {}
|
|
||||||
for ty = ty0, ty1 - 1 do
|
for ty = ty0, ty1 - 1 do
|
||||||
local by = math.floor(ty / 4)
|
local by = math.floor(ty / 4)
|
||||||
local ty4 = ty % 4
|
local ty4 = ty % 4
|
||||||
@@ -859,25 +818,6 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
|||||||
anim.batch:add(wx, wy)
|
anim.batch:add(wx, wy)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- bottom tile row of a 2×2 cell (ty is odd) that is a grass cell:
|
|
||||||
-- record it for the post-sprite grass overdraw pass.
|
|
||||||
if ty % 2 == 1 then
|
|
||||||
local cx, cy = math.floor(tx / 2), math.floor(ty / 2)
|
|
||||||
local key = cy * 65536 + cx
|
|
||||||
if not grassSeen[key] and map:isGrassCell(cx, cy) then
|
|
||||||
grassSeen[key] = true
|
|
||||||
self.grassCells[#self.grassCells + 1] = { cx, cy }
|
|
||||||
-- DMG/SGB path: bake both tiles of the bottom row into the batch
|
|
||||||
if self.grassBatch then
|
|
||||||
-- left tile (tx = cx*2)
|
|
||||||
local lq = quads[map:tileAt(cx * 2, ty)]
|
|
||||||
if lq then self.grassBatch:add(lq, cx * 16, ty * 8) end
|
|
||||||
-- right tile (tx = cx*2+1)
|
|
||||||
local rq = quads[map:tileAt(cx * 2 + 1, ty)]
|
|
||||||
if rq then self.grassBatch:add(rq, cx * 16 + 8, ty * 8) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -953,8 +893,6 @@ end
|
|||||||
-- :release on eviction.
|
-- :release on eviction.
|
||||||
function TileRenderer:releaseBatches()
|
function TileRenderer:releaseBatches()
|
||||||
safeRelease(self.winBatch); self.winBatch = nil
|
safeRelease(self.winBatch); self.winBatch = nil
|
||||||
safeRelease(self.grassBatch); self.grassBatch = nil
|
|
||||||
self.grassCells = nil
|
|
||||||
safeRelease(self.borderFill); self.borderFill = nil
|
safeRelease(self.borderFill); self.borderFill = nil
|
||||||
safeRelease(self.borderQuad); self.borderQuad = nil
|
safeRelease(self.borderQuad); self.borderQuad = nil
|
||||||
-- shared shift-variant cache; only drop the reference
|
-- shared shift-variant cache; only drop the reference
|
||||||
|
|||||||
+812
-280
File diff suppressed because it is too large
Load Diff
@@ -5303,15 +5303,24 @@ function OverworldState:drawWorld()
|
|||||||
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
|
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
|
||||||
and e == self.player) then
|
and e == self.player) then
|
||||||
e:draw(cam.x, cam.y)
|
e:draw(cam.x, cam.y)
|
||||||
end
|
-- tall grass overdraws the sprite's feet (GB sprite priority);
|
||||||
end
|
-- the overdraw is BG tiles, so it rides the shake offset too
|
||||||
-- tall grass overdraws every visible grass cell's feet row after all
|
|
||||||
-- sprites (GB sprite-priority parity). One pass regardless of how many
|
|
||||||
-- entities are on screen -- see TileRenderer:drawGrassOverdraw.
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
self.map.renderer:drawGrassOverdraw(cam.x, bgY)
|
if self.map:isGrassCell(e.cellX, e.cellY) then
|
||||||
|
self.map.renderer:drawCellBottom(e.cellX, e.cellY, cam.x, bgY)
|
||||||
if grassColors then
|
if grassColors then
|
||||||
self.map.renderer:markGrassOverdrawRedraw(cam.x, bgY, grassColors)
|
self.map.renderer:markCellBottomRedraw(e.cellX, e.cellY,
|
||||||
|
cam.x, bgY, grassColors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then
|
||||||
|
self.map.renderer:drawCellBottom(e.targetX, e.targetY, cam.x, bgY)
|
||||||
|
if grassColors then
|
||||||
|
self.map.renderer:markCellBottomRedraw(e.targetX, e.targetY,
|
||||||
|
cam.x, bgY, grassColors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
fxHeal()
|
fxHeal()
|
||||||
fxDust()
|
fxDust()
|
||||||
@@ -5348,18 +5357,6 @@ function OverworldState:drawWorld()
|
|||||||
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
|
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- Inject grass-cell overdraw items into the same depth-sorted queue so
|
|
||||||
-- they occlude entities at lower y correctly (back-to-front by cell foot).
|
|
||||||
-- Each cell's foot y = cy*16 + 16 in world pixels (bottom of its two rows).
|
|
||||||
local grassCells = self.map.renderer.grassCells
|
|
||||||
if grassCells then
|
|
||||||
for _, c in ipairs(grassCells) do
|
|
||||||
local cx, cy = c[1], c[2]
|
|
||||||
-- world-pixel foot of the grass cell's bottom tile row
|
|
||||||
local cellFootY = (cy * 2 + 2) * 8 -- == cy*16+16
|
|
||||||
items[#items + 1] = { y = cellFootY, kind = "grass", cx = cx, cy = cy }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(items, function(a, b) return a.y < b.y end)
|
table.sort(items, function(a, b) return a.y < b.y end)
|
||||||
|
|
||||||
for _, it in ipairs(items) do
|
for _, it in ipairs(items) do
|
||||||
@@ -5371,20 +5368,6 @@ function OverworldState:drawWorld()
|
|||||||
local fy = g.npc.py - cam.y + g.oy + 16
|
local fy = g.npc.py - cam.y + g.oy + 16
|
||||||
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false,
|
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false,
|
||||||
function() g.npc:draw(cam.x - g.ox, cam.y - g.oy) end)
|
function() g.npc:draw(cam.x - g.ox, cam.y - g.oy) end)
|
||||||
elseif it.kind == "grass" then
|
|
||||||
-- tall-grass bottom-row overdraw: billboarded at the cell's foot so
|
|
||||||
-- it depth-sorts correctly against any entity in the same y column.
|
|
||||||
-- bgY keeps the elevator-shake offset; drawCellBottomRaw lets the
|
|
||||||
-- billboard own the shader (color-0 keying baked into the keyed image
|
|
||||||
-- on GBC, or applied by drawCellBottom's shader on DMG/SGB).
|
|
||||||
local cx, cy = it.cx, it.cy
|
|
||||||
local fx = cx * 16 - cam.x + 8 -- horizontal centre of the cell
|
|
||||||
local fy = (cy * 2 + 2) * 8 - cam.y -- foot of the bottom tile row
|
|
||||||
local colors = zoneColorsAt(zones, fx, fy)
|
|
||||||
self:billboard(fx, fy, vw, vh, colors, true, function()
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
self.map.renderer:drawCellBottomRaw(cx, cy, cam.x, bgY)
|
|
||||||
end)
|
|
||||||
else
|
else
|
||||||
local e = it.e
|
local e = it.e
|
||||||
local fx = e.px - cam.x + 8
|
local fx = e.px - cam.x + 8
|
||||||
@@ -5392,6 +5375,22 @@ function OverworldState:drawWorld()
|
|||||||
local colors = zoneColorsAt(zones, fx, fy)
|
local colors = zoneColorsAt(zones, fx, fy)
|
||||||
self:billboard(fx, fy, vw, vh, colors, false,
|
self:billboard(fx, fy, vw, vh, colors, false,
|
||||||
function() e:draw(cam.x, cam.y) end)
|
function() e:draw(cam.x, cam.y) end)
|
||||||
|
-- tall-grass feet overdraw glued to the sprite: same anchor + depth
|
||||||
|
-- so it keeps hiding the feet, color-0-keyed palette so its white
|
||||||
|
-- gaps still show the sprite through (drawCellBottomRaw lets the
|
||||||
|
-- billboard own the shader; bgY keeps the elevator-shake offset).
|
||||||
|
if self.map:isGrassCell(e.cellX, e.cellY) then
|
||||||
|
self:billboard(fx, fy, vw, vh, colors, true, function()
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
self.map.renderer:drawCellBottomRaw(e.cellX, e.cellY, cam.x, bgY)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then
|
||||||
|
self:billboard(fx, fy, vw, vh, colors, true, function()
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
self.map.renderer:drawCellBottomRaw(e.targetX, e.targetY, cam.x, bgY)
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+55
-33
@@ -313,9 +313,8 @@ local TROPHY_BOXES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- Script_FishCastRod ends on `pause 40`, and Script_GotABite pauses another 40
|
-- Script_FishCastRod ends on `pause 40`, and Script_GotABite pauses another 40
|
||||||
-- over the bobbing rod before the text lands. ShakeHeadbuttTree counts down
|
-- over the bobbing rod before the text lands.
|
||||||
-- wFrameCounter from 32. All three are frames at 60 Hz, which is the same
|
-- All are frames at 60 Hz, which is the same clock World:step runs on.
|
||||||
-- clock World:step runs on.
|
|
||||||
local FISH_CAST_FRAMES = 40
|
local FISH_CAST_FRAMES = 40
|
||||||
local FISH_BITE_FRAMES = 40
|
local FISH_BITE_FRAMES = 40
|
||||||
local HEADBUTT_SHAKE_FRAMES = 32
|
local HEADBUTT_SHAKE_FRAMES = 32
|
||||||
@@ -346,10 +345,12 @@ local function sameEncounter(enc) return enc end
|
|||||||
-- (engine/events/fish.asm Fish) byte for byte.
|
-- (engine/events/fish.asm Fish) byte for byte.
|
||||||
local FISH_ROD_KEY = { OLD_ROD = "old", GOOD_ROD = "good", SUPER_ROD = "super" }
|
local FISH_ROD_KEY = { OLD_ROD = "old", GOOD_ROD = "good", SUPER_ROD = "super" }
|
||||||
|
|
||||||
local function fishVanilla(rod, _mapId, candidates)
|
local function fishVanilla(rod, _mapId, candidates, ctx)
|
||||||
if not candidates then return nil end
|
if not candidates then return nil end
|
||||||
return Encounter.fish({ fishGroups = { hooked = candidates } }, "hooked",
|
local tod = ctx and (ctx.tod or ctx.daytime)
|
||||||
FISH_ROD_KEY[rod] or rod or "old", nil)
|
return Encounter.fish({ fishGroups = { hooked = candidates },
|
||||||
|
timeFishGroups = ctx and ctx.encounters and ctx.encounters.timeFishGroups },
|
||||||
|
"hooked", FISH_ROD_KEY[rod] or rod or "old", tod, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function speciesByIndex(pokemon, index)
|
local function speciesByIndex(pokemon, index)
|
||||||
@@ -4291,6 +4292,7 @@ function World:rollFishing(rod)
|
|||||||
return "nibble"
|
return "nibble"
|
||||||
end
|
end
|
||||||
local roll
|
local roll
|
||||||
|
local tod = self.tod or "DAY"
|
||||||
if Runtime.wantsHook("encounter.fishing") then
|
if Runtime.wantsHook("encounter.fishing") then
|
||||||
-- Gen 1's three arguments, in Gen 1's order: the rod, the map, and the
|
-- Gen 1's three arguments, in Gen 1's order: the rod, the map, and the
|
||||||
-- candidate list the chain may inspect or replace before the roll. Gold's
|
-- candidate list the chain may inspect or replace before the roll. Gold's
|
||||||
@@ -4304,10 +4306,10 @@ function World:rollFishing(rod)
|
|||||||
roll = Runtime.call("encounter.fishing", fishVanilla, rod, map.id,
|
roll = Runtime.call("encounter.fishing", fishVanilla, rod, map.id,
|
||||||
groups and groups[group],
|
groups and groups[group],
|
||||||
{ fishGroup = group, swarm = swarm, encounters = self.encounters,
|
{ fishGroup = group, swarm = swarm, encounters = self.encounters,
|
||||||
maps = self.maps, data = game.data })
|
maps = self.maps, data = game.data, tod = tod, daytime = tod })
|
||||||
else
|
else
|
||||||
roll = Encounter.fishSlot(self.encounters, map.id, rod, nil, self.maps,
|
roll = Encounter.fishSlot(self.encounters, map.id, rod, nil, self.maps,
|
||||||
swarm)
|
swarm, tod)
|
||||||
end
|
end
|
||||||
if not roll or not roll.species then return "nibble" end
|
if not roll or not roll.species then return "nibble" end
|
||||||
local wild = Mon.new(game.data, roll.species, roll.level)
|
local wild = Mon.new(game.data, roll.species, roll.level)
|
||||||
@@ -4770,19 +4772,36 @@ function World:runQueuedScript()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Script_FishCastRod, then Script_NotEvenANibble or Script_GotABite. Held as
|
-- Script_FishCastRod, then Script_NotEvenANibble or Script_GotABite. Held as
|
||||||
-- a frame counter rather than a movement byte stream because the three
|
-- an exact frame counter matching 60 Hz engine ticks.
|
||||||
-- commands involved -- fish_cast_rod ($52), fish_got_bite ($51) and show_emote
|
|
||||||
-- ($54) -- are object ACTION changes, not steps, and Movement.decodeByte has
|
|
||||||
-- nothing to say about them.
|
|
||||||
function World:beginFishing(outcome, wild)
|
function World:beginFishing(outcome, wild)
|
||||||
self.fishing = {
|
local p = self.player
|
||||||
phase = "cast", timer = FISH_CAST_FRAMES, outcome = outcome, wild = wild,
|
local d = Map.DELTA[p and p.facing or "down"] or Map.DELTA.down
|
||||||
|
local targetCellX = p and (p.cellX + d[1]) or 0
|
||||||
|
local targetCellY = p and (p.cellY + d[2]) or 0
|
||||||
|
local bobber = {
|
||||||
|
cellX = targetCellX,
|
||||||
|
cellY = targetCellY,
|
||||||
|
px = targetCellX * 16,
|
||||||
|
py = targetCellY * 16,
|
||||||
}
|
}
|
||||||
|
self.fishing = {
|
||||||
|
phase = "cast",
|
||||||
|
timer = FISH_CAST_FRAMES,
|
||||||
|
outcome = outcome,
|
||||||
|
wild = wild,
|
||||||
|
bobber = bobber,
|
||||||
|
facing = p and p.facing or "down",
|
||||||
|
}
|
||||||
|
if self.player then
|
||||||
|
self.player.fishing = true
|
||||||
|
self.player.fishingState = self.fishing
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function World:updateFishing()
|
function World:updateFishing()
|
||||||
local st = self.fishing
|
local st = self.fishing
|
||||||
if not st then return end
|
if not st then return end
|
||||||
|
if self.player then self.player.fishingState = st end
|
||||||
-- A text box owns the frame while it is up; the script only moves on when
|
-- A text box owns the frame while it is up; the script only moves on when
|
||||||
-- its own callback fires.
|
-- its own callback fires.
|
||||||
if self.textbox or self.choicebox then return end
|
if self.textbox or self.choicebox then return end
|
||||||
@@ -4791,7 +4810,7 @@ function World:updateFishing()
|
|||||||
-- StepFunction_GotBite (engine/overworld/map_objects.asm:1430) is one byte
|
-- StepFunction_GotBite (engine/overworld/map_objects.asm:1430) is one byte
|
||||||
-- of animation: OBJECT_SPRITE_Y_OFFSET flipped between 0 and 1 once a
|
-- of animation: OBJECT_SPRITE_Y_OFFSET flipped between 0 and 1 once a
|
||||||
-- frame for the length of the bite, which is the rod jerking in the
|
-- frame for the length of the bite, which is the rod jerking in the
|
||||||
-- player's hands. The cast holds still, so only the bite bobs.
|
-- player's hands.
|
||||||
if self.player then
|
if self.player then
|
||||||
self.player.spriteYOffset =
|
self.player.spriteYOffset =
|
||||||
(st.phase == "bite" and st.timer % 2 == 1) and 1 or 0
|
(st.phase == "bite" and st.timer % 2 == 1) and 1 or 0
|
||||||
@@ -4801,34 +4820,33 @@ function World:updateFishing()
|
|||||||
if self.player then self.player.spriteYOffset = 0 end
|
if self.player then self.player.spriteYOffset = 0 end
|
||||||
if st.phase == "cast" then
|
if st.phase == "cast" then
|
||||||
if st.outcome == "battle" then
|
if st.outcome == "battle" then
|
||||||
-- Script_GotABite: four fish_got_bite bobs with the EMOTE_SHOCK bubble
|
|
||||||
-- over the player, then `pause 40` before the rod comes back.
|
|
||||||
st.phase = "bite"
|
st.phase = "bite"
|
||||||
st.timer = FISH_BITE_FRAMES
|
st.timer = FISH_BITE_FRAMES
|
||||||
self:showEmote(EMOTE_SHOCK, 0, FISH_BITE_FRAMES)
|
self:showEmote(EMOTE_SHOCK, 0, FISH_BITE_FRAMES)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- Script_NotEvenANibble (queued by $1 .FishNoBite) and
|
|
||||||
-- Script_NotEvenANibble2 (by $4 .FishNoFish) differ only in the
|
|
||||||
-- wFishingResult they record; both write RodNothingText and fall through
|
|
||||||
-- to the same PutTheRodAway.
|
|
||||||
st.phase = "done"
|
st.phase = "done"
|
||||||
self:showText(Strings(TEXT_ROD_NOTHING), function() self.fishing = nil end)
|
self:showText(Strings(TEXT_ROD_NOTHING), function()
|
||||||
|
self.fishing = nil
|
||||||
|
if self.player then
|
||||||
|
self.player.fishing = nil
|
||||||
|
self.player.fishingState = nil
|
||||||
|
end
|
||||||
|
end)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if st.phase == "bite" then
|
if st.phase == "bite" then
|
||||||
st.phase = "done"
|
st.phase = "done"
|
||||||
self:showText(Strings(TEXT_ROD_BITE), function()
|
self:showText(Strings(TEXT_ROD_BITE), function()
|
||||||
local wild = st.wild
|
local wild = st.wild
|
||||||
-- PutTheRodAway and closetext come before startbattle, and the state has
|
|
||||||
-- to be gone before the battle is pushed or World:busy would still be
|
|
||||||
-- holding the world when it returns.
|
|
||||||
self.fishing = nil
|
self.fishing = nil
|
||||||
-- FishFunction's `.goodtofish` writes BATTLETYPE_FISH into wBattleType
|
if self.player then
|
||||||
-- alongside the species and level it hooked (engine/events/overworld.asm),
|
self.player.fishing = nil
|
||||||
-- which is the one condition LureBallMultiplier reads for its x3.
|
self.player.fishingState = nil
|
||||||
|
end
|
||||||
if wild then self:startBattle({ wild = wild, battleType = "fish" }) end
|
if wild then self:startBattle({ wild = wild, battleType = "fish" }) end
|
||||||
end)
|
end)
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -7819,7 +7837,10 @@ function World:drawGrassOver(entity, ox, oy, s)
|
|||||||
local tilePalettes = tileset.tilePalettes
|
local tilePalettes = tileset.tilePalettes
|
||||||
local tilesPerRow = tileset.tilesPerRow or 16
|
local tilesPerRow = tileset.tilesPerRow or 16
|
||||||
local aw, ah = atlas:getDimensions()
|
local aw, ah = atlas:getDimensions()
|
||||||
local rx, ry = entity.px, entity.py + 4
|
-- Only draw the bottom 8px tile row of the cell (ty = py + 8) over the feet,
|
||||||
|
-- matching Gen 1's drawCellBottomRaw. Starting at py + 4 sampled the top
|
||||||
|
-- tile row and drew grass tufts over the face and torso.
|
||||||
|
local rx, ry = entity.px, entity.py + 8
|
||||||
self.grassQuad = self.grassQuad or G.newQuad(0, 0, 8, 8, aw, ah)
|
self.grassQuad = self.grassQuad or G.newQuad(0, 0, 8, 8, aw, ah)
|
||||||
local quad = self.grassQuad
|
local quad = self.grassQuad
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
@@ -9794,13 +9815,14 @@ function World:drawPeople(s, billboard)
|
|||||||
else
|
else
|
||||||
entry.npc:draw(ox, oy, s)
|
entry.npc:draw(ox, oy, s)
|
||||||
end
|
end
|
||||||
-- ShakeGrass rustle only. The cart also ORs OAM_PRIO onto the lower
|
-- ShakeGrass rustle only while moving; drawGrassOver when standing/in grass
|
||||||
-- 16x8 (drawGrassOver / IN_GRASS) so the BG tuft covers the feet, but
|
-- so the BG tuft covers the feet.
|
||||||
-- stacking that plain grass tile on top of the character with the
|
|
||||||
-- rustle reads as a double overlay here -- keep the walk-through anim.
|
|
||||||
-- Only the current map's own entities: a ghost's cells belong to a
|
-- Only the current map's own entities: a ghost's cells belong to a
|
||||||
-- neighbour's block list.
|
-- neighbour's block list.
|
||||||
if entry.ox == 0 and entry.oy == 0 then
|
if entry.ox == 0 and entry.oy == 0 then
|
||||||
|
if entity.inGrass and not (entity.grassShake and entity.moving) then
|
||||||
|
self:drawGrassOver(entity, ox, oy, s)
|
||||||
|
end
|
||||||
self:drawGrassShake(entity, ox, oy, s)
|
self:drawGrassShake(entity, ox, oy, s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
-- Gen 2 time-dependent fishing encounters (TimeFishGroups).
|
||||||
|
--
|
||||||
|
-- luajit tests/gen2_fishing_time_test.lua
|
||||||
|
--
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local S = require("tests.harness").suite("gen2 fishing time")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
love = require("tests.love_stub")
|
||||||
|
|
||||||
|
local World = require("src.world.gen2.World")
|
||||||
|
local Encounter = require("src.battle.gen2.Encounter")
|
||||||
|
local Permissions = require("src.world.gen2.Permissions")
|
||||||
|
|
||||||
|
local COLL_FLOOR, COLL_WATER = 0x00, 0x29
|
||||||
|
local MAP_W, MAP_H = 10, 10
|
||||||
|
|
||||||
|
local function mon(name, level)
|
||||||
|
return { species = name, id = name, name = name, baseStats = { hp = 50 }, level = level or 10 }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fakeData()
|
||||||
|
return {
|
||||||
|
pokemon = {
|
||||||
|
MAGIKARP = mon("MAGIKARP"),
|
||||||
|
KRABBY = mon("KRABBY"),
|
||||||
|
KINGLER = mon("KINGLER"),
|
||||||
|
CORSOLA = mon("CORSOLA"),
|
||||||
|
STARYU = mon("STARYU"),
|
||||||
|
SHELLDER = mon("SHELLDER"),
|
||||||
|
CHINCHOU = mon("CHINCHOU"),
|
||||||
|
LANTURN = mon("LANTURN"),
|
||||||
|
TENTACRUEL = mon("TENTACRUEL"),
|
||||||
|
},
|
||||||
|
items = {
|
||||||
|
OLD_ROD = { keyItem = true },
|
||||||
|
GOOD_ROD = { keyItem = true },
|
||||||
|
SUPER_ROD = { keyItem = true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fishWorld(mapId, fishGroup, daytime)
|
||||||
|
local map = {
|
||||||
|
id = mapId,
|
||||||
|
def = { id = mapId, width = MAP_W, height = MAP_H,
|
||||||
|
environment = "ROUTE", fishGroup = fishGroup },
|
||||||
|
cellCollision = function(self, cx, cy)
|
||||||
|
return (cx == 5 and cy == 4) and COLL_WATER or COLL_FLOOR
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local game = {
|
||||||
|
save = {
|
||||||
|
timeOfDay = daytime or "DAY",
|
||||||
|
dailyFlags = {},
|
||||||
|
player = { cellX = 5, cellY = 5, facing = "up" },
|
||||||
|
},
|
||||||
|
data = fakeData(),
|
||||||
|
}
|
||||||
|
local world = World.new(game)
|
||||||
|
world.map = map
|
||||||
|
world.maps = { [mapId] = map.def }
|
||||||
|
world.player = game.save.player
|
||||||
|
world.playerState = 0
|
||||||
|
world.tod = daytime or "DAY"
|
||||||
|
world.daytime = daytime or "DAY"
|
||||||
|
return world, game
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test Shore group with TimeFishGroups (Corsola / Staryu)
|
||||||
|
local SHORE_ENCOUNTERS = {
|
||||||
|
fishGroups = {
|
||||||
|
FISHGROUP_SHORE = {
|
||||||
|
id = "FISHGROUP_SHORE",
|
||||||
|
chance = 255, -- always bite
|
||||||
|
old = {
|
||||||
|
{ chance = 179, species = "MAGIKARP", level = 10 },
|
||||||
|
{ chance = 217, species = "MAGIKARP", level = 10 },
|
||||||
|
{ chance = 255, species = "KRABBY", level = 10 },
|
||||||
|
},
|
||||||
|
good = {
|
||||||
|
{ chance = 89, species = "MAGIKARP", level = 20 },
|
||||||
|
{ chance = 178, species = "KRABBY", level = 20 },
|
||||||
|
{ chance = 230, species = "KRABBY", level = 20 },
|
||||||
|
{ chance = 255, species = "CORSOLA", level = 20, timeGroup = 0,
|
||||||
|
day = { species = "CORSOLA", level = 20 },
|
||||||
|
nite = { species = "STARYU", level = 20 } },
|
||||||
|
},
|
||||||
|
super = {
|
||||||
|
{ chance = 102, species = "KRABBY", level = 40 },
|
||||||
|
{ chance = 178, species = "CORSOLA", level = 40, timeGroup = 1,
|
||||||
|
day = { species = "CORSOLA", level = 40 },
|
||||||
|
nite = { species = "STARYU", level = 40 } },
|
||||||
|
{ chance = 230, species = "KRABBY", level = 40 },
|
||||||
|
{ chance = 255, species = "KINGLER", level = 40 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Test Ocean group with TimeFishGroups (Shellder)
|
||||||
|
local OCEAN_ENCOUNTERS = {
|
||||||
|
fishGroups = {
|
||||||
|
FISHGROUP_OCEAN = {
|
||||||
|
id = "FISHGROUP_OCEAN",
|
||||||
|
chance = 255,
|
||||||
|
old = {
|
||||||
|
{ chance = 179, species = "MAGIKARP", level = 10 },
|
||||||
|
{ chance = 217, species = "MAGIKARP", level = 10 },
|
||||||
|
{ chance = 255, species = "TENTACOOL", level = 10 },
|
||||||
|
},
|
||||||
|
good = {
|
||||||
|
{ chance = 89, species = "MAGIKARP", level = 20 },
|
||||||
|
{ chance = 178, species = "TENTACOOL", level = 20 },
|
||||||
|
{ chance = 230, species = "CHINCHOU", level = 20 },
|
||||||
|
{ chance = 255, species = "SHELLDER", level = 20, timeGroup = 2,
|
||||||
|
day = { species = "SHELLDER", level = 20 },
|
||||||
|
nite = { species = "SHELLDER", level = 20 } },
|
||||||
|
},
|
||||||
|
super = {
|
||||||
|
{ chance = 102, species = "CHINCHOU", level = 40 },
|
||||||
|
{ chance = 178, species = "SHELLDER", level = 40, timeGroup = 3,
|
||||||
|
day = { species = "SHELLDER", level = 40 },
|
||||||
|
nite = { species = "SHELLDER", level = 40 } },
|
||||||
|
{ chance = 230, species = "TENTACRUEL", level = 40 },
|
||||||
|
{ chance = 255, species = "LANTURN", level = 40 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ---- Direct Encounter.fishSlot tests ---------------------------------------
|
||||||
|
do
|
||||||
|
-- Shore Good Rod at value 240 (slot 4) during DAY -> Corsola lv20
|
||||||
|
local rollDay = Encounter.fishSlot(SHORE_ENCOUNTERS, "ROUTE_34", "GOOD_ROD",
|
||||||
|
function(_n) return 240 end, { ROUTE_34 = { fishGroup = "FISHGROUP_SHORE" } }, nil, "DAY")
|
||||||
|
check(rollDay ~= nil, "Shore Good Rod bites during DAY")
|
||||||
|
eq(rollDay.species, "CORSOLA", "Shore Good Rod slot 4 is CORSOLA during DAY")
|
||||||
|
eq(rollDay.level, 20, "Shore Good Rod slot 4 level is 20")
|
||||||
|
|
||||||
|
-- Shore Good Rod at value 240 (slot 4) during NITE -> Staryu lv20
|
||||||
|
local rollNite = Encounter.fishSlot(SHORE_ENCOUNTERS, "ROUTE_34", "GOOD_ROD",
|
||||||
|
function(_n) return 240 end, { ROUTE_34 = { fishGroup = "FISHGROUP_SHORE" } }, nil, "NITE")
|
||||||
|
check(rollNite ~= nil, "Shore Good Rod bites during NITE")
|
||||||
|
eq(rollNite.species, "STARYU", "Shore Good Rod slot 4 is STARYU during NITE")
|
||||||
|
eq(rollNite.level, 20, "Shore Good Rod slot 4 level is 20")
|
||||||
|
|
||||||
|
-- Shore Super Rod at value 150 (slot 2) during DAY -> Corsola lv40
|
||||||
|
local rollSuperDay = Encounter.fishSlot(SHORE_ENCOUNTERS, "ROUTE_34", "SUPER_ROD",
|
||||||
|
function(_n) return 150 end, { ROUTE_34 = { fishGroup = "FISHGROUP_SHORE" } }, nil, "DAY")
|
||||||
|
check(rollSuperDay ~= nil, "Shore Super Rod bites during DAY")
|
||||||
|
eq(rollSuperDay.species, "CORSOLA", "Shore Super Rod slot 2 is CORSOLA during DAY")
|
||||||
|
eq(rollSuperDay.level, 40, "Shore Super Rod slot 2 level is 40")
|
||||||
|
|
||||||
|
-- Shore Super Rod at value 150 (slot 2) during NITE -> Staryu lv40
|
||||||
|
local rollSuperNite = Encounter.fishSlot(SHORE_ENCOUNTERS, "ROUTE_34", "SUPER_ROD",
|
||||||
|
function(_n) return 150 end, { ROUTE_34 = { fishGroup = "FISHGROUP_SHORE" } }, nil, "NITE")
|
||||||
|
check(rollSuperNite ~= nil, "Shore Super Rod bites during NITE")
|
||||||
|
eq(rollSuperNite.species, "STARYU", "Shore Super Rod slot 2 is STARYU during NITE")
|
||||||
|
eq(rollSuperNite.level, 40, "Shore Super Rod slot 2 level is 40")
|
||||||
|
|
||||||
|
-- Ocean Super Rod at value 150 (slot 2) during DAY/NITE -> Shellder lv40
|
||||||
|
local rollOceanSuper = Encounter.fishSlot(OCEAN_ENCOUNTERS, "ROUTE_41", "SUPER_ROD",
|
||||||
|
function(_n) return 150 end, { ROUTE_41 = { fishGroup = "FISHGROUP_OCEAN" } }, nil, "DAY")
|
||||||
|
check(rollOceanSuper ~= nil, "Ocean Super Rod bites")
|
||||||
|
eq(rollOceanSuper.species, "SHELLDER", "Ocean Super Rod slot 2 is SHELLDER")
|
||||||
|
eq(rollOceanSuper.level, 40, "Ocean Super Rod slot 2 level is 40")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- World:rollFishing integration tests -----------------------------------
|
||||||
|
do
|
||||||
|
-- World with Shore group in DAY time
|
||||||
|
local worldDay, _ = fishWorld("ROUTE_34", "FISHGROUP_SHORE", "DAY")
|
||||||
|
worldDay.encounters = SHORE_ENCOUNTERS
|
||||||
|
|
||||||
|
-- Mock math.random / love.math.random to land on slot 2 of super rod (value ~150)
|
||||||
|
love.math.random = function(n) return 151 end
|
||||||
|
local outcome, wild = worldDay:rollFishing("SUPER_ROD")
|
||||||
|
eq(outcome, "battle", "Super Rod triggers battle on Corsola slot")
|
||||||
|
check(wild ~= nil, "Wild mon is created")
|
||||||
|
eq(wild.species, "CORSOLA", "Wild mon is CORSOLA during DAY")
|
||||||
|
eq(wild.level, 40, "Wild mon level is 40")
|
||||||
|
|
||||||
|
-- World with Shore group in NITE time
|
||||||
|
local worldNite, _ = fishWorld("ROUTE_34", "FISHGROUP_SHORE", "NITE")
|
||||||
|
worldNite.encounters = SHORE_ENCOUNTERS
|
||||||
|
local outcomeN, wildN = worldNite:rollFishing("SUPER_ROD")
|
||||||
|
eq(outcomeN, "battle", "Super Rod triggers battle on Staryu slot")
|
||||||
|
check(wildN ~= nil, "Wild mon is created")
|
||||||
|
eq(wildN.species, "STARYU", "Wild mon is STARYU during NITE")
|
||||||
|
eq(wildN.level, 40, "Wild mon level is 40")
|
||||||
|
|
||||||
|
-- World with Ocean group -> Shellder
|
||||||
|
local worldOcean, _ = fishWorld("ROUTE_41", "FISHGROUP_OCEAN", "DAY")
|
||||||
|
worldOcean.encounters = OCEAN_ENCOUNTERS
|
||||||
|
local outcomeO, wildO = worldOcean:rollFishing("SUPER_ROD")
|
||||||
|
eq(outcomeO, "battle", "Super Rod triggers battle on Shellder slot")
|
||||||
|
check(wildO ~= nil, "Wild mon is created")
|
||||||
|
eq(wildO.species, "SHELLDER", "Wild mon is SHELLDER")
|
||||||
|
eq(wildO.level, 40, "Wild mon level is 40")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- Real Gold cache integration tests (if present) ------------------------
|
||||||
|
do
|
||||||
|
local cache = os.getenv("GOLD_CACHE")
|
||||||
|
if not cache then
|
||||||
|
local home = os.getenv("HOME") or ""
|
||||||
|
cache = home .. "/.local/share/love/pokemon-love2d/gold"
|
||||||
|
end
|
||||||
|
local encChunk = loadfile(cache .. "/data/generated/encounters.lua")
|
||||||
|
if not encChunk then
|
||||||
|
check(true, "no gold cache: time fish groups (SKIP)")
|
||||||
|
else
|
||||||
|
local enc = encChunk() or {}
|
||||||
|
local groups = enc.fishGroups or {}
|
||||||
|
local shore = groups.FISHGROUP_SHORE
|
||||||
|
check(shore ~= nil, "cache carries FISHGROUP_SHORE")
|
||||||
|
if shore and shore.super then
|
||||||
|
local slot2 = shore.super[2]
|
||||||
|
check(slot2 ~= nil, "shore super rod has slot 2")
|
||||||
|
if slot2 then
|
||||||
|
eq(slot2.timeGroup, 1, "slot 2 timeGroup is 1")
|
||||||
|
check(slot2.day ~= nil, "slot 2 has day entry")
|
||||||
|
check(slot2.nite ~= nil, "slot 2 has nite entry")
|
||||||
|
if slot2.day then eq(slot2.day.species, "CORSOLA", "day is CORSOLA") end
|
||||||
|
if slot2.nite then eq(slot2.nite.species, "STARYU", "nite is STARYU") end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ocean = groups.FISHGROUP_OCEAN
|
||||||
|
check(ocean ~= nil, "cache carries FISHGROUP_OCEAN")
|
||||||
|
if ocean and ocean.super then
|
||||||
|
local slot2 = ocean.super[2]
|
||||||
|
check(slot2 ~= nil, "ocean super rod has slot 2")
|
||||||
|
if slot2 then
|
||||||
|
eq(slot2.timeGroup, 3, "slot 2 timeGroup is 3")
|
||||||
|
check(slot2.day ~= nil, "slot 2 has day entry")
|
||||||
|
if slot2.day then eq(slot2.day.species, "SHELLDER", "day is SHELLDER") end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- ---- Animation & Bobber State Machine tests --------------------------------
|
||||||
|
do
|
||||||
|
local world, _ = fishWorld("ROUTE_34", "FISHGROUP_SHORE", "DAY")
|
||||||
|
world.encounters = SHORE_ENCOUNTERS
|
||||||
|
world.player.cellX = 5
|
||||||
|
world.player.cellY = 5
|
||||||
|
world.player.facing = "left"
|
||||||
|
|
||||||
|
local wildMon = { species = "CORSOLA", level = 40 }
|
||||||
|
world:beginFishing("battle", wildMon)
|
||||||
|
|
||||||
|
check(world.fishing ~= nil, "Fishing state is initialized")
|
||||||
|
eq(world.fishing.phase, "cast", "Initial phase is cast")
|
||||||
|
eq(world.fishing.timer, 40, "Cast timer is 40 frames")
|
||||||
|
check(world.fishing.bobber ~= nil, "Bobber is created")
|
||||||
|
eq(world.fishing.bobber.cellX, 4, "Bobber target X is 4 (one cell left)")
|
||||||
|
eq(world.fishing.bobber.cellY, 5, "Bobber target Y is 5")
|
||||||
|
eq(world.fishing.bobber.px, 64, "Bobber px is 64")
|
||||||
|
eq(world.fishing.bobber.py, 80, "Bobber py is 80")
|
||||||
|
eq(world.player.fishing, true, "Player has fishing flag")
|
||||||
|
|
||||||
|
-- Step through cast phase (40 frames + transition tick)
|
||||||
|
for f = 1, 40 + 1 do
|
||||||
|
world:updateFishing()
|
||||||
|
end
|
||||||
|
eq(world.fishing.phase, "bite", "Transitions to bite phase on battle outcome")
|
||||||
|
eq(world.fishing.timer, 40, "Bite timer is 40 frames")
|
||||||
|
|
||||||
|
-- Step through bite phase and check 1px alternating offset
|
||||||
|
local seenOdd, seenEven = false, false
|
||||||
|
for f = 1, 40 do
|
||||||
|
world:updateFishing()
|
||||||
|
if world.player.spriteYOffset == 1 then seenOdd = true end
|
||||||
|
if world.player.spriteYOffset == 0 then seenEven = true end
|
||||||
|
end
|
||||||
|
check(seenOdd and seenEven, "Player sprite Y offset alternates during bite phase")
|
||||||
|
-- Transition on next tick triggers text/battle and clears fishing state
|
||||||
|
world:updateFishing()
|
||||||
|
eq(world.fishing, nil, "Fishing state is cleared after bite completion")
|
||||||
|
eq(world.player.fishing, nil, "Player fishing flag is cleared after bite completion")
|
||||||
|
end
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
if not _G.love then
|
||||||
|
_G.love = {
|
||||||
|
audio = {
|
||||||
|
newSource = function() return { stop = function() end, play = function() end, setVolume = function() end } end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local SurfingMinigame = require("src.ui.SurfingMinigame")
|
||||||
|
|
||||||
|
local function assert_eq(got, want, msg)
|
||||||
|
if got ~= want then
|
||||||
|
error(string.format("%s: got %s, want %s", msg or "assertion failed", tostring(got), tostring(want)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assert_true(cond, msg)
|
||||||
|
if not cond then
|
||||||
|
error(string.format("%s: expected true", msg or "assertion failed"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mock game environment
|
||||||
|
local mockInput = {
|
||||||
|
keysDown = {},
|
||||||
|
keysPressed = {},
|
||||||
|
isDown = function(self, k) return not not self.keysDown[k] end,
|
||||||
|
wasPressed = function(self, k) return not not self.keysPressed[k] end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local mockGame = {
|
||||||
|
save = { surfingHighScore = 1000 },
|
||||||
|
input = mockInput,
|
||||||
|
stack = {
|
||||||
|
items = {},
|
||||||
|
pop = function(self) table.remove(self.items) end,
|
||||||
|
push = function(self, item) table.insert(self.items, item) end,
|
||||||
|
},
|
||||||
|
data = { audio = { sfx = {} } },
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Running SurfingMinigame unit tests...")
|
||||||
|
|
||||||
|
-- Test 1: Initialization
|
||||||
|
local mg = SurfingMinigame.new(mockGame)
|
||||||
|
assert_eq(mg.hp, 6000, "Initial HP must be 6000 (60.00s)")
|
||||||
|
assert_eq(mg.speed, 0.25, "Initial speed must be 0.25")
|
||||||
|
assert_eq(mg.distance, 0, "Initial distance must be 0")
|
||||||
|
assert_eq(mg.routine, 0, "Initial routine must be ROUTINE_START_GAME (0)")
|
||||||
|
assert_eq(mg.pikaState, 0, "Initial Pikachu state must be PIKA_STATE_RIDING (0)")
|
||||||
|
print("✓ Initial state test passed")
|
||||||
|
|
||||||
|
-- Test 2: Start banner transition to RunGame
|
||||||
|
for _ = 1, 40 do
|
||||||
|
mg:update()
|
||||||
|
end
|
||||||
|
assert_eq(mg.routine, 1, "Routine should advance to ROUTINE_RUN_GAME (1)")
|
||||||
|
print("✓ Start banner transition test passed")
|
||||||
|
|
||||||
|
-- Test 3: Automatic acceleration and HP countdown
|
||||||
|
local initialSpeed = mg.speed
|
||||||
|
local initialHp = mg.hp
|
||||||
|
mg:update()
|
||||||
|
assert_true(mg.speed > initialSpeed, "Pikachu should automatically accelerate while riding")
|
||||||
|
assert_eq(mg.hp, initialHp - 1, "HP should decrease by 1 each frame")
|
||||||
|
print("✓ Auto acceleration and HP countdown test passed")
|
||||||
|
|
||||||
|
-- Test 4: Landing Evaluation Matrix
|
||||||
|
mg.frameSet = 5
|
||||||
|
assert_eq(mg:evaluateLanding(), "rough", "Angle 5 on open water should be rough landing")
|
||||||
|
mg.frameSet = 6
|
||||||
|
assert_eq(mg:evaluateLanding(), "hard", "Angle 6 on open water should be hard landing")
|
||||||
|
mg.frameSet = 4
|
||||||
|
assert_eq(mg:evaluateLanding(), "clean", "Angle 4 (flat) on open water should be clean landing")
|
||||||
|
mg.frameSet = 1
|
||||||
|
assert_eq(mg:evaluateLanding(), "wipeout", "Angle 1 on open water should be wipeout")
|
||||||
|
for f = 8, 14 do
|
||||||
|
mg.frameSet = f
|
||||||
|
assert_eq(mg:evaluateLanding(), "wipeout", "Upside-down frame " .. f .. " must be wipeout")
|
||||||
|
end
|
||||||
|
print("✓ Landing evaluation matrix test passed (including upside-down frames 8..14)")
|
||||||
|
|
||||||
|
-- Test 5: Stunt Scoring
|
||||||
|
mg.radnessMeter = 1
|
||||||
|
mg.trickFlags = 1
|
||||||
|
mg.radness = 0
|
||||||
|
mg:calculateStuntPoints()
|
||||||
|
assert_eq(mg.radness, 50, "Single flip should award +50 radness points")
|
||||||
|
|
||||||
|
mg.radnessMeter = 2
|
||||||
|
mg.trickFlags = 1
|
||||||
|
mg.radness = 0
|
||||||
|
mg:calculateStuntPoints()
|
||||||
|
assert_eq(mg.radness, 150, "Double flip (same direction) should award +150 points")
|
||||||
|
|
||||||
|
mg.radnessMeter = 3
|
||||||
|
mg.trickFlags = 1
|
||||||
|
mg.radness = 0
|
||||||
|
mg:calculateStuntPoints()
|
||||||
|
assert_eq(mg.radness, 350, "Triple flip (same direction) should award +350 points")
|
||||||
|
|
||||||
|
mg.radnessMeter = 2
|
||||||
|
mg.trickFlags = 3
|
||||||
|
mg.radness = 0
|
||||||
|
mg:calculateStuntPoints()
|
||||||
|
assert_eq(mg.radness, 180, "Double flip (mixed) should award +180 points")
|
||||||
|
|
||||||
|
mg.radnessMeter = 3
|
||||||
|
mg.trickFlags = 3
|
||||||
|
mg.radness = 0
|
||||||
|
mg:calculateStuntPoints()
|
||||||
|
assert_eq(mg.radness, 500, "Triple flip (mixed) should award +500 points")
|
||||||
|
print("✓ Stunt scoring calculation test passed")
|
||||||
|
|
||||||
|
-- Test 6: Non-fatal wipeout crash recovery
|
||||||
|
mg.pikaState = 3 -- PIKA_STATE_CRASHED
|
||||||
|
mg.crashTimer = 96
|
||||||
|
mg.speed = 0.25
|
||||||
|
for _ = 1, 95 do
|
||||||
|
mg:update()
|
||||||
|
assert_eq(mg.pikaState, 3, "Pikachu should remain in crashed state during timer")
|
||||||
|
end
|
||||||
|
mg:update()
|
||||||
|
assert_eq(mg.pikaState, 0, "Pikachu should recover and return to PIKA_STATE_RIDING after 96 frames")
|
||||||
|
print("✓ Wipeout crash recovery test passed")
|
||||||
|
|
||||||
|
-- Test 7: Results tally countdown sequence
|
||||||
|
mg.routine = 7 -- ROUTINE_WRITE_TOTAL
|
||||||
|
mg.hp = 100
|
||||||
|
mg.radness = 200
|
||||||
|
mg.totalScore = 0
|
||||||
|
mg.routineTimer = 1
|
||||||
|
mg:update()
|
||||||
|
assert_eq(mg.routine, 8, "Routine should advance to ROUTINE_ADD_HP_TOTAL (8)")
|
||||||
|
|
||||||
|
while mg.routine == 8 do
|
||||||
|
mg:update()
|
||||||
|
end
|
||||||
|
assert_eq(mg.hp, 0, "HP should be tallied down to 0")
|
||||||
|
assert_eq(mg.totalScore, 100, "Total score should include 100 from HP")
|
||||||
|
assert_eq(mg.routine, 9, "Routine should advance to ROUTINE_ADD_RAD_TOTAL (9)")
|
||||||
|
|
||||||
|
while mg.routine == 9 do
|
||||||
|
mg:update()
|
||||||
|
end
|
||||||
|
assert_eq(mg.radness, 0, "Radness should be tallied down to 0")
|
||||||
|
assert_eq(mg.totalScore, 300, "Total score should be 300 (100 HP + 200 Radness)")
|
||||||
|
assert_eq(mg.routine, 10, "Routine should advance to ROUTINE_WAIT_LAST (10)")
|
||||||
|
print("✓ Results tally countdown test passed")
|
||||||
|
|
||||||
|
print("All SurfingMinigame unit tests passed successfully!")
|
||||||
@@ -668,7 +668,7 @@ REQUIRED_SYMBOLS = {
|
|||||||
# Wild encounters (data/wild/*)
|
# Wild encounters (data/wild/*)
|
||||||
"JohtoGrassWildMons", "JohtoWaterWildMons",
|
"JohtoGrassWildMons", "JohtoWaterWildMons",
|
||||||
"KantoGrassWildMons", "KantoWaterWildMons",
|
"KantoGrassWildMons", "KantoWaterWildMons",
|
||||||
"FishGroups", "TreeMons", "TreeMonMaps",
|
"FishGroups", "TimeFishGroups", "TreeMons", "TreeMonMaps",
|
||||||
# data/wild/treemon_maps.asm RockMonMaps: the four maps whose smashable
|
# data/wild/treemon_maps.asm RockMonMaps: the four maps whose smashable
|
||||||
# rocks roll TREEMON_SET_ROCK, read by RockMonEncounter.
|
# rocks roll TREEMON_SET_ROCK, read by RockMonEncounter.
|
||||||
"RockMonMaps",
|
"RockMonMaps",
|
||||||
@@ -785,6 +785,7 @@ REQUIRED_SYMBOLS = {
|
|||||||
# Emote bubbles (data/sprites/emotes.asm): showemote's ! over a trainer
|
# Emote bubbles (data/sprites/emotes.asm): showemote's ! over a trainer
|
||||||
# who just spotted the player, and the other faces scripts use.
|
# who just spotted the player, and the other faces scripts use.
|
||||||
"ShockEmote", "QuestionEmote", "HappyEmote", "SadEmote",
|
"ShockEmote", "QuestionEmote", "HappyEmote", "SadEmote",
|
||||||
|
"HeartEmote", "BoltEmote", "SleepEmote", "FishEmote",
|
||||||
# The Pokecenter heal machine's OBJ art (engine/events/
|
# The Pokecenter heal machine's OBJ art (engine/events/
|
||||||
# heal_machine_anim.asm): two tiles -- the machine's light ($7c) and the
|
# heal_machine_anim.asm): two tiles -- the machine's light ($7c) and the
|
||||||
# ball ($7d) -- plus the CGB palette .LoadPalettes copies over
|
# ball ($7d) -- plus the CGB palette .LoadPalettes copies over
|
||||||
|
|||||||
@@ -16096,6 +16096,22 @@
|
|||||||
5,
|
5,
|
||||||
17417
|
17417
|
||||||
],
|
],
|
||||||
|
"HeartEmote": [
|
||||||
|
5,
|
||||||
|
17673
|
||||||
|
],
|
||||||
|
"BoltEmote": [
|
||||||
|
5,
|
||||||
|
17737
|
||||||
|
],
|
||||||
|
"SleepEmote": [
|
||||||
|
5,
|
||||||
|
17801
|
||||||
|
],
|
||||||
|
"FishEmote": [
|
||||||
|
5,
|
||||||
|
17865
|
||||||
|
],
|
||||||
"Shrink1Pic": [
|
"Shrink1Pic": [
|
||||||
62,
|
62,
|
||||||
30142
|
30142
|
||||||
@@ -16512,6 +16528,10 @@
|
|||||||
5,
|
5,
|
||||||
22206
|
22206
|
||||||
],
|
],
|
||||||
|
"TimeFishGroups": [
|
||||||
|
36,
|
||||||
|
27614
|
||||||
|
],
|
||||||
"TitleScreenGFX1": [
|
"TitleScreenGFX1": [
|
||||||
38,
|
38,
|
||||||
16384
|
16384
|
||||||
|
|||||||
Reference in New Issue
Block a user