CLOSES #1474, CLOSES #1475, CLOSES #1477, CLOSES #1482, CLOSES #1512, CLOSES #1516, CLOSES #1558, CLOSES #1569, CLOSES #1578, CLOSES #1579

This commit is contained in:
bryanthaboi
2026-08-20 16:04:12 -04:00
parent 83463a5a59
commit 0f8f6d0e4f
51 changed files with 2331 additions and 205 deletions
+87 -23
View File
@@ -1364,7 +1364,7 @@ function BattleState:updateQueue()
-- subanimation (or just the coarse fx when animations are off).
-- item.hit carries the target's blink + damage sound, applied when
-- the animation ends (hitRow rows carry a hit with no animation --
-- thrash/rage continuation turns that skip the announcement).
-- Mimic, whose animation waits on a successful copy).
if item.anim or item.hitRow then
-- PlayMoveAnimation writes wAnimationID, calls Delay3, and only then
-- jumps to MoveAnimation (core.asm:6635-6640), so three frames pass
@@ -2708,9 +2708,11 @@ function BattleState:resolveSwitch(newMon)
self.afterQueue = "menu"
self:act(function()
-- SwitchPlayerMon (core.asm:2419-2423): RetreatMon prints over the
-- outgoing pic and holds 50 frames before the mon is recalled
-- outgoing pic and holds 50 frames, then AnimateRetreatingPlayerMon
-- runs before the mon is recalled
self:sayNextAuto(self:withdrawText(self.player.name),
Timing.SWITCH_PLAYER_MON)
self:queueRetreatAnim()
self:actNext(function()
self:restoreMimicked(self.player) -- the battle copy leaves with it
local previous = self.player
@@ -3322,6 +3324,23 @@ function BattleState:queueSendOutAnim(append)
if append then self:act(fn) else self:actNext(fn) end
end
-- AnimateRetreatingPlayerMon (core.asm:1769-1796); the Yellow starter Pikachu
-- slides off instead (pokeyellow core.asm:1862-1866, animations.asm:1259)
function BattleState:queueRetreatAnim()
if self:starterPikachuSendOut() then
self:actNext(function() self:slidePic("playerMon", 0, -64, 8, 3) end)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { wait = 24 })
self:actNext(function() self:slidePic("playerMon") end)
else
self:actNext(function()
self.shrinkOut = { battler = self.player, frame = 0 }
end)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { wait = 7 })
end
end
-- Should the low-health alarm sound this frame? pokered keys it off
-- the drawn bar color: DrawPlayerHUDAndHPBar (core.asm:1846-1875) sets
-- wLowHealthAlarm bit 7 when GetHealthBarColor says the player bar is
@@ -3538,6 +3557,12 @@ function BattleState:updateFx()
self.growIn.frame = self.growIn.frame + 1
if self.growIn.frame >= 12 then self.growIn = nil end
end
-- the retreat shrink (AnimateRetreatingPlayerMon): 4+3 frames, then the
-- 7x7 area is cleared for good
if self.shrinkOut then
self.shrinkOut.frame = self.shrinkOut.frame + 1
if self.shrinkOut.frame >= 7 then self.shrinkOut = nil end
end
-- low-HP alarm (audio/low_health_alarm.asm): the two-tone siren
-- loops while the player's bar is red; see lowHealthAlarmActive
local Sound = require("src.core.Sound")
@@ -3802,6 +3827,8 @@ function BattleState:statusInterrupt(user, target, selectedId)
{ rng = self.rng, forceCrit = false, typeless = true,
screens = target })
self:sayNext(self:romText("_HurtItselfText", "It hurt itself in\nits confusion!"))
-- HandleSelfConfusionDamage (core.asm:3706-3714, enemy side :5807-5811)
self:animNext("POUND", not user.isPlayer)
self:clearVolatiles(user, true)
self:applyDamage(user, dmg)
if user.mon.hp <= 0 then self:onFaint(user) end
@@ -3894,18 +3921,26 @@ function BattleState:performMove(user, target, moveInst, isCalled)
end
self.moveAnimRow = nil
if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then
self:sayNextAuto(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
-- the move's animation plays right after the announcement; the
-- damage path attaches the target's hit blink to this row so the
-- blink follows the animation (pokered's order). Mimic is the
-- exception (announceAnim = false): PlayCurrentMoveAnimation runs
-- only after a successful copy, never on a miss -- applyMimic queues it
if not (record and record.announceAnim == false) then
self.nextInsert = (self.nextInsert or 0) + 1
self.moveAnimRow = { anim = move.id, attackerIsPlayer = user.isPlayer }
table.insert(self.queue, self.nextInsert, self.moveAnimRow)
local thrashing = user.thrashTurns and moveInst == user.thrashMove
and user.thrashAnnounced or false
if thrashing then
-- .ThrashingAboutCheck (core.asm:3531-3552)
self:sayNextAuto(self:romText("_ThrashingAboutText", "%s's\nthrashing about!",
displayName(user)))
user.thrashTurns = user.thrashTurns - 1
if user.thrashTurns <= 0 then
user.thrashTurns, user.thrashMove, user.thrashAnnounced = nil, nil, nil
if not user.confusedTurns then user.confusedTurns = self.rng(2, 5) end
end
else
self:sayNextAuto(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
end
-- PlayCurrentMoveAnimation follows the announcement; Mimic (announceAnim
-- = false) queues it from applyMimic after a successful copy
if not (record and record.announceAnim == false) then
self.nextInsert = (self.nextInsert or 0) + 1
self.moveAnimRow = { anim = move.id, attackerIsPlayer = user.isPlayer }
table.insert(self.queue, self.nextInsert, self.moveAnimRow)
end
Runtime.emit("battle.move_used", {
battle = self, user = user, target = target, move = move,
@@ -3913,6 +3948,9 @@ function BattleState:performMove(user, target, moveInst, isCalled)
})
local ctx = EffectRegistry.makeCtx(self, user, target, move, moveInst, isCalled)
-- .ThrashingAboutCheck jumps past JumpMoveEffect into PlayerCalcMoveDamage
-- (core.asm:3540), so SpecialEffectsCont never re-runs on a locked turn
ctx.thrashing = thrashing
-- Metronome / Mirror Move re-entry; a nil pick means the record
-- already said its failure text
@@ -4185,8 +4223,9 @@ function BattleState:awardExp()
end
local function applyShare(mon, split, announce)
local playerId = self.game.save.player and self.game.save.player.id
local traded = mon.traded == true
or (mon.otId ~= nil and playerId ~= nil and mon.otId ~= playerId)
-- GainExperience (engine/battle/experience.asm:69-88) compares the
-- stored MON_OTID against wPlayerID every award; no persistent flag
local traded = mon.otId ~= nil and playerId ~= nil and mon.otId ~= playerId
local levels, gained = Experience.apply(self.data, mon, self.enemy.def,
self.enemy.mon.level, self.kind == "trainer",
split, traded)
@@ -4338,21 +4377,35 @@ function BattleState:enemyMonFainted()
-- the battle queue's own \f handling (not TextBox.lua's) does not
-- page a sayChoice string the same way -- left as two calls.
self:say(Strings("%s is\nabout to use\v%s!", self.trainer.name, nextName))
-- EnemySendOutFirstMon .next9/.next8 (core.asm:1390-1409) and
-- HasMonFainted's NoWillText (core.asm:1473-1488)
self:sayChoice(
Strings("Will %s\nchange POKéMON?", self.game.save.player.name),
function(yes)
if not yes then return end
local game = self.game
Screens.push(game, "PartyMenu", {
local shiftOpts, reopenShift
reopenShift = function(text)
table.insert(self.queue, 1, { ui = function()
return self:buildScreen("PartyMenu", shiftOpts)
end })
table.insert(self.queue, 1, { text = text })
end
shiftOpts = {
battle = self,
party = self:playerPartyView(),
forceSwitch = true,
onSwitch = function(mon)
if mon ~= self.player.mon and mon.hp > 0 then
if mon == self.player.mon then
reopenShift(Strings("%s is\nalready out!", self.player.name))
elseif mon.hp <= 0 then
reopenShift(self:romText("_NoWillText", "There's no will\nto fight!"))
else
shiftSwitchMon = mon
end
end,
})
}
Screens.push(game, "PartyMenu", shiftOpts)
end, { box = Theme.trainerSwitchBox })
end
self:act(function()
@@ -4395,10 +4448,11 @@ function BattleState:enemyMonFainted()
local mon = shiftSwitchMon
if not mon then return end
-- SwitchPlayerMon (core.asm:2419-2423): RetreatMon, the 50-frame
-- hold, then the recall and the send-out
-- hold, AnimateRetreatingPlayerMon, then the recall and the send-out
self.nextInsert = 0
self:sayNextAuto(self:withdrawText(self.player.name),
Timing.SWITCH_PLAYER_MON)
self:queueRetreatAnim()
self:actNext(function()
local previous = self.player
self.player = makeBattler(self.data, mon, true, self.game.save)
@@ -5088,10 +5142,13 @@ function BattleState:openParty()
battle = self,
party = self:playerPartyView(),
onSwitch = function(mon)
-- PartyMenuOrRockOrRun's SWITCH .partyMonDeselected (core.asm:2396-2408)
if mon == self.player.mon then
self:say(Strings("%s is\nalready out!", self.player.name))
self:act(function() self:openParty() end)
elseif mon.hp <= 0 then
self:say(self:romText("_NoWillText", "There's no will\nto fight!"))
self:act(function() self:openParty() end)
else
self:resolveSwitch(mon)
end
@@ -5235,6 +5292,14 @@ function BattleState:growInScale(battler)
return f < 3 and 0 or f < 7 and 3 / 7 or 5 / 7
end
-- AnimateRetreatingPlayerMon's CopyDownscaledMonTiles stages
-- (core.asm:1769-1796)
function BattleState:shrinkOutScale(battler)
local shrink = self.shrinkOut
if not shrink or shrink.battler ~= battler then return nil end
return shrink.frame < 4 and 5 / 7 or 3 / 7
end
-- battler hidden this frame? (damage blink)
--
-- AnimationBlinkMon hides the pic, waits DelayFrames 5, shows it, waits
@@ -5871,11 +5936,10 @@ function BattleState:drawPicsLayer(slide, sx, sy, onlySide, skipMenuClip)
local s = BattleState.resolveBattleScale(self.data, "back",
imagePathOf(self.player.sprite),
self.player.mon and self.player.mon.species)
local gs = self:growInScale(self.player)
local gs = self:growInScale(self.player) or self:shrinkOutScale(self.player)
if gs then
-- the player-side AnimateSendingOutMon grow (after the poof,
-- core.asm:1757-1762): feet pinned at y=96, horizontal centre
-- pinned, mod scale composed with the grow stage
-- the player-side AnimateSendingOutMon grow (core.asm:1757-1762) and
-- the AnimateRetreatingPlayerMon shrink (core.asm:1769-1796)
local eff = s * gs
if eff > 0 then
love.graphics.draw(img,
+1 -1
View File
@@ -221,7 +221,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- replay PlayMoveAnimation per strike (pokered: GetPlayerAnimationType
-- / GetEnemyAnimationType loop on wNumAttacksLeft); hit 1 reuses the
-- announcement-time moveAnimRow, later hits queue fresh anim rows.
-- Thrash/rage continuations have no announcement anim -- a bare
-- Mimic queues no announcement anim (announceAnim = false) -- a bare
-- hitRow carries the blink instead.
-- PlayApplyingAttackSound (engine/battle/animations.asm, the routine after
-- PlayApplyingAttackAnimation) picks the sound off wDamageMultipliers -- 10
+7 -22
View File
@@ -582,30 +582,15 @@ MoveEffects.full = {
},
THRASH_PETAL_DANCE_EFFECT = {
-- ThrashPetalDanceEffect (effects.asm:791-808) runs before damage
-- (data/battle/special_effects.asm:22) and animates the setup turn
-- (data/battle/special_effects.asm:22, core.asm:3531-3552)
beforeAccuracy = function(ctx)
local user = ctx.user
if not user.thrashTurns then
ctx.battle:animBeforeMove(
user.isPlayer and "SHRINKING_SQUARE_ANIM" or "ANIM_B1", user.isPlayer)
end
end,
afterDamage = function(ctx)
local user = ctx.user
if not user.thrashTurns then
user.thrashTurns = ctx.rng(2, 3) -- 3-4 attacks total, then confusion
user.thrashMove = ctx.moveInst
user.thrashAnnounced = true
else
user.thrashTurns = user.thrashTurns - 1
if user.thrashTurns <= 0 then
user.thrashTurns, user.thrashMove, user.thrashAnnounced = nil, nil, nil
if not user.confusedTurns then
user.confusedTurns = ctx.rng(2, 5)
ctx.say(romText(ctx.battle.data, "_BecameConfusedText", "%s\nbecame confused!", displayName(user)))
end
end
end
if ctx.thrashing or user.thrashTurns then return end
user.thrashTurns = ctx.rng(2, 3) -- 3-4 attacks total, then confusion
user.thrashMove = ctx.moveInst
user.thrashAnnounced = true
ctx.battle:animBeforeMove(
user.isPlayer and "SHRINKING_SQUARE_ANIM" or "ANIM_B1", user.isPlayer)
end,
},
JUMP_KICK_EFFECT = {
+35 -13
View File
@@ -166,6 +166,9 @@ Battle.SUBSTATUS_ITEMS = {
-- be run from or Roared away.
Battle.BATTLETYPE_FORCESHINY = 7
Battle.BATTLETYPE_TRAP = 9
-- LostBattle's .canlose arm (engine/battle/core.asm:2766): the only battle
-- type whose loss still prints the trainer's own line instead of a whiteout.
Battle.BATTLETYPE_CANLOSE = 1
-- BadgeStatBoosts (engine/battle/core.asm:6534): each of these Johto badges
-- raises the PLAYER's in-battle stat by 1/8. The routine walks every other
@@ -889,10 +892,11 @@ Battle.PRIORITY = {
EFFECT_ENDURE = 3,
EFFECT_COUNTER = -1,
EFFECT_MIRROR_COAT = -1,
EFFECT_VITAL_THROW = -1,
}
function Battle:movePriority(moveId)
-- engine/battle/core.asm:786 GetMovePriority, `cp VITAL_THROW / ld a, 0`.
if moveId == "VITAL_THROW" then return -1 end
local def = self:moveDef(moveId)
return (def and Battle.PRIORITY[def.effect]) or 0
end
@@ -2403,10 +2407,10 @@ Battle.MOVE_EFFECTS.EFFECT_BATON_PASS = function(self, attacker)
self.enemy = party[target]
self.enemy.volatile = carried
end
self:emit({ kind = "send", side = side,
mon = side == "player" and self.player or self.enemy,
text = "Go! " .. self:monName(side == "player" and self.player
or self.enemy) .. "!" })
local sent = side == "player" and self.player or self.enemy
self:emit({ kind = "send", side = side, mon = sent,
hp = sent.hp or 0, status = sent.status or false,
text = "Go! " .. self:monName(sent) .. "!" })
end
-- BattleCommand_TrapTarget's .Traps table, one line per move: target first,
@@ -2665,6 +2669,7 @@ Battle.MOVE_EFFECTS.EFFECT_FORCE_SWITCH = function(self, attacker, defender,
self.stages.enemy = Battle.newStages()
end
self:emit({ kind = "send", side = self:sideOf(incoming), mon = incoming,
hp = incoming.hp or 0, status = incoming.status or false,
text = self:monName(incoming) .. " was dragged out!" })
self:breakTrapsOnSend(incoming)
self:spikesDamage(incoming)
@@ -3084,6 +3089,7 @@ function Battle:resolveFaints()
if self.trainer then
self:emit({ kind = "message",
text = (self.trainer.name or "TRAINER") .. " was defeated!" })
self:printWinLossText("win")
self:awardPrizeMoney()
end
-- CheckPayDay, on the win arm only (engine/battle/core.asm:7971-7976,
@@ -3110,6 +3116,7 @@ function Battle:resolveFaints()
-- can offer a shift on (engine/battle/core.asm:2241-2278).
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
replacement = true,
hp = self.enemy.hp or 0, status = self.enemy.status or false,
text = (self.trainer and self.trainer.name or "Foe") .. " sent out "
.. self:monName(self.enemy) .. "!" })
Runtime.emit("battle.battler_switched", {
@@ -3152,6 +3159,11 @@ function Battle:resolveFaints()
local nextIndex = Battle.firstHealthy(self.party)
if not nextIndex then
self:emit({ kind = "message", text = "You have no more POKéMON!" })
-- LostBattle (engine/battle/core.asm:2763-2782): only BATTLETYPE_CANLOSE
-- reaches PrintWinLossText on a loss; every other loss whites out.
if self.battleType == Battle.BATTLETYPE_CANLOSE then
self:printWinLossText("lose")
end
self:endBattle("lose")
return true
end
@@ -3177,14 +3189,22 @@ function Battle:resolveFaints()
return false
end
-- WinTrainerBattle's money arm, which runs after BattleText_EnemyWasDefeated
-- and the frontpic slide: the four quarters are dealt between the wallet and
-- Mom's savings and then one StdBattleTextbox names the figure.
--
-- The `ld a, [wDebugFlags] / bit DEBUG_BATTLE_F` skip in front of
-- PrintWinLossText is the trainer's own after-battle line, which this port
-- runs from the script on the way out of the battle rather than from here.
-- The payout is not gated on it either way.
-- WinTrainerBattle (engine/battle/core.asm:2310-2323), LostBattle's .canlose
-- arm (:2769-2782), PrintWinLossText (home/trainers.asm:230)
function Battle:printWinLossText(result)
local trainer = self.trainer
if not trainer then return end
-- The DEBUG_BATTLE_F skip sits in front of PrintWinLossText alone, behind
-- the slide (engine/battle/core.asm:2310, :2320-2323).
self:emit({ kind = "trainer-return" })
local text = (result == "lose") and trainer.lossText or trainer.winText
if type(text) ~= "string" or text == "" then return end
-- FarPrintText prints the pointer alone: no trainer-name tag in front of it,
-- unlike Gen 1's TrainerEndBattleText (pokered home/trainers.asm:341).
self:emit({ kind = "win-text", text = text })
end
-- WinTrainerBattle's money arm (engine/battle/core.asm:2310-2323)
function Battle:awardPrizeMoney()
local save = self.save
if not (save and save.player) then return nil end
@@ -3505,6 +3525,7 @@ function Battle:switch(index)
self.participants[index] = true
self.stages.player = Battle.newStages()
self:emit({ kind = "send", side = "player", mon = mon,
hp = mon.hp or 0, status = mon.status or false,
text = "Go! " .. self:monName(mon) .. "!" })
-- battle.battler_switched, the payload BattleState:resolveSwitch emits on
-- Gen 1: the side record, whoever walked in, and whoever walked out.
@@ -3969,6 +3990,7 @@ function Battle:enemyTrySwitchOrItem()
self:clearVolatile(self.enemy)
self.stages.enemy = Battle.newStages()
self:emit({ kind = "send", side = "enemy", mon = self.enemy,
hp = self.enemy.hp or 0, status = self.enemy.status or false,
text = (self.trainer.name or "TRAINER") .. " sent out "
.. self:monName(self.enemy) .. "!" })
Runtime.emit("battle.battler_switched", {
+26
View File
@@ -3439,6 +3439,18 @@ function RomExtractorGen2:extractScriptsAndText(maps, stdScripts)
elseif info.name == "givepoke" then
cmd.species, cmd.level, cmd.item, cmd.trainer =
args[1], args[2], args[3], args[4]
-- Script_givepoke (engine/overworld/scripting.asm:1806)
if size == 8 then
local function readAt(lo, hi)
local addr = (args[lo] or 0) + (args[hi] or 0) * 0x100
if not romAddrOk(bank, addr) then return nil end
local okStr, str = pcall(self.rom.readString, self.rom,
bank, addr, charmap, 0x50, 16)
return okStr and str or nil
end
cmd.name = readAt(5, 6)
cmd.otName = readAt(7, 8)
end
elseif info.name == "pokepic" or info.name == "disappear" then
cmd.species = args[1] -- pokepic
cmd.object = args[1] -- disappear (same byte)
@@ -5231,6 +5243,20 @@ function RomExtractorGen2:extractMenuGfx()
end
if eggHatch.egg or eggHatch.shell then out.eggHatch = eggHatch end
-- StatsScreenPageTilesGFX (gfx/font.asm:23), the 17 tiles
-- LoadStatsScreenPageTilesGFX lands at vTiles2 $31 (engine/gfx/load_font.asm:90).
local hpBarBorder = self.symbols["EnemyHPBarBorderGFX"]
if hpBarBorder then
local address = hpBarBorder[2] - 17 * 16
self:write2bpp(self.rom:bytes(hpBarBorder[1], address, 17 * 16),
17 * 8, 8, "menu/stats_tiles.png")
out.stats = {
sheet = "assets/generated/menu/stats_tiles.png",
tiles = 17,
firstTile = 0x31,
}
end
-- Goldenrod Game Corner: Slot Machine graphics assets
if self.symbols["Slots1LZ"] then
local raw1 = self:decompressLz3Symbol("Slots1LZ")
+6
View File
@@ -93,6 +93,12 @@ function ItemEffects.healsHP(id)
or id == "REVIVE" or id == "MAX_REVIVE"
end
-- .useRareCandy prints over the still-drawn party menu
-- (engine/items/item_effects.asm:1392-1418)
function ItemEffects.keepsPartyMenuOpen(id)
return ItemEffects.healsHP(id) or id == "RARE_CANDY"
end
function ItemEffects.isBattleMedicine(id)
return HEAL_AMOUNT[id] ~= nil or STATUS_HEAL[id] ~= nil
or id == "MAX_POTION" or id == "FULL_RESTORE"
+19 -11
View File
@@ -19,6 +19,9 @@ local romText = require("src.core.RomText")
local Evolution = {}
-- engine/pokemon/evos_moves.asm:122-123 (ld c, 50 / call DelayFrames)
local EVOLVING_TEXT_FRAMES = 50
Evolution.METHODS = {
LEVEL = {
check = function(game, mon, evo, trigger)
@@ -157,27 +160,32 @@ end
-- Play the evolution movie (flashing forms), then apply + text.
-- Headless (no real graphics) falls back to the plain text flow.
function Evolution.evolve(game, mon, newSpecies, onDone, via)
local oldName = mon.nickname or game.data.pokemon[mon.species].name
-- IsEvolvingText, DelayFrames 50, then ClearScreenArea before EvolveMon
-- (engine/pokemon/evos_moves.asm:120-134)
local isEvolving = romText(game.data, "_IsEvolvingText",
"What?\n%s is\nevolving!", oldName)
if love.image and love.image.newImageData then
-- forward `via` so EvolutionState can keep trade evolutions
-- non-cancelable (LINK_STATE_TRADING) while others accept B (#213)
Screens.push(game, "EvolutionState", mon, newSpecies, onDone, via)
game.stack:push(TextBox.new(game, isEvolving, function()
-- forward `via` so EvolutionState can keep trade evolutions
-- non-cancelable (LINK_STATE_TRADING) while others accept B (#213)
Screens.push(game, "EvolutionState", mon, newSpecies, onDone, via)
end, { auto = { delay = EVOLVING_TEXT_FRAMES } }))
return
end
Music.play(game.data, Music.special(game.data, "evolution"))
local oldName = mon.nickname or game.data.pokemon[mon.species].name
Evolution.apply(game, mon, newSpecies, via)
-- the congrats page keeps the engine wording: _EvolvedText extracts
-- truncated (it stops at a dynamic marker the decoder does not follow)
local msg = romText(game.data, "_IsEvolvingText",
"What?\n%s is\nevolving!", oldName)
.. "\f" .. Strings("Congratulations!\nYour %s\nevolved into\n%s!",
oldName, game.data.pokemon[newSpecies].name)
-- EvolvedText then IntoText in the same box (evos_moves.asm:136-150)
local msg = isEvolving .. "\f"
.. romText(game.data, "_EvolvedText", "%s evolved", oldName)
.. romText(game.data, "_IntoText", "\ninto %s!",
game.data.pokemon[newSpecies].name)
game.stack:push(TextBox.new(game, msg, function()
Music.restoreMap(game.data)
-- re-run the evolved species' level-up learn check before onDone
-- (evos_moves.asm EvolveMon -> learn_move.asm LearnMoveFromLevelUp, #12)
Evolution.learnEvolutionMoves(game, mon, onDone)
end))
end, TextBox.soundOpts(game, "Get_Item2")))
end
-- Entry point for mods whose methods fire outside the vanilla moments
+6 -2
View File
@@ -543,10 +543,14 @@ local function runCmd(self, cmd, op)
local species = cmd.species or arg1(cmd)
local level = cmd.level or (cmd.args and cmd.args[2]) or 5
local item = cmd.item or (cmd.args and cmd.args[3]) or 0
local trainer = cmd.trainer or (cmd.args and cmd.args[4]) or 0
if self.givePokeFn then
local mon = self.givePokeFn(species, level, item)
-- engine/pokemon/move_mon.asm:1695-1736: the trainer arm copies the
-- script's own nickname and OT name in instead of asking for one.
local named = trainer ~= 0
and { nickname = cmd.name, otName = cmd.otName } or nil
local mon = self.givePokeFn(species, level, item, named)
-- engine/pokemon/move_mon.asm:1753-1757
local trainer = cmd.trainer or (cmd.args and cmd.args[4]) or 0
if mon and trainer == 0 then
Specials.askNickname(self, mon)
end
+13 -6
View File
@@ -17,10 +17,13 @@ local function buildItems(game)
local items = {}
for _, id in ipairs(Bag.order(game.save)) do
local def = game.data.items[id]
-- PrintListMenuEntries skips the quantity for anything IsKeyItem_ owns:
-- the KeyItemFlags bitfield plus the HMs (item_effects.asm:2616-2641)
local unsellable = (def and def.keyItem) or id:find("^HM_") ~= nil
table.insert(items, {
value = id,
label = def and def.name or id,
right = "x" .. game.save.inventory[id],
right = (not unsellable) and ("x" .. game.save.inventory[id]) or nil,
})
end
return items
@@ -334,6 +337,9 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
local Evolution = require("src.pokemon.Evolution")
local evoTo, evo = Evolution.pendingFor(game, target,
{ kind = "levelup" })
-- the party menu stays up through TryEvolvingMon and only
-- comes down at RemoveUsedItem (item_effects.asm:1392-1418)
closePicker()
if evoTo then
Evolution.evolve(game, target, evoTo, nil, evo and evo.method)
end
@@ -399,10 +405,9 @@ local function pickTargetAndUse(game, battle, id, list)
local opts = {
pickOnly = true,
battle = battle,
-- HP medicine animates its bar with the picker still up (#252). Only
-- out of battle: the in-battle tail closes the bag list underneath
-- first, which needs the picker already gone.
keepOpen = (not battle) and ItemEffects.healsHP(id),
-- HP medicine animates with the picker up (#252), RARE CANDY prints over
-- the party menu (item_effects.asm:1392-1418)
keepOpen = (not battle) and ItemEffects.keepsPartyMenuOpen(id),
onSwitch = function(mon, picker)
if not wantsMove then
useOn(game, battle, id, mon, list, nil, picker)
@@ -470,7 +475,9 @@ function BagMenu.new(game, opts)
local list
list = ListMenu.new(game, "ITEMS", buildItems(game), {
kind = "bag",
footer = ("¥%d"):format(game.save.money),
-- StartMenu_Item zeroes wPrintItemPrices and draws no money box: the
-- LIST_MENU_BOX floats over the map (engine/menus/start_sub_menus.asm)
itemBox = true,
-- B returns to the start menu when the bag was opened from it
onCancel = opts.onCancel,
-- SELECT reorders items like the original bag (swap_items.asm)
+9 -16
View File
@@ -1,6 +1,7 @@
-- The evolution movie (engine/movie/evolution.asm): the mon's pic
-- flashes back and forth with the evolved form, speeding up, then the
-- new form appears with its cry and the congratulations text.
-- new form appears with its cry and the "evolved into" text
-- (engine/pokemon/evos_moves.asm:120-128).
-- pokered engine/movie/evolution.asm (Evolution_CheckForCancel) polls the
-- joypad during the flash: a fresh B press aborts the evolution -- the mon
-- keeps its species and _StoppedEvolvingText ("Huh? MON stopped evolving!")
@@ -9,9 +10,7 @@
-- and stone evolutions, where the B press is read but thrown away because
-- ItemUseEvoStone left wForceEvolution set (#290).
local Font = require("src.render.Font")
local Music = require("src.core.Music")
local Strings = require("src.core.Strings")
local romText = require("src.core.RomText")
local EvolutionState = {}
@@ -127,11 +126,11 @@ function EvolutionState:update(dt)
require("src.core.Sound").playCry(game.data, self.newSpecies)
local TextBox = require("src.render.TextBox")
local newName = game.data.pokemon[self.newSpecies].name
-- _EvolvedText extracts truncated (it stops at a dynamic marker the
-- decoder does not follow), so the engine's wording stands here
game.stack:push(TextBox.new(game,
Strings("Congratulations!\nYour %s\nevolved into\n%s!",
self.oldName, newName),
-- EvolvedText then IntoText in the same box (PrintText_NoCreatingTextBox),
-- then SFX_GET_ITEM_2 (engine/pokemon/evos_moves.asm:136-153)
local msg = romText(game.data, "_EvolvedText", "%s evolved", self.oldName)
.. romText(game.data, "_IntoText", "\ninto %s!", newName)
game.stack:push(TextBox.new(game, msg,
function()
Music.restoreMap(game.data)
game.stack:pop() -- the evolution screen itself
@@ -141,7 +140,8 @@ function EvolutionState:update(dt)
-- first so the "learned MOVE!" text / forget prompt push onto the
-- overworld / battle-return, not this state.
Evolution.learnEvolutionMoves(game, self.mon, self.onDone)
end))
end,
TextBox.soundOpts(game, "Get_Item2")))
end
end
@@ -172,13 +172,6 @@ function EvolutionState:draw()
require("src.render.PaletteFX").markTrueColor(x, y, sprite:getDimensions())
end
end
love.graphics.setColor(0, 0, 0, 1)
if not self.done then
Font.draw(Strings("What?"), 8, 104)
Font.draw(self.oldName .. " is", 8, 114)
Font.draw(Strings("evolving!"), 8, 124)
end
love.graphics.setColor(1, 1, 1, 1)
end
+55 -1
View File
@@ -17,6 +17,14 @@ function ListMenu:sgbPalettes(game)
end
local ROWS = 7
-- LIST_MENU_BOX 4,2 - 19,12 (data/text_boxes.asm:13); 4 names from
-- hlcoord 6,4 two rows apart (home/list_menu.asm:51-52, 364-365, 471-479)
local ITEM_BOX = { tx = 4, ty = 2, tw = 16, th = 11 }
local ITEM_ROWS = 4
local ITEM_NAME_X, ITEM_TOP_Y = 48, 32
local ITEM_CURSOR_X = 40
local ITEM_QTY_X, ITEM_QTY_END = 112, 136
local ITEM_MORE_X, ITEM_MORE_Y = 144, 88
-- frames to wait before key-repeat kicks in, then between repeats
local REPEAT_DELAY = 16
local REPEAT_RATE = 4
@@ -83,7 +91,12 @@ function ListMenu.new(game, title, items, opts)
-- for their whole run (engine/menus/pc.asm, engine/menus/players_pc.asm),
-- so their lists opt out of the A/B beep the same way Menu's noSound does
self.noSound = opts.noSound or false
self.rows = opts.rows or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
-- the bag's item list: a partial box the map stays visible around, not a
-- screen of its own (home/list_menu.asm:29-31)
self.itemBox = opts.itemBox or false
if self.itemBox then self.isOpaque = false end
self.rows = opts.rows or (self.itemBox and ITEM_ROWS)
or ((opts.dialogue or opts.messageBox) and 4 or ROWS)
return self
end
@@ -206,7 +219,48 @@ function ListMenu:close()
if top == self then self.game.stack:pop() end
end
-- PrintListMenuEntries, minus the price column StartMenu_Item never asks for
-- (wPrintItemPrices = 0, engine/menus/start_sub_menus.asm)
function ListMenu:drawItemBox()
love.graphics.setColor(1, 1, 1, 1)
Font.drawBox(ITEM_BOX.tx, ITEM_BOX.ty, ITEM_BOX.tw, ITEM_BOX.th)
love.graphics.setColor(0, 0, 0, 1)
if #self.items == 0 then
Font.draw(Strings("Nothing here."), ITEM_NAME_X, ITEM_TOP_Y)
end
local shown = 0
for row = 1, self.rows do
local i = self.scroll + row
local item = self.items[i]
if not item then break end
shown = shown + 1
local y = ITEM_TOP_Y + (row - 1) * 16
Font.draw(item.label, ITEM_NAME_X, y)
if item.right then
-- '×' at column 14, PrintNumber's two right-aligned digits after it
-- (home/list_menu.asm:479-490)
local count = item.right:sub(2)
Font.draw(item.right:sub(1, 1), ITEM_QTY_X, y + 8)
Font.draw(count, ITEM_QTY_END - Font.width(count), y + 8)
end
if i == self.index then
Font.drawCode(self.hollowIndex == i
and Theme.cursorHollow or Theme.cursor, ITEM_CURSOR_X, y)
end
if self.swapIndex == i and i ~= self.index then
Font.drawCode(Theme.cursorHollow, ITEM_CURSOR_X, y)
end
end
-- the terminator prints CANCEL and returns before the '▼'
-- (home/list_menu.asm:372, 518-524)
if shown == self.rows then
Font.drawCode(Theme.moreArrow, ITEM_MORE_X, ITEM_MORE_Y)
end
love.graphics.setColor(1, 1, 1, 1)
end
function ListMenu:draw()
if self.itemBox then return self:drawItemBox() end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
love.graphics.setColor(0, 0, 0, 1)
+15 -5
View File
@@ -34,6 +34,9 @@ function Menu.new(game, items, opts)
if self.tx + self.tw > 20 then self.tx = math.max(0, 20 - self.tw) end
end
self.rowStep = opts.rowStep or 2
-- engine/movie/oak_speech/oak_speech2.asm:162 (DisplayIntroNameTextBox)
self.title = opts.title
self.itemY = opts.itemY
-- maxVisible: cap the box to this many rows and scroll the rest instead
-- of growing past it (e.g. the start menu, whose row count varies with
-- save state and mod hooks); nil/unset keeps every caller's old
@@ -117,6 +120,10 @@ function Menu:draw()
end
Font.drawBox(self.tx, self.ty, self.tw, self.th)
love.graphics.setColor(0, 0, 0, 1)
-- PlaceString at hlcoord 3,0 writes over the border row it was just drawn on
if self.title then
Font.draw(self.title, (self.tx + 3) * 8, self.ty * 8)
end
local visible = (self.maxVisible and math.min(self.maxVisible, #self.items))
or #self.items
-- Row Y: pokered's boxed menus anchor the choices to the BOTTOM interior
@@ -130,15 +137,18 @@ function Menu:draw()
-- USE/TOSS is th = 5 for two choices (#284, matching text_boxes.asm's
-- USE_TOSS_MENU_TEMPLATE rows 10..14), and a top anchor pushed TOSS onto
-- the bottom border (#564, #572).
local function rowY(row)
if self.itemY then
return (self.ty + self.itemY + (row - 1) * self.rowStep) * 8
end
return (self.ty + self.th - 2 - (visible - row) * self.rowStep) * 8
end
for row = 1, visible do
local item = self.items[self.scroll + row]
if not item then break end
Font.draw(item.label, (self.tx + 2) * 8,
(self.ty + self.th - 2 - (visible - row) * self.rowStep) * 8)
Font.draw(item.label, (self.tx + 2) * 8, rowY(row))
end
local cursorRow = self.index - self.scroll
Font.drawCode(Theme.cursor, (self.tx + 1) * 8,
(self.ty + self.th - 2 - (visible - cursorRow) * self.rowStep) * 8)
Font.drawCode(Theme.cursor, (self.tx + 1) * 8, rowY(self.index - self.scroll))
-- moreArrow ($EE): the same "more below" glyph OptionRows/ManagerState
-- use, sat on the bottom border like TextBox's page-advance cursor. It
-- has to be the border row, not ty + th - 2: that is the last interior
+13 -3
View File
@@ -67,6 +67,7 @@ function NamingScreen.new(game, opts)
self.game = game
self.title = opts.title or Strings("YOUR NAME?")
self.presets = opts.presets
self.introBox = opts.introBox
self.maxLen = opts.maxLen or 7
self.default = opts.default
self.onDone = opts.onDone
@@ -99,9 +100,18 @@ function NamingScreen:enter()
end,
})
end
self.game.stack:push(Menu.new(self.game, items, {
tx = 4, ty = 0, tw = 12, th = #items * 2 + 2, cancelable = false,
}))
if self.introBox then
-- DisplayIntroNameTextBox (oak_speech2.asm:162): TextBoxBorder at
-- hlcoord 0,0 with b=$a c=$9, "NAME" at hlcoord 3,0, list at hlcoord 2,2
self.game.stack:push(Menu.new(self.game, items, {
tx = 0, ty = 0, tw = 11, th = #items * 2 + 4,
itemY = 2, title = Strings("NAME"), cancelable = false,
}))
else
self.game.stack:push(Menu.new(self.game, items, {
tx = 4, ty = 0, tw = 12, th = #items * 2 + 2, cancelable = false,
}))
end
end
end
+69 -16
View File
@@ -35,6 +35,23 @@ OakSpeech.letterboxWhite = true
local FADE_FRAMES = 24
local WIPE_FRAMES = 32
-- OakSpeechSlidePicRight / OakSpeechSlidePicLeft (oak_speech2.asm:67-89)
local SLIDE_TILES = 6
local SLIDE_FRAMES = 3
local PicSlide = {}
PicSlide.__index = PicSlide
function PicSlide:update(dt)
self.t = self.t + 1
local tiles = math.min(SLIDE_TILES, math.floor(self.t / SLIDE_FRAMES))
self.speech.picSlide = (self.dir > 0 and tiles or (SLIDE_TILES - tiles)) * 8
if tiles >= SLIDE_TILES then
self.game.stack:pop()
if self.onDone then self.onDone() end
end
end
-- naming presets are boot config (field.boot.namePresets), which a total
-- conversion replaces; the Red/Blue lists remain the fallback
local function namePresets(game, who, fallback)
@@ -155,6 +172,10 @@ function OakSpeech.defaultSteps(speech)
kind = "say",
textKey = "_IntroducePlayerText",
pic = "player",
-- oak_speech.asm:89-92: MovePicLeft, then IntroducePlayerText ends in
-- text_end, so PrintText returns with the box still up under the names
reveal = "wipe",
stay = true,
},
{
id = "name_player",
@@ -177,6 +198,10 @@ function OakSpeech.defaultSteps(speech)
kind = "say",
textKey = "_IntroduceRivalText",
pic = "rival",
-- oak_speech.asm:98-101: FadeInIntroPic, then IntroduceRivalText's
-- text_end leaves the box up for ChooseRivalName
reveal = "fade",
stay = true,
},
{
id = "name_rival",
@@ -378,7 +403,14 @@ function OakSpeech:runStep(step)
self:applyPic(step)
self:afterReveal(step, function()
self:runCry(step)
self:sayText(self:stepText(step), function() self:advance() end)
if step.stay then
local box = TextBox.new(self.game, self:stepText(step), nil,
{ stay = { onShown = function() self:advance() end } })
self.holdBox = box
self.game.stack:push(box)
else
self:sayText(self:stepText(step), function() self:advance() end)
end
end)
elseif kind == "demo" then
-- NIDORINO show-off: mirrored front sprite + wipe + cry + text 2A
@@ -394,20 +426,26 @@ function OakSpeech:runStep(step)
local presets = step.presets
or namePresets(self.game, step.presetsWho or who,
step.presetsFallback or { "RED" })
require("src.ui.Screens").push(self.game, "NamingScreen", {
title = step.title or (who == "rival" and "HIS NAME?" or Strings("YOUR NAME?")),
presets = presets,
maxLen = step.maxLen or self.nameLen,
onDone = function(name)
if who == "rival" then
self.game.save.player.rival = name
else
self.game.save.player.name = name
end
self:recordAnswer(step, 1, name, name)
self:advance()
end,
})
local function openNaming()
require("src.ui.Screens").push(self.game, "NamingScreen", {
title = step.title or (who == "rival" and "HIS NAME?" or Strings("YOUR NAME?")),
presets = presets,
introBox = true,
maxLen = step.maxLen or self.nameLen,
onDone = function(name)
if who == "rival" then
self.game.save.player.rival = name
else
self.game.save.player.name = name
end
self:recordAnswer(step, 1, name, name)
-- YourNameIsText/HisNameIsText print into the box this one held
self:closeHoldBox()
self:slidePic(-1, function() self:advance() end)
end,
})
end
self:slidePic(1, openNaming)
elseif kind == "choice" then
self:applyPic(step)
self:afterReveal(step, function()
@@ -518,6 +556,21 @@ function OakSpeech:revealPic(kind, next)
}
end
-- ..(engine/movie/oak_speech/oak_speech2.asm ln 67)
function OakSpeech:slidePic(dir, onDone)
self.picSlide = (dir > 0 and 0 or SLIDE_TILES * 8)
self.game.stack:push(setmetatable({
game = self.game, speech = self, dir = dir, t = 0, onDone = onDone,
}, PicSlide))
end
-- IntroducePlayerText's text_end box (oak_speech.asm:90) is ours to close
function OakSpeech:closeHoldBox()
local box = self.holdBox
self.holdBox = nil
if box and self.game.stack:top() == box then self.game.stack:pop() end
end
function OakSpeech:advance()
self.step = self.step + 1
-- picFlip belongs to the pic, not to the step: OakSpeechText2 prints 2A
@@ -615,7 +668,7 @@ function OakSpeech:draw()
-- it like the sprite buffer does ((8 - w) >> 1) tiles across,
-- bottom-aligned
local w, h = self.pic:getDimensions()
local x = 48 + math.floor((8 - w / 8) / 2) * 8
local x = 48 + math.floor((8 - w / 8) / 2) * 8 + (self.picSlide or 0)
local y = 32 + (7 - h / 8) * 8
local reveal = self.picReveal
local off = 0
+30 -7
View File
@@ -59,14 +59,37 @@ function StartMenu.new(game)
for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do
owned = owned + 1
end
local t = math.floor(game.save.playTime or 0)
local panel = Strings("PLAYER %s\nBADGES %d\nPOKéDEX %3d\nTIME %6d:%02d",
game.save.player.name or "RED", badges, owned,
math.floor(t / 3600), math.floor(t / 60) % 60)
-- PrintSaveScreenText draws its own border at hlcoord 4,0 (b=8, c=$e) and
-- leaves it up under the prompt -- engine/menus/main_menu.asm:381-405
local panel = {
update = function() end,
draw = function()
local t = math.floor(game.save.playTime or 0)
Font.drawBox(4, 0, 16, 10)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings("PLAYER"), 5 * 8, 2 * 8)
Font.draw(game.save.player.name or "RED", 12 * 8, 2 * 8)
Font.draw(Strings("BADGES"), 5 * 8, 4 * 8)
Font.draw(("%2d"):format(badges), 17 * 8, 4 * 8)
Font.draw(Strings("POKéDEX"), 5 * 8, 6 * 8)
Font.draw(("%3d"):format(owned), 16 * 8, 6 * 8)
Font.draw(Strings("TIME"), 5 * 8, 8 * 8)
Font.draw(("%3d:%02d"):format(math.floor(t / 3600),
math.floor(t / 60) % 60), 13 * 8, 8 * 8)
love.graphics.setColor(1, 1, 1, 1)
end,
}
local function closePanel()
if game.stack:top() == panel then game.stack:pop() end
end
game.stack:push(panel)
game.stack:push(TextBox.new(game,
panel .. Strings("\fWould you like to\nSAVE the game?"), nil, {
Strings("Would you like to\nSAVE the game?"), nil, {
-- SaveTheGame_YesOrNo pins its TWO_OPTION_MENU at hlcoord 0, 7 rather
-- than the shared right-hand one -- engine/menus/save.asm:186-192
choiceBox = { tx = 0, ty = 7, tw = 6, th = 5 },
choice = function(yes)
if not yes then return end
if not yes then closePanel() return end
-- SaveMenu .save (engine/menus/save.asm:164-181): "Now saving..."
-- is a bare PlaceString held by DelayFrames 120, then GameSavedText,
-- which ends in `done` and so never reaches TX_PROMPT_BUTTON.
@@ -78,7 +101,7 @@ function StartMenu.new(game)
game:writeSave()
game.stack:push(TextBox.new(game,
Strings("%s saved\nthe game!", game.save.player.name or "RED"),
nil, { auto = {
closePanel, { auto = {
sound = function()
return require("src.core.Sound").play(game.data, "Save")
end,
+30 -7
View File
@@ -511,9 +511,12 @@ function TitleState:openMenu()
table.insert(items, { label = Strings("NEW GAME"), onSelect = function()
if self.onNewGame then self.onNewGame() end
end })
table.insert(items, { label = Strings("OPTION"), onSelect = function()
require("src.ui.Screens").push(game, "OptionsMenu")
end })
-- DisplayOptionMenu returns to .mainMenuLoop, which redraws the box
-- (engine/menus/main_menu.asm ln 87-90)
table.insert(items, { label = Strings("OPTION"), keepOpen = true,
onSelect = function()
require("src.ui.Screens").push(game, "OptionsMenu")
end })
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
if self.onExit then
self.onExit()
@@ -530,6 +533,12 @@ function TitleState:openMenu()
end
local th = #items * 2 + 2
local menu = Menu.new(game, items, { tx = 0, ty = 0, tw = 13, th = th })
-- .mainMenuLoop's B branch jumps back to DisplayTitleScreen, which opens
-- with GBPalWhiteOut (engine/menus/main_menu.asm:69, title.asm:29)
menu.onCancel = function()
game.stack:push(require("src.render.Transition").whiteFlash(game, nil,
function() self.menuOpen = false end))
end
-- full-width title LOGO zones would recolor this box; see sgbPalettes.
-- Menu.new may have grown tw for longer (e.g. localized) labels, so the
-- recolor zone follows the box's real width instead of the vanilla 13.
@@ -537,6 +546,17 @@ function TitleState:openMenu()
game.stack:push(menu)
end
-- .finishedWaiting: GBPalWhiteOutWithDelay3 then ClearScreen before MainMenu,
-- which clears again itself (engine/movie/title.asm ln 243, main_menu.asm ln 26)
function TitleState:toMenu()
local game = self.game
game.stack:push(require("src.render.Transition").whiteFlash(game, nil,
function()
self.menuOpen = true
self:openMenu()
end))
end
-- ..(engine/movie/title.asm ln 271)
function TitleState:pickNewMon()
if #self.cycleSpecies < 2 then return end
@@ -602,7 +622,7 @@ function TitleState:update(dt)
if not Sound.playPikaCry(self.game.data, 11) then
Sound.playCry(self.game.data, "PIKACHU")
end
self:openMenu()
self:toMenu()
end
return
end
@@ -616,20 +636,23 @@ function TitleState:update(dt)
require("src.core.Sound").playCry(self.game.data,
self.yellowLayout and "PIKACHU"
or self.cycleSpecies[self.cycleIndex])
self:openMenu()
self:toMenu()
end
end
-- ..(engine/movie/title.asm ln 28)
function TitleState:draw()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
-- MainMenu's own ClearScreen wipes the logo, mon and sprites before the
-- CONTINUE / NEW GAME border is drawn (engine/menus/main_menu.asm ln 26)
if self.menuOpen then return end
local PaletteFX = require("src.render.PaletteFX")
local playerImage = self.player
if playerImage and PaletteFX.usesSpriteObp() then
playerImage = require("src.render.SpriteRenderer").obpImage(
self.playerPath, PaletteFX.ogObj())
end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
local scrollY = -(self.scy or 0)
-- ..(engine/movie/title.asm ln 28)
local preRibbon = not self.yellowLayout
+108 -14
View File
@@ -38,6 +38,9 @@ local Sound = require("src.core.Sound")
-- Only for playerPic: the player.sprite raiser both generations share.
local Sprites = require("src.pokemon.Sprites")
local Strings = require("src.core.Strings")
-- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text
-- carries into the battle box (PrintWinLossText, home/trainers.asm:230).
local TextBox = require("src.render.TextBox")
local Unown = require("src.core.gen2.Unown")
local BattleState = {}
@@ -87,6 +90,20 @@ local TRAINER_SLIDE_STEPS = 8
local TRAINER_SLIDE_FRAMES_PER_STEP = 2
local TRAINER_SLIDE_FRAMES = TRAINER_SLIDE_STEPS * TRAINER_SLIDE_FRAMES_PER_STEP
-- BattleWinSlideInEnemyTrainerFrontpic (engine/battle/core.asm:6279-6318) and
-- WinTrainerBattle's DelayFrames 40 (:2311)
local WIN_SLIDE_STEPS = 6
local WIN_SLIDE_FRAMES_PER_STEP = 4
local WIN_SLIDE_FRAMES = WIN_SLIDE_STEPS * WIN_SLIDE_FRAMES_PER_STEP
local WIN_SLIDE_REST_TILES = 2
local WIN_SLIDE_DELAY_FRAMES = 40
local function winSlideTiles(frames)
local step = math.min(WIN_SLIDE_STEPS,
math.floor(frames / WIN_SLIDE_FRAMES_PER_STEP) + 1)
return WIN_SLIDE_STEPS + WIN_SLIDE_REST_TILES - step
end
-- MonFaintedAnimation (engine/battle/core.asm), which PlayerMonFaintedAnimation
-- and EnemyMonFaintedAnimation both fall into with the fainted side's pic
-- corner: the pic's tilemap rows are copied DOWN one row per step and the row
@@ -141,6 +158,10 @@ local MENU_COL_SPACING = 6
-- the count PrintNum writes after it (two digits, leading zeros) at (13,16).
local CONTEST_MENU_BOX_X = 2
local CONTEST_MENU_COL_SPACING = 12
-- PrintMoveType prints the type table's own names; only these two differ from
-- the constant (data/types/names.asm).
local TYPE_NAMES = { PSYCHIC_TYPE = "PSYCHIC", CURSE_TYPE = "???" }
-- charmap.asm's quantity glyph, spelled the way MartMenu spells it.
local CONTEST_BALL_LABEL = "PARKBALL\xc3\x97"
@@ -508,6 +529,14 @@ function BattleState:pushAll(events)
for _, event in ipairs(events or {}) do self:push(event) end
end
-- LearnMove returns before HandleEnemyMonFaint's send-out/prize arms
-- (engine/battle/core.asm:1959-2010).
function BattleState:pushFront(events)
for i = #(events or {}), 1, -1 do
table.insert(self.queue, 1, events[i])
end
end
function BattleState:pic(mon, back)
local def = self.pokemon and mon and self.pokemon[mon.species]
local path = def and (back and def.spriteBack or def.spriteFront)
@@ -686,6 +715,8 @@ function BattleState:drawPic(mon, back)
-- One tile per two frames to the right, SlideBattlePicOut's own step.
if enemyTrainer and self.trainerSlide then
px = px + math.floor(self.trainerSlide / TRAINER_SLIDE_FRAMES_PER_STEP) * 8
elseif enemyTrainer and self.winSlide then
px = px + winSlideTiles(self.winSlide) * 8
end
-- The pic's own scale (battle_sprite_scales, then the species record, then
-- 1x) composed with whatever square BattleBGEffect_RunPicResizeScript has
@@ -1371,7 +1402,7 @@ function BattleState:advanceQueue()
and self.shownHp[event.side] ~= event.hp then
self.hpAnim = { side = event.side, to = event.hp }
elseif event.kind == "send" and event.side and event.mon and self.shownHp then
self.shownHp[event.side] = event.mon.hp or 0
self.shownHp[event.side] = event.hp or event.mon.hp or 0
if self.hpAnim and self.hpAnim.side == event.side then self.hpAnim = nil end
end
-- And the same lag for the status tag (home/battle.asm:150); a send snaps it
@@ -1379,8 +1410,9 @@ function BattleState:advanceQueue()
if self.shownStatus and event.side
and (event.kind == "status"
or (event.kind == "send" and event.mon)) then
self.shownStatus[event.side] =
(event.kind == "send" and event.mon.status or event.status) or false
local shown = event.status
if event.kind == "send" and shown == nil then shown = event.mon.status end
self.shownStatus[event.side] = shown or false
end
-- AnimateExpBar (engine/battle/core.asm:7191) is called from INSIDE
-- GiveExperiencePoints before the exp is committed (the call at :6888 sits
@@ -1435,6 +1467,24 @@ function BattleState:advanceQueue()
self.trainerSlide = 0
return
end
-- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it
-- (engine/battle/core.asm:2310-2312)
if event.kind == "trainer-return" then
if not self.enemyTrainerImage then return self:advanceQueue() end
self.showEnemyTrainer = true
self.picHidden.enemy = false
self.winSlide = 0
self.winSliding = true
return
end
-- PrintWinLossText (home/trainers.asm:230): one FarPrintText of the trainer
-- struct's own line, paged and held for A/B like any other map text.
if event.kind == "win-text" then
local text = event.text
if self.game then text = TextBox.substitute(self.game, text) end
self:showPages(text)
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
@@ -1917,6 +1967,17 @@ function BattleState:update(_dt)
return
end
-- BattleWinSlideInEnemyTrainerFrontpic plus WinTrainerBattle's DelayFrames
-- 40 (engine/battle/core.asm:6279-6318, :2310-2312)
if self.winSliding then
self.winSlide = self.winSlide + 1
if self.winSlide >= WIN_SLIDE_FRAMES + WIN_SLIDE_DELAY_FRAMES then
self.winSliding = nil
self:advanceQueue()
end
return
end
-- SlideBattlePicOut is a plain loop with DelayFrames in it, so it owns the
-- screen the same way (engine/battle/core.asm:2882).
if self.trainerSlide then
@@ -2004,6 +2065,8 @@ function BattleState:update(_dt)
self.phase = "stats-box"
return
end
-- PrintWinLossText's line pages like any map text (home/text.asm:403-448)
if self:nextPage() then return end
self:advanceQueue()
return
end
@@ -2292,7 +2355,7 @@ function BattleState:update(_dt)
learn.move, learn.moveName)
self.pendingLearn = nil
self.phase = "resolving"
self:pushAll(self.battle:takeEvents())
self:pushFront(self.battle:takeEvents())
self:advanceQueue()
end
return
@@ -2872,7 +2935,7 @@ function BattleState:finishDecline()
self.pendingLearn = nil
self.phase = "resolving"
if learn then self.battle:declineForget(learn.index, learn.moveName) end
self:pushAll(self.battle:takeEvents())
self:pushFront(self.battle:takeEvents())
self:advanceQueue()
end
@@ -3475,6 +3538,23 @@ function BattleState:printMessage()
end
end
-- MoveInfoBox (engine/battle/core.asm:5403-5478): "TYPE/" at (1,9), the type
-- at (2,10), cur/max PP at (5,11), or "Disabled!" at (1,10).
function BattleState:drawMoveInfoBox(move)
if not move then return end
local fighter = self.battle and self.battle.player
if fighter and self.battle:moveDisabled(fighter, move.id) then
Chrome.print("Disabled!", 1, 10)
return
end
local def = self.game and self.game.data and self.game.data.moves
and self.game.data.moves[move.id]
Chrome.print("TYPE/", 1, 9)
local moveType = def and def.type
Chrome.print(moveType and (TYPE_NAMES[moveType] or moveType) or "", 2, 10)
Chrome.print(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0), 5, 11)
end
function BattleState:drawPanel()
Chrome.clear()
-- A tutorial battle legitimately has no player mon, so only the enemy is
@@ -3494,7 +3574,15 @@ function BattleState:drawPanel()
-- Message box across the bottom, with the menu window over its right half --
-- the cart draws the prompt into the full-width box and then opens the menu
-- on top, so the tail of a long name is simply covered.
Chrome.box(0, 12, 20, 6)
-- MoveSelectionScreen type 0 is two boxes: the name-only list
-- (engine/battle/core.asm:5074-5084) and MoveInfoBox's (:5407-5410).
local moveMenu = self.phase == "moves"
if moveMenu then
Chrome.box(0, 8, 11, 5)
Chrome.box(4, 12, 16, 6)
else
Chrome.box(0, 12, 20, 6)
end
if self.phase == "menu" then
self:printMessage()
local boxX = self.contest and CONTEST_MENU_BOX_X or MENU_BOX_X
@@ -3520,24 +3608,30 @@ function BattleState:drawPanel()
moves = (mon and mon.moves) or moves
end
local cursorRow = forgetting and self.forgetIndex or self.moveIndex
-- w2DMenuCursorInitX 5 with the names at hlcoord 6 (core.asm:5086-5107).
local cursorCol = moveMenu and 5 or 1
local nameCol = moveMenu and 6 or 2
for i, move in ipairs(moves) do
local ty = 13 + (i - 1)
-- Cursor in the box's own gutter, not clipped against the border.
if i == cursorRow then Chrome.cursor(1, ty) end
if i == cursorRow then Chrome.cursor(cursorCol, ty) end
-- The held slot's marker. `.battle_player_moves` writes '▷' into the
-- row wSwappingMove names (engine/battle/core.asm:5157-5165) so a move
-- picked up for a swap is visible while the cursor moves off it. It
-- sits a column right of the cursor gutter, where the cart puts it
-- (hlcoord 5, 13 against the cursor's own column), and only while the
-- move list itself is up -- the forget picker has no swapping.
if not forgetting and self.moveSwapIndex == i then
Chrome.print("\u{25B7}", 0, ty)
-- hlcoord 5, 13 is the cursor's own gutter, so PlaceMenuCursor covers
-- the marker on the cursor's row.
if not forgetting and self.moveSwapIndex == i and i ~= cursorRow then
Chrome.print("\u{25B7}", cursorCol, ty)
end
local def = self.game and self.game.data and self.game.data.moves
and self.game.data.moves[move.id]
Chrome.print((def and def.name) or move.id, 2, ty)
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0), 19, ty)
Chrome.print((def and def.name) or move.id, nameCol, ty)
if not moveMenu then
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
19, ty)
end
end
if moveMenu then self:drawMoveInfoBox(moves[cursorRow]) end
else
-- Battle messages wrap inside the box rather than running off the frame.
self:printMessage()
+6 -1
View File
@@ -337,10 +337,15 @@ function PackMenu:useSelected()
return
end
local world = self.world
local result = world and world.useFieldItem and world:useFieldItem(row.id)
local result, extra = nil, nil
if world and world.useFieldItem then result, extra = world:useFieldItem(row.id) end
if result then
if result == "nowhere" then
self.message = OAK_THIS_ISNT_THE_TIME
elseif result == "coin_case" then
-- _CoinCaseCountText (data/text/common_3.asm:336): "Coins:" then the
-- count, text_decimal 4 digits with PRINTNUM_LEFTALIGN_F so no padding.
self.message = { "Coins:", tostring(extra or 0) }
elseif result == "repel_used" then
-- ItemUsedText (data/text/common_3.asm): "<PLAYER> used the\n<ITEM>."
-- World already wrote the counter and took the item out of the bag, so
+14 -3
View File
@@ -150,7 +150,11 @@ function PokedexMenu.new(game, opts)
self.pokemon = opts.pokemon or data.pokemon
self.palettes = opts.palettes or data.gen2Palettes
self.onClose = opts.onClose
-- InitPokedex: wLastDexMode -> wCurDexMode (engine/pokedex/pokedex.asm:97).
self.modeIndex = 1
for i, name in ipairs(MODES) do
if self.save and name == self.save.lastDexMode then self.modeIndex = i end
end
self.index = 1
self.scroll = 0
self.view = "list" -- list | entry | area | option | search | results | unown
@@ -317,6 +321,13 @@ function PokedexMenu:cursorVisible()
return ((self.entryBlink or 0) % 32) < 20
end
-- Pokedex: wCurDexMode -> wLastDexMode on the way out
-- (engine/pokedex/pokedex.asm:60), which lives in the saved game data.
function PokedexMenu:close()
if self.save then self.save.lastDexMode = MODES[self.modeIndex] end
if self.onClose then self.onClose() end
end
function PokedexMenu:update(_dt)
self.entryBlink = (self.entryBlink or 0) + 1
local input = self.game and self.game.input
@@ -328,8 +339,8 @@ function PokedexMenu:update(_dt)
if input:wasPressed("a") or input:wasPressed("b") then
if self.page == 1 then
self.page = 2
elseif self.onClose then
self.onClose()
else
self:close()
end
end
return
@@ -367,7 +378,7 @@ function PokedexMenu:update(_dt)
if self.view == "search" then return self:updateSearch(input) end
if self.view == "unown" then return self:updateUnown(input) end
if input:wasPressed("b") then
if self.onClose then self.onClose() end
self:close()
return
elseif input:wasPressed("select") then
-- Pokedex_UpdateMainScreen: SELECT opens the OPTION screen and START the
+3 -1
View File
@@ -2280,7 +2280,9 @@ function Pokegear:drawPanel()
self:drawClock()
end
-- Last: the arrow is an OBJ and composites over whatever the card drew.
self:drawModeArrow()
-- _FlyMap has no card strip and never animates it
-- (engine/pokegear/pokegear.asm:1999).
if not self.fly then self:drawModeArrow() end
G.setColor(1, 1, 1, 1)
end
+58 -8
View File
@@ -36,9 +36,8 @@
-- $3f the shiny ⁂ icon (stats_tiles tile 14)
-- $40 / $41 the left and right HP/exp bar end caps
--
-- The extractor does not carry that sheet yet, so `pageTile` draws those seven
-- shapes directly and takes the sheet the moment menu_gfx grows a `stats`
-- entry. Everything that IS a glyph goes through the font: ◀ ($71), ▶ ($ed),
-- The extractor writes that sheet as menu_gfx.stats, which `pageTile` draws.
-- Everything that IS a glyph goes through the font: ◀ ($71), ▶ ($ed),
-- № ($74), <ID> ($73), <LV> ($6e) and the row-7 rule's $62 (the empty HP/exp
-- bar cell, which is FontBattleExtra's -- hence Font.useBattleExtra(true)
-- around the whole screen, exactly as the party menu does).
@@ -95,6 +94,14 @@ local TILE_BAR_CAP_RIGHT = 0x41
-- made of (StatsScreen_PlaceHorizontalDivider).
local TILE_HORIZONTAL_DIVIDER = 0x62
-- gfx/stats/pages.pal, the three palettes _CGB_StatsScreenHPPals copies to
-- wBGPals1 slots 3-5 (engine/gfx/cgb_layouts.asm:199-212)
local PAGE_PALETTES = {
{ { 255, 255, 255 }, { 255, 156, 255 }, { 255, 123, 255 }, { 0, 0, 0 } },
{ { 255, 255, 255 }, { 173, 255, 115 }, { 140, 255, 0 }, { 0, 0, 0 } },
{ { 255, 255, 255 }, { 140, 255, 255 }, { 140, 255, 255 }, { 0, 0, 0 } },
}
-- PrintTempMonStats' .StatNames, and the wTempMon fields it prints beside
-- them. <NEXT> steps two rows, so the five labels are 2 rows apart and the
-- values start one row below the first label.
@@ -821,12 +828,37 @@ end
-- ----------------------------------------------------------------- drawing
-- A tile out of StatsScreenPageTilesGFX. The extractor does not carry that
-- sheet, so each of the seven shapes it needs is drawn here; the moment
-- menu_gfx grows a `stats` entry this can take the real tiles instead.
-- menu_gfx.stats, the 17 tiles LoadStatsScreenPageTilesGFX lands at vTiles2
-- tile $31 (engine/gfx/load_font.asm:90-95)
function SummaryMenu:statsTiles()
if self.statsSheet ~= nil then return self.statsSheet or nil end
local gfx = (self.menuGfx or {}).stats
local image = gfx and self:picImage(gfx.sheet)
if not image then
self.statsSheet = false
return nil
end
local w, h = image:getDimensions()
local quads = {}
for index = 0, (gfx.tiles or 17) - 1 do
quads[(gfx.firstTile or 0x31) + index] =
love.graphics.newQuad(index * 8, 0, 8, 8, w, h)
end
self.statsSheet = { image = image, quads = quads }
return self.statsSheet
end
-- A tile out of StatsScreenPageTilesGFX. The fallback arm draws each of the
-- seven shapes by hand for a cache built before menu_gfx.stats existed.
function SummaryMenu:pageTile(id, tx, ty)
local G = love.graphics
local px, py = tx * 8, ty * 8
local sheet = self:statsTiles()
if sheet and sheet.quads[id] then
G.setColor(1, 1, 1, 1)
G.draw(sheet.image, sheet.quads[id], px, py)
return
end
G.setColor(0, 0, 0, 1)
if id == TILE_VERTICAL_DIVIDER then
G.rectangle("fill", px + 3, py, 2, 8)
@@ -847,11 +879,29 @@ end
-- (17,5), all small ($36) first, then the one for this page redrawn large
-- ($3a). The routine writes the four tiles as [hli]/[hld], a row down, then
-- [hli]/[hl] -- which is why it is a 2x2 block and not a 2x1 strip.
function SummaryMenu:drawPageSquare(tx, ty, large)
function SummaryMenu:drawPageSquare(tx, ty, large, colors)
local G = love.graphics
local px, py = tx * 8, ty * 8
-- $3a..$3d for the page that is up, $36..$39 for the other two.
local first = large and TILE_SQUARE_LARGE or TILE_SQUARE_SMALL
local sheet = self:statsTiles()
if sheet and sheet.quads[first] then
-- [hli] / [hld], a row down, [hli] / [hl]: the four tiles in that order.
local function body()
G.setColor(1, 1, 1, 1)
G.draw(sheet.image, sheet.quads[first], px, py)
G.draw(sheet.image, sheet.quads[first + 1], px + 8, py)
G.draw(sheet.image, sheet.quads[first + 2], px, py + 8)
G.draw(sheet.image, sheet.quads[first + 3], px + 8, py + 8)
end
if colors and GbcPalette.available() then
GbcPalette.with(colors, body)
else
body()
end
G.setColor(1, 1, 1, 1)
return
end
local inset = first == TILE_SQUARE_LARGE and 2 or 5
local size = 16 - inset * 2
G.setColor(0, 0, 0, 1)
@@ -862,7 +912,7 @@ end
function SummaryMenu:drawPageIndicators()
local columns = { 13, 15, 17 }
for i, tx in ipairs(columns) do
self:drawPageSquare(tx, 5, i == self.page)
self:drawPageSquare(tx, 5, i == self.page, PAGE_PALETTES[i])
end
end
+30 -4
View File
@@ -2969,6 +2969,25 @@ function OverworldState:openPC(onDone)
})
end
-- PKMN LEAGUE sits between PROF.OAK's PC and LOG OFF once wNumHoFTeams
-- is nonzero (engine/pokemon/bills_pc.asm:5, :49)
if #(Game.save.hallOfFame or {}) > 0 then
table.insert(items, {
label = Strings("<PK><MN>LEAGUE"),
keepOpen = true,
onSelect = function()
-- pc.asm PKMNLeague plays SFX_ENTER_PC, then PKMNLeaguePC prints
-- AccessedHoFPCText (engine/menus/pc.asm:67, league_pc.asm:2)
require("src.core.Sound").play(Game.data, "Enter_PC")
Game.stack:push(TextBox.new(Game,
romText(Game.data, "_AccessedHoFPCText",
"Accessed POKéMON\nLEAGUE's site.\fAccessed the HALL\nOF FAME List."),
function() Screens.push(Game, "LeaguePC") end))
done()
end,
})
end
local hooked = Runtime.call("ui.pc.items", sameItems, Game, items)
if type(hooked) == "table" then
items = hooked
@@ -3366,7 +3385,8 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
-- checkVictoryRewards pushes the badge/prize box and starts the map's
-- onVictory script UNDER whatever runs next, so the player still sees
-- EndBattle (now inside the battle), then the reward, then AfterBattle
self:checkVictoryRewards(d.trainerClass, d.trainerParty)
self:checkVictoryRewards(d.trainerClass, d.trainerParty,
endBattleText ~= nil)
self:afterBattle(result, battle)
if onDone then onDone() end
else
@@ -3477,7 +3497,10 @@ end
-- SetEvent / SetEventRange do after the leader victory.
-- `hide` is { { mapId, objName }, ... } -- HideObject on those toggles
-- (e.g. Brock victory clears PEWTERCITY_YOUNGSTER / ROUTE22_RIVAL1).
function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
-- `shownOnBattleScreen`: `dialogue` already rode the battle screen as the
-- armed end-battle line (scripts/CeruleanGym.asm:113)
function OverworldState:checkVictoryRewards(trainerClass, partyIndex,
shownOnBattleScreen)
local victories = require("data.scripts.victories")
local reward = victories[trainerClass .. "#" .. tostring(partyIndex or 1)]
if not reward then return self:runVictoryHook() end
@@ -3510,7 +3533,9 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
end
local chain = rewardChain()
if reward.dialogue then
chain.add(reward.dialogue, reward.badgeSound)
if not shownOnBattleScreen then
chain.add(reward.dialogue, reward.badgeSound)
end
if reward.item then
chain.add(reward.tmPre)
if tmGiven then
@@ -4423,7 +4448,8 @@ function OverworldState:restoreBattleContinuation(battle, origin)
if result == "win" then
game.save.defeatedTrainers[origin.npcId] = true
if origin.event then game.save.flags[origin.event] = true end
self:checkVictoryRewards(battle.oppClass, battle.partyIndex)
self:checkVictoryRewards(battle.oppClass, battle.partyIndex,
battle.endBattleText ~= nil)
end
self:afterBattle(result, battle)
self.engaging = false
+37 -9
View File
@@ -391,15 +391,19 @@ end
-- Gen 2 moveset lives in `levelMoves` (EvosAttacks). So every scripted gift,
-- the STARTER included, arrived knowing nothing: FIGHT listed no moves and the
-- battle had no legal action left in it.
local function givePokeMon(data, speciesIndex, level, itemIndex)
local function givePokeMon(data, speciesIndex, level, itemIndex, opts)
local id = speciesByIndex(data.pokemon, speciesIndex)
if not id then return nil end
return Mon.new(data, id, level or 5, {
item = itemIndex and itemIndex ~= 0
and itemByIndex(data.items, itemIndex) or nil,
nickname = opts and opts.nickname or nil,
})
end
-- GivePoke's trainer arm (engine/pokemon/move_mon.asm:1698-1736)
local RANDY_OT_ID = 1001
local function loadGenerated(path)
-- Same NX gold/ fallback Game2 uses. World:load is what surfaces
-- "Gold cache incomplete" when maps.lua is invisible at the unprefixed path.
@@ -997,13 +1001,18 @@ function World:load()
setStringBuffer = function(value)
if self.game then self.game.stringBuffer = value end
end,
givePoke = function(speciesIndex, level, item)
givePoke = function(speciesIndex, level, item, opts)
local data = self.game and self.game.data
local save = self.game and self.game.save
if not (data and save) then return end
save.party = save.party or {}
local mon = givePokeMon(data, speciesIndex, level, item)
local mon = givePokeMon(data, speciesIndex, level, item, opts)
if mon then
if opts and opts.otName then
mon.ot = opts.otName
mon.otName = opts.otName
mon.otId = RANDY_OT_ID
end
-- GivePoke -> TryAddMonToParty -> AddPartyMon (move_mon.asm:44-56, :143-149).
Mon.stampOT(save, mon)
Party.add(save.party, mon)
@@ -1458,13 +1467,11 @@ function World:mapSceneOf(group, mapNum)
end
-- wTimeOfDay (constants/ram_constants.asm): MORN_F 0, DAY_F 1, NITE_F 2,
-- DARKNESS_F 3. Palettes.daytimeFor has already resolved the clock and the
-- map's own PALETTE_* override into one of four names, so this is a rename
-- rather than a second clock.
-- DARKNESS_F 3, off the RTC hour (engine/tilesets/timeofday_pals.asm:5-11)
local TIME_OF_DAY_ID = { MORN = 0, DAY = 1, NITE = 2, DARK = 3 }
function World:timeOfDayId()
return TIME_OF_DAY_ID[self.daytime or "DAY"] or 1
return TIME_OF_DAY_ID[self.tod or self.daytime or "DAY"] or 1
end
-- GetWeekday -> wCurDay, which the RTC counts SUNDAY 0 .. SATURDAY 6 -- the
@@ -4369,6 +4376,8 @@ function World:useFieldItem(itemId)
if itemId == "SACRED_ASH" then return self:useSacredAsh() end
if itemId == "ESCAPE_ROPE" then return self:useEscapeRope(itemId) end
if itemId == "SQUIRTBOTTLE" then return self:useSquirtbottle() end
-- CoinCaseEffect (engine/items/item_effects.asm:2243).
if itemId == "COIN_CASE" then return "coin_case", self:coins() end
if REPEL_STEPS[itemId] then return self:useRepel(itemId) end
if TROPHY_BOXES[itemId] then return self:openTrophyBox(itemId) end
if not World.isRod(itemId, items) then return nil end
@@ -5802,7 +5811,9 @@ function World:battleMusicContext(opts)
members = trainer and trainer.classId and members
and members[trainer.classId] or nil,
landmark = self.map and self.map.def and self.map.def.landmark,
daytime = self.daytime,
-- PlayBattleMusic reads wTimeOfDay (engine/battle/start_battle.asm:24),
-- not the map's pinned palette set.
daytime = self.tod,
}
end
@@ -5999,6 +6010,18 @@ function World:startBattle(opts, onDone)
return true
end
-- wWinTextPointer / wLossTextPointer (home/trainers.asm:120), overwritten by
-- `winlosstext` (engine/overworld/scripting.asm:651)
function World:trainerWinLossText()
local vm = self.vm
if not vm then return nil, nil end
local obj = vm.trainerObject or {}
local text = self.text or {}
local winKey = vm.winTextOverride or obj.winText
local lossKey = vm.lossTextOverride or obj.lossText
return winKey and text[winKey] or nil, lossKey and text[lossKey] or nil
end
-- `startbattle` from a script: a trainer record (class + member) or a
-- loadwildmon pair. The VM is parked on the yield until onDone fires, so the
-- rest of the trainer script (flag set, after-battle text) runs on return.
@@ -6057,6 +6080,9 @@ function World:startScriptedBattle(record, wild, onDone)
attributes = record.attributes,
items = record.items,
}
-- wWinTextPointer / wLossTextPointer, read by PrintWinLossText on the
-- battle screen (home/trainers.asm:230) (#1512)
opts.trainer.winText, opts.trainer.lossText = self:trainerWinLossText()
elseif wild and wild.species then
local id, def = speciesByIndex(data and data.pokemon, wild.species)
-- InitEnemyMon `.NotRoaming` / BATTLETYPE.FORCESHINY: the DV pair is
@@ -9194,8 +9220,10 @@ function World:stepContext()
local def = self.map and self.map.def
return {
data = self.game and self.game.data,
-- CheckTime reads wTimeOfDay (engine/events/checktime.asm:2), so the
-- caller windows follow the clock even inside a pinned-palette room.
phone = {
map = def, maps = self.maps, daytime = self.daytime,
map = def, maps = self.maps, daytime = self.tod,
clock = self.game and self.game.clock,
},
-- GetMapPhoneService: zero means the map HAS service, which maps.lua has