mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
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:
@@ -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,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"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user