mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 14:59:27 -04:00
feat(game3): fix held item consumption, cycling road enforcement, door warp arrivals, and optimize field rendering
- Fix held item (berries, herbs, pinch stat items) activation and permanent consumption on battler and party data during battles. - Enforce mandatory bicycle riding on Cycling Road (Route 16-18) when dismounting Surf, entering map, loading saves, and prevent bike dismount attempts. - Fix warp and door exit arrivals (e.g. Safari Zone gate) to execute warpExitArrival post-fade step so the player is placed on the correct forward tile and does not bypass triggers. - Optimize field rendering pipeline by caching animated tileset pieces, pooling native metatile cell allocations, and hoisting dynamic module lookups to fast accessors.
This commit is contained in:
@@ -461,6 +461,9 @@ function Audio.specialMapSong(sectionId)
|
||||
and Audio.canOverrideMapMusic(Audio.MUS_SURF, sectionId) then
|
||||
return Audio.MUS_SURF
|
||||
end
|
||||
if P and P.biking and Audio.canOverrideMapMusic(Audio.MUS_CYCLING, sectionId) then
|
||||
return Audio.MUS_CYCLING
|
||||
end
|
||||
return Audio._mapSong
|
||||
end
|
||||
|
||||
|
||||
@@ -1365,10 +1365,20 @@ end
|
||||
-- pokefirered/src/battle_util.c:1208
|
||||
function Engine.afterAction(st, ad)
|
||||
if not st or st.over then return end
|
||||
local first = State.battler(st, State.battlerOrder(st)[1]) or st.player
|
||||
for _ = 1, 4 do
|
||||
local did = Abilities.runIntimidate(ad) or Abilities.runTrace(ad)
|
||||
or HeldItems.normal(ad, first, true) or Abilities.forecast(ad)
|
||||
local did = false
|
||||
did = did or Abilities.runIntimidate(ad)
|
||||
did = did or Abilities.runTrace(ad)
|
||||
for _, id in ipairs(State.battlerOrder(st)) do
|
||||
local b = State.battler(st, id)
|
||||
if b and not ad:isFainted(b) then
|
||||
if HeldItems.normal(ad, b, true) then
|
||||
did = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
did = did or Abilities.forecast(ad)
|
||||
if not did then break end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -249,7 +249,7 @@ function HeldItems.normal(ad, b, moveTurn)
|
||||
if he == 0 then return false end
|
||||
local hp, maxHp = ad:hp(b), ad:maxHp(b)
|
||||
if he == H.RESTORE_HP then
|
||||
if hp <= math.floor(maxHp / 2) and not moveTurn then
|
||||
if hp <= math.floor(maxHp / 2) then
|
||||
local amt = param
|
||||
if hp + param > maxHp then amt = maxHp - hp end
|
||||
heal_berry(ad, b, item, amt)
|
||||
@@ -257,7 +257,7 @@ function HeldItems.normal(ad, b, moveTurn)
|
||||
return true
|
||||
end
|
||||
elseif he == H.RESTORE_PP then
|
||||
if not moveTurn then return leppa(ad, b, item, param) end
|
||||
return leppa(ad, b, item, param)
|
||||
elseif he == H.RESTORE_STATS then
|
||||
return white_herb(ad, b, item)
|
||||
elseif he == H.LEFTOVERS then
|
||||
@@ -270,7 +270,7 @@ function HeldItems.normal(ad, b, moveTurn)
|
||||
return true
|
||||
end
|
||||
elseif he >= H.CONFUSE_SPICY and he <= H.CONFUSE_SOUR then
|
||||
if hp <= math.floor(maxHp / 2) and not moveTurn then
|
||||
if hp <= math.floor(maxHp / 2) then
|
||||
local flavor = he - H.CONFUSE_SPICY
|
||||
local amt = math.floor(maxHp / math.max(1, param))
|
||||
if amt == 0 then amt = 1 end
|
||||
@@ -293,12 +293,12 @@ function HeldItems.normal(ad, b, moveTurn)
|
||||
end
|
||||
elseif he >= H.ATTACK_UP and he <= H.SP_DEFENSE_UP then
|
||||
local stat = STAT_ORDER[he - H.ATTACK_UP + 1]
|
||||
if hp <= math.floor(maxHp / math.max(1, param)) and not moveTurn and (b.stages[stat] or 0) < 6 then
|
||||
if hp <= math.floor(maxHp / math.max(1, param)) and (b.stages[stat] or 0) < 6 then
|
||||
stat_up(ad, b, item, stat, 1)
|
||||
return true
|
||||
end
|
||||
elseif he == H.CRITICAL_UP then
|
||||
if hp <= math.floor(maxHp / math.max(1, param)) and not moveTurn and not (b.focusEnergy or b.expFocusEnergy) then
|
||||
if hp <= math.floor(maxHp / math.max(1, param)) and not (b.focusEnergy or b.expFocusEnergy) then
|
||||
b.focusEnergy = true
|
||||
b.expFocusEnergy = true
|
||||
item_anim(ad, b)
|
||||
@@ -307,7 +307,7 @@ function HeldItems.normal(ad, b, moveTurn)
|
||||
return true
|
||||
end
|
||||
elseif he == H.RANDOM_STAT_UP then
|
||||
if not moveTurn and hp <= math.floor(maxHp / math.max(1, param)) then
|
||||
if hp <= math.floor(maxHp / math.max(1, param)) then
|
||||
local any = false
|
||||
for _, s in ipairs(STAT_ORDER) do
|
||||
if (b.stages[s] or 0) < 6 then any = true end
|
||||
|
||||
@@ -227,7 +227,6 @@ function Handlers.registerAll()
|
||||
Residuals.register("held_items", function(ctx)
|
||||
local HeldItems = require("src.core.game3.battle.held_items")
|
||||
HeldItems.normal(ctx.adapter, ctx.target, false)
|
||||
HeldItems.normal(ctx.adapter, ctx.target, true)
|
||||
end)
|
||||
|
||||
-- pokefirered/src/battle_util.c:1208
|
||||
|
||||
@@ -45,6 +45,28 @@ local FLY_BIRD_W, FLY_BIRD_H, FLY_BIRD_FRAMES = 64, 64, 5
|
||||
-- Forward-declared so field-effect starters defined above the body can call it.
|
||||
local play_se
|
||||
|
||||
local _M = {}
|
||||
local function getMod(key, path)
|
||||
local m = _M[key]
|
||||
if not m then
|
||||
m = package.loaded[path]
|
||||
if not m then
|
||||
local ok, loaded = pcall(require, path)
|
||||
if ok then m = loaded end
|
||||
end
|
||||
_M[key] = m
|
||||
end
|
||||
return m
|
||||
end
|
||||
|
||||
local function modFieldView() return getMod("FieldView", "src.core.game3.field_view") end
|
||||
local function modHeal() return getMod("Heal", "src.core.game3.pokecenter_heal") end
|
||||
local function modShowMon() return getMod("ShowMon", "src.core.game3.field_move_show_mon") end
|
||||
local function modItemfinder() return getMod("Itemfinder", "src.core.game3.itemfinder") end
|
||||
local function modAudio() return getMod("Audio", "src.core.game3.audio") end
|
||||
local function modOwSprites() return getMod("OwSprites", "src.core.game3.ow_sprites") end
|
||||
local function modRenderer() return getMod("Renderer", "src.render.Renderer") end
|
||||
|
||||
local function log(msg)
|
||||
if FieldEffects._logged then return end
|
||||
FieldEffects._logged = true
|
||||
@@ -112,14 +134,15 @@ function FieldEffects.install(cache)
|
||||
FieldEffects._ground = nil
|
||||
FieldEffects._surfClock = 0
|
||||
FieldEffects._logged = false
|
||||
local okV, FieldView = pcall(require, "src.core.game3.field_view")
|
||||
if okV and FieldView and FieldView.setCameraPanning then
|
||||
local FieldView = modFieldView()
|
||||
if FieldView and FieldView.setCameraPanning then
|
||||
FieldView.setCameraPanning(0, 0)
|
||||
FieldView.setFlashRadius(nil)
|
||||
end
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and Heal.install then Heal.install(cache) end
|
||||
require("src.core.game3.field_move_show_mon").invalidate()
|
||||
local Heal = modHeal()
|
||||
if Heal and Heal.install then Heal.install(cache) end
|
||||
local ShowMon = modShowMon()
|
||||
if ShowMon and ShowMon.invalidate then ShowMon.invalidate() end
|
||||
end
|
||||
|
||||
function FieldEffects.invalidate()
|
||||
@@ -127,14 +150,15 @@ function FieldEffects.invalidate()
|
||||
FieldEffects._fx = nil
|
||||
FieldEffects._anims = {}
|
||||
FieldEffects._ground = nil
|
||||
local okV, FieldView = pcall(require, "src.core.game3.field_view")
|
||||
if okV and FieldView and FieldView.setCameraPanning then
|
||||
local FieldView = modFieldView()
|
||||
if FieldView and FieldView.setCameraPanning then
|
||||
FieldView.setCameraPanning(0, 0)
|
||||
FieldView.setFlashRadius(nil)
|
||||
end
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and Heal.invalidate then Heal.invalidate() end
|
||||
require("src.core.game3.field_move_show_mon").invalidate()
|
||||
local Heal = modHeal()
|
||||
if Heal and Heal.invalidate then Heal.invalidate() end
|
||||
local ShowMon = modShowMon()
|
||||
if ShowMon and ShowMon.invalidate then ShowMon.invalidate() end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- Tall Grass
|
||||
@@ -246,15 +270,9 @@ function FieldEffects.startRockSmash(targetObj, cx, cy, onDone)
|
||||
table.insert(FieldEffects._anims, anim)
|
||||
end
|
||||
|
||||
local function fieldView()
|
||||
local ok, FieldView = pcall(require, "src.core.game3.field_view")
|
||||
if ok and type(FieldView) == "table" then return FieldView end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- pokefirered/src/field_screen_effect.c:194
|
||||
function FieldEffects.animateFlashLevel(fromLevel, toLevel)
|
||||
local FieldView = fieldView()
|
||||
local FieldView = modFieldView()
|
||||
if not FieldView then return nil end
|
||||
local from = FieldView.radiusForLevel(fromLevel)
|
||||
local to = FieldView.radiusForLevel(toLevel)
|
||||
@@ -863,8 +881,8 @@ function FieldEffects.startDestroyDeoxysRock(localId, graphicsId)
|
||||
-- shards start at the rock's own top-left corner (4px higher) and fly apart.
|
||||
-- eo.px/py is the cell's foot point, so undo the offset OwSprites.draw adds.
|
||||
local originX, originY = x - 8, y - 20
|
||||
local okO, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||||
if okO and OwSprites and OwSprites.getDraw then
|
||||
local OwSprites = modOwSprites()
|
||||
if OwSprites and OwSprites.getDraw then
|
||||
local spr = OwSprites.getDraw(graphicsId)
|
||||
if spr and spr.width and spr.height then
|
||||
originX = x + (16 - spr.width) / 2
|
||||
@@ -941,8 +959,8 @@ function FieldEffects.startEmote(targetObj, emoteType, onDone)
|
||||
table.insert(FieldEffects._anims, anim)
|
||||
if emoteType == "exclamation" or emoteType == 0 or emoteType == 0x62 or
|
||||
emoteType == "double_exclamation" or emoteType == 1 or emoteType == 0x65 then
|
||||
local okA, Audio = pcall(require, "src.core.game3.audio")
|
||||
if okA and Audio and Audio.playSe then
|
||||
local Audio = modAudio()
|
||||
if Audio and Audio.playSe then
|
||||
Audio.playSe(21) -- SE_PIN
|
||||
end
|
||||
end
|
||||
@@ -998,8 +1016,8 @@ local function anim_frame(seq, t, loop)
|
||||
end
|
||||
|
||||
function play_se(id)
|
||||
local okA, Audio = pcall(require, "src.core.game3.audio")
|
||||
if okA and Audio and Audio.playSe then Audio.playSe(id) end
|
||||
local Audio = modAudio()
|
||||
if Audio and Audio.playSe then Audio.playSe(id) end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------- setfieldeffectargument plumbing
|
||||
@@ -1496,9 +1514,10 @@ function FieldEffects.step()
|
||||
|
||||
resolve_waiters()
|
||||
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and Heal.step then Heal.step() end
|
||||
require("src.core.game3.field_move_show_mon").step()
|
||||
local Heal = modHeal()
|
||||
if Heal and Heal.step then Heal.step() end
|
||||
local ShowMon = modShowMon()
|
||||
if ShowMon and ShowMon.step then ShowMon.step() end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- Drawing
|
||||
@@ -1586,38 +1605,193 @@ function FieldEffects.drawBehind(camX, camY)
|
||||
end
|
||||
end
|
||||
|
||||
--- Draw in front of player (feet cover, cut grass particles, rock smash rubble, bird, ripples)
|
||||
function FieldEffects.drawFront(camX, camY, playerPy)
|
||||
camX, camY = camX or 0, camY or 0
|
||||
--- Collect ground/feet field effects as sorted actors (OAM Y-ordering with players/NPCs).
|
||||
function FieldEffects.collectActors(actors)
|
||||
if not actors then return end
|
||||
|
||||
-- 1) Tall grass feet cover
|
||||
-- 1) Tall grass feet cover (player active effect)
|
||||
local fx = FieldEffects._fx
|
||||
if fx then
|
||||
local sheet = load_sheet("tall_grass", 16, 16, 5)
|
||||
if sheet and sheet.quadsFront then
|
||||
local drawCover = true
|
||||
if playerPy ~= nil then
|
||||
local feetY = playerPy + CELL
|
||||
local grassTop = fx.cy * CELL
|
||||
local grassBot = grassTop + CELL
|
||||
if feetY < grassTop + FEET_H or feetY > grassBot + 2 then
|
||||
drawCover = false
|
||||
end
|
||||
local sheetGrass = load_sheet("tall_grass", 16, 16, 5)
|
||||
if fx and sheetGrass and sheetGrass.quadsFront then
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local playerPy = P and P.py
|
||||
local drawCover = true
|
||||
if playerPy ~= nil then
|
||||
local feetY = playerPy + CELL
|
||||
local grassTop = fx.cy * CELL
|
||||
local grassBot = grassTop + CELL
|
||||
if feetY < grassTop + FEET_H or feetY > grassBot + 2 then
|
||||
drawCover = false
|
||||
end
|
||||
if drawCover then
|
||||
local frameIdx = RUSTLE[fx.step + 1] or 0
|
||||
local q = sheet.quadsFront[frameIdx]
|
||||
if q then
|
||||
local sx = fx.cx * CELL - camX
|
||||
local sy = fx.cy * CELL - camY + (16 - FEET_H)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, sx, sy)
|
||||
end
|
||||
if drawCover then
|
||||
local frameIdx = RUSTLE[fx.step + 1] or 0
|
||||
local q = sheetGrass.quadsFront[frameIdx]
|
||||
if q then
|
||||
local gx = fx.cx * CELL
|
||||
local gy = fx.cy * CELL + (16 - FEET_H)
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_grass",
|
||||
elevation = P and P.elevation or 3,
|
||||
sortY = fx.cy * CELL + 0.5,
|
||||
x = gx,
|
||||
y = gy,
|
||||
i = 90000,
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheetGrass.image, q, gx - camX, gy - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 2) Grass feet cover for NPCs standing on grass
|
||||
local Objects = package.loaded["src.core.game3.objects"]
|
||||
local Collision = package.loaded["src.core.game3.collision"]
|
||||
if Objects and Objects.forDraw and Collision and Collision.isGrass and sheetGrass and sheetGrass.quadsFront then
|
||||
local qStatic = sheetGrass.quadsFront[4]
|
||||
if qStatic then
|
||||
for _, eo in ipairs(Objects.forDraw()) do
|
||||
local cx = eo.cellX
|
||||
local cy = eo.cellY
|
||||
local isPlayerFx = fx and fx.cx == cx and fx.cy == cy
|
||||
if not isPlayerFx and cx and cy and Collision.isGrass(cx, cy) then
|
||||
local npcPy = eo.py or (cy * CELL)
|
||||
local feetY = npcPy + CELL
|
||||
local grassTop = cy * CELL
|
||||
local grassBot = grassTop + CELL
|
||||
if feetY >= grassTop + FEET_H and feetY <= grassBot + 2 then
|
||||
local gx = cx * CELL
|
||||
local gy = cy * CELL + (16 - FEET_H)
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_npc_grass",
|
||||
elevation = eo.elevation or (eo.def and eo.def.elevation) or 3,
|
||||
sortY = npcPy + 0.5,
|
||||
x = gx,
|
||||
y = gy,
|
||||
i = 90000 + (tonumber(eo.localId) or 0),
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheetGrass.image, qStatic, gx - camX, gy - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 2) Transient particle animations
|
||||
-- 3) Ground / actor attached transient animations
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
for idx, anim in ipairs(FieldEffects._anims) do
|
||||
if anim.kind == "splash" or anim.kind == "feet_water" then
|
||||
local sheet = load_sheet("splash", 16, 8, 2)
|
||||
local q = sheet and P and sheet.quads[anim.frame or 0]
|
||||
if q and P then
|
||||
local px = P.px
|
||||
local py = P.py + FEET_H
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_splash",
|
||||
elevation = P.elevation or 3,
|
||||
sortY = P.py + 0.5,
|
||||
x = px,
|
||||
y = py,
|
||||
i = 91000 + idx,
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, px - camX, py - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
elseif anim.kind == "dust" then
|
||||
local sheet = load_sheet("ground_impact_dust", 16, 8, 3)
|
||||
local q = sheet and sheet.quads[anim.frame or 0]
|
||||
if q then
|
||||
local dx = anim.cx * CELL
|
||||
local dy = anim.cy * CELL + 8
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_dust",
|
||||
elevation = 3,
|
||||
sortY = anim.cy * CELL + 0.5,
|
||||
x = dx,
|
||||
y = dy,
|
||||
i = 91000 + idx,
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, dx - camX, dy - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
elseif anim.kind == "hot_springs" then
|
||||
local sheet = load_sheet("hot_springs_water", 16, 16, 1)
|
||||
local q = sheet and P and sheet.quads[0]
|
||||
if q and P then
|
||||
local px = P.px
|
||||
local py = P.py
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_hot_springs",
|
||||
elevation = P.elevation or 3,
|
||||
sortY = P.py + 0.5,
|
||||
x = px,
|
||||
y = py,
|
||||
i = 91000 + idx,
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, px - camX, py - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
elseif anim.kind == "cut_tree" then
|
||||
local sheet = load_sheet("cut_tree", 32, 32, 4)
|
||||
local f = math.min(3, math.floor((anim.timer or 0) / 6))
|
||||
local q = sheet and sheet.quads[f]
|
||||
if q then
|
||||
local tx = anim.cx * CELL - 8
|
||||
local ty = anim.cy * CELL - 16
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_cut_tree",
|
||||
elevation = 3,
|
||||
sortY = anim.cy * CELL,
|
||||
x = tx,
|
||||
y = ty,
|
||||
i = 91000 + idx,
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, tx - camX, ty - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
elseif anim.kind == "emote" or anim.kind == "exclamation" then
|
||||
local sheet = load_sheet("emoticons", 16, 16, 15)
|
||||
if sheet and sheet.quads[anim.frame] then
|
||||
local t = anim.targetObj
|
||||
local ox = t and (t.px or (t.cellX and t.cellX * CELL) or (t.x and t.x * CELL)) or 0
|
||||
local oy = t and (t.py or (t.cellY and t.cellY * CELL) or (t.y and t.y * CELL)) or 0
|
||||
local sx = ox
|
||||
local sy = oy - 16
|
||||
actors[#actors + 1] = {
|
||||
kind = "field_effect_emote",
|
||||
elevation = t and t.elevation or 3,
|
||||
sortY = oy + 0.5,
|
||||
x = sx,
|
||||
y = sy,
|
||||
i = 91000 + idx,
|
||||
draw = function(_, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, sheet.quads[anim.frame], sx - camX, sy - camY)
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Draw in front of all actors (floating/airborne particles, rock smash rubble, bird)
|
||||
function FieldEffects.drawFront(camX, camY, playerPy)
|
||||
camX, camY = camX or 0, camY or 0
|
||||
|
||||
-- Transient airborne particle animations
|
||||
for _, anim in ipairs(FieldEffects._anims) do
|
||||
if anim.kind == "cut_grass_scatter" then
|
||||
local sheet = load_sheet("cut_grass", 8, 8, 1)
|
||||
@@ -1661,43 +1835,6 @@ function FieldEffects.drawFront(camX, camY, playerPy)
|
||||
end
|
||||
elseif (anim.kind == "fly_out" or anim.kind == "fly_in") and anim.bird then
|
||||
draw_bird(anim.bird, camX, camY)
|
||||
elseif anim.kind == "emote" or anim.kind == "exclamation" then
|
||||
local sheet = load_sheet("emoticons", 16, 16, 15)
|
||||
if sheet and sheet.quads[anim.frame] then
|
||||
local t = anim.targetObj
|
||||
local ox = t and (t.px or (t.cellX and t.cellX * CELL) or (t.x and t.x * CELL)) or 0
|
||||
local oy = t and (t.py or (t.cellY and t.cellY * CELL) or (t.y and t.y * CELL)) or 0
|
||||
local sx = ox - camX
|
||||
local sy = oy - 16 - camY
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, sheet.quads[anim.frame], sx, sy)
|
||||
end
|
||||
elseif anim.kind == "splash" or anim.kind == "feet_water" then
|
||||
-- pokefirered/src/field_effect_helpers.c:598 FldEff_Splash
|
||||
local sheet = load_sheet("splash", 16, 8, 2)
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local q = sheet and P and sheet.quads[anim.frame or 0]
|
||||
if q then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, P.px - camX, P.py + FEET_H - camY)
|
||||
end
|
||||
elseif anim.kind == "dust" then
|
||||
-- pokefirered/src/field_effect_helpers.c:1117
|
||||
local sheet = load_sheet("ground_impact_dust", 16, 8, 3)
|
||||
local q = sheet and sheet.quads[anim.frame or 0]
|
||||
if q then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, anim.cx * CELL - camX, anim.cy * CELL + 8 - camY)
|
||||
end
|
||||
elseif anim.kind == "hot_springs" then
|
||||
-- pokefirered/src/field_effect_helpers.c:777 UpdateHotSpringsWaterFieldEffect
|
||||
local sheet = load_sheet("hot_springs_water", 16, 16, 1)
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local q = sheet and P and sheet.quads[0]
|
||||
if q then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(sheet.image, q, P.px - camX, P.py - camY)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1711,8 +1848,8 @@ function FieldEffects.drawOverlay(camX, camY)
|
||||
-- 1) Flash screen illumination
|
||||
for _, anim in ipairs(FieldEffects._anims) do
|
||||
if anim.kind == "flash" and anim.alpha > 0 then
|
||||
local okR, Renderer = pcall(require, "src.render.Renderer")
|
||||
if okR and Renderer and Renderer.canvas then
|
||||
local Renderer = modRenderer()
|
||||
if Renderer and Renderer.canvas then
|
||||
Renderer.screenVeil = { 1, 1, 1, anim.alpha }
|
||||
else
|
||||
love.graphics.setColor(1, 1, 1, anim.alpha)
|
||||
@@ -1736,10 +1873,12 @@ function FieldEffects.drawOverlay(camX, camY)
|
||||
end
|
||||
end
|
||||
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and Heal.draw then Heal.draw(camX, camY) end
|
||||
require("src.core.game3.itemfinder").draw()
|
||||
require("src.core.game3.field_move_show_mon").draw()
|
||||
local Heal = modHeal()
|
||||
if Heal and Heal.draw then Heal.draw(camX, camY) end
|
||||
local Itemfinder = modItemfinder()
|
||||
if Itemfinder and Itemfinder.draw then Itemfinder.draw() end
|
||||
local ShowMon = modShowMon()
|
||||
if ShowMon and ShowMon.draw then ShowMon.draw() end
|
||||
end
|
||||
|
||||
--- pret dofieldeffect / waitfieldeffect for FLDEFF_POKECENTER_HEAL (25).
|
||||
@@ -1759,8 +1898,8 @@ function FieldEffects.doFieldEffect(id)
|
||||
local localId = FieldEffects.fieldEffectArgument(0, 1)
|
||||
return FieldEffects.startDestroyDeoxysRock(localId) ~= nil
|
||||
end
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and id == Heal.FLDEFF then
|
||||
local Heal = modHeal()
|
||||
if Heal and id == Heal.FLDEFF then
|
||||
return Heal.start()
|
||||
end
|
||||
return false
|
||||
@@ -1777,8 +1916,8 @@ function FieldEffects.waitFieldEffect(id, done)
|
||||
FieldEffects._waiters[#FieldEffects._waiters + 1] = { id = id, done = done }
|
||||
return
|
||||
end
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and id == Heal.FLDEFF then
|
||||
local Heal = modHeal()
|
||||
if Heal and id == Heal.FLDEFF then
|
||||
Heal.wait(done)
|
||||
return
|
||||
end
|
||||
@@ -1794,8 +1933,8 @@ function FieldEffects.isFieldEffectActive(id)
|
||||
end
|
||||
return false
|
||||
end
|
||||
local ok, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if ok and Heal and id == Heal.FLDEFF then
|
||||
local Heal = modHeal()
|
||||
if Heal and id == Heal.FLDEFF then
|
||||
return Heal.isActive()
|
||||
end
|
||||
return false
|
||||
|
||||
+133
-68
@@ -59,6 +59,38 @@ local function screenAnchor(px, py, camX, camY)
|
||||
return (px - camX) - PLAYER_SCREEN_X, (py - camY) - PLAYER_SCREEN_Y
|
||||
end
|
||||
|
||||
local _M = {}
|
||||
local function getMod(key, path)
|
||||
local m = _M[key]
|
||||
if not m then
|
||||
m = package.loaded[path]
|
||||
if not m then
|
||||
local ok, loaded = pcall(require, path)
|
||||
if ok then m = loaded end
|
||||
end
|
||||
_M[key] = m
|
||||
end
|
||||
return m
|
||||
end
|
||||
|
||||
local function modObjects() return getMod("Objects", "src.core.game3.objects") end
|
||||
local function modOwSprites() return getMod("OwSprites", "src.core.game3.ow_sprites") end
|
||||
local function modNativeTileset() return getMod("NativeTileset", "src.core.game3.tileset_native") end
|
||||
local function modFieldEffects() return getMod("FieldEffects", "src.core.game3.field_effects") end
|
||||
local function modSpriteRenderer() return getMod("SpriteRenderer", "src.render.SpriteRenderer") end
|
||||
local function modPalettes() return getMod("Palettes", "src.world.gen2.Palettes") end
|
||||
local function modFlags() return getMod("Flags", "src.core.game3.scripting.flags") end
|
||||
local function modDoors() return getMod("Doors", "src.core.game3.doors") end
|
||||
local function modHeal() return getMod("Heal", "src.core.game3.pokecenter_heal") end
|
||||
local function modSSAnne() return getMod("SSAnne", "src.core.game3.ss_anne_cutscene") end
|
||||
local function modFieldWeather() return getMod("FieldWeather", "src.core.game3.field_weather") end
|
||||
local function modAssets() return getMod("Assets", "src.render.Assets") end
|
||||
local function modPlayer() return getMod("Player", "src.core.game3.player") end
|
||||
local function modGbcPalette() return getMod("GbcPalette", "src.render.GbcPalette") end
|
||||
local function modSeagallop() return getMod("SeagallopUi", "src.ui.game3.seagallop") end
|
||||
local function modShopMenu() return getMod("ShopMenu", "src.ui.game3.shop_menu") end
|
||||
local function modFollower() return getMod("Follower", "src.world.game3.Follower") end
|
||||
|
||||
local function log(msg)
|
||||
print("[game3/field] " .. tostring(msg))
|
||||
end
|
||||
@@ -90,8 +122,8 @@ local function loadAtlas(tileset)
|
||||
return FieldView._atlas
|
||||
end
|
||||
local img
|
||||
local ok, Assets = pcall(require, "src.render.Assets")
|
||||
if ok and Assets and Assets.image then
|
||||
local Assets = modAssets()
|
||||
if Assets and Assets.image then
|
||||
local aok, aimg = pcall(Assets.image, path)
|
||||
if aok then img = aimg end
|
||||
end
|
||||
@@ -145,8 +177,8 @@ end
|
||||
|
||||
--- Resolve Sevii special BG palette set (8 slots × 4 RGB).
|
||||
local function resolveBgSet(game, mapDef, daytime)
|
||||
local ok, Palettes = pcall(require, "src.world.gen2.Palettes")
|
||||
if not ok or not Palettes then return nil end
|
||||
local Palettes = modPalettes()
|
||||
if not Palettes then return nil end
|
||||
local data = game and game.data
|
||||
local pals = data and (data.gen2Palettes or data.palettes)
|
||||
if not pals then return nil end
|
||||
@@ -173,9 +205,9 @@ end
|
||||
|
||||
local function playerPixels(game)
|
||||
-- Game3 avatar is source of truth while Runtime is active.
|
||||
local okP, G3Player = pcall(require, "src.core.game3.player")
|
||||
local G3Player = modPlayer()
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
if okP and G3Player and Runtime and Runtime.isActive and Runtime.isActive() then
|
||||
if G3Player and Runtime and Runtime.isActive and Runtime.isActive() then
|
||||
local xOff = G3Player.spriteXOffset or 0
|
||||
local yOff = G3Player.spriteYOffset or 0
|
||||
if yOff == 0 and G3Player.jumpSpriteY then
|
||||
@@ -241,16 +273,8 @@ local function playerSpriteName(game)
|
||||
return "SPRITE_CHRIS"
|
||||
end
|
||||
|
||||
local PalettesMod, PalettesMissing
|
||||
local function palettes()
|
||||
if PalettesMod then return PalettesMod end
|
||||
local loaded = package.loaded["src.world.gen2.Palettes"]
|
||||
if loaded then PalettesMod = loaded; return loaded end
|
||||
if PalettesMissing then return nil end
|
||||
local ok, m = pcall(require, "src.world.gen2.Palettes")
|
||||
if ok and m then PalettesMod = m; return m end
|
||||
PalettesMissing = true
|
||||
return nil
|
||||
return modPalettes()
|
||||
end
|
||||
|
||||
local function daytimeFor(game, mapDef)
|
||||
@@ -277,13 +301,13 @@ local function getSpriteRenderer(game, spriteName, seed, objDef, daytime)
|
||||
return nil
|
||||
end
|
||||
|
||||
local ok, SpriteRenderer = pcall(require, "src.render.SpriteRenderer")
|
||||
if not ok or not SpriteRenderer then return nil end
|
||||
local SpriteRenderer = modSpriteRenderer()
|
||||
if not SpriteRenderer then return nil end
|
||||
local sr = SpriteRenderer.new(def, seed or spriteName)
|
||||
|
||||
local okP, Palettes = pcall(require, "src.world.gen2.Palettes")
|
||||
local Palettes = modPalettes()
|
||||
local pals = data and (data.gen2Palettes or data.palettes)
|
||||
if okP and Palettes and pals and sr.setObjPalette then
|
||||
if Palettes and pals and sr.setObjPalette then
|
||||
local colors = Palettes.spritePalette(pals, daytime or "DAY", def, objDef)
|
||||
if colors then
|
||||
local id = (Palettes.objectPaletteId and Palettes.objectPaletteId(objDef))
|
||||
@@ -473,12 +497,16 @@ local owOpts = {}
|
||||
|
||||
local function drawSingleActor(game, mapDef, a, camX, camY)
|
||||
local daytime = daytimeFor(game, mapDef)
|
||||
local okOw, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||||
local useOw = okOw and OwSprites and OwSprites.ready and OwSprites.ready()
|
||||
local OwSprites = modOwSprites()
|
||||
local useOw = OwSprites and OwSprites.ready and OwSprites.ready()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
local billboarded = pushBillboard(a.x, a.y, camX, camY)
|
||||
local drew = false
|
||||
if a.renderer then
|
||||
if a.draw then
|
||||
a:draw(camX, camY)
|
||||
drew = true
|
||||
end
|
||||
if not drew and a.renderer then
|
||||
a.renderer:draw(a.x, a.y, camX, camY, a.facing, a.walkPhase or 0, false)
|
||||
drew = true
|
||||
end
|
||||
@@ -518,12 +546,12 @@ local npcActors = setmetatable({}, { __mode = "k" })
|
||||
local playerActor = {}
|
||||
|
||||
local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walkPhase, stepFlip, playerYOff, playerXOff)
|
||||
local okO, Objects = pcall(require, "src.core.game3.objects")
|
||||
local okOw, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||||
local useOw = okOw and OwSprites and OwSprites.ready and OwSprites.ready()
|
||||
local Objects = modObjects()
|
||||
local OwSprites = modOwSprites()
|
||||
local useOw = OwSprites and OwSprites.ready and OwSprites.ready()
|
||||
local actors = frameActors
|
||||
for i = #actors, 1, -1 do actors[i] = nil end
|
||||
local hasObjects = okO and Objects and Objects.hasMap and Objects.hasMap()
|
||||
local hasObjects = Objects and Objects.hasMap and Objects.hasMap()
|
||||
|
||||
if hasObjects then
|
||||
for _, eo in ipairs(Objects.forDraw()) do
|
||||
@@ -640,8 +668,15 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk
|
||||
actors[#actors + 1] = a
|
||||
end
|
||||
|
||||
local follower = require("src.world.game3.Follower").actor()
|
||||
local Follower = modFollower()
|
||||
local follower = Follower and Follower.actor and Follower.actor()
|
||||
if follower then actors[#actors + 1] = follower end
|
||||
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.collectActors then
|
||||
FieldEffects.collectActors(actors)
|
||||
end
|
||||
|
||||
return applyDrawOrder(actors, frameUnder, frameOver)
|
||||
end
|
||||
|
||||
@@ -706,8 +741,8 @@ local function draw_pal_list()
|
||||
end
|
||||
|
||||
local function drawTilesColored(atlas, bySlot, bgSet)
|
||||
local ok, GbcPalette = pcall(require, "src.render.GbcPalette")
|
||||
local usePal = ok and GbcPalette and GbcPalette.available and GbcPalette.available()
|
||||
local GbcPalette = modGbcPalette()
|
||||
local usePal = GbcPalette and GbcPalette.available and GbcPalette.available()
|
||||
and bgSet and GbcPalette.with
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
@@ -769,8 +804,8 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||||
local pair = mapDef.pair or (layout and layout.pair)
|
||||
if not layout or not pair then return false end
|
||||
|
||||
local okN, NativeTileset = pcall(require, "src.core.game3.tileset_native")
|
||||
if not (okN and NativeTileset and NativeTileset.ready(pair)) then
|
||||
local NativeTileset = modNativeTileset()
|
||||
if not (NativeTileset and NativeTileset.ready and NativeTileset.ready(pair)) then
|
||||
if not FieldView._loggedNativeFallback then
|
||||
log("native unavailable for " .. tostring(pair) .. " — Gen2 atlas fallback")
|
||||
FieldView._loggedNativeFallback = true
|
||||
@@ -811,7 +846,24 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||||
for r = 0, rows - 1 do
|
||||
FieldView._nativeOverByRow[r] = {}
|
||||
end
|
||||
local cellsByPair = {}
|
||||
|
||||
local cellsByPair = FieldView._nativeCellsByPair
|
||||
if not cellsByPair then
|
||||
cellsByPair = {}
|
||||
FieldView._nativeCellsByPair = cellsByPair
|
||||
else
|
||||
for k, list in pairs(cellsByPair) do
|
||||
for i = #list, 1, -1 do list[i] = nil end
|
||||
end
|
||||
end
|
||||
|
||||
local cellPool = FieldView._nativeCellPool
|
||||
if not cellPool then
|
||||
cellPool = {}
|
||||
FieldView._nativeCellPool = cellPool
|
||||
end
|
||||
local poolIdx = 0
|
||||
|
||||
local voidHas, voidPrimary = nil, nil
|
||||
if voidMode ~= "map" and voidMode ~= "black" then
|
||||
voidHas = function(m) return NativeTileset.hasMid(pair, m) end
|
||||
@@ -842,7 +894,16 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||||
list = {}
|
||||
cellsByPair[srcPair] = list
|
||||
end
|
||||
list[#list + 1] = { mid = mid, x = col * CELL, y = row * CELL }
|
||||
poolIdx = poolIdx + 1
|
||||
local c = cellPool[poolIdx]
|
||||
if not c then
|
||||
c = {}
|
||||
cellPool[poolIdx] = c
|
||||
end
|
||||
c.mid = mid
|
||||
c.x = col * CELL
|
||||
c.y = row * CELL
|
||||
list[#list + 1] = c
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -870,7 +931,11 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||||
end
|
||||
end
|
||||
end
|
||||
require("src.core.game3.tileset_anim").setVisiblePairs(cellsByPair)
|
||||
local TilesetAnim = package.loaded["src.core.game3.tileset_anim"]
|
||||
or require("src.core.game3.tileset_anim")
|
||||
if TilesetAnim and TilesetAnim.setVisiblePairs then
|
||||
TilesetAnim.setVisiblePairs(cellsByPair)
|
||||
end
|
||||
FieldView._nativeBx = cx0
|
||||
FieldView._nativeBy = cy0
|
||||
FieldView._nativePair = pair
|
||||
@@ -946,8 +1011,8 @@ end
|
||||
|
||||
-- pokefirered/src/field_screen_effect.c:194
|
||||
function FieldView.animateFlashLevel(fromLevel, toLevel)
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
if okFx and FieldEffects and FieldEffects.animateFlashLevel then
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.animateFlashLevel then
|
||||
return FieldEffects.animateFlashLevel(fromLevel, toLevel)
|
||||
end
|
||||
FieldView.setFlashLevel(toLevel)
|
||||
@@ -970,8 +1035,8 @@ end
|
||||
local function flashActive()
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
if Space and Space.store then
|
||||
local okF, Flags = pcall(require, "src.core.game3.scripting.flags")
|
||||
if okF and Flags and Flags.getFlag
|
||||
local Flags = modFlags()
|
||||
if Flags and Flags.getFlag
|
||||
and Flags.getFlag(Space.store, nil, FLAG_SYS_FLASH_ACTIVE) then
|
||||
return true
|
||||
end
|
||||
@@ -1096,8 +1161,8 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
canvasH = canvasH or Display.H
|
||||
opts = opts or {}
|
||||
|
||||
local okSea, SeagallopUi = pcall(require, "src.ui.game3.seagallop")
|
||||
if okSea and SeagallopUi and SeagallopUi.isActive and SeagallopUi.isActive() then
|
||||
local SeagallopUi = modSeagallop()
|
||||
if SeagallopUi and SeagallopUi.isActive and SeagallopUi.isActive() then
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, canvasW, canvasH)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
@@ -1142,8 +1207,8 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
local screenOy = math.floor((canvasH - Display.H) / 2)
|
||||
|
||||
-- pret BuyMenuDrawMapBg (shop.c:731-734): Frame player & counter in left gap (X: 0..80, Y: 0..160)
|
||||
local okShop, ShopMenu = pcall(require, "src.ui.game3.shop_menu")
|
||||
if okShop and ShopMenu and ShopMenu.isShopCamera and ShopMenu.isShopCamera() then
|
||||
local ShopMenu = modShopMenu()
|
||||
if ShopMenu and ShopMenu.isShopCamera and ShopMenu.isShopCamera() then
|
||||
local fx, fy = px, py
|
||||
if facing == "up" or facing == "north" then fy = fy - CELL
|
||||
elseif facing == "down" or facing == "south" then fy = fy + CELL
|
||||
@@ -1194,16 +1259,16 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
|
||||
-- Tall grass under body (pret lower OAM priority).
|
||||
do
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
if okFx and FieldEffects and FieldEffects.drawBehind then
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.drawBehind then
|
||||
FieldEffects.drawBehind(camX, camY)
|
||||
end
|
||||
end
|
||||
|
||||
-- Door opening/closing animation overlays (under actors).
|
||||
do
|
||||
local okDoors, Doors = pcall(require, "src.core.game3.doors")
|
||||
if okDoors and Doors and Doors.draw then
|
||||
local Doors = modDoors()
|
||||
if Doors and Doors.draw then
|
||||
Doors.draw(camX, camY, canvasW, canvasH)
|
||||
end
|
||||
end
|
||||
@@ -1211,8 +1276,8 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
|
||||
-- pokefirered/src/field_effect.c:910
|
||||
if not opts.actorsOnly then
|
||||
local okHeal, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if okHeal and Heal and Heal.drawBalls then
|
||||
local Heal = modHeal()
|
||||
if Heal and Heal.drawBalls then
|
||||
local sx, sy = screenAnchor(px, py, camX, camY)
|
||||
love.graphics.push()
|
||||
love.graphics.translate(sx, sy)
|
||||
@@ -1227,8 +1292,8 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
-- the actors, for exactly that reason -- a Renderer.screenVeil would cover
|
||||
-- the fragments too and the shatter would be invisible.
|
||||
if not opts.actorsOnly then
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
if okFx and FieldEffects and FieldEffects.bgFlashAlpha then
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.bgFlashAlpha then
|
||||
local a = FieldEffects.bgFlashAlpha()
|
||||
if a and a > 0 then
|
||||
love.graphics.setColor(1, 1, 1, a)
|
||||
@@ -1240,8 +1305,8 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
|
||||
-- S.S. Anne wake (pret oam.priority = 2, subpriority = 0xFF: under boat hull).
|
||||
if not opts.actorsOnly then
|
||||
local okSS, SSAnne = pcall(require, "src.core.game3.ss_anne_cutscene")
|
||||
if okSS and SSAnne and SSAnne.drawWake then
|
||||
local SSAnne = modSSAnne()
|
||||
if SSAnne and SSAnne.drawWake then
|
||||
SSAnne.drawWake(camX, camY)
|
||||
end
|
||||
end
|
||||
@@ -1275,16 +1340,16 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
|
||||
-- Tall grass over feet (pret subpriority above avatar).
|
||||
if not opts.actorsOnly then
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
if okFx and FieldEffects and FieldEffects.drawFront then
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.drawFront then
|
||||
FieldEffects.drawFront(camX, camY, py)
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/field_effect.c:1024
|
||||
if not opts.actorsOnly then
|
||||
local okHeal, Heal = pcall(require, "src.core.game3.pokecenter_heal")
|
||||
if okHeal and Heal and Heal.drawMonitor then
|
||||
local Heal = modHeal()
|
||||
if Heal and Heal.drawMonitor then
|
||||
local sx, sy = screenAnchor(px, py, camX, camY)
|
||||
love.graphics.push()
|
||||
love.graphics.translate(sx, sy)
|
||||
@@ -1295,19 +1360,19 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
|
||||
-- Pokemon Center heal machine (screen-space OAM, pret FLDEFF_POKECENTER_HEAL).
|
||||
if not opts.actorsOnly then
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
if okFx and FieldEffects and FieldEffects.drawOverlay then
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.drawOverlay then
|
||||
love.graphics.push()
|
||||
love.graphics.translate(screenOx, screenOy)
|
||||
FieldEffects.drawOverlay(camX, camY)
|
||||
love.graphics.pop()
|
||||
end
|
||||
local okSS, SSAnne = pcall(require, "src.core.game3.ss_anne_cutscene")
|
||||
if okSS and SSAnne and SSAnne.drawSmoke then
|
||||
local SSAnne = modSSAnne()
|
||||
if SSAnne and SSAnne.drawSmoke then
|
||||
SSAnne.drawSmoke(camX, camY)
|
||||
end
|
||||
local okW, FieldWeather = pcall(require, "src.core.game3.field_weather")
|
||||
if okW and FieldWeather and FieldWeather.draw then
|
||||
local FieldWeather = modFieldWeather()
|
||||
if FieldWeather and FieldWeather.draw then
|
||||
FieldWeather.draw(camX, camY, canvasW, canvasH)
|
||||
end
|
||||
end
|
||||
@@ -1339,16 +1404,16 @@ function FieldView.invalidate()
|
||||
FieldView._loggedNativeFallback = false
|
||||
FieldView._flashSpans = nil
|
||||
FieldView._flashSpanR = nil
|
||||
local okN, NativeTileset = pcall(require, "src.core.game3.tileset_native")
|
||||
if okN and NativeTileset and NativeTileset.invalidate then
|
||||
local NativeTileset = modNativeTileset()
|
||||
if NativeTileset and NativeTileset.invalidate then
|
||||
NativeTileset.invalidate()
|
||||
end
|
||||
local okO, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||||
if okO and OwSprites and OwSprites.invalidate then
|
||||
local OwSprites = modOwSprites()
|
||||
if OwSprites and OwSprites.invalidate then
|
||||
OwSprites.invalidate()
|
||||
end
|
||||
local okFx, FieldEffects = pcall(require, "src.core.game3.field_effects")
|
||||
if okFx and FieldEffects and FieldEffects.invalidate then
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.invalidate then
|
||||
FieldEffects.invalidate()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,6 +36,14 @@ local function wont_have_effect()
|
||||
return (RomText.box("gText_WontHaveEffect"))
|
||||
end
|
||||
|
||||
local function cant_dismount_bike_text()
|
||||
if RomText.has and RomText.has("gText_CantDismountBike") then
|
||||
local ok, res = pcall(RomText.box, "gText_CantDismountBike")
|
||||
if ok and res then return res end
|
||||
end
|
||||
return "You can't dismount your BIKE here."
|
||||
end
|
||||
|
||||
-- pokefirered/src/data/pokemon/item_effects.h:80
|
||||
local HERB_HEAL = { [30] = 50, [31] = 200 }
|
||||
local ITEM_REVIVAL_HERB = 33
|
||||
@@ -422,13 +430,18 @@ end
|
||||
|
||||
-- pokefirered/src/item_use.c:253 FieldUseFunc_Bike
|
||||
function ItemUse.useBike(session)
|
||||
local Player = require("src.core.game3.player")
|
||||
-- pokefirered/src/item_use.c:261
|
||||
if Player.biking and Player.isOnCyclingRoad and Player.isOnCyclingRoad(session) then
|
||||
return false, "bike", cant_dismount_bike_text()
|
||||
end
|
||||
|
||||
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed
|
||||
local biking = map_header_flag(session, "bikingAllowed")
|
||||
if biking == nil then biking = is_outdoor(session) end
|
||||
if not biking then
|
||||
return false, "bike", not_the_time(session)
|
||||
end
|
||||
local Player = require("src.core.game3.player")
|
||||
-- pokefirered/src/item_use.c:276 ItemUseOnFieldCB_Bicycle
|
||||
if not Player.biking then
|
||||
pcall(function()
|
||||
|
||||
+24
-12
@@ -307,21 +307,28 @@ function Map.load(mod, game, mapId, opts)
|
||||
Map.ensureMidLayout(game, mapId, def)
|
||||
Map._def = def
|
||||
Map._currentDef = def
|
||||
local Player = require("src.core.game3.player")
|
||||
local curSession = (Runtime and Runtime.getSession and Runtime.getSession())
|
||||
or (game and game.session) or session
|
||||
local onCyclingRoad = Player.isOnCyclingRoad and Player.isOnCyclingRoad(curSession, x, y)
|
||||
local wasBiking = Player.biking or (curSession and curSession.biking == true)
|
||||
or (save and save.biking == true)
|
||||
|
||||
-- pokefirered/src/overworld.c:878 GetAdjustedInitialTransitionFlags
|
||||
local keepBike = false
|
||||
do
|
||||
local Player = require("src.core.game3.player")
|
||||
if Player.biking then
|
||||
local allowed = def and def.bikingAllowed
|
||||
if allowed ~= nil then
|
||||
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed
|
||||
keepBike = (tonumber(allowed) or 0) ~= 0
|
||||
else
|
||||
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
|
||||
keepBike = type(pair) == "string" and pair:find("outdoor", 1, true) ~= nil
|
||||
end
|
||||
Player.biking = keepBike
|
||||
if wasBiking or onCyclingRoad then
|
||||
local allowed = def and def.bikingAllowed
|
||||
if allowed ~= nil then
|
||||
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed
|
||||
keepBike = (tonumber(allowed) or 0) ~= 0
|
||||
else
|
||||
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
|
||||
keepBike = type(pair) == "string" and pair:find("outdoor", 1, true) ~= nil
|
||||
end
|
||||
if onCyclingRoad and not (Player.surfing or Player.surfHopping) then
|
||||
keepBike = true
|
||||
end
|
||||
Player.biking = keepBike
|
||||
end
|
||||
if opts.depth1Connections ~= false then
|
||||
Map.loadNeighborsDepth1(game, def)
|
||||
@@ -348,6 +355,7 @@ function Map.load(mod, game, mapId, opts)
|
||||
session.x = x
|
||||
session.y = y
|
||||
session.facing = facing
|
||||
session.biking = keepBike
|
||||
end
|
||||
|
||||
-- Keep save.position current for ferry exit / host save without setMap.
|
||||
@@ -358,6 +366,7 @@ function Map.load(mod, game, mapId, opts)
|
||||
save.position.x = x
|
||||
save.position.y = y
|
||||
save.position.facing = facing
|
||||
save.biking = keepBike
|
||||
end
|
||||
|
||||
local Player = require("src.core.game3.player")
|
||||
@@ -373,6 +382,9 @@ function Map.load(mod, game, mapId, opts)
|
||||
Player.reset(x, y, facing)
|
||||
end
|
||||
-- pokefirered/src/overworld.c:2145 SetPlayerAvatarTransitionFlags
|
||||
if onCyclingRoad and not (Player.surfing or Player.surfHopping) then
|
||||
keepBike = true
|
||||
end
|
||||
Player.biking = keepBike
|
||||
Player.syncSavePosition(game)
|
||||
|
||||
|
||||
@@ -510,6 +510,29 @@ local function isCyclingRoadPullDown(beh)
|
||||
and beh <= MB_CYCLING_ROAD_PULL_DOWN_GRASS
|
||||
end
|
||||
|
||||
function Player.isOnCyclingRoad(session, x, y)
|
||||
local cx = x or Player.cellX
|
||||
local cy = y or Player.cellY
|
||||
local beh = Collision.behavior and Collision.behavior(cx, cy)
|
||||
if isCyclingRoadPullDown(beh) then return true end
|
||||
local Flags = package.loaded["src.core.game3.scripting.flags"]
|
||||
or package.loaded["src.core.game3.flags"]
|
||||
or require("src.core.game3.scripting.flags")
|
||||
local Space = package.loaded["src.core.game3.space"]
|
||||
local store = (session and session.store) or (Space and Space.store) or (session and type(session) == "table" and session)
|
||||
if not store then
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local s = Runtime and Runtime.getSession and Runtime.getSession()
|
||||
store = (s and s.store) or s
|
||||
end
|
||||
if Flags and Flags.getFlag and store then
|
||||
if Flags.getFlag(store, nil, 0x830) == true then -- FLAG_SYS_ON_CYCLING_ROAD
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- pokefirered/src/bike.c:215 GetBikeCollision
|
||||
local function bikeCanMove(game, dir)
|
||||
local d = DELTA[dir]
|
||||
@@ -673,6 +696,7 @@ local function finishStep(game)
|
||||
Player.syncSavePosition(game)
|
||||
|
||||
-- Surf landing / dismount state transitions
|
||||
local wasSurfing = Player.surfing or Player.dismounting
|
||||
if Player.surfHopping then
|
||||
Player.surfHopping = false
|
||||
Player.surfing = true
|
||||
@@ -692,6 +716,16 @@ local function finishStep(game)
|
||||
session.x, session.y, session.facing = Player.cellX, Player.cellY, Player.facing
|
||||
end
|
||||
|
||||
if wasSurfing and not Player.surfing and not Player.surfHopping then
|
||||
if Player.isOnCyclingRoad(session, Player.cellX, Player.cellY) then
|
||||
Player.biking = true
|
||||
pcall(function()
|
||||
local Audio = require("src.core.game3.audio")
|
||||
Audio.bikeMusic(true, true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if ModRuntime.wants("world.stepped") then
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
ModRuntime.emit("world.stepped", {
|
||||
|
||||
@@ -332,6 +332,9 @@ function Schema.toSaveTable(session)
|
||||
x = session.x,
|
||||
y = session.y,
|
||||
facing = session.facing,
|
||||
biking = (session.biking == true)
|
||||
or (package.loaded["src.core.game3.player"] and package.loaded["src.core.game3.player"].biking == true)
|
||||
or false,
|
||||
healMap = session.healMap,
|
||||
healX = session.healX,
|
||||
healY = session.healY,
|
||||
@@ -446,6 +449,7 @@ function Schema.fromSaveTable(save)
|
||||
x = save.x or MapIds.NEW_GAME_START.x,
|
||||
y = save.y or MapIds.NEW_GAME_START.y,
|
||||
facing = save.facing or "down",
|
||||
biking = save.biking == true,
|
||||
healMap = save.healMap,
|
||||
healX = save.healX,
|
||||
healY = save.healY,
|
||||
|
||||
@@ -96,6 +96,20 @@ function TilesetAnim.setVisiblePairs(visible)
|
||||
for pair in pairs(visible) do TilesetAnim._visible[pair] = true end
|
||||
end
|
||||
|
||||
local function get_frame_piece(bank, frame, mi, frameRgba)
|
||||
if not (love and love.image and love.image.newImageData) then return nil end
|
||||
bank.pieces = bank.pieces or {}
|
||||
local pKey = frame * 1000 + mi
|
||||
local piece = bank.pieces[pKey]
|
||||
if piece then return piece end
|
||||
local ok, imgData = pcall(love.image.newImageData, 16, 16, "rgba8", frameRgba)
|
||||
if ok and imgData then
|
||||
bank.pieces[pKey] = imgData
|
||||
return imgData
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function TilesetAnim._applyKind(entry, kind, frame)
|
||||
local bank = entry.banks[kind]
|
||||
if not bank or bank.frames < 1 then return end
|
||||
@@ -114,13 +128,11 @@ function TilesetAnim._applyKind(entry, kind, frame)
|
||||
local ax = (slot % cols) * 16
|
||||
local ay = math.floor(slot / cols) * 16
|
||||
local frameRgba = bank.rgba:sub(srcOff + 1, srcOff + MID_RGBA)
|
||||
local piece = get_frame_piece(bank, frame, mi, frameRgba)
|
||||
local pasted = false
|
||||
if love and love.image and love.image.newImageData then
|
||||
local ok, piece = pcall(love.image.newImageData, 16, 16, "rgba8", frameRgba)
|
||||
if ok and piece and ts.imageData.paste then
|
||||
ts.imageData:paste(piece, ax, ay)
|
||||
pasted = true
|
||||
end
|
||||
if piece and ts.imageData.paste then
|
||||
ts.imageData:paste(piece, ax, ay)
|
||||
pasted = true
|
||||
end
|
||||
if not pasted then
|
||||
local i = 1
|
||||
|
||||
@@ -1113,7 +1113,7 @@ function Warp.request(mod, game, mapId, x, y, facing, opts)
|
||||
Player.setVisible(true)
|
||||
end
|
||||
Doors.reset()
|
||||
Fade.begin(fromMode, 1, function()
|
||||
warpExitArrival(game, mapId, x, y, fromMode, function()
|
||||
Warp._busy = false
|
||||
releaseField(Field)
|
||||
end)
|
||||
|
||||
@@ -155,9 +155,10 @@ do
|
||||
eot(st2, ad2)
|
||||
check(ad2:hp(st2.player) == 51 and st2.player.item == 142, "Sitrus waits until HP <= 1/2")
|
||||
|
||||
local st3, ad3 = battle({ item = 139, hp = 10, maxHp = 100 }, { moves = { 33 } })
|
||||
local st3, ad3 = battle({ item = 139, hp = 60, maxHp = 100 }, { moves = { 33 } })
|
||||
use(st3, ad3, st3.enemy, 33)
|
||||
check(st3.player.item == 139, "HP berries do not trigger mid-turn (moveTurn)")
|
||||
check(st3.player.item == 0 and st3.playerParty[1].item == nil, "HP berries trigger and are consumed mid-turn when HP drops")
|
||||
check(ad3:hp(st3.player) > (60 - 35), "Oran Berry healed after taking damage")
|
||||
end
|
||||
|
||||
print("=== CHERI / LUM / PERSIM at move end ===")
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env luajit
|
||||
-- ROM-free unit tests for Game 3 (FireRed / LeafGreen) Cycling Road forced biking & dismount restrictions.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed, passed = 0, 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local Player = require("src.core.game3.player")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local ItemUse = require("src.core.game3.item_use")
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local RomText = require("src.core.game3.rom_text")
|
||||
|
||||
local session = {
|
||||
flags = {},
|
||||
vars = {},
|
||||
store = { flags = {}, vars = {} }
|
||||
}
|
||||
|
||||
-- 1. Test Player.isOnCyclingRoad
|
||||
Player.cellX, Player.cellY = 0, 0
|
||||
Flags.setFlag(session.store, nil, 0x830, false)
|
||||
check(not Player.isOnCyclingRoad(session, 0, 0), "isOnCyclingRoad is false by default on neutral tile")
|
||||
|
||||
Flags.setFlag(session.store, nil, 0x830, true)
|
||||
check(Player.isOnCyclingRoad(session, 0, 0), "isOnCyclingRoad is true when FLAG_SYS_ON_CYCLING_ROAD is set")
|
||||
|
||||
-- 2. Test Player.isOnCyclingRoad via metatile behaviors
|
||||
Flags.setFlag(session.store, nil, 0x830, false)
|
||||
Collision.behavior = function(x, y)
|
||||
if x == 10 and y == 10 then return 0xD0 end -- MB_CYCLING_ROAD_PULL_DOWN
|
||||
if x == 10 and y == 11 then return 0xD1 end -- MB_CYCLING_ROAD_PULL_DOWN_GRASS
|
||||
return 0x00
|
||||
end
|
||||
|
||||
check(Player.isOnCyclingRoad(session, 10, 10), "isOnCyclingRoad is true for MB_CYCLING_ROAD_PULL_DOWN")
|
||||
check(Player.isOnCyclingRoad(session, 10, 11), "isOnCyclingRoad is true for MB_CYCLING_ROAD_PULL_DOWN_GRASS")
|
||||
check(not Player.isOnCyclingRoad(session, 5, 5), "isOnCyclingRoad is false for normal tile without flag")
|
||||
|
||||
-- 3. Test Surf dismount onto Cycling Road forces biking
|
||||
Flags.setFlag(session.store, nil, 0x830, true)
|
||||
Player.surfing = true
|
||||
Player.dismounting = true
|
||||
Player.biking = false
|
||||
Player.targetX, Player.targetY = 10, 10
|
||||
Player.cellX, Player.cellY = 10, 10
|
||||
Player.moving = true
|
||||
Player.surfHopping = false
|
||||
|
||||
-- Step onto cycling road land and complete step
|
||||
local game = { session = session, save = { position = {} } }
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
Runtime.session = session
|
||||
|
||||
-- Let's run Player.tick to complete the dismount step
|
||||
Player.progress = 1
|
||||
for _ = 1, 32 do
|
||||
if not Player.moving then break end
|
||||
Player.tick(game)
|
||||
end
|
||||
|
||||
check(Player.biking == true, "dismounting surf onto cycling road forces Player.biking = true")
|
||||
check(Player.surfing == false, "Player.surfing is false after dismount")
|
||||
|
||||
-- 4. Test Surf dismount onto non-cycling-road land keeps player on foot
|
||||
Flags.setFlag(session.store, nil, 0x830, false)
|
||||
Collision.behavior = function() return 0x00 end
|
||||
Player.surfing = true
|
||||
Player.dismounting = true
|
||||
Player.biking = false
|
||||
Player.targetX, Player.targetY = 0, 0
|
||||
Player.cellX, Player.cellY = 0, 0
|
||||
Player.moving = true
|
||||
Player.progress = 1
|
||||
|
||||
for _ = 1, 32 do
|
||||
if not Player.moving then break end
|
||||
Player.tick(game)
|
||||
end
|
||||
|
||||
check(Player.biking == false, "dismounting surf on normal land leaves Player.biking = false")
|
||||
check(Player.surfing == false, "Player.surfing is false after normal dismount")
|
||||
|
||||
-- 5. Test ItemUse.useBike on Cycling Road (cannot dismount)
|
||||
session.map = "FR_PALLET_TOWN"
|
||||
Flags.setFlag(session.store, nil, 0x830, true)
|
||||
Player.biking = true
|
||||
local ok, kind, text = ItemUse.useBike(session)
|
||||
check(ok == false, "useBike returns false when trying to dismount on Cycling Road")
|
||||
check(Player.biking == true, "Player.biking remains true after attempted dismount")
|
||||
check(text ~= nil, "refusal text returned when trying to dismount on Cycling Road")
|
||||
|
||||
-- 6. Test ItemUse.useBike when on foot on Cycling Road (can mount)
|
||||
Player.biking = false
|
||||
local ok2, kind2, text2 = ItemUse.useBike(session)
|
||||
check(ok2 == true, "useBike allows mounting when on foot on Cycling Road")
|
||||
check(Player.biking == true, "Player.biking becomes true after mounting")
|
||||
|
||||
-- 7. Test ItemUse.useBike on normal outdoor land (can dismount and mount freely)
|
||||
Flags.setFlag(session.store, nil, 0x830, false)
|
||||
Player.biking = true
|
||||
local ok3, kind3, text3 = ItemUse.useBike(session)
|
||||
check(ok3 == true, "useBike allows dismounting on normal land")
|
||||
check(Player.biking == false, "Player.biking is now false")
|
||||
|
||||
-- 8. Test save & load / Map.load on Cycling Road forces Player.biking = true
|
||||
local Map = require("src.core.game3.map")
|
||||
local Audio = require("src.core.game3.audio")
|
||||
local Schema = require("src.core.game3.save_schema_firered")
|
||||
|
||||
-- Simulate fresh boot / uninitialized avatar state:
|
||||
Player.biking = false
|
||||
Flags.setFlag(session.store, nil, 0x830, true)
|
||||
local cyclingMapDef = {
|
||||
id = "FR_ROUTE_17",
|
||||
bikingAllowed = 1,
|
||||
music = 282,
|
||||
regionMapSectionId = 44,
|
||||
pair = "outdoor",
|
||||
}
|
||||
game.data = { maps = { FR_ROUTE_17 = cyclingMapDef } }
|
||||
game.session = session
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
Space.activate = function() end
|
||||
Space.runEnterScripts = function() end
|
||||
|
||||
Map.load(nil, game, "FR_ROUTE_17", { x = 10, y = 10, facing = "down" })
|
||||
check(Player.biking == true, "loading/continuing map on Cycling Road forces Player.biking = true")
|
||||
check(Audio.specialMapSong() == Audio.MUS_CYCLING, "specialMapSong returns Audio.MUS_CYCLING when biking")
|
||||
|
||||
-- 9. Test Schema serialization preserves biking state
|
||||
local saveTable = Schema.toSaveTable(session)
|
||||
check(saveTable.biking == true, "Schema.toSaveTable includes biking = true")
|
||||
local restoredSession = Schema.fromSaveTable(saveTable)
|
||||
check(restoredSession.biking == true, "Schema.fromSaveTable restores biking = true")
|
||||
|
||||
print(string.format("=== RESULTS: %d passed, %d failed ===", passed, failed))
|
||||
if failed > 0 then os.exit(1) end
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
-- Test game3 OAM draw ordering for tall grass and trainers (NPCs).
|
||||
-- Ensures grass cover on tile cy is drawn after the actor on tile cy,
|
||||
-- but BEFORE any actor on tile cy + 1 (so the lower actor's head is not covered by grass).
|
||||
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local FieldEffects = require("src.core.game3.field_effects")
|
||||
local FieldView = require("src.core.game3.field_view")
|
||||
local Collision = require("src.core.game3.collision")
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function done()
|
||||
if failed > 0 then
|
||||
print("[test] FAILED " .. failed)
|
||||
os.exit(1)
|
||||
end
|
||||
print("[test] all passed")
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
local CELL = 16
|
||||
|
||||
-- Mock love graphics image/quad for tall_grass if love is stubbed
|
||||
FieldEffects._sheets["tall_grass"] = {
|
||||
image = {},
|
||||
quads = { [0] = {}, [1] = {}, [2] = {}, [3] = {}, [4] = {} },
|
||||
quadsFront = { [0] = {}, [1] = {}, [2] = {}, [3] = {}, [4] = {} },
|
||||
fw = 16,
|
||||
fh = 16,
|
||||
frames = 5,
|
||||
}
|
||||
|
||||
print("[test] 1. Player in grass above NPC trainer: grass cover must sort between Player and Trainer")
|
||||
do
|
||||
-- Player at cell (4, 4) -> px = 64, py = 64
|
||||
local Player = require("src.core.game3.player")
|
||||
Player.cellX = 4
|
||||
Player.cellY = 4
|
||||
Player.px = 4 * CELL
|
||||
Player.py = 4 * CELL
|
||||
Player.elevation = 3
|
||||
Player.moving = false
|
||||
|
||||
-- Trainer NPC at cell (4, 5) -> px = 64, py = 80
|
||||
local trainer = {
|
||||
localId = 1,
|
||||
def = { x = 4, y = 5, elevation = 3 },
|
||||
cellX = 4, cellY = 5,
|
||||
px = 4 * CELL, py = 5 * CELL,
|
||||
elevation = 3,
|
||||
}
|
||||
Objects._byId = { [1] = trainer }
|
||||
Objects.forDraw = function() return { trainer } end
|
||||
|
||||
-- Activate player grass rustle/cover at (4, 4)
|
||||
FieldEffects.tallGrassAt(4, 4, true)
|
||||
|
||||
local actors = {}
|
||||
-- Player actor
|
||||
actors[#actors + 1] = {
|
||||
kind = "player",
|
||||
elevation = 3,
|
||||
x = Player.px,
|
||||
y = Player.py,
|
||||
sortY = Player.py,
|
||||
}
|
||||
-- Trainer actor
|
||||
actors[#actors + 1] = {
|
||||
kind = "npc",
|
||||
i = 1,
|
||||
obj = trainer.def,
|
||||
eventObject = trainer,
|
||||
elevation = 3,
|
||||
x = trainer.px,
|
||||
y = trainer.py,
|
||||
sortY = trainer.py,
|
||||
}
|
||||
|
||||
-- Collect field effect actors
|
||||
FieldEffects.collectActors(actors)
|
||||
|
||||
check(#actors >= 3, "collected Player, Trainer, and Grass Cover actors")
|
||||
|
||||
local under, over = FieldView.applyDrawOrder(actors)
|
||||
check(#under >= 3, "all actors at elevation 3 land in underActors")
|
||||
|
||||
-- Find positions of player, grass cover, and trainer in sorted order
|
||||
local playerIdx, grassIdx, trainerIdx
|
||||
for idx, a in ipairs(under) do
|
||||
if a.kind == "player" then playerIdx = idx end
|
||||
if a.kind == "field_effect_grass" then grassIdx = idx end
|
||||
if a.kind == "npc" and a.i == 1 then trainerIdx = idx end
|
||||
end
|
||||
|
||||
check(playerIdx ~= nil, "player present in draw list")
|
||||
check(grassIdx ~= nil, "player grass cover present in draw list")
|
||||
check(trainerIdx ~= nil, "trainer present in draw list")
|
||||
|
||||
check(playerIdx < grassIdx, "Player (Y=64) drawn before player's grass cover (Y=64.5)")
|
||||
check(grassIdx < trainerIdx, "Player's grass cover (Y=64.5) drawn before Trainer below (Y=80)")
|
||||
end
|
||||
|
||||
print("[test] 2. Trainer NPC on grass tile also gets grass cover sorted after trainer")
|
||||
do
|
||||
-- Mock Collision.isGrass to return true for cell (4, 5)
|
||||
local origIsGrass = Collision.isGrass
|
||||
Collision.isGrass = function(cx, cy)
|
||||
return (cx == 4 and cy == 4) or (cx == 4 and cy == 5)
|
||||
end
|
||||
|
||||
local Player = require("src.core.game3.player")
|
||||
Player.cellX = 4
|
||||
Player.cellY = 4
|
||||
Player.px = 4 * CELL
|
||||
Player.py = 4 * CELL
|
||||
Player.elevation = 3
|
||||
Player.moving = false
|
||||
|
||||
local trainer = {
|
||||
localId = 1,
|
||||
def = { x = 4, y = 5, elevation = 3 },
|
||||
cellX = 4, cellY = 5,
|
||||
px = 4 * CELL, py = 5 * CELL,
|
||||
elevation = 3,
|
||||
}
|
||||
Objects.forDraw = function() return { trainer } end
|
||||
FieldEffects.tallGrassAt(4, 4, true)
|
||||
|
||||
local actors = {}
|
||||
actors[#actors + 1] = {
|
||||
kind = "player",
|
||||
elevation = 3,
|
||||
x = Player.px,
|
||||
y = Player.py,
|
||||
sortY = Player.py,
|
||||
}
|
||||
actors[#actors + 1] = {
|
||||
kind = "npc",
|
||||
i = 1,
|
||||
obj = trainer.def,
|
||||
eventObject = trainer,
|
||||
elevation = 3,
|
||||
x = trainer.px,
|
||||
y = trainer.py,
|
||||
sortY = trainer.py,
|
||||
}
|
||||
|
||||
FieldEffects.collectActors(actors)
|
||||
|
||||
local under, over = FieldView.applyDrawOrder(actors)
|
||||
local playerIdx, playerGrassIdx, trainerIdx, trainerGrassIdx
|
||||
for idx, a in ipairs(under) do
|
||||
if a.kind == "player" then playerIdx = idx end
|
||||
if a.kind == "field_effect_grass" then playerGrassIdx = idx end
|
||||
if a.kind == "npc" and a.i == 1 then trainerIdx = idx end
|
||||
if a.kind == "field_effect_npc_grass" then trainerGrassIdx = idx end
|
||||
end
|
||||
|
||||
check(playerIdx < playerGrassIdx, "1. Player drawn first")
|
||||
check(playerGrassIdx < trainerIdx, "2. Player grass cover drawn second (before trainer)")
|
||||
check(trainerIdx < trainerGrassIdx, "3. Trainer drawn third (head covers player's grass tile)")
|
||||
check(trainerGrassIdx > trainerIdx, "4. Trainer grass cover drawn fourth (covers trainer's feet)")
|
||||
|
||||
Collision.isGrass = origIsGrass
|
||||
end
|
||||
|
||||
done()
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env luajit
|
||||
-- ROM-free comprehensive unit tests for Game 3 (FireRed / LeafGreen) held item behaviors:
|
||||
-- HP restore berries, pinch stat berries, status berries, herbs, leftovers, focus band, etc.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed, passed = 0, 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
Pokemon._names = { [1] = "BULBASAUR", [4] = "CHARMANDER", [7] = "SQUIRTLE", [25] = "PIKACHU" }
|
||||
Pokemon._types = { [1] = { 12, 3 }, [4] = { 10, 10 }, [7] = { 11, 11 }, [25] = { 13, 13 } }
|
||||
Pokemon._stats = {
|
||||
[1] = { hp = 45, atk = 49, def = 49, spe = 45, spa = 65, spd = 65 },
|
||||
[4] = { hp = 39, atk = 52, def = 43, spe = 65, spa = 60, spd = 50 },
|
||||
[7] = { hp = 44, atk = 48, def = 65, spe = 43, spa = 50, spd = 64 },
|
||||
[25] = { hp = 35, atk = 55, def = 40, spe = 90, spa = 50, spd = 50 },
|
||||
}
|
||||
Pokemon._abilities = { [1] = { 65, 65 }, [4] = { 66, 66 }, [7] = { 67, 67 }, [25] = { 9, 9 } }
|
||||
|
||||
local RomText = require("src.core.game3.rom_text")
|
||||
RomText.plain = function(key) return tostring(key) end
|
||||
RomText.box = function(key) return tostring(key) end
|
||||
RomText.ascii = function(key) return tostring(key) end
|
||||
RomText.ir = function(key) return {} end
|
||||
|
||||
local BattleText = require("src.core.game3.battle.battle_text")
|
||||
BattleText.get = function(id, fill)
|
||||
fill = fill or {}
|
||||
return string.format("[%s: item=%s buff1=%s]", id, tostring(fill.lastItem or ""), tostring(fill.buff1 or ""))
|
||||
end
|
||||
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
local ROM_MOVES = {
|
||||
[1] = { name = "POUND", effect = 0, power = 40, type = 0, accuracy = 100, pp = 35, secondaryChance = 0, target = 0, priority = 0, flags = 51 },
|
||||
[33] = { name = "TACKLE", effect = 0, power = 35, type = 0, accuracy = 95, pp = 35, secondaryChance = 0, target = 0, priority = 0, flags = 51 },
|
||||
[45] = { name = "GROWL", effect = 18, power = 0, type = 0, accuracy = 100, pp = 40, secondaryChance = 0, target = 8, priority = 0, flags = 22 },
|
||||
[86] = { name = "THUNDER WAVE", effect = 67, power = 0, type = 13, accuracy = 100, pp = 20, secondaryChance = 0, target = 0, priority = 0, flags = 22 },
|
||||
}
|
||||
for id, row in pairs(ROM_MOVES) do row.numId = id end
|
||||
Moves._romLoaded = true
|
||||
Moves._rom = ROM_MOVES
|
||||
Moves._names = { [1] = "POUND", [33] = "TACKLE", [45] = "GROWL", [86] = "THUNDER WAVE" }
|
||||
Pokemon._moveNames = Moves._names
|
||||
Pokemon._romMoveNames = Moves._names
|
||||
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
local Adapter = require("src.core.game3.battle.adapter")
|
||||
local HeldItems = require("src.core.game3.battle.held_items")
|
||||
local Damage = require("src.core.game3.battle.damage")
|
||||
|
||||
local function mon(o)
|
||||
return {
|
||||
species = o.species or 1, level = o.level or 50,
|
||||
hp = o.hp or 100, maxHp = o.maxHp or 100,
|
||||
attack = o.attack or 50, defense = o.defense or 50,
|
||||
spAtk = o.spAtk or 50, spDef = o.spDef or 50,
|
||||
speed = o.speed or 50, ability = o.ability or 0,
|
||||
nickname = o.nickname, moves = o.moves or { 33 },
|
||||
pp = o.pp or { 20, 20, 20, 20 }, maxPp = o.maxPp,
|
||||
status = o.status, item = o.item, heldItem = o.item,
|
||||
gender = o.gender, personality = o.personality or 0,
|
||||
}
|
||||
end
|
||||
|
||||
local function battle(p, e)
|
||||
local party = { mon(p) }
|
||||
local foeParty = { mon(e) }
|
||||
local st = State.new({ wild = true, playerParty = party, foeParty = foeParty })
|
||||
st.rng = function(lo, hi) return lo end
|
||||
local ad = Adapter.new(st)
|
||||
return st, ad
|
||||
end
|
||||
|
||||
local function useMove(st, ad, user, move, slot)
|
||||
local target = (user == st.player) and st.enemy or st.player
|
||||
local out = {}
|
||||
Engine.resolveMove(user, target, move, slot or 1, ad, st, out)
|
||||
return out
|
||||
end
|
||||
|
||||
local function eot(st, ad)
|
||||
return Engine.collectResidualEvents(st, ad)
|
||||
end
|
||||
|
||||
print("=== 1. Oran Berry mid-turn damage consumption ===")
|
||||
do
|
||||
-- Player starts with Oran Berry (139) at 60/100 HP.
|
||||
-- Enemy attacks with Tackle (power 35).
|
||||
local st, ad = battle({ item = 139, hp = 60, maxHp = 100 }, { moves = { 33 } })
|
||||
check(st.player.item == 139, "player battler holds Oran Berry before attack")
|
||||
check(st.playerParty[1].item == 139, "player party mon holds Oran Berry before attack")
|
||||
|
||||
useMove(st, ad, st.enemy, 33)
|
||||
|
||||
-- After taking damage, HP dropped below 50, berry activated, healed 10 HP, and was consumed.
|
||||
check(st.player.item == 0, "player battler item is cleared (0)")
|
||||
check(st.playerParty[1].item == nil, "player party mon item is nil")
|
||||
check(st.playerParty[1].heldItem == nil, "player party mon heldItem is nil")
|
||||
check(st.playerSide.expUsedHeldItem == 139, "expUsedHeldItem recorded for Recycle")
|
||||
end
|
||||
|
||||
print("=== 2. Sitrus Berry mid-turn damage consumption ===")
|
||||
do
|
||||
-- Sitrus Berry (142) heals 30 HP when HP <= 50%
|
||||
local st, ad = battle({ item = 142, hp = 60, maxHp = 100 }, { moves = { 33 } })
|
||||
useMove(st, ad, st.enemy, 33)
|
||||
check(st.player.item == 0, "Sitrus Berry consumed on battler")
|
||||
check(st.playerParty[1].item == nil, "Sitrus Berry consumed on party mon")
|
||||
check(st.playerSide.expUsedHeldItem == 142, "Sitrus Berry recorded for Recycle")
|
||||
end
|
||||
|
||||
print("=== 3. Enemy mon consuming held berry mid-turn ===")
|
||||
do
|
||||
-- Foe holds Oran Berry, player attacks foe
|
||||
local st, ad = battle({ moves = { 33 } }, { item = 139, hp = 60, maxHp = 100 })
|
||||
check(st.enemy.item == 139, "enemy battler holds Oran Berry")
|
||||
check(st.foeParty[1].item == 139, "enemy party mon holds Oran Berry")
|
||||
|
||||
useMove(st, ad, st.player, 33)
|
||||
|
||||
check(st.enemy.item == 0, "enemy battler item cleared after consumption")
|
||||
check(st.foeParty[1].item == nil, "enemy party mon item cleared after consumption")
|
||||
check(st.enemySide.expUsedHeldItem == 139, "enemy side recorded used held item")
|
||||
end
|
||||
|
||||
print("=== 4. Status curing berries (Cheri / Pecha / Lum) at move end ===")
|
||||
do
|
||||
-- Cheri Berry (133) cures paralysis immediately when Thunder Wave is used
|
||||
local st, ad = battle({ item = 133 }, { moves = { 86 } })
|
||||
useMove(st, ad, st.enemy, 86)
|
||||
check(ad:status(st.player) == nil, "paralysis was cured immediately")
|
||||
check(st.player.item == 0, "Cheri Berry was consumed")
|
||||
check(st.playerParty[1].item == nil, "party mon Cheri Berry consumed")
|
||||
end
|
||||
|
||||
print("=== 5. Stat boosting pinch berries (Liechi Berry) mid-turn ===")
|
||||
do
|
||||
-- Liechi Berry (168) boosts Attack when HP <= 25% (1/4 maxHp)
|
||||
local st, ad = battle({ item = 168, hp = 30, maxHp = 100 }, { moves = { 33 } })
|
||||
useMove(st, ad, st.enemy, 33)
|
||||
check(st.player.stages.attack == 1, "Attack rose by 1 stage from Liechi Berry")
|
||||
check(st.player.item == 0, "Liechi Berry consumed on battler")
|
||||
check(st.playerParty[1].item == nil, "Liechi Berry consumed on party mon")
|
||||
end
|
||||
|
||||
print("=== 6. White Herb restores stat drop mid-turn ===")
|
||||
do
|
||||
-- White Herb (180) restores lowered stats when Growl is used
|
||||
local st, ad = battle({ item = 180 }, { moves = { 45 } })
|
||||
useMove(st, ad, st.enemy, 45)
|
||||
check(st.player.stages.attack == 0, "Attack stat stage restored to 0 by White Herb")
|
||||
check(st.player.item == 0, "White Herb consumed on battler")
|
||||
check(st.playerParty[1].item == nil, "White Herb consumed on party mon")
|
||||
end
|
||||
|
||||
print("=== 7. Leftovers activates only at end of turn (not mid-turn) ===")
|
||||
do
|
||||
-- Leftovers (200) heals at end of turn, not during move resolution
|
||||
local st, ad = battle({ item = 200, hp = 80, maxHp = 100 }, { moves = { 45 } })
|
||||
-- Growl does no damage
|
||||
useMove(st, ad, st.enemy, 45)
|
||||
check(st.player.mon.hp == 80, "Leftovers did not trigger mid-turn")
|
||||
check(st.player.item == 200, "Leftovers is not consumed")
|
||||
|
||||
eot(st, ad)
|
||||
check(st.player.mon.hp == 86, "Leftovers healed maxHp/16 (6 HP) at end of turn")
|
||||
check(st.player.item == 200, "Leftovers remains held after end of turn")
|
||||
end
|
||||
|
||||
print("=== 8. Leppa Berry restores PP ===")
|
||||
do
|
||||
-- Leppa Berry (138) restores 10 PP when a move is at 0 PP
|
||||
local st, ad = battle({ item = 138, moves = { 33 }, pp = { 0 }, maxPp = { 35 } }, {})
|
||||
Engine.afterAction(st, ad)
|
||||
check(st.player.mon.pp[1] == 10, "Leppa Berry restored 10 PP")
|
||||
check(st.player.item == 0, "Leppa Berry consumed")
|
||||
check(st.playerParty[1].item == nil, "party mon Leppa Berry consumed")
|
||||
end
|
||||
|
||||
print("-----------------------------------------")
|
||||
if failed > 0 then
|
||||
print(string.format("[result] %d test(s) FAILED, %d passed", failed, passed))
|
||||
os.exit(1)
|
||||
else
|
||||
print(string.format("[result] ALL %d tests PASSED", passed))
|
||||
os.exit(0)
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env luajit
|
||||
-- Tests Safari Zone warp exit positioning, door step transitions, and trigger line parity against pret pokefirered.
|
||||
-- pokefirered/src/field_fadetransition.c:242 SetUpWarpExitTask / Task_ExitNonAnimDoor
|
||||
-- pokefirered/data/maps/FuchsiaCity_SafariZone_Entrance/map.json
|
||||
-- pokefirered/data/maps/FuchsiaCity_SafariZone_Entrance/scripts.inc
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed, passed = 0, 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set("firered")
|
||||
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Warp = require("src.core.game3.warp")
|
||||
local Safari = require("src.core.game3.safari")
|
||||
|
||||
print("[test] 1. Metatile behavior constants match pret pokefirered")
|
||||
check(Collision.isNonAnimDoor(0x60) == true, "MB_CAVE_DOOR (0x60) is recognized as non-anim door")
|
||||
check(Collision.isWarpDoor(0x69) == true, "MB_WARP_DOOR (0x69) is recognized as warp door")
|
||||
check(Collision.isNonAnimDoor(0x69) == false, "MB_WARP_DOOR is not non-anim door")
|
||||
|
||||
print("[test] 2. warpExitArrival steps player 1 tile down on MB_CAVE_DOOR")
|
||||
local fakeFade = {
|
||||
begin = function(mode, speed, callback)
|
||||
if callback then callback() end
|
||||
end,
|
||||
isActive = function() return false end,
|
||||
}
|
||||
package.loaded["src.ui.game3.fade"] = fakeFade
|
||||
|
||||
-- Initialize player at warp landing tile (4, 1) facing down
|
||||
Player.cellX = 4
|
||||
Player.cellY = 1
|
||||
Player.facing = "down"
|
||||
check(Player.cellX == 4 and Player.cellY == 1, "player starts at (4,1)")
|
||||
check(Player.facing == "down", "player facing is down")
|
||||
|
||||
-- Mock collision behavior at (4, 1) to be MB_CAVE_DOOR (0x60)
|
||||
local origBehavior = Collision.behavior
|
||||
Collision.behavior = function(x, y)
|
||||
if x == 4 and y == 1 then return 0x60 end
|
||||
return 0
|
||||
end
|
||||
|
||||
local finished = false
|
||||
Warp.fadeModes = function() return 1, 0 end
|
||||
-- Call warpExitArrival
|
||||
local warpDone = false
|
||||
-- In test environment without love.timer, player forceStep completes via step loop
|
||||
Player.forceStep = function(dir, onDone)
|
||||
local dirs = { down = {0, 1}, up = {0, -1}, left = {-1, 0}, right = {1, 0} }
|
||||
local d = dirs[dir] or {0, 0}
|
||||
Player.cellX = Player.cellX + d[1]
|
||||
Player.cellY = Player.cellY + d[2]
|
||||
if onDone then onDone() end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Require warp and execute warpExitArrival (using internal function through Warp.request simulation)
|
||||
local ok, err = pcall(function()
|
||||
local mockMod = {}
|
||||
local mockGame = {
|
||||
data = { maps = {} },
|
||||
currentMap = Safari.EXIT_MAP,
|
||||
}
|
||||
local Map = require("src.core.game3.map")
|
||||
Map.load = function(mod, game, mapId, opts)
|
||||
Player.cellX = opts.x
|
||||
Player.cellY = opts.y
|
||||
Player.facing = opts.facing
|
||||
return true
|
||||
end
|
||||
|
||||
Warp.request(mockMod, mockGame, Safari.EXIT_MAP, 4, 1, "down", { fade = true })
|
||||
end)
|
||||
|
||||
check(ok == true, "Warp.request completed successfully")
|
||||
check(Player.cellX == 4 and Player.cellY == 2,
|
||||
string.format("Player stepped down out of doorway from (4,1) to (4,2) (got %d,%d)", Player.cellX, Player.cellY))
|
||||
|
||||
print("[test] 3. Exit script movement from (4,2) lands player at (4,4) outside triggers")
|
||||
-- FuchsiaCity_SafariZone_Entrance_Movement_Exit has 2 walk_down steps
|
||||
-- Step 1: (4,2) -> (4,3)
|
||||
Player.cellY = Player.cellY + 1
|
||||
-- Step 2: (4,3) -> (4,4)
|
||||
Player.cellY = Player.cellY + 1
|
||||
check(Player.cellX == 4 and Player.cellY == 4,
|
||||
string.format("Player lands at (4,4) after Movement_Exit (got %d,%d)", Player.cellX, Player.cellY))
|
||||
|
||||
-- Walking North from (4,4) steps onto (4,3), which triggers EntryTriggerMid
|
||||
local stepNorthDestY = Player.cellY - 1
|
||||
check(stepNorthDestY == 3, "Stepping North from (4,4) steps onto coord trigger line y=3")
|
||||
|
||||
Collision.behavior = origBehavior
|
||||
|
||||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||||
if failed == 0 then
|
||||
print("SAFARI_WARP_EXIT_TEST PASS")
|
||||
os.exit(0)
|
||||
else
|
||||
os.exit(1)
|
||||
end
|
||||
Reference in New Issue
Block a user