CLOSES #941, CLOSES #1081, CLOSES #1349, CLOSES #1602, CLOSES #1618, CLOSES #1624, CLOSES #1630, CLOSES #1633, CLOSES #1661, CLOSES #1669, CLOSES #1676, CLOSES #1731, CLOSES #1739, CLOSES #1740, CLOSES #1742, CLOSES #1744, CLOSES #1753, CLOSES #1757, CLOSES #1758, CLOSES #1760, CLOSES #1761, CLOSES #1762, CLOSES #1763, CLOSES #1765, CLOSES #1770, CLOSES #1777, CLOSES #1781, CLOSES #1783, CLOSES #1784, CLOSES #1785, CLOSES #1787, CLOSES #1788, CLOSES #1789, CLOSES #1790, CLOSES #1792, CLOSES #1797, CLOSES #1803

This commit is contained in:
bryanthaboi
2026-08-25 07:55:52 -04:00
parent ba201aa4ad
commit 1850b50ee6
50 changed files with 659 additions and 290 deletions
+21 -25
View File
@@ -42,27 +42,29 @@ local LINK_HEADINGS = { "HOW TO LINK", "COLOSSEUM", "TRADE CENTER" }
local function linkCableHelp(game)
local text = game.data.text or {}
local items, showMenu, askHeading
function showMenu()
game.stack:push(Menu.new(game, items,
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 1 }))
end
function askHeading()
game.stack:push(TextBox.new(game,
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
showMenu))
local items, menu, openMenu
local function closeAll()
game.stack:pop()
end
items = {}
for i, label in ipairs(LINK_HEADINGS) do
items[i] = { label = label, onSelect = function()
items[i] = { label = label, keepOpen = true, onSelect = function()
game.stack:push(TextBox.new(game,
text["_LinkCableInfoText" .. i] or label, askHeading))
text["_LinkCableInfoText" .. i] or label))
end }
end
items[#items + 1] = { label = "STOP READING" }
items[#items + 1] = { label = "STOP READING", onSelect = closeAll }
menu = Menu.new(game, items,
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 2, itemY = 2,
onCancel = closeAll })
function openMenu() game.stack:push(menu) end
game.stack:push(TextBox.new(game,
text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable",
askHeading))
function()
game.stack:push(TextBox.new(game,
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
nil, { stay = { onShown = openMenu } }))
end))
end
return {
@@ -84,21 +86,15 @@ return {
{ "jump_if_false", 5 }, -- 2
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 3 (old saves)
{ "jump", 13 }, -- 4
{ "give_pokemon", "EEVEE", 25 }, -- 5
{ "jump_if_false", 12 }, -- 6 (party+box full)
-- flag + HideObject before the jingle and GotMonText: the nickname
-- prompt inside give_pokemon and the text row both yield, and a
-- script that dies there would leave the EEVEE taken with the ball
-- still on the table and the gift claimable again (#426). The row
-- count is unchanged, so the numeric jump targets still hold.
{ "jump", 11 }, -- 4
{ "give_pokemon", "EEVEE", 25, false, true }, -- 5
{ "jump_if_false", 10 }, -- 6 (party+box full)
-- scripts/CeladonMansionRoofHouse.asm:17-20 (#426)
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
{ "jump", 13 }, -- 11
{ "show_text", "_BoxIsFullText" }, -- 12
{ "jump", 11 }, -- 9
{ "show_text", "_BoxIsFullText" }, -- 10
},
},
}
+2 -4
View File
@@ -2,12 +2,10 @@
return {
CELADON_CITY = {
talk = {
-- CeladonCityPoliwrathText (scripts/CeladonCity.asm): text_far
-- _CeladonCityPoliwrathText, then plays the POLIWRATH cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONCITY_POLIWRATH = {
{"face_player"},
-- pokered/scripts/CeladonCity.asm:90
{"play_cry", "POLIWRATH", true},
{"show_text", "_CeladonCityPoliwrathText"},
},
},
+6 -12
View File
@@ -2,30 +2,24 @@
return {
CELADON_MANSION_1F = {
talk = {
-- CeladonMansion1FClefairyText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FClefairyText, then plays the CLEFAIRY cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_CLEFAIRY = {
{"face_player"},
-- pokered/scripts/CeladonMansion1F.asm:27
{"play_cry", "CLEFAIRY", true},
{"show_text", "_CeladonMansion1FClefairyText"},
},
-- CeladonMansion1FMeowthText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FMeowthText, then plays the MEOWTH cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_MEOWTH = {
{"face_player"},
-- pokered/scripts/CeladonMansion1F.asm:18
{"play_cry", "MEOWTH", true},
{"show_text", "_CeladonMansion1FMeowthText"},
},
-- CeladonMansion1FNidoranFText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FNidoranFText, then plays the NIDORAN_F cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_NIDORANF = {
{"face_player"},
-- pokered/scripts/CeladonMansion1F.asm:33
{"play_cry", "NIDORAN_F", true},
{"show_text", "_CeladonMansion1FNidoranFText"},
},
},
@@ -4,11 +4,10 @@
return {
LAVENDER_CUBONE_HOUSE = {
talk = {
-- LavenderCuboneHouseCuboneText: text_far _LavenderCuboneHouseCuboneText,
-- then text_asm plays the CUBONE cry. Cry playback has no Commands
-- equivalent in this port, so just show the line.
-- LavenderCuboneHouse.asm:10
TEXT_LAVENDERCUBONEHOUSE_CUBONE = {
{ "face_player" },
{ "play_cry", "CUBONE", true },
{ "show_text", "_LavenderCuboneHouseCuboneText" },
},
+4 -5
View File
@@ -31,16 +31,15 @@ M.MR_FUJIS_HOUSE = {
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
},
-- scripts/MrFujisHouse.asm MrFujisHousePsyduckText: text_far then
-- PlayCry(PSYDUCK). Cry playback isn't modeled by Commands, so just
-- show the flavor text.
-- scripts/MrFujisHouse.asm:56
TEXT_MRFUJISHOUSE_PSYDUCK = {
{ "play_cry", "PSYDUCK", true },
{ "show_text", "_MrFujisHousePsyduckText" },
},
-- scripts/MrFujisHouse.asm MrFujisHouseNidorinoText: text_far then
-- PlayCry(NIDORINO).
-- scripts/MrFujisHouse.asm:63
TEXT_MRFUJISHOUSE_NIDORINO = {
{ "play_cry", "NIDORINO", true },
{ "show_text", "_MrFujisHouseNidorinoText" },
},
},
+2 -3
View File
@@ -4,10 +4,9 @@
return {
ROUTE_16_FLY_HOUSE = {
talk = {
-- Route16FlyHouseFearowText: text_asm just prints the one line and
-- plays the FEAROW cry (no cry-playback command exists in this
-- port's Commands vocabulary, so only the text is ported).
-- scripts/Route16FlyHouse.asm:45-51
TEXT_ROUTE16FLYHOUSE_FEAROW = {
{ "play_cry", "FEAROW", true },
{ "show_text", "_Route16FlyHouseFearowText" },
},
},
+1
View File
@@ -1047,6 +1047,7 @@ local championsRoomRivalScript = {
-- numeric 26 into a jump ONTO the closing HALL_OF_FAME warp instead of past
-- it, so a returning champion warped straight into the induction.
{ "jump_if_true", "end" }, -- 3
{ "set_option", "animations", true }, -- scripts/ChampionsRoom.asm:57
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
-- the intro text, before the battle itself (#706); pushBattle's wipe-time
+9 -5
View File
@@ -359,18 +359,22 @@ M.ROUTE_16_FLY_HOUSE = {
TEXT_ROUTE16FLYHOUSE_BRUNETTE_GIRL = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_GOT_HM02 then
push(game, t._Route16FlyHouseBrunetteGirlHm02ExplanationText
or "HM02 is FLY!\fIt will whisk you\nback to any town!", done)
push(game, t._Route16FlyHouseBrunetteGirlHM02ExplanationText
or "HM02 is FLY.\nIt will take you\vback to any town.\fPut it to good\nuse!", done)
return
end
push(game, t._Route16FlyHouseBrunetteGirlText
or "Shh! It's a\nsecret!\fMy POKéMON's\nHM02, take it!", function()
or "Oh, you found my\nsecret retreat!", function()
if not require("src.inventory.Bag").add(game.save, "HM_FLY", 1) then
push(game, "You don't have\nroom for HM02!", done)
push(game, t._Route16FlyHouseBrunetteGirlHM02NoRoomText
or "You don't have any\nroom for this.", done)
return
end
game.save.flags.EVENT_GOT_HM02 = true
push(game, ("%s got\nHM02!"):format(game.save.player.name), done)
-- scripts/Route16FlyHouse.asm:32-35
push(game, fill(t._Route16FlyHouseBrunetteGirlReceivedHM02Text
or "{PLAYER} received\nHM02!", { player = game.save.player.name }),
done, require("src.render.TextBox").soundOpts(game, "Get_Key_Item"))
end)
end,
},
+34 -6
View File
@@ -1170,6 +1170,17 @@ function love.run()
-- per-frame sleep-granularity jitter.
local nextFrame = love.timer and love.timer.getTime() or 0
local dt = 0
local idleFor = 0
local SLEEP_FLOOR = 0.001
local WAKE = {
keypressed = true, keyreleased = true, textinput = true,
mousepressed = true, mousereleased = true, mousemoved = true,
wheelmoved = true, touchpressed = true, touchreleased = true,
touchmoved = true, joystickpressed = true, joystickreleased = true,
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
joystickadded = true, joystickremoved = true, filedropped = true,
directorydropped = true, focus = true, visible = true, resize = true,
}
return function()
-- process events
@@ -1188,17 +1199,34 @@ function love.run()
return a or 0
end
end
if WAKE[name] then
idleFor = 0
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
idleFor = 0
end
love.handlers[name](a, b, c, d, e, f)
end
end
-- update dt
if love.timer then dt = love.timer.step() end
idleFor = idleFor + dt
-- call update and draw
if love.update then love.update(dt) end
if love.graphics and love.graphics.isActive() then
local visible = not (love.window and love.window.isVisible)
or love.window.isVisible()
local focused = not (love.window and love.window.hasFocus)
or love.window.hasFocus()
local cap = FrameCap.current
if not visible then
cap = 10
elseif Importer and (not focused or idleFor > 30) then
cap = 15
end
if visible and love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
@@ -1209,9 +1237,9 @@ function love.run()
if paced then
-- Sleep out the remainder of the frame budget, measured from the
-- carried deadline, in small chunks so the OS timer stays
-- responsive. vsync is untouched: when it already paces slower
-- than the cap the remainder is <= 0 and this rounds to a no-op.
local budget = 1 / FrameCap.current
-- responsive. The pacer yields to vsync inside a 1ms dead band, so
-- when the panel already paces at or below the cap it is a no-op.
local budget = 1 / cap
nextFrame = nextFrame + budget
local now = love.timer.getTime()
-- A stall (alt-tab, a GC pause, a blocked import) can leave the
@@ -1222,8 +1250,8 @@ function love.run()
end
while true do
local remaining = nextFrame - love.timer.getTime()
if remaining <= 0 then break end
love.timer.sleep(remaining < 0.001 and remaining or 0.001)
if remaining <= SLEEP_FLOOR then break end
love.timer.sleep(0.001)
end
else
love.timer.sleep(0.001)
+77 -28
View File
@@ -97,7 +97,7 @@ end
-- space.
function BattleState:extendedHUD()
local options = self.game and self.game.save and self.game.save.options
local bg = options and options.battleBg
local bg = options and self:bgMode()
return self:wideLayout()
and options and options.battleHud == "extended"
and ((options.battleFit == "fixed"
@@ -108,12 +108,12 @@ end
function BattleState:extendedWorldHUD()
local options = self.game and self.game.save and self.game.save.options
return self:extendedHUD() and options
and options.battleFit == "fixed" and options.battleBg == "world"
and options.battleFit == "fixed" and self:bgMode() == "world"
end
function BattleState:extendedBlackHUD()
local options = self.game and self.game.save and self.game.save.options
return self:extendedHUD() and options and options.battleBg == "black"
return self:extendedHUD() and options and self:bgMode() == "black"
end
-- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids
@@ -130,6 +130,10 @@ end
-- opaque 160x144 field over the top, so only the surround changes.
function BattleState:bgMode()
local options = self.game and self.game.save and self.game.save.options
if options and self:wideLayout() and options.battleFit == "fill"
and options.battleHud == "extended" then
return "white"
end
local mode = options and options.battleBg
if mode == "black" or mode == "world" then return mode end
return "white"
@@ -212,6 +216,13 @@ local BALL_ANIMS = {
SHAKE_ANIM = true, SHOWPIC_ANIM = true,
}
-- engine/battle/effects.asm:552
local ENEMY_STAT_DOWN_MISS = {
ATTACK_DOWN1_EFFECT = true, DEFENSE_DOWN1_EFFECT = true,
DEFENSE_DOWN2_EFFECT = true, SPEED_DOWN1_EFFECT = true,
ACCURACY_DOWN1_EFFECT = true,
}
local imageCache = {}
-- The three tables below are keyed by the Image OBJECT, not by a path, and a
-- running battle holds the pics it built at enter() (battler.sprite,
@@ -229,6 +240,11 @@ local imagePadLeft = setmetatable({}, WEAK_KEYS)
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be
-- rebuilt for any battle pic, whatever code loaded it
local imageMeta = setmetatable({}, WEAK_KEYS)
local function mattedPic(path)
return path:sub(1, 17) == "assets/generated/"
or path:sub(1, 17) == "save/mod-derived/"
end
-- pal = { name, colors } recolors the 4 GB shades like the Super Game Boy.
-- trueColor art (14 §the 4-shade contract) opts out of the quantize
-- entirely, so its palette variant collapses back onto the plain path.
@@ -254,29 +270,31 @@ local function getImage(path, pal, trueColor)
end)
end
local w, h = id:getDimensions()
local bottom = h - 1
while bottom >= 0 do
local opaque = false
for x = 0, w - 1 do
local _, _, _, a = id:getPixel(x, bottom)
if a > 0 then opaque = true break end
if mattedPic(Assets.resolve(path)) then
local bottom = h - 1
while bottom >= 0 do
local opaque = false
for x = 0, w - 1 do
local _, _, _, a = id:getPixel(x, bottom)
if a > 0 then opaque = true break end
end
if opaque then break end
bottom = bottom - 1
end
if opaque then break end
bottom = bottom - 1
end
local left = 0
while left < w do
local opaque = false
for y = 0, h - 1 do
local _, _, _, a = id:getPixel(left, y)
if a > 0 then opaque = true break end
local left = 0
while left < w do
local opaque = false
for y = 0, h - 1 do
local _, _, _, a = id:getPixel(left, y)
if a > 0 then opaque = true break end
end
if opaque then break end
left = left + 1
end
if opaque then break end
left = left + 1
pad = h - 1 - bottom
padL = left
end
img = love.graphics.newImage(id)
pad = h - 1 - bottom
padL = left
else
img = Assets.image(path) -- headless stub: no pixel access
end
@@ -2214,6 +2232,9 @@ function BattleState:update(dt)
self.waitFrames = nil
if destination == "menu" then
self.introSlide = nil
-- engine/battle/core.asm:2007
self.msgHold = nil
self.shown = nil
self.phase = "menu"
elseif destination == "finish" then
self:finish()
@@ -3041,18 +3062,33 @@ local BGP_INVERT = { [0] = 3, 2, 1, 0 } -- $1b (flash phase 1)
local BGP_WHITE = { [0] = 0, 0, 0, 0 } -- $00 (flash phase 2)
local BGP_DARK = { [0] = 3, 3, 2, 1 } -- $6f DarkScreenPalette
local BGP_LIGHT = { [0] = 0, 0, 1, 2 } -- $90 LightScreenPalette
local BGP_DARKEN = { [0] = 0, 1, 3, 3 } -- $f4 DarkenMonPalette (SGB)
local BGP_DARKEN_SGB = { [0] = 0, 1, 3, 3 } -- $f4
local BGP_DARKEN_MONO = { [0] = 1, 2, 3, 3 } -- $f9
-- engine/battle/animations.asm:1090
local function onSgb()
local m = require("src.render.PaletteFX").mode
return m == "gbc" or m == "gbc_inv"
end
-- FlashScreenLongSGB (animations.asm:1010): 12 BGP values per cycle,
-- 3 cycles; the first cycle holds each for 2 frames, the rest for 1
-- (FlashScreenLongDelay)
local FLASH_LONG_MAPS = {
local FLASH_LONG_SGB = {
{ [0] = 0, 2, 3, 3 }, { [0] = 0, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
{ [0] = 0, 3, 3, 3 }, { [0] = 0, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
}
-- engine/battle/animations.asm:992
local FLASH_LONG_MONO = {
{ [0] = 1, 2, 3, 3 }, { [0] = 2, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
{ [0] = 2, 3, 3, 3 }, { [0] = 1, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
}
-- the shade map in force this frame (a running flash wins over the
-- persistent palette)
function BattleState:activeBgp()
@@ -3186,7 +3222,7 @@ function BattleState:applyAnimEffect(ev)
elseif e == "SE_LIGHT_SCREEN_PALETTE" then
fx.bgp = BGP_LIGHT
elseif e == "SE_DARKEN_MON_PALETTE" then
fx.bgp = BGP_DARKEN
fx.bgp = onSgb() and BGP_DARKEN_SGB or BGP_DARKEN_MONO
elseif e == "SE_RESET_SCREEN_PALETTE" then
fx.bgp = nil
elseif e == "SE_DARK_SCREEN_FLASH" then
@@ -3196,8 +3232,9 @@ function BattleState:applyAnimEffect(ev)
idx = 1, left = 2 }
elseif e == "SE_FLASH_SCREEN_LONG" then
local steps = {}
local maps = onSgb() and FLASH_LONG_SGB or FLASH_LONG_MONO
for cycle = 1, 3 do
for _, m in ipairs(FLASH_LONG_MAPS) do
for _, m in ipairs(maps) do
steps[#steps + 1] = { map = m, frames = (cycle == 1) and 2 or 1 }
end
end
@@ -4139,6 +4176,12 @@ function BattleState:performMove(user, target, moveInst, isCalled)
-- pure status moves
if move.power == 0 and record and record.kind == "primary" and record.run then
if ENEMY_STAT_DOWN_MISS[move.effect] and not user.isPlayer
and self.kind ~= "link" and self.rng(0, 255) < 64 then
self:cancelMoveAnim()
self:sayNext(self:romText("_AttackMissedText", "%s's\nattack missed!", displayName(user)))
return
end
-- accuracy-checked status effects run MoveHitTest, which has no
-- 100%-accuracy early-out (even Thunder Wave misses on the 255
-- roll) and misses outright against a mid-Fly/Dig target; the
@@ -4417,7 +4460,11 @@ function BattleState:awardExp()
-- new current HP (house convention: potions drain the bar too, see
-- itemUsed) so the bar grows instead. Only the active player battler
-- shares its table with the HUD; other party mons (EXP.ALL) have no bar.
if mon == self.player.mon then self:drainNext() end
if mon == self.player.mon then
-- engine/battle/experience.asm:236
self.player.badgeExtraBoosts = nil
self:drainNext()
end
for _, moveId in ipairs(Experience.movesLearnedAt(
self.data.pokemon[mon.species], lv)) do
self:learnMove(mon, moveId)
@@ -5920,7 +5967,9 @@ function BattleState:animSpriteColors(s, px, py)
local P
-- engine/battle/animations.asm:551 (.notSGB)
if PaletteFX.usesSpriteObp() then
P = PaletteFX.ogObj()
-- engine/battle/init_battle_variables.asm:18
P = require("src.core.GameVersion").isBlue() and PaletteFX.GBC_OBJ_BLUE
or PaletteFX.GBC_OBJ
if key == "f0" then key = "e4" elseif key == "f0x" then key = "e4x" end
else
P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4))
+30 -8
View File
@@ -41,6 +41,34 @@ local function badgeBoost(battler, stat)
return nil
end
-- engine/battle/core.asm:6454
function Damage.applyBadgeBoost(battler, stat, value)
if battler.hazeStatReset then return value end
local row = badgeBoost(battler, stat)
if not row then return value end
local extra = battler.badgeExtraBoosts and battler.badgeExtraBoosts[stat] or 0
for _ = 1, 1 + extra do
value = math.min(999, math.floor(value * (row.num or 9) / (row.den or 8)))
end
return value
end
-- engine/battle/effects.asm:498,689
function Damage.reapplyBadgeBoosts(battler, changedStat)
if not battler or not battler.badges then return end
local extra = battler.badgeExtraBoosts
if not extra then extra = {} battler.badgeExtraBoosts = extra end
for _, row in ipairs(battler.badgeBoosts or Damage.BADGE_BOOSTS) do
if battler.badges[row.badge] then
if row.stat == changedStat then
extra[row.stat] = 0
else
extra[row.stat] = (extra[row.stat] or 0) + 1
end
end
end
end
-- the merged status record for a battler's persistent condition, or nil
local function statusRecord(battler)
return Status.recordFor(battler.statuses, battler.mon.status)
@@ -178,14 +206,8 @@ function Damage.compute(ruleset, attacker, defender, move, opts)
-- badge boosts (x9/8), engine/battle/core.asm ApplyBadgeStatBoosts:
-- Boulder -> attack, Thunder -> defense, Soul -> speed (TurnOrder),
-- Volcano -> special
local atkBoost = badgeBoost(attacker, atkStat)
if atkBoost then
atk = math.floor(atk * (atkBoost.num or 9) / (atkBoost.den or 8))
end
local defBoost = badgeBoost(defender, defStat)
if defBoost then
dfn = math.floor(dfn * (defBoost.num or 9) / (defBoost.den or 8))
end
atk = Damage.applyBadgeBoost(attacker, atkStat, atk)
dfn = Damage.applyBadgeBoost(defender, defStat, dfn)
-- burn halves physical attack (applied as part of the stat in Gen 1;
-- the status record's statPenalty names the stat it cuts).
-- hazeStatReset suppresses it: Haze (haze.asm ResetStats) copied the
+11
View File
@@ -11,6 +11,7 @@
-- is the registry view of all three -- the merged Data.move_effects a
-- battle dispatches on serves these same objects.
local Damage = require("src.battle.Damage")
local Logger = require("src.core.Logger")
local StatusRegistry = require("src.battle.StatusRegistry")
local TurnOrder = require("src.battle.TurnOrder")
@@ -57,6 +58,10 @@ local function changeStage(battle, who, stat, delta, fromEnemy)
-- recomputed and QuarterSpeedDueToParalysis/HalveAttackDueToBurn re-run,
-- re-baking the burn/para penalty and ending Haze's temporary lift.
who.hazeStatReset = nil
if battle.ruleset and battle.ruleset.badgeBoostReapplyBug
and battle.kind ~= "link" and who == battle.player then
Damage.reapplyBadgeBoosts(who, stat)
end
-- _MonsStatsRoseText/_MonsStatsFellText: "X's / STAT rose!"; the
-- two-stage variants scroll "greatly" onto a third line
local label = Strings(STAT_LABEL[stat]) -- looked up here, not at require (#811)
@@ -132,6 +137,11 @@ end
local function statDownSide(stat)
return function(battle, user, target)
-- engine/battle/effects.asm:552
if not user.isPlayer and battle.kind ~= "link"
and battle.rng(0, 255) < 64 then
return {}
end
if target.substituteHP then return {} end
if battle.rng(0, 255) >= 85 then return {} end -- 33 percent + 1 (85/256)
-- StatModifierDownEffect's side-effect branch never runs MoveHitTest,
@@ -254,6 +264,7 @@ MoveEffects.primary = {
-- Attack-halving and paralysis Speed-quartering on BOTH battlers
-- until the next stat recompute (a stage change or switch-in).
b.hazeStatReset = true
b.badgeExtraBoosts = nil
end
-- Gen 1 also removes the enemy's major status; if that cured sleep
-- or freeze, the target forfeits its move this turn (haze.asm
+1 -9
View File
@@ -14,15 +14,7 @@ local function effectiveSpeed(battler)
battler.stages and battler.stages.speed or 0)
-- ApplyBadgeStatBoosts: the SOULBADGE boosts speed; the rows come from
-- the battler's merged badgeBoosts with the vanilla list as fallback
local badges = battler.badges
if badges then
for _, row in ipairs(battler.badgeBoosts or Damage.BADGE_BOOSTS) do
if row.stat == "speed" and badges[row.badge] then
spd = math.floor(spd * (row.num or 9) / (row.den or 8))
break
end
end
end
spd = Damage.applyBadgeBoost(battler, "speed", spd)
-- paralysis quarters speed (the status record's statPenalty);
-- hazeStatReset suppresses it because Haze (haze.asm ResetStats)
-- copied the unmodified speed over the quartered battle stat, lifting
+14 -1
View File
@@ -97,6 +97,19 @@ local function battleIsTopState(battle)
return not (stack and stack.top) or stack:top() == battle
end
-- engine/menus/party_menu.asm:4
local function coveredByOpaqueState(battle)
local stack = battle.game and battle.game.stack
local states = stack and stack.states
if not states then return false end
local above = false
for i = 1, #states do
if above and states[i] and states[i].isOpaque then return true end
if states[i] == battle then above = true end
end
return false
end
local function anchorHUD(battle, x, y, w, h, anchor)
if not battle:extendedHUD() or not battleIsTopState(battle) then return end
local renderer = battle.game and battle.game.renderer
@@ -351,7 +364,7 @@ function WideBattle.draw(battle)
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
end
-- AskName clears the field the same way the classic layout does
if battle.blankForAskName then return end
if battle.blankForAskName or coveredByOpaqueState(battle) then return end
local fx = battle.fx
local sx = (fx and fx.shakeX) or 0
+64 -7
View File
@@ -645,6 +645,7 @@ function Battle:smartAiState()
-- wPlayerSubStatus5 & SUBSTATUS_LOCK_ON: the enemy's OWN Lock-On, since
-- BattleCommand_LockOn sets the bit on the target it was aimed at.
playerLockOn = playerState.lockOn or nil,
playerIdentified = playerState.identified or nil,
playerPhysicalMoves = physical,
enemyRage = enemyState.rage,
enemyRageCount = enemyState.rageCount,
@@ -1082,7 +1083,7 @@ function Battle:hitOnce(attacker, defender, def, opts)
local attackerStages = self.stages[self:sideOf(attacker)]
local defenderStages = self.stages[self:sideOf(defender)]
local types = self.data.type_chart and self.data.type_chart.types
local matchups = self.data.type_chart and self.data.type_chart.matchups
local matchups = self:matchupsAgainst(defender)
local heldEffect, heldParam = self:heldEffect(attacker, "damage")
-- BattleCommand_Critical: SUBSTATUS_FOCUS_ENERGY (Focus Energy or a
@@ -1202,6 +1203,8 @@ function Battle:hitOnce(attacker, defender, def, opts)
if def.effect == "EFFECT_FALSE_SWIPE" and damage >= (defender.hp or 0) then
damage = math.max(0, (defender.hp or 0) - 1)
end
-- engine/battle_anims/anim_commands.asm:1200
if self.moveEvent then self.moveEvent.effectiveness = info.effectiveness end
return self:dealDamage(attacker, defender, damage, {
critical = critical, effectiveness = info.effectiveness,
-- Counter answers physical damage and Mirror Coat special, so what kind
@@ -1690,6 +1693,13 @@ function Battle:useMove(attacker, defender, moveId)
text = ("Magnitude %d!"):format(number) })
end
-- data/moves/effects.asm:1607, :1649
if def.effect == "EFFECT_RETURN" then
powerOverride = Effects.happinessPower(attacker.happiness)
elseif def.effect == "EFFECT_FRUSTRATION" then
powerOverride = Effects.happinessPower(attacker.happiness, true)
end
if not sureHit
and not self:accuracyRoll(def, attacker, defender) then
-- data/moves/effects.asm:148-151: `selfdestruct` sits between checkhit and
@@ -1744,7 +1754,7 @@ function Battle:useMove(attacker, defender, moveId)
-- thing that stops SONIC BOOM, NIGHT SHADE or SUPER FANG.
local defenderTypes = (self:speciesDef(defender) or {}).types
or defender.types
local matchups = self.data.type_chart and self.data.type_chart.matchups
local matchups = self:matchupsAgainst(defender)
if Damage.typeMultiplier(def.type, defenderTypes, matchups) == 0 then
self:markMissed()
self:emit({ kind = "message",
@@ -2119,6 +2129,21 @@ Battle.MOVE_EFFECTS.EFFECT_LOCK_ON = function(self, attacker, defender)
text = self:monName(attacker) .. " took aim!" })
end
-- engine/battle/move_effects/foresight.asm
Battle.MOVE_EFFECTS.EFFECT_FORESIGHT = function(self, attacker, defender,
def, _, sureHit)
if not sureHit
and not self:accuracyRoll(def, attacker, defender) then
return fail(self)
end
local target = self:volatile(defender)
if target.vanished or target.identified then return fail(self) end
target.identified = true
self:emit({ kind = "message",
text = self:monName(attacker) .. " identified "
.. self:monName(defender) .. "!" })
end
-- BattleCommand_CheckHit's .LockOn: the flag is read AND cleared by the next
-- move aimed at the mon carrying it, whether or not that move was the one the
-- lock-on was meant for, and whether or not the exception at :1683-1688 then
@@ -2166,9 +2191,15 @@ function Battle:accuracyRoll(def, attacker, defender, accuracy)
end
function Battle:vanillaAccuracyRoll(accuracy, attacker, defender)
return Damage.rollHit(self:moveAccuracy(accuracy, defender),
self.stages[self:sideOf(attacker)].accuracy,
self.stages[self:sideOf(defender)].evasion, self.random)
local acc = self.stages[self:sideOf(attacker)].accuracy
local eva = self.stages[self:sideOf(defender)].evasion
-- engine/battle/effect_commands.asm:1786
if defender and self:volatile(defender).identified
and (eva or 0) >= (acc or 0) then
acc, eva = 0, 0
end
return Damage.rollHit(self:moveAccuracy(accuracy, defender), acc, eva,
self.random)
end
-- BattleCommand_StatDown's SUBSTATUS_MIST arm (a GUARD SPEC): a drop the FOE
@@ -2354,7 +2385,7 @@ Battle.MOVE_EFFECTS.EFFECT_FUTURE_SIGHT = function(self, attacker, defender, def
stages = self.stages[self:sideOf(defender)],
},
types = self.data.type_chart and self.data.type_chart.types,
matchups = self.data.type_chart and self.data.type_chart.matchups,
matchups = self:matchupsAgainst(defender),
random = self.random,
})
state.futureSight = Effects.FUTURE_SIGHT_TURNS
@@ -3072,6 +3103,30 @@ function Battle:safeguarded(mon)
return (self.screens[self:sideOf(mon)].safeguard or 0) > 0
end
-- engine/battle/effect_commands.asm:1305
function Battle:matchupsAgainst(defender)
local chart = self.data.type_chart
local rows = chart and chart.matchups
if not rows or not defender then return rows end
if not self:volatile(defender).identified then return rows end
local skipped = chart.foresightMatchups
if not skipped or #skipped == 0 then return rows end
if not self.identifiedMatchups then
local drop = {}
for _, row in ipairs(skipped) do
drop[tostring(row.attacker) .. "/" .. tostring(row.defender)] = true
end
local out = {}
for _, row in ipairs(rows) do
if not drop[tostring(row.attacker) .. "/" .. tostring(row.defender)] then
out[#out + 1] = row
end
end
self.identifiedMatchups = out
end
return self.identifiedMatchups
end
-- `source` is the battler that inflicted it, carried only so
-- battle.status_inflicted can name it the way Gen 1's does.
-- BattleCommand_Paralyze and BattleCommand_Poison refuse on a zero matchup,
@@ -3084,7 +3139,7 @@ function Battle:statusRefusedByType(defender, moveType, status)
end
local types = (self:speciesDef(defender) or {}).types or defender.types or {}
if moveType then
local matchups = self.data.type_chart and self.data.type_chart.matchups
local matchups = self:matchupsAgainst(defender)
if Damage.typeMultiplier(moveType, types, matchups) == 0 then return true end
end
if status == "poison" or status == "toxic" then
@@ -3917,6 +3972,8 @@ end
-- check. Split out because playerAttack needs the same answer.
function Battle:lockedInMove(mon)
local state = self:volatile(mon)
-- engine/battle/core.asm:543
if state.chargeMove then return state.chargeMove end
if state.rolloutLock then return state.rolloutLock end
if state.rampageMove and (state.rampageTurns or 0) > 0 then
return state.rampageMove
+8
View File
@@ -284,6 +284,14 @@ function Effects.magnitudePower(random)
return last[2], last[3]
end
-- engine/battle/move_effects/return.asm:1-24, frustration.asm:1-25
function Effects.happinessPower(happiness, frustration)
local h = happiness or 0
if h < 0 then h = 0 elseif h > 255 then h = 255 end
if frustration then h = 255 - h end
return math.floor(h * 10 / 25)
end
-- ------------------------------------------------------------------- weather
--
-- BattleCommand_StartRain / StartSun / StartSandstorm all set wWeatherCount to
+5 -2
View File
@@ -58,9 +58,12 @@ Prize.AMULET_COIN = "AMULET_COIN"
-- data/text/battle.asm. Declared here and formatted at the call site so
-- Strings.source is what registers them, the same way Decorations declares
-- its own five. No line markers: every battle message in this port is one
-- flowing string that Chrome.wrap breaks to the box.
-- flowing string that Chrome.wrap breaks to the box, except SentSomeToMomText,
-- which keeps the cart's own `line`/`cont` breaks because it does not fit two
-- rows.
local GOT_MONEY = Strings.source("%s got %s%d for winning!")
local SENT_SOME = Strings.source("%s got %s%d for winning! Sent some to MOM!")
-- data/text/battle.asm:179-185
local SENT_SOME = Strings.source("%s got %s%d\nfor winning!\vSent some to MOM!")
-- The half and all texts really are this short on the cart: they replace the
-- money line rather than following it, which is a quirk no Gold player can
-- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F.
+2
View File
@@ -23,4 +23,6 @@ return {
-- (core.asm:426-464): poison/burn/leech seed tick before the slower
-- mon acts, not in an end-of-round sweep like Gen 3+.
residualAfterMove = true,
-- engine/battle/effects.asm:498,689
badgeBoostReapplyBug = true,
}
+1 -1
View File
@@ -27,7 +27,7 @@ local BATTLER_FIELDS = {
"chargeReady", "invulnerable", "mustRecharge",
"thrashTurns", "thrashAnnounced", "focusEnergy", "leechSeeded",
"lightScreen", "reflect", "mist", "xAccuracy", "lastMove", "flinched",
"skipMove", "hazeStatReset", "drainFloor", "drainHold", "trappingTurns",
"skipMove", "hazeStatReset", "badgeExtraBoosts", "drainFloor", "drainHold", "trappingTurns",
"trapMove", "trapDamage", "fainted",
"aiLayer2",
}
+19 -1
View File
@@ -6,6 +6,9 @@ local FixedStep = {}
FixedStep.STEP = 1 / 60
local MAX_ACCUM = 0.25 -- avoid spiral of death after a stall
local SMOOTH_FRAMES = 4
local SMOOTH_MAX = 1 / 60 * 2.5
local STEP_EPS = 1 / 60 * 0.02
-- Phase the accumulator is re-seeded with once an absorbed hitch frame has
-- been paid for. Half a step is the balanced point: a frame has to come in
@@ -23,6 +26,7 @@ function FixedStep:init(callback)
self.accum = 0
self.callback = callback
self.suppressCatchup = false
self.dtHistory, self.dtSum = nil, 0
end
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
@@ -39,12 +43,26 @@ function FixedStep:update(dt)
-- the burst it would otherwise release doesn't play out as a slide.
if self.suppressCatchup then
self.suppressCatchup = false
self.dtHistory, self.dtSum = nil, 0
self.accum = self.STEP * RESEED_PHASE
self.callback(self.STEP)
return
end
if dt > 0 and dt <= SMOOTH_MAX then
local hist = self.dtHistory
if not hist then hist = {}; self.dtHistory = hist; self.dtSum = 0 end
hist[#hist + 1] = dt
self.dtSum = self.dtSum + dt
if #hist > SMOOTH_FRAMES then
self.dtSum = self.dtSum - hist[1]
table.remove(hist, 1)
end
dt = self.dtSum / #hist
else
self.dtHistory, self.dtSum = nil, 0
end
self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM)
while self.accum >= self.STEP do
while self.accum >= self.STEP - STEP_EPS do
self.accum = self.accum - self.STEP
self.callback(self.STEP)
end
+1 -3
View File
@@ -1324,9 +1324,7 @@ function Game:restoreSave(loaded, recovered, opts)
self:adoptSave(loaded)
-- SaveData.load already attached the standalone options.lua table
self:applyOptions(loaded.options)
-- saves from before OT/ID stamping: backfill with the player's (after
-- the scrub, so every mon the stamp loop sees is known)
SaveData.repairTradedOtIds(loaded)
-- saves from before OT/ID stamping: backfill with the player's
local stamp = require("src.battle.BattleState").stampOT
for _, mon in ipairs(loaded.party or {}) do stamp(loaded, mon) end
for _, box in ipairs(loaded.boxes or {}) do
+4
View File
@@ -350,6 +350,10 @@ function Game2:showTitle()
onContinue = function()
self:showMainMenu()
end,
-- engine/menus/intro_menu.asm:848-889
onTimeout = function()
self:showCopyright()
end,
})
end
+7 -2
View File
@@ -1983,6 +1983,11 @@ SaveData.addCoreMigration(3, function(save)
end
end)
-- #1461 repair, one-shot on pre-format-5 saves
SaveData.addCoreMigration(4, function(save)
SaveData.repairTradedOtIds(save)
end)
-- ------- write
-- Game progress only; options are written separately via saveOptions.
@@ -2394,8 +2399,8 @@ function SaveData.applyPostGameHome(save, boot)
end
-- 0.1.82-0.1.9x loads stamped the player's own id onto traded mons and saved
-- it, which reads back as home-caught. A caught mon can never carry
-- traded=true, so clearing that pair is safe on every load. #1461
-- it, which reads back as home-caught. Run only as the pre-format-5
-- migration: a same-id trade partner makes that pair legitimate. #1461
function SaveData.repairTradedOtIds(save)
local playerId = save and save.player and save.player.id
if playerId == nil then return 0 end
-4
View File
@@ -217,10 +217,6 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
local abW = dpadW * 0.46
local ssW = dpadW * 0.30
local margin = dpadW * 0.12
local ok, GameVersion = pcall(require, "src.core.GameVersion")
if ok and GameVersion.generation and GameVersion.generation() == 2 then
margin = math.max(margin, math.min(ww * 0.10, 72))
end
return {
dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW },
a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW },
+1 -1
View File
@@ -18,7 +18,7 @@ local Version = {
-- exceeds the shell it provides.
modApi = 2, -- mod API major (manifest `api`)
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
saveFormat = 4, -- save.meta.format
saveFormat = 5, -- save.meta.format
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
}
+8 -3
View File
@@ -89,10 +89,12 @@ local function handle(cmd)
return false
end
local idleWait = false
while true do
-- drain every pending command first, so a stop/new-play is seen promptly
local quit = false
local cmd = cmdCh:pop()
local cmd = idleWait and cmdCh:demand(0.05) or cmdCh:pop()
while cmd do
if handle(cmd) then quit = true end
cmd = cmdCh:pop()
@@ -100,6 +102,7 @@ while true do
if quit then break end
if engine and not finished and gen and outCh:getCount() < LOOKAHEAD then
idleWait = false
local activeGen = gen
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
if not ok then
@@ -113,8 +116,10 @@ while true do
finished = true
end
end
elseif engine and not finished and gen then
idleWait = false
love.timer.sleep(0.005)
else
-- nothing to do (idle, or the look-ahead is full): yield the core
love.timer.sleep(0.001)
idleWait = true
end
end
+6 -8
View File
@@ -164,14 +164,12 @@ local function receiveItemToPc(save, id, data)
save.pcItems = save.pcItems or {}
local pc = save.pcItems
local held = pc[id] or 0
if held == 0 then
local cap = (data and data.field and data.field.pcItemCap) or PC_ITEM_CAPACITY
local stacks = 0
for _ in pairs(pc) do stacks = stacks + 1 end
if stacks >= cap then return false end
elseif held + 1 > MAX_STACK then
return false
end
local cap = (data and data.field and data.field.pcItemCap) or PC_ITEM_CAPACITY
local function stacksFor(n) return math.ceil((n or 0) / MAX_STACK) end
local used = 0
for _, count in pairs(pc) do used = used + stacksFor(count) end
-- engine/items/items.asm:156 PutItemInPocket
if used + stacksFor(held + 1) - stacksFor(held) > cap then return false end
pc[id] = held + 1
return true
end
+4 -6
View File
@@ -343,6 +343,7 @@ Save.OPTIONS_KEY = "gold"
local SHARED_KEYS = {
touchControls = true, haptics = true, screenPos = true,
videoMode = true,
mods = true, modsByVersion = true, modsGen2 = true,
modOptions = true, modProfiles = true, modProfilesSeeded = true,
activeProfile = true,
@@ -361,12 +362,9 @@ function Save.loadOptions(fs)
end
if type(loaded) == "table" then
for key in pairs(SHARED_KEYS) do
if loaded[key] ~= nil then options[key] = loaded[key] end
end
end
if type(stored) == "table" then
for key in pairs(SHARED_KEYS) do
if options[key] == nil and stored[key] ~= nil then
if loaded[key] ~= nil then
options[key] = loaded[key]
elseif type(stored) == "table" and stored[key] ~= nil then
options[key] = stored[key]
end
end
+1 -1
View File
@@ -10,7 +10,7 @@ local CacheContract = {}
CacheContract.FORMAT = "rom-cache-v10:"
CacheContract.VERSION_FORMAT = {
crystal = "rom-cache-v10-crystal2:",
crystal = "rom-cache-v10-crystal3:",
}
CacheContract.MARKER_PATH = "rom-cache.complete"
+12 -24
View File
@@ -23,6 +23,11 @@ local SaveData = require("src.core.SaveData")
local LauncherSettings = {}
local function bgLocked(opts)
return opts.battleLayout == "wide" and opts.battleFit == "fill"
and opts.battleHud == "extended"
end
local function wrapIndex(i, n)
i = i % n
if i < 0 then i = i + n end
@@ -156,8 +161,6 @@ local function coreRows(opts, hooks)
opts.battleLayout = opts.battleLayout == "wide" and "og" or "wide"
if opts.battleLayout ~= "wide" then
opts.battleHud = "standard"
elseif opts.battleFit == "fill" and opts.battleHud == "extended" then
opts.battleBg = "white"
end
return true
end)
@@ -167,10 +170,6 @@ local function coreRows(opts, hooks)
end,
function()
opts.battleFit = opts.battleFit == "fill" and "fixed" or "fill"
if opts.battleFit == "fill" and opts.battleLayout == "wide"
and opts.battleHud == "extended" then
opts.battleBg = "white"
end
return true
end)
add(Strings("BATTLE HUD"),
@@ -185,28 +184,17 @@ local function coreRows(opts, hooks)
return false
end
opts.battleHud = opts.battleHud == "extended" and "standard" or "extended"
if opts.battleHud == "extended" and opts.battleFit == "fill" then
opts.battleBg = "white"
end
return true
end)
add(Strings("BATTLE BG"),
function()
if opts.battleLayout == "wide" and opts.battleFit == "fill"
and opts.battleHud == "extended" then
opts.battleBg = "white"
return Strings("AUTO")
end
if bgLocked(opts) then return Strings("AUTO (FILL HUD)") end
if opts.battleBg == "black" then return Strings("BLACK") end
if opts.battleBg == "world" then return Strings("WORLD") end
return Strings("WHITE")
end,
function(dir)
if opts.battleLayout == "wide" and opts.battleFit == "fill"
and opts.battleHud == "extended" then
opts.battleBg = "white"
return false
end
if bgLocked(opts) then return false end
local order = { "white", "black", "world" }
local cur = 1
for i, mode in ipairs(order) do
@@ -563,7 +551,7 @@ end
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
local GEN2_KEY = "gold"
local function gen2Rows(opts, hooks)
local function gen2Rows(opts, hooks, shared)
local rows = {}
local function add(label, value, step)
rows[#rows + 1] = { label = label, value = value, step = step }
@@ -669,9 +657,9 @@ local function gen2Rows(opts, hooks)
local okVm, VideoMode = pcall(require, "src.core.VideoMode")
if okVm then
add(Strings("VIDEO MODE"),
function() return VideoMode.modeLabel(opts.videoMode) end,
function() return VideoMode.modeLabel(shared.videoMode) end,
function(dir)
opts.videoMode = VideoMode.cycle(opts.videoMode, dir)
shared.videoMode = VideoMode.cycle(shared.videoMode, dir)
return true
end)
end
@@ -690,7 +678,7 @@ local function gen2Rows(opts, hooks)
add(Strings("BATTLE BG"), ladder(opts, "battleBg",
{ { "white", "WHITE" }, { "black", "BLACK" } }, "white"))
addTouchRows(rows, add, opts, hooks)
addTouchRows(rows, add, shared, hooks)
return rows
end
@@ -718,7 +706,7 @@ function LauncherSettings.open(hooks, version)
opts[GEN2_KEY] = block
end
sections = {
{ title = Strings("OPTIONS"), rows = gen2Rows(block, hooks) },
{ title = Strings("OPTIONS"), rows = gen2Rows(block, hooks, opts) },
}
else
sections = {
+18 -1
View File
@@ -151,6 +151,19 @@ local TEXT_BUFFERS = {
[0xc602] = "wOTTrademonSpeciesName",
[0xc618] = "wOTTrademonSenderName",
}
-- ../pokecrystal/ram/wram.asm:1925,2333
local TEXT_BUFFERS_CRYSTAL = {
[0xd050] = "wMonOrItemNameBuffer",
[0xd073] = "wStringBuffer1",
[0xd086] = "wStringBuffer2",
[0xd099] = "wStringBuffer3",
[0xd0ac] = "wStringBuffer4",
[0xd0bf] = "wStringBuffer5",
[0xc6d1] = "wPlayerTrademonSpeciesName",
[0xc6e7] = "wPlayerTrademonSenderName",
[0xc703] = "wOTTrademonSpeciesName",
[0xc719] = "wOTTrademonSenderName",
}
-- The text commands that print nothing and carry no argument
-- (macros/scripts/text.asm, in TextCommands order): TX_LOW, TX_SCROLL,
-- TX_PAUSE, TX_WAIT_BUTTON, TX_DAY, and the six TX_SOUND_* jingles.
@@ -2295,6 +2308,8 @@ function RomExtractorGen2:extractTitle()
trailBobAmplitude = silver and 3 or 2,
trailPhaseStep = silver and 7 or 3,
trailPhase = silver and 0 or nil,
-- TitleScreenTimer (engine/menus/intro_menu.asm:951-966).
timeoutFrames = silver and (73 * 60 + 36) or (84 * 60 + 16),
}
self:write("title", data)
return data
@@ -2805,6 +2820,8 @@ end
-- is which alongside the text lets a caller fill them in order without
-- changing a marker every screen already reads.
function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
local textBuffers = (self.edition == "crystal") and TEXT_BUFFERS_CRYSTAL
or TEXT_BUFFERS
local out = {}
local i = 0
local hops = 0
@@ -2840,7 +2857,7 @@ function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
out[#out + 1] = "{STRBUF}"
if buffers then
local target = self.rom:word(bank, address + i + 1)
buffers[#buffers + 1] = TEXT_BUFFERS[target] or target
buffers[#buffers + 1] = textBuffers[target] or target
end
i = i + 2
elseif b == 0x4e or b == 0x4f then
+5
View File
@@ -310,6 +310,11 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
"Nothing happened!") }
end
b.stages[stat] = cur + 1
b.hazeStatReset = nil
if battle.ruleset and battle.ruleset.badgeBoostReapplyBug
and battle.kind ~= "link" then
require("src.battle.Damage").reapplyBadgeBoosts(b, stat)
end
return "consumed", { Strings("%s's\n%s rose!", b.name, Strings(STAT_LABEL[stat])) }
end
-- ItemUseDireHit/ItemUseGuardSpec always set the bit and consume
+1 -1
View File
@@ -817,7 +817,7 @@ function Renderer:frameRects()
-- GB pixel stops being a whole number of screen pixels, which is the trade
-- the setting exists to offer. Clamped on the horizontal too, so a narrow
-- window scales to fit instead of overflowing off both sides.
if self.uiFill then
if self.uiFill and not FaithfulRes.scaleCap() then
Up = math.min(ph / uih, pw / uiw)
end
if uiw * Up > pw or uih * Up > ph then
+24 -5
View File
@@ -529,6 +529,16 @@ function Commands.set_field(ctx, key, value)
ctx.save[key] = value
end
-- scripts/ChampionsRoom.asm:57
function Commands.set_option(ctx, key, value)
local o = ctx.save.options
if not o then
o = {}
ctx.save.options = o
end
o[key] = value
end
function Commands.load_player_starter_name(ctx)
local flags = ctx.save.flags or {}
local species = flags.EVENT_CHOSE_PIKACHU and "PIKACHU"
@@ -720,7 +730,8 @@ end
-- Box deposits also print SentToBoxText (give_pokemon.asm:36-37).
-- skipNickname suppresses AskName for callers that name the gift themselves;
-- no vanilla script uses it (pokeyellow scripts/OaksLab.asm, #1013)
function Commands.give_pokemon(ctx, species, level, skipNickname)
-- gotText prints GotMonText ahead of AskName (give_pokemon.asm:46).
function Commands.give_pokemon(ctx, species, level, skipNickname, gotText)
-- Native mods can transform a gift before the Pokémon object is created.
-- This is intentionally an event rather than a special-case starter hook:
-- mods can use the same seam for story gifts, fossils, or custom scripts.
@@ -754,6 +765,11 @@ function Commands.give_pokemon(ctx, species, level, skipNickname)
ctx.lastCheck = true
ctx.addedToParty = addedToParty
ctx.boxNum = boxNum
-- engine/events/give_pokemon.asm:46
if gotText and ctx.runner then
Commands.text_sound(ctx, "Get_Item1")
Commands.show_text(ctx, "_GotMonText", { RAM = species })
end
-- AskName: both AddPartyMon and SendNewMonToBox; skip mod-set nicks
-- and callback-style callers with no script runner to yield on.
if not gift.nickname and not skipNickname and ctx.runner then
@@ -1303,11 +1319,14 @@ function FadeOverlay:update()
end
function FadeOverlay:draw()
if self.color == "white" then
love.graphics.setColor(1, 1, 1, self.alpha)
else
love.graphics.setColor(0, 0, 0, self.alpha)
local shade = (self.color == "white") and 1 or 0
-- home/fade.asm:26
local r = self.game and self.game.renderer
if r then
r.screenVeil = { shade, self.alpha }
return
end
love.graphics.setColor(shade, shade, shade, self.alpha)
love.graphics.rectangle("fill", 0, 0, 160, 144)
love.graphics.setColor(1, 1, 1, 1)
end
+63 -1
View File
@@ -120,6 +120,67 @@ CallAsm.SITES = {
["04:66d1"] = "TryReceiveItem",
}
-- pokegold-symbols/pokesilver.sym: bank $03 sits two bytes earlier in Silver.
CallAsm.SITES_SILVER = {
["03:4749"] = "GetPartyNickname",
["03:4853"] = "CutDownTreeOrGrass",
["03:4b47"] = "CheckContinueWaterfall",
["03:4d13"] = "SetStrengthFlag",
["03:4d79"] = "TryStrengthOW",
["03:4e1e"] = "DisappearWhirlpool",
["03:4f7d"] = "HasRockSmash",
["03:5094"] = "PutTheRodAway",
["03:506b"] = "Fishing_CheckFacingUp",
["03:51c5"] = "AskCutScript_CheckMap",
}
-- pokecrystal-symbols/pokecrystal.sym: Crystal's own addresses for the same sites.
CallAsm.SITES_CRYSTAL = {
["25:6f76"] = "GiveItemScript_DummyFunction",
["04:65cd"] = "StartMenu",
["04:7327"] = "SelectMenu",
["05:6f5e"] = "OverworldHatchEgg",
["25:6706"] = "EnableWildEncounters",
["24:426f"] = "RingTwice_StartCall",
["24:42eb"] = "HangUp",
["04:53e5"] = "InitCallReceiveDelay",
["24:425c"] = "LoadBillScript",
["24:426a"] = "LoadElmScript",
["3f:5017"] = "MomTriesToBuySomething_ASMFunction",
["04:6599"] = "ItemfinderSound",
["14:46ef"] = "SweetScentEncounter",
["02:431e"] = "TrainerWalkToPlayer",
["14:4753"] = "CheckCanUseSquirtbottle",
["04:764f"] = "SetMemEvent",
["14:4658"] = "PlayPoisonSFX",
["14:467b"] = "CheckWhitedOut",
["04:64fa"] = "OverworldBGMap",
["04:650a"] = "BattleBGMap",
["04:6513"] = "HalveMoney",
["04:6527"] = "GetWhiteoutSpawn",
["03:4706"] = "GetPartyNickname",
["03:4810"] = "CutDownTreeOrGrass",
["23:47e1"] = "BlindingFlash",
["00:3016"] = "HideSprites",
["23:4aed"] = "FlyFromAnim",
["05:54f1"] = "SkipUpdateMapSprites",
["23:4b33"] = "FlyToAnim",
["05:4157"] = "LoadWalkingSpritesGFX",
["03:4b38"] = "CheckContinueWaterfall",
["03:4d12"] = "SetStrengthFlag",
["03:4d78"] = "TryStrengthOW",
["03:4e1d"] = "DisappearWhirlpool",
["23:480a"] = "ShakeHeadbuttTree",
["03:4f7c"] = "HasRockSmash",
["03:5095"] = "PutTheRodAway",
["03:506c"] = "Fishing_CheckFacingUp",
["2e:44b3"] = "LoadFishingGFX",
["03:51ba"] = "AskCutScript_CheckMap",
["2e:41ea"] = "TreeMonEncounter",
["2e:4219"] = "RockMonEncounter",
["04:62f8"] = "TryReceiveItem",
}
-- The three WRAM addresses `memcall` / `memcallasm` / `memjump` take instead of
-- a routine. Not sites: the pointer AT the address is written at run time
-- (LoadMemScript for the queued script, the phone engine for the other two),
@@ -714,7 +775,8 @@ end
-- at import time), and the address pair is the fallback that always works.
function CallAsm.nameFor(label, bank, addr)
if label and CallAsm.ALL[label] then return label end
return CallAsm.SITES[CallAsm.key(bank, addr)]
local key = CallAsm.key(bank, addr)
return CallAsm.SITES[key] or CallAsm.SITES_SILVER[key] or CallAsm.SITES_CRYSTAL[key]
end
-- Run a routine by name. Returns the byte the asm leaves in wScriptVar, or
+35 -14
View File
@@ -8,8 +8,13 @@ local Assets = require("src.render.Assets")
local Font = require("src.render.Font")
local PaletteFX = require("src.render.PaletteFX")
local Sprites = require("src.pokemon.Sprites")
local SpriteRenderer = require("src.render.SpriteRenderer")
local Strings = require("src.core.Strings")
-- engine/events/diploma.asm:65
local OBP0_90 = { { 255, 255, 255 }, { 255, 255, 255 },
{ 170, 170, 170 }, { 85, 85, 85 } }
local Diploma = {}
Diploma.__index = Diploma
Diploma.isOpaque = true
@@ -49,20 +54,18 @@ local function drawFrameBox(frame, tx, ty, tw, th)
local img = frame.img
local q = frame.quads
love.graphics.setColor(1, 1, 1, 1)
-- corners
love.graphics.draw(img, q[0], tx * 8, ty * 8)
love.graphics.draw(img, q[2], (tx + tw - 1) * 8, ty * 8)
-- engine/link/cable_club.asm:937
love.graphics.draw(img, q[2], tx * 8, ty * 8)
love.graphics.draw(img, q[4], (tx + tw - 1) * 8, ty * 8)
love.graphics.draw(img, q[6], tx * 8, (ty + th - 1) * 8)
love.graphics.draw(img, q[8], (tx + tw - 1) * 8, (ty + th - 1) * 8)
-- horizontal edges
love.graphics.draw(img, q[7], (tx + tw - 1) * 8, (ty + th - 1) * 8)
for x = 1, tw - 2 do
love.graphics.draw(img, q[1], (tx + x) * 8, ty * 8)
love.graphics.draw(img, q[7], (tx + x) * 8, (ty + th - 1) * 8)
love.graphics.draw(img, q[3], (tx + x) * 8, ty * 8)
love.graphics.draw(img, q[0], (tx + x) * 8, (ty + th - 1) * 8)
end
-- vertical edges
for y = 1, th - 2 do
love.graphics.draw(img, q[3], tx * 8, (ty + y) * 8)
love.graphics.draw(img, q[5], (tx + tw - 1) * 8, (ty + y) * 8)
love.graphics.draw(img, q[5], tx * 8, (ty + y) * 8)
love.graphics.draw(img, q[1], (tx + tw - 1) * 8, (ty + y) * 8)
end
end
@@ -91,13 +94,31 @@ function Diploma.render(game)
drawFrameBox(frame, 0, 0, 20, 18)
-- 2. Draw Player character sprite: farcall DrawPlayerCharacter
-- Shifted +33 px right from title screen base (82 + 33 = 115, y = 80)
local picPath, picTrueColor = Sprites.playerPath(
game.data, "front", { kind = "diploma" })
local pic = tryImage(picPath)
-- engine/movie/title.asm:321
local title = (game.data and game.data.field and game.data.field.title) or {}
local titlePlayer = title.player
if type(titlePlayer) == "table" then titlePlayer = titlePlayer.path end
local picPath, picTrueColor = Sprites.playerPic(
titlePlayer or "assets/generated/title/player.png",
{ side = "front", kind = "diploma", data = game.data })
local pic
if picPath then
if picTrueColor then
pic = tryImage(picPath)
else
local ok, faded = pcall(SpriteRenderer.obpImage, picPath, OBP0_90,
"diploma")
pic = (ok and faded) or tryImage(picPath)
end
end
if pic then
love.graphics.setColor(1, 1, 1, 1)
local sx, sy, sw, sh = love.graphics.getScissor()
-- engine/events/diploma.asm:44
love.graphics.setScissor(8, 8, 144, 128)
love.graphics.draw(pic, 115, 80)
if sx then love.graphics.setScissor(sx, sy, sw, sh)
else love.graphics.setScissor() end
if picTrueColor then
PaletteFX.markTrueColor(115, 80, pic:getDimensions())
end
+18 -1
View File
@@ -95,11 +95,28 @@ function EvolutionState.new(game, mon, newSpecies, onDone, via)
self.t = 0
self.done = false
self.canceled = false
Music.play(game.data, Music.special(game.data, "evolution"))
-- engine/movie/evolution.asm:41-46
Music.stop()
self.crySrc = require("src.core.Sound").playCry(game.data, mon.species)
self.cryT = 0
self.cryWait = self.crySrc ~= nil
if not self.cryWait then
Music.play(game.data, Music.special(game.data, "evolution"))
end
return self
end
function EvolutionState:update(dt)
if self.cryWait then
self.cryT = self.cryT + 1
local src = self.crySrc
local playing = src and src.isPlaying and src:isPlaying()
if self.cryT >= 3 and (not playing or self.cryT > 180) then
self.cryWait, self.crySrc = false, nil
Music.play(self.game.data, Music.special(self.game.data, "evolution"))
end
return
end
self.t = self.t + 1
if self.done then return end
local game = self.game
+7 -19
View File
@@ -157,6 +157,11 @@ end
-- new SHADER FX 2 row below it -- when both are set, ShaderFX.render() runs
-- main's chain into secondary's, same as stacking two RetroArch presets.
local function bgLocked(o)
return o.battleLayout == "wide" and o.battleFit == "fill"
and o.battleHud == "extended"
end
-- the vanilla rows as descriptors; each step body is the old per-index
-- ladder's, so the save.options mutations are unchanged
local function buildRows(game)
@@ -197,8 +202,6 @@ local function buildRows(game)
o.battleLayout = o.battleLayout == "wide" and "og" or "wide"
if o.battleLayout ~= "wide" then
o.battleHud = "standard"
elseif o.battleFit == "fill" and o.battleHud == "extended" then
o.battleBg = "white"
end
return true
end },
@@ -215,10 +218,6 @@ local function buildRows(game)
step = function(g)
local o = g.save.options
o.battleFit = o.battleFit == "fill" and "fixed" or "fill"
if o.battleFit == "fill" and o.battleLayout == "wide"
and o.battleHud == "extended" then
o.battleBg = "white"
end
return true
end },
{ id = "battleHud", label = Strings("BATTLE HUD"),
@@ -237,9 +236,6 @@ local function buildRows(game)
return false
end
o.battleHud = o.battleHud == "extended" and "standard" or "extended"
if o.battleHud == "extended" and o.battleFit == "fill" then
o.battleBg = "white"
end
return true
end },
-- What sits behind and around the battle. WHITE is the classic paper
@@ -249,11 +245,7 @@ local function buildRows(game)
{ id = "battleBg", label = Strings("BATTLE BG"),
value = function(g)
local o = g.save.options
if o.battleLayout == "wide" and o.battleFit == "fill"
and o.battleHud == "extended" then
o.battleBg = "white"
return Strings("AUTO")
end
if bgLocked(o) then return Strings("AUTO (FILL HUD)") end
local m = o.battleBg
if m == "black" then return Strings("BLACK") end
if m == "world" then return Strings("WORLD") end
@@ -261,11 +253,7 @@ local function buildRows(game)
end,
step = function(g, dir)
local o = g.save.options
if o.battleLayout == "wide" and o.battleFit == "fill"
and o.battleHud == "extended" then
o.battleBg = "white"
return false
end
if bgLocked(o) then return false end
local order = { "white", "black", "world" }
local cur = 1
for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end
+17 -15
View File
@@ -19,17 +19,22 @@ local function itemName(game, id)
return def and def.name or id
end
local function buildItems(game, store)
local function buildItems(game, store, order)
local items = {}
local ids = {}
for id in pairs(store) do table.insert(ids, id) end
table.sort(ids)
local ids = order
if not ids then
ids = {}
for id in pairs(store) do table.insert(ids, id) end
table.sort(ids)
end
for _, id in ipairs(ids) do
table.insert(items, {
value = id,
label = itemName(game, id),
right = "x" .. store[id],
})
if store[id] then
table.insert(items, {
value = id,
label = itemName(game, id),
right = "x" .. store[id],
})
end
end
return items
end
@@ -105,12 +110,9 @@ local function deposit(game)
local pc = game.save.pcItems
local inv = game.save.inventory
local Bag = require("src.inventory.Bag")
-- badges live in save.inventory alongside items but are not depositable
local depositable = {}
for id, count in pairs(inv) do
if not Bag.isBadge(id) then depositable[id] = count end
end
game.stack:push(ListMenu.new(game, "DEPOSIT ITEM", buildItems(game, depositable), {
-- engine/menus/players_pc.asm:99 wListPointer = wNumBagItems, so deposit order == bag order
local order = Bag.order(game.save, game.data)
game.stack:push(ListMenu.new(game, "DEPOSIT ITEM", buildItems(game, inv, order), {
kind = "pc_item_deposit",
messageBox = true,
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
+3 -1
View File
@@ -687,8 +687,10 @@ function Studio.deleteEntry(id)
local opts = SaveData.loadOptions()
local tc = type(opts.touchControls) == "table" and opts.touchControls or {}
if tc.skin == id then
tc.enabled, tc.skin = false, nil
tc.enabled, tc.skin = true, nil
opts.touchControls = tc
SaveData.saveOptions(opts)
TouchControls:applyOptions(opts)
end
Studio.refreshAvailable()
setStatus("Deleted " .. id)
+4 -9
View File
@@ -334,10 +334,9 @@ function TownMap:update(dt)
end
end
-- OG RED bakes the boot-ROM OBJ palette in, so the marker has to be replayed
-- over the screen-wide TOWNMAP zone pass the way every other OBJ is (#301)
-- OG RED and ADVANCED bake an OBJ palette in, so the marker replays over the TOWNMAP zone pass (#301)
function TownMap:markPlayerRedraw(x, y)
if not PaletteFX.usesSpriteObp() then return end
if not (PaletteFX.usesSpriteObp() or PaletteFX.usesGbcPack()) then return end
PaletteFX.markUiSpriteRedraw(self.playerSheet, self.playerQuad, x, y)
end
@@ -402,11 +401,7 @@ function TownMap:draw()
love.graphics.setColor(1, 1, 1, 1)
end
end
-- blinking cursor on the selected location. markerXY is the 8x8 cell's
-- top-left; the cursor asset is a 16x16 hollow frame centered on its own
-- (8,8), so draw it -4,-4 to enclose the cell (engine/menus/town_map.asm
-- draws the box cursor CENTERED on the selected location). Drawing it at
-- the cell top-left put the square in the frame's top-left quadrant (#152).
-- WriteTownMapSpriteOAM carry quirk: -4 X, -3 Y for cursor and player alike -- engine/items/town_map.asm:454
local showCursor = true
if GameVersion.generation() == 1 then
showCursor = self.blink < 25
@@ -416,7 +411,7 @@ function TownMap:draw()
if selected and showCursor then
local x, y = markerXY(selected)
if self.bg.cursor then
love.graphics.draw(self.bg.cursor, x - 4, y - 4)
love.graphics.draw(self.bg.cursor, x - 4, y - 3)
else
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("line", x + 0.5, y + 0.5, 7, 7)
+25 -4
View File
@@ -826,7 +826,10 @@ function BattleState:drawPic(mon, back)
if trainerBack then
-- PAL_BATTLE_OB_PLAYER: the player's own colours, which are row 0 of
-- TrainerPalettes (Chris shares Cal's).
colors = Palettes.trainerColors(self.palettes, "PLAYER") or colors
-- engine/gfx/color.asm:683-696
local row = Gen2Save.isFemale(self.save) and "FALKNER" or "PLAYER"
colors = Palettes.trainerColors(self.palettes, row)
or Palettes.trainerColors(self.palettes, "PLAYER") or colors
elseif enemyTrainer then
-- The opponent's class row out of the same TrainerPalettes table.
colors = Palettes.trainerColors(self.palettes, self.enemyTrainerClass)
@@ -1258,7 +1261,15 @@ function BattleState:afterAnimFor(side)
return "ANIM_PLAYER_DAMAGE"
end
function BattleState:animForMove(moveId, side, param)
-- engine/battle_anims/anim_commands.asm:1200 PlayHitSound
function BattleState:playHitSound(effectiveness)
if not effectiveness or effectiveness == 0 then return end
if effectiveness > 10 then self:playSfx("Sfx_SuperEffective")
elseif effectiveness < 10 then self:playSfx("Sfx_NotVeryEffective")
else self:playSfx("Sfx_Damage") end
end
function BattleState:animForMove(moveId, side, param, effectiveness)
local key = self.anims and self.anims.moves and self.anims.moves[moveId]
local started = self:startAnim(key, {
turn = self:turnFor(side), animId = moveId, isMove = true, param = param,
@@ -1267,7 +1278,8 @@ function BattleState:animForMove(moveId, side, param)
-- BattleAnimRunScript (anim_commands.asm:55-72): after the move script
-- restores HUDs it immediately runs wBattleAfterAnim (the hit shake).
-- Queue it so stepAnim chains without waiting on the next event.
self.pendingAfterAnim = { name = self:afterAnimFor(side), side = side }
self.pendingAfterAnim = { name = self:afterAnimFor(side), side = side,
effectiveness = effectiveness }
end
return started
end
@@ -1278,6 +1290,7 @@ function BattleState:startPendingAfterAnim()
if not pending then return false end
self.pendingAfterAnim = nil
if self:animForId(pending.name, pending.side) then
self:playHitSound(pending.effectiveness)
-- dealDamage's default ANIM_x_DAMAGE is this same shake; skip it there.
self.afterAnimPlayed = true
return true
@@ -1575,6 +1588,11 @@ function BattleState:advanceQueue()
self:showPages(text)
return
end
-- data/text/battle.asm:184
if event.kind == "money" then
self:showPages(event.text or "")
return
end
-- The shiny sparkle: hBattleTurn 1 and wBattleAnimParam 1 pick
-- BattleAnim_SendOutMon's `.Shiny` arm on the enemy (core.asm:8708-8715).
if event.kind == "shiny-flash" then
@@ -1692,12 +1710,14 @@ function BattleState:advanceQueue()
if event.kind == "move" and not event.missed then
self.afterAnimPlayed = nil
self.pendingAfterAnim = nil
if not self:animForMove(event.move, event.side, event.animParam) then
if not self:animForMove(event.move, event.side, event.animParam,
event.effectiveness) then
-- BATTLE SCENE off skips the move script but still runs wBattleAfterAnim
-- (anim_commands.asm:55-72 .disabled fallthrough).
local options = self.game and self.game.options
if options and options.battleScene == false then
if self:animForId(self:afterAnimFor(event.side), event.side) then
self:playHitSound(event.effectiveness)
self.afterAnimPlayed = true
end
end
@@ -1725,6 +1745,7 @@ function BattleState:advanceQueue()
and (hit == "ANIM_ENEMY_DAMAGE" or hit == "ANIM_PLAYER_DAMAGE") then
self.afterAnimPlayed = nil
else
self:playHitSound(event.effectiveness)
self:animForId(hit, from)
end
end
+22 -16
View File
@@ -41,6 +41,8 @@ ItemPcMenu.isOpaque = true
local PC_ITEM_CAPACITY = 50
local MAX_STACK = 99
local function stacksFor(n) return math.ceil((n or 0) / MAX_STACK) end
-- PlayersPCMenuData .PlayersPCMenuPointers strings, verbatim. .WhichPC picks
-- which rows a caller sees: PLAYERSPC_NORMAL ends on LOG OFF, PLAYERSPC_HOUSE
-- carries DECORATION and ends on TURN OFF.
@@ -167,18 +169,24 @@ function ItemPcMenu:rebuild()
for id, count in pairs(pc) do
if (count or 0) > 0 then
local def = self:def(id)
rows[#rows + 1] = {
id = id, count = count,
name = (def and def.name) or id,
index = def and def.index or math.huge,
}
local remaining = count
while remaining > 0 do
local n = math.min(remaining, MAX_STACK)
rows[#rows + 1] = {
id = id, count = n,
name = (def and def.name) or id,
index = def and def.index or math.huge,
}
remaining = remaining - n
end
end
end
-- wPCItems keeps acquisition order; without that recorded, item id order is
-- the stable choice, the same sort the PACK uses.
table.sort(rows, function(a, b)
if a.index ~= b.index then return a.index < b.index end
return a.id < b.id
if a.id ~= b.id then return a.id < b.id end
return a.count > b.count
end)
self.rows = rows
if self.listIndex > #rows + 1 then self.listIndex = #rows + 1 end
@@ -200,20 +208,18 @@ function ItemPcMenu:ensureVisible()
math.max(0, self:listTotal() - VISIBLE_ROWS)))
end
-- ReceiveItem over wPCItems: a new id needs one of the fifty stacks, a grown
-- one may not pass 99. False is the no-carry the deposit turns into
-- ReceiveItem over wPCItems: the add tops up every existing stack of that id
-- and spills the rest into a new one, so it only needs a free stack when the
-- room in place is short. False is the no-carry the deposit turns into
-- _PlayersPCNoRoomDepositText.
-- engine/items/items.asm:156 PutItemInPocket
function ItemPcMenu:pcAdd(id, qty)
local pc = self.save.pcItems
local held = pc[id] or 0
if held == 0 then
local stacks = 0
for _, count in pairs(pc) do
if (count or 0) > 0 then stacks = stacks + 1 end
end
if stacks >= PC_ITEM_CAPACITY then return false end
end
if held + qty > MAX_STACK then return false end
local used = 0
for _, count in pairs(pc) do used = used + stacksFor(count) end
local need = stacksFor(held + qty) - stacksFor(held)
if used + need > PC_ITEM_CAPACITY then return false end
pc[id] = held + qty
return true
end
+13 -6
View File
@@ -156,7 +156,9 @@ function TitleState.new(game, opts)
self.entranceHideBelow = entrance and tonumber(entrance.hideBelow) or nil
self.gemY = entrance and (tonumber(title.gemFromY) or -50) or self.gemRestY
self.entranceSfx = title.entranceSfx
-- engine/menus/intro_menu.asm:951-966
self.timeoutFrames = tonumber(title.timeoutFrames)
or ((self.trailMode == "silver") and (73 * 60 + 36) or (84 * 60 + 16))
self.onTimeout = opts.onTimeout
-- The copyright window line is pal 7 (engine/movie/title.asm:40-43); its
-- colour 0 backs the whole band.
@@ -295,12 +297,17 @@ function TitleState:update(_dt)
return
end
-- TitleScreenTimer / TitleScreenMain's run-down back into the attract
-- loop (engine/menus/intro_menu.asm:1125-1236).
if self.timeoutFrames and self.onTimeout
and self.frameCounter - (self.timeoutStart or 0) >= self.timeoutFrames then
self.onTimeout()
return
-- engine/menus/intro_menu.asm:1023-1059
if self.timeoutFrames and self.onTimeout then
if self.fadeStart then
if self.frameCounter - self.fadeStart >= 60 then self.onTimeout() end
return
end
if self.frameCounter - (self.timeoutStart or 0) >= self.timeoutFrames then
self.fadeStart = self.frameCounter
Music.fadeOut(8)
return
end
end
local input = self.game.input
+16 -4
View File
@@ -1232,6 +1232,10 @@ function OverworldState:update(dt)
self.player.inputLocked = false
end
end
if self.spinArrive and not self.player.spinFrames then
self.spinArrive = nil
self.player.inputLocked = false
end
-- EnterMapAnim's .done tail re-enables the companion once the swoop or the
-- spin-down has landed (player_animations.asm:40)
@@ -1311,7 +1315,7 @@ function OverworldState:update(dt)
local scripted = self.runner:isRunning() or #self.scriptMoves > 0
or (self.hopLand or 0) > 0
or self.engaging or self.emote or self.teleportOut
or self.flyAnim or self.flyArrive
or self.flyAnim or self.flyArrive or self.spinArrive
if not scripted and not self.transitioning then
self:checkTrainerSight()
-- CheckFightingMapTrainers (home/trainers.asm) zeroes hJoyHeld and
@@ -1321,7 +1325,7 @@ function OverworldState:update(dt)
scripted = self.runner:isRunning() or #self.scriptMoves > 0
or (self.hopLand or 0) > 0
or self.engaging or self.emote or self.teleportOut
or self.flyAnim or self.flyArrive
or self.flyAnim or self.flyArrive or self.spinArrive
end
-- a scriptMove's onDone can push a text box on the frame it retires, and
-- DisplayTextID owns the loop from there (home/text_script.asm:3)
@@ -4802,6 +4806,10 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
self.doorWarp = nil
local arriveWarp = self.arriveWarp
self.arriveWarp = nil
if self.spinArrive then
self.spinArrive = nil
self.player.inputLocked = false
end
-- PlayMapChangeSound (home/overworld.asm) plays before the tail-called
-- GBFadeOutToBlack, so the SFX starts with the fade (#961)
if doorWarp then
@@ -4849,6 +4857,9 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
self.player.spinFrames = 48
self.player.spinTotal = 48
self.player.spinDrop = true
-- engine/overworld/player_animations.asm:19
self.spinArrive = true
self.player.inputLocked = true
end
if doorWarp then
-- PlayerStepOutFromDoor (engine/overworld/auto_movement.asm): any
@@ -5329,8 +5340,9 @@ function OverworldState:drawWorld()
-- cry with no bubble still pauses the world for its beat)
if self.emote.bubble == false then return end
local npc = self.emote.npc
local ex = npc.px - cam.x + 4
local ey = npc.py - cam.y - 14
-- engine/overworld/emotion_bubbles.asm:41
local ex = npc.px - cam.x
local ey = npc.py - cam.y - 20
local bubble = Game.data.field.emotionBubbles
local drawn = false
if bubble and bubble.path then
+4 -4
View File
@@ -551,6 +551,7 @@ function World.new(game)
lastSfx = nil,
pokePic = nil,
pendingSceneScript = false,
startedOverworld = false,
-- GBC color state (engine/gfx/color.asm). `daytime` is the resolved
-- MORN/DAY/NITE/DARK the map is currently lit by; clockHour overrides
-- World:hour for drivers and tests, so the palette, the hour windows and
@@ -9045,10 +9046,9 @@ function World:setMap(mapId, cx, cy, facing, opts)
if not opts.seamless then
self.pendingSceneScript = true
end
-- StartMap (engine/overworld/events.asm): `farcall InitCallReceiveDelay` on
-- every map entry, connections included -- which is why a player who keeps
-- warping is never rung (src/core/gen2/Phone.lua's receive timer).
if self.game and self.game.save then
-- engine/overworld/events.asm:98
if not self.startedOverworld and self.game and self.game.save then
self.startedOverworld = true
require("src.core.gen2.Phone").onMapLoad(self.game.save,
self:stepContext().phone)
end
@@ -23,10 +23,10 @@ return function(game)
fitRow.step(game, 1)
assert(options.battleFit == "fill", "battle size switched to FILL")
assert(options.battleBg == "white", "FILL + EXTENDED normalized background to WHITE")
assert(bgRow.value(game) == "AUTO", "adaptive background is labeled AUTO")
assert(options.battleBg == "black", "FILL + EXTENDED preserves the stored background")
assert(bgRow.value(game) == "AUTO (FILL HUD)", "adaptive background is labeled AUTO")
assert(bgRow.step(game, 1) == false, "AUTO background row is locked")
assert(options.battleBg == "white", "locked AUTO retains the WHITE value")
assert(options.battleBg == "black", "locked AUTO does not overwrite the stored value")
menu.index = bgIndex
menu.scroll = math.max(0, bgIndex - 5)
@@ -37,11 +37,11 @@ return function(game)
fitRow.step(game, -1)
assert(options.battleFit == "fixed", "battle size switched back to FIXED")
assert(bgRow.value(game) == "WHITE", "FIXED exposes the stored WHITE choice")
assert(bgRow.step(game, 1) == true and options.battleBg == "black",
"FIXED can select BLACK")
assert(bgRow.value(game) == "BLACK", "FIXED exposes the stored BLACK choice")
assert(bgRow.step(game, 1) == true and options.battleBg == "world",
"FIXED can select WORLD")
assert(bgRow.step(game, 1) == true and options.battleBg == "white",
"FIXED can select WHITE")
U.wait(2)
local fixedPath = DIR .. "/fixed_extended_background_choices.png"
os.remove(fixedPath)
@@ -48,7 +48,7 @@ end
-- #1461: saves that passed through 0.1.82-0.1.9x already have the player's
-- own id written onto traded mons, so stamping correctly from now on does
-- not help them. The repair runs on every load, not behind a format gate.
-- not help them. The repair is a one-shot pre-format-5 migration.
do
local SaveData = require("src.core.SaveData")
local save = newSave()
+9 -9
View File
@@ -229,16 +229,16 @@ check(runScript("VERMILION_TRADE_HOUSE", "TEXT_VERMILIONTRADEHOUSE_LITTLE_GIRL")
"DUX post-trade script completes")
shownIs({ "_AfterTrade3Text" }, "DUX uses the happy dialogset")
-- === 9) Celadon Eevee: no confirm prompt, AskName then GotMonText,
-- ball hidden (GivePokemon -> AddPartyMon AskName; script still
-- prints GotMonText after the silent give_pokemon row) ===
-- === 9) Celadon Eevee: no confirm prompt, GotMonText then AskName,
-- ball hidden (GivePokemon -> SetPokedexOwnedFlag prints GotMonText
-- before AddPartyMon's AskName) ===
local EEVEE_MAP, EEVEE_BALL =
"CELADON_MANSION_ROOF_HOUSE", "CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL"
local EEVEE_TEXT = "TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL"
Game.save = SaveData.newGame()
check(runScript(EEVEE_MAP, EEVEE_TEXT), "Eevee ball script completes")
shownIs({ "_DoYouWantToNicknameText", "_GotMonText" },
"Eevee gives immediately (nickname ask, then GotMonText)")
shownIs({ "_GotMonText", "_DoYouWantToNicknameText" },
"Eevee gives immediately (GotMonText, then nickname ask)")
eq(#Game.save.party, 1, "Eevee joins the party")
eq(Game.save.party[1].species, "EEVEE", "gift species is EEVEE")
eq(Game.save.party[1].level, 25, "Eevee is level 25")
@@ -261,13 +261,13 @@ shownIs({}, "old save: silent")
eq(#Game.save.party, 0, "old save: no Eevee re-gift")
eq(toggleOf(EEVEE_MAP, EEVEE_BALL), false, "old save: leftover ball hidden")
-- === 11) GivePokemon party full, box has room: AskName + SentToBoxText
-- (SendNewMonToBox), then script GotMonText; ball hidden ===
-- === 11) GivePokemon party full, box has room: GotMonText, then AskName
-- + SentToBoxText (SendNewMonToBox); ball hidden ===
Game.save = SaveData.newGame()
for i = 1, 6 do Game.save.party[i] = Pokemon.new(Data, "PIDGEY", 5) end
check(runScript(EEVEE_MAP, EEVEE_TEXT), "full-party Eevee script completes")
shownIs({ "_DoYouWantToNicknameText", "_SentToBoxText", "_GotMonText" },
"full-party gift: nickname, sent-to-box, then GotMonText")
shownIs({ "_GotMonText", "_DoYouWantToNicknameText", "_SentToBoxText" },
"full-party gift: GotMonText, nickname, then sent-to-box")
eq(#Game.save.party, 6, "full-party gift leaves party size at 6")
local Boxes = require("src.pokemon.Boxes")
local boxed = false
+11 -1
View File
@@ -20,7 +20,8 @@ local yellow = GameVersion.isYellow()
-- pokered/scripts/*.asm, PlayCry line in brackets:
-- SSAnne1FRooms.asm:66 [:70], SSAnneB1FRooms.asm:82 [:86],
-- VermilionPidgeyHouse.asm:15 [:19], VermilionCity.asm:224 [:228],
-- PokemonFanClub.asm:71 [:76] and :84 [:89].
-- PokemonFanClub.asm:71 [:76] and :84 [:89],
-- MrFujisHouse.asm:56 [:60] and :63 [:67], LavenderCuboneHouse.asm:10 [:14].
local PETS = {
{ map = "SS_ANNE_1F_ROOMS", const = "TEXT_SSANNE1FROOMS_WIGGLYTUFF",
object = "SSANNE1FROOMS_WIGGLYTUFF", species = "WIGGLYTUFF",
@@ -40,6 +41,15 @@ local PETS = {
{ map = "POKEMON_FAN_CLUB", const = "TEXT_POKEMONFANCLUB_SEEL",
object = "POKEMONFANCLUB_SEEL", species = "SEEL",
label = "_PokemonFanClubSeelText" },
{ map = "MR_FUJIS_HOUSE", const = "TEXT_MRFUJISHOUSE_PSYDUCK",
object = "MRFUJISHOUSE_PSYDUCK", species = "PSYDUCK",
label = "_MrFujisHousePsyduckText" },
{ map = "MR_FUJIS_HOUSE", const = "TEXT_MRFUJISHOUSE_NIDORINO",
object = "MRFUJISHOUSE_NIDORINO", species = "NIDORINO",
label = "_MrFujisHouseNidorinoText" },
{ map = "LAVENDER_CUBONE_HOUSE", const = "TEXT_LAVENDERCUBONEHOUSE_CUBONE",
object = "LAVENDERCUBONEHOUSE_CUBONE", species = "CUBONE",
label = "_LavenderCuboneHouseCuboneText" },
}
if yellow then