Bug squashing and translation mods (#311)

* audio timing stuff

* bug fixes and translation additions

* translation stuff

* Update modkit.py

* better asset resolution
This commit is contained in:
bryanthaboi
2026-07-27 13:37:05 -04:00
committed by GitHub
parent 31365d8dfd
commit f0a88ea473
78 changed files with 3691 additions and 787 deletions
+151 -120
View File
@@ -26,6 +26,7 @@ local Status = require("src.battle.Status")
local TrainerAI = require("src.battle.TrainerAI")
local TurnOrder = require("src.battle.TurnOrder")
local TypeChart = require("src.battle.TypeChart")
local Strings = require("src.core.Strings")
local BattleState = {}
BattleState.__index = BattleState
@@ -208,13 +209,17 @@ local TRAINER_DVS = { attack = 9, defense = 8, speed = 8, special = 8, hp = 8 }
-- charge-turn texts by move id; the move record's chargeText field wins
-- (ChargeEffect's per-move text pointers)
-- Strings.source, not Strings: this table is built at require time, before
-- Strings.load has a catalog, so translating here would freeze the English.
-- The marker is a no-op that puts these lines in the catalog anyway; the
-- lookup happens where chargeText is formatted below.
local CHARGE_TEXT = {
FLY = "%s\nflew up high!",
DIG = "%s\ndug a hole!",
RAZOR_WIND = "%s\nmade a whirlwind!",
SOLARBEAM = "%s\ntook in sunlight!",
SKULL_BASH = "%s\nlowered its head!",
SKY_ATTACK = "%s\nis glowing!",
FLY = Strings.source("%s\nflew up high!"),
DIG = Strings.source("%s\ndug a hole!"),
RAZOR_WIND = Strings.source("%s\nmade a whirlwind!"),
SOLARBEAM = Strings.source("%s\ntook in sunlight!"),
SKULL_BASH = Strings.source("%s\nlowered its head!"),
SKY_ATTACK = Strings.source("%s\nis glowing!"),
}
-- pokered's <USER>/<TARGET> text macros (home/text.asm
@@ -416,9 +421,9 @@ function BattleState.newWild(game, species, level, opts)
self.enemy = makeBattler(game.data, Pokemon.new(game.data, species, level), false)
markSeen(game, species)
if opts and opts.hooked then
self.introText = ("The hooked\n%s\nattacked!"):format(self.enemy.name)
self.introText = Strings("The hooked\n%s\nattacked!", self.enemy.name)
else
self.introText = ("Wild %s\nappeared!"):format(self.enemy.name)
self.introText = Strings("Wild %s\nappeared!", self.enemy.name)
end
return self
end
@@ -539,7 +544,7 @@ function BattleState.newTrainer(game, oppClass, partyIndex)
-- wEnemyMonSpecies2 before the intro's SET_PAL_BATTLE
-- (engine/battle/core.asm:6682, engine/gfx/palettes.asm SetPal_Battle)
self.trainerPic = getImage(self.trainer.pic, namedPalette(game.data, "MEWMON"))
self.introText = ("%s wants\nto fight!"):format(self.trainer.name)
self.introText = Strings("%s wants\nto fight!", self.trainer.name)
return self
end
@@ -553,7 +558,7 @@ function BattleState:makeGhost()
-- (engine/battle/core.asm InitWildBattle .isGhost)
self.enemy.sprite = getImage("assets/generated/battle/front/ghost.png",
monPalette(self.data, self.enemy.mon.species))
self.introText = "The GHOST\nappeared!"
self.introText = Strings("The GHOST\nappeared!")
end
-- The old man's catch tutorial (BATTLE_TYPE_OLD_MAN,
@@ -947,10 +952,10 @@ function BattleState:sendOutText(name)
if e and e.hp > 0 and math.floor(e.stats.hp / 4) > 0 then
pct = math.floor(e.hp * 25 / math.floor(e.stats.hp / 4))
end
if pct >= 70 then return ("Go! %s!"):format(name) end
if pct >= 40 then return ("Do it! %s!"):format(name) end
if pct >= 10 then return ("Get'm! %s!"):format(name) end
return ("The enemy's weak!\nGet'm! %s!"):format(name)
if pct >= 70 then return Strings("Go! %s!", name) end
if pct >= 40 then return Strings("Do it! %s!", name) end
if pct >= 10 then return Strings("Get'm! %s!", name) end
return Strings("The enemy's weak!\nGet'm! %s!", name)
end
-- audio/play_battle_music.asm: gym leaders (wGymLeaderNo) get the
@@ -1020,37 +1025,54 @@ function BattleState:enter()
-- MonsterPalettes[0] = PAL_MEWMON (wBattleMonSpecies is still 0 when
-- the intro's SET_PAL_BATTLE runs -- SetPal_Battle,
-- engine/gfx/palettes.asm:28)
self.playerBackPic = getImage(self.demo
and "assets/generated/battle/oldmanb.png"
or "assets/generated/battle/redb.png",
namedPalette(self.data, "MEWMON"))
-- field.playerPics picks the pic (the catch tutorial's old man fights in
-- the player's place), then the player.sprite hook gets the last word so
-- a mod can vary it per save. It loads through getImage like every other
-- battle pic, so a replacement keeps the SGB recolor, the transition
-- fade, the ground-padding measurement and battle_sprite_scales.
local backPath, backTrueColor =
require("src.pokemon.Sprites").playerPath(self.data, "back",
{ kind = "battle", demo = self.demo, battle = self })
self.playerBackPic = getImage(backPath,
namedPalette(self.data, "MEWMON"), backTrueColor)
self.showPlayerBack = self.playerBackPic ~= nil
-- the enemy's cry as it appears (data/pokemon/cries.asm); PlayCry sits at
-- a different point in each battle kind, so queue it per branch
local function queueEnemyCry()
self:act(function()
require("src.core.Sound").playCry(self.data, self.enemy.mon.species)
end)
end
-- PrintBeginningBattleText (engine/battle/common_text.asm:10-19): a wild
-- battle calls PlayCry BEFORE PrintText WildMonAppearedText, so the cry
-- sounds with the "Wild X appeared!" box instead of waiting on the A
-- press that clears its `prompt` (#303). The Silph-Scope-less tower
-- ghost gets no cry at all (common_text.asm:43-48).
if self.kind ~= "trainer" and self.kind ~= "link" and not self.ghost then
queueEnemyCry()
end
self:say(self.introText)
if self.kind == "trainer" then
self:say(("%s sent\nout %s!"):format(self.trainer.name, self.enemy.name))
self:say(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
self:act(function()
-- EnemySendOutFirstMon (core.asm:1421-1434): after the text the
-- pic grows out of the ball (AnimateSendingOutMon), then the cry
self.showEnemyTrainer = false
self:startGrowIn(self.enemy)
end)
queueEnemyCry()
elseif self.kind == "link" then
-- Colosseum has no foe trainer pic, but the enemy mon still grows
-- out of the ball after "X sent out Y!" (not the wild "already there"
-- intro that LinkBattle previously inherited from newWild).
self.enemySendingOut = true
self:say(("%s sent\nout %s!"):format(self.opponentName or "FOE",
self:say(Strings("%s sent\nout %s!", self.opponentName or "FOE",
self.enemy.name))
self:act(function()
self.enemySendingOut = false
self:startGrowIn(self.enemy)
end)
end
if not self.ghost then
-- the enemy's cry plays as it appears (data/pokemon/cries.asm)
self:act(function()
require("src.core.Sound").playCry(self.data, self.enemy.mon.species)
end)
queueEnemyCry()
end
if not self.safari and not self.demo then
self:say(self:sendOutText(self.player.name))
@@ -1218,7 +1240,7 @@ function BattleState:update(dt)
if self.phase == "menu" and self.safari then
if self.safari.balls <= 0 then
self:say("PA: You're out of\nSAFARI BALLs!\nGame over!")
self:say(Strings("PA: You're out of\nSAFARI BALLs!\nGame over!"))
self.phase = "messages"
self.result = "run"
self.afterQueue = "finish"
@@ -1273,7 +1295,7 @@ function BattleState:update(dt)
if input:wasPressed("a") then
local choice = ({ "fight", "pkmn", "item", "run" })[self.menuIndex]
if choice == "fight" and self.ghost then
self:say(("%s is too\nscared to move!"):format(self.player.name))
self:say(Strings("%s is too\nscared to move!", self.player.name))
self.phase = "messages"
self.afterQueue = "menu"
self:act(function()
@@ -1290,7 +1312,7 @@ function BattleState:update(dt)
end
if not self:playerHasPP() then
-- _NoMovesLeftText, then Struggle engages
self:say(("%s has no\nmoves left!"):format(self.player.name))
self:say(Strings("%s has no\nmoves left!", self.player.name))
self:resolveTurn({ id = "STRUGGLE", pp = 1, struggle = true })
return
end
@@ -1332,11 +1354,11 @@ function BattleState:update(dt)
end
local mv = moves[self.moveIndex]
if self.player.disabledSlot == self.moveIndex then
self:say("The move is\ndisabled!")
self:say(Strings("The move is\ndisabled!"))
self.phase = "messages"
self.afterQueue = "menu"
elseif mv.pp <= 0 then
self:say("No PP left for\nthis move!")
self:say(Strings("No PP left for\nthis move!"))
self.phase = "messages"
self.afterQueue = "menu"
else
@@ -1380,7 +1402,7 @@ function BattleState:resolveMimic(user, target, move, moveInst)
table.insert(self.queue, self.nextInsert, { wait = 50 })
if target.invulnerable
or not self:accuracyRoll(move, user, target) then
self:sayNext("But, it failed!")
self:sayNext(Strings("But, it failed!"))
return
end
local slots = {}
@@ -1390,7 +1412,7 @@ function BattleState:resolveMimic(user, target, move, moveInst)
if #slots == 0 then
-- .getRandomMove rerolls empty slots forever; a moveless target
-- can't happen in practice, so just fail instead of hanging
self:sayNext("But, it failed!")
self:sayNext(Strings("But, it failed!"))
return
end
if user.isPlayer and self.kind ~= "link" then
@@ -1451,7 +1473,7 @@ function BattleState:applyMimic(user, target, moveInst, slot)
entry.mimic = true
self:animNext("MIMIC", user.isPlayer)
-- _MimicLearnedMoveText: "<USER> / learned / MOVE!"
self:sayNext(("%s\nlearned\n%s!"):format(displayName(user),
self:sayNext(Strings("%s\nlearned\n%s!", displayName(user),
self.data.moves[src.id].name))
end
@@ -1491,7 +1513,7 @@ function BattleState:openOldManBag()
self:ui(function()
local list
list = ListMenu.new(game, "ITEMS", {
{ value = "POKE_BALL", label = "POKé BALL", right = "x50" },
{ value = "POKE_BALL", label = Strings("POKé BALL"), right = "x50" },
}, {
script = function(l)
l.scriptTimer = (l.scriptTimer or 0) + 1
@@ -1521,7 +1543,7 @@ function BattleState:oldManThrow()
self.phase = "messages"
self.afterQueue = "finish"
self.result = "run" -- nothing is kept; wBattleResult only ends the demo
self:say("OLD MAN used\nPOKé BALL!")
self:say(Strings("OLD MAN used\nPOKé BALL!"))
self:act(function()
require("src.core.Sound").play(self.data, "Ball_Toss")
-- ItemUseBall's beat before the toss chain (like throwBall)
@@ -1531,7 +1553,7 @@ function BattleState:oldManThrow()
self:actNext(function()
require("src.core.Sound").play(self.data, "Caught_Mon")
end)
self:sayNext(("All right!\n%s was\ncaught!"):format(self.enemy.name))
self:sayNext(Strings("All right!\n%s was\ncaught!", self.enemy.name))
end)
end
@@ -2283,7 +2305,7 @@ function BattleState:executeAction(user, target, action)
-- ghost battles: the ghost never attacks; its whole turn is the
-- GetOutText (ExecuteEnemyMove -> PrintGhostText, core.asm:5462-5463)
if self.ghost and not user.isPlayer then
self:sayNext(self.data.text._GetOutText or "GHOST: Get out...\nGet out...")
self:sayNext(self.data.text._GetOutText or Strings("GHOST: Get out...\nGet out..."))
return
end
@@ -2320,8 +2342,8 @@ function BattleState:executeAction(user, target, action)
self.aiUses = self:aiUsesFor()
markSeen(self.game, self.enemy.mon.species)
-- _AIBattleWithdrawText: "X with-/drew Y!"
self:sayNext(("%s with-\ndrew %s!"):format(self.trainer.name, oldName))
self:sayNext(("%s sent\nout %s!"):format(self.trainer.name, self.enemy.name))
self:sayNext(Strings("%s with-\ndrew %s!", self.trainer.name, oldName))
self:sayNext(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
return
end
@@ -2334,7 +2356,7 @@ function BattleState:executeAction(user, target, action)
-- 3392): sleep/freeze/held/flinch keep the mon recharging next turn
if self:preRechargeChecks(user, target) then return end
user.mustRecharge = nil
self:sayNext(("%s\nmust recharge!"):format(displayName(user)))
self:sayNext(Strings("%s\nmust recharge!", displayName(user)))
return
end
if action.special == "bound" then
@@ -2379,8 +2401,8 @@ function BattleState:statusOnomatopoeia(user, kind)
anim = isPlayer and "CONF_PLAYER_ANIM" or "CONF_ANIM"
end
local text = kind == "sleep"
and (displayName(user) .. "\nis fast asleep!")
or (displayName(user) .. "\nis confused!")
and Strings("%s\nis fast asleep!", displayName(user))
or Strings("%s\nis confused!", displayName(user))
if kind == "sleep" and isPlayer then
self:animNext(anim, isPlayer)
self:sayNext(text)
@@ -2419,18 +2441,18 @@ function BattleState:preRechargeChecks(user, target)
user.sleepTurns = (user.sleepTurns or 1) - 1
if user.sleepTurns <= 0 then
mon.status = nil
self:sayNext(displayName(user) .. "\nwoke up!")
self:sayNext(Strings("%s\nwoke up!", displayName(user)))
else
self:statusOnomatopoeia(user, "sleep")
end
return true
end
if mon.status == "FRZ" then
self:sayNext(displayName(user) .. "\nis frozen solid!")
self:sayNext(Strings("%s\nis frozen solid!", displayName(user)))
return true
end
if target.trappingTurns then
self:sayNext(displayName(user) .. "\ncan't move!")
self:sayNext(Strings("%s\ncan't move!", displayName(user)))
return true
end
if user.flinched then
@@ -2438,7 +2460,7 @@ function BattleState:preRechargeChecks(user, target)
-- player recharges, so the flinch eats the recharge turn and the
-- flag survives (the Hyper Beam flinch glitch)
user.flinched = false
self:sayNext(displayName(user) .. "\nflinched!")
self:sayNext(Strings("%s\nflinched!", displayName(user)))
return true
end
return false
@@ -2459,7 +2481,7 @@ function BattleState:statusInterrupt(user, target)
{ id = "CONFUSED", power = 40, type = "NORMAL", accuracy = 100 },
{ rng = self.rng, forceCrit = false, typeless = true,
screens = target })
self:sayNext("It hurt itself in\nits confusion!")
self:sayNext(Strings("It hurt itself in\nits confusion!"))
self:clearVolatiles(user, true)
self:applyDamage(user, dmg)
if user.mon.hp <= 0 then self:onFaint(user) end
@@ -2547,7 +2569,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
self.moveAnimRow = nil
if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then
self:sayNext(("%s\nused %s!"):format(displayName(user), move.name))
self:sayNext(Strings("%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
@@ -2607,8 +2629,10 @@ function BattleState:performMove(user, target, moveInst, isCalled)
self:animNext(chargeAnim, user.isPlayer)
end
local chargeText = move.chargeText or CHARGE_TEXT[move.id]
or "%s\nis charging up!"
self:sayNext(chargeText:format(displayName(user)))
or Strings.source("%s\nis charging up!")
-- the template is a source string (a move record may supply its own),
-- so translate it here rather than where it was declared
self:sayNext(Strings(chargeText, displayName(user)))
return
end
@@ -2630,7 +2654,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
-- SleepEffect/PoisonEffect/... call PlayCurrentMoveAnimation only
-- after the effect lands; a miss skips it
self:cancelMoveAnim()
self:sayNext(("%s's\nattack missed!"):format(displayName(user)))
self:sayNext(Strings("%s's\nattack missed!", displayName(user)))
return
end
local msgs = record.run(ctx)
@@ -2648,7 +2672,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
if move.power == 0 and not (record and record.kind == "full") then
MoveEffects.warnUnknown(move.effect)
self:cancelMoveAnim()
self:sayNext("But, it failed!")
self:sayNext(Strings("But, it failed!"))
return
end
@@ -2657,7 +2681,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
end
function BattleState:continueTrapping(user, target)
self:sayNext(("%s's\nattack continues!"):format(displayName(user)))
self:sayNext(Strings("%s's\nattack continues!", displayName(user)))
-- .MultiturnMoveCheck (core.asm:3554-3566) prints AttackContinuesText
-- then jumps to GetPlayerAnimationType, so the trapping move's full
-- animation replays each locked turn (same damage, animation shown).
@@ -2680,15 +2704,15 @@ end
function BattleState:continueBide(user, target)
user.bideTurns = user.bideTurns - 1
if user.bideTurns > 0 then
self:sayNext(("%s\nis storing energy!"):format(displayName(user)))
self:sayNext(Strings("%s\nis storing energy!", displayName(user)))
return
end
self:sayNext(("%s\nunleashed energy!"):format(displayName(user)))
self:sayNext(Strings("%s\nunleashed energy!", displayName(user)))
local dmg = (user.bideDamage or 0) * 2
user.bideTurns, user.bideDamage = nil, nil
if dmg <= 0 then
self:cancelMoveAnim()
self:sayNext("But, it failed!")
self:sayNext(Strings("But, it failed!"))
return
end
self:applyDamage(target, dmg)
@@ -2707,9 +2731,9 @@ function BattleState:applyDamage(target, dmg)
target.substituteHP = target.substituteHP - dmg
if target.substituteHP <= 0 then
target.substituteHP = nil
self:sayNext(("%s's\nSUBSTITUTE broke!"):format(displayName(target)))
self:sayNext(Strings("%s's\nSUBSTITUTE broke!", displayName(target)))
else
self:sayNext(("The SUBSTITUTE\ntook damage for\n%s!"):format(displayName(target)))
self:sayNext(Strings("The SUBSTITUTE\ntook damage for\n%s!", displayName(target)))
end
return dmg
end
@@ -2721,7 +2745,7 @@ function BattleState:applyDamage(target, dmg)
end
if target.rageMove and dealt > 0 then
target.stages.attack = math.min(6, (target.stages.attack or 0) + 1)
self:sayNext(("%s's\nRAGE is building!"):format(displayName(target)))
self:sayNext(Strings("%s's\nRAGE is building!", displayName(target)))
end
return dealt
end
@@ -2760,7 +2784,7 @@ function BattleState:onFaint(battler)
self:actNext(function() self:playVictoryMusic() end)
end
-- _EnemyMonFaintedText "Enemy X fainted!" / _PlayerMonFaintedText
self:sayNext(("%s\nfainted!"):format(displayName(battler)))
self:sayNext(Strings("%s\nfainted!", displayName(battler)))
if battler.isPlayer then
self:act(function() self:playerMonFainted() end)
else
@@ -2809,19 +2833,19 @@ function BattleState:enemyMonFainted()
-- _BoostedText / _WithExpAllText end in the CONT code (\v, "...\011"
-- in data/generated/text.lua): the box waits for A/B + ▼ then scrolls
-- the amount line in, so it stays on-screen instead of at y=144 (#216).
local tail = "%d EXP. Points!"
local text = Strings.source("%s gained\n%d EXP. Points!")
if announce == "expAll" then
tail = "with EXP.ALL,\v" .. tail
text = Strings.source("%s gained\nwith EXP.ALL,\v%d EXP. Points!")
elseif mon.traded then
tail = "a boosted\v" .. tail
text = Strings.source("%s gained\na boosted\v%d EXP. Points!")
end
self:sayNext(("%s gained\n" .. tail):format(name, gained))
self:sayNext(Strings(text, name, gained))
end
-- per level: GrewLevelText -> the stats window (PrintStatsBox) ->
-- the move-learn checks (experience.asm:245-256)
local game = self.game
for _, lv in ipairs(levels) do
self:sayNext(("%s grew\nto level %d!"):format(name, lv))
self:sayNext(Strings("%s grew\nto level %d!", name, lv))
self:uiNext(function()
require("src.core.Sound").play(game.data, "Level_Up")
return StatBox.new(game, mon)
@@ -2894,10 +2918,10 @@ function BattleState:enemyMonFainted()
if style ~= "set" and partyCount > 1 and self.player.mon.hp > 0 then
-- _TrainerAboutToUseText: "X is" / "about to use" then cont nick,
-- then para "Will PLAYER" / "change POKéMON?" with YES/NO.
self:say(("%s is\nabout to use"):format(self.trainer.name))
self:say(("%s!"):format(nextName))
self:say(Strings("%s is\nabout to use", self.trainer.name))
self:say(Strings("%s!", nextName))
self:sayChoice(
("Will %s\nchange POKéMON?"):format(self.game.save.player.name),
Strings("Will %s\nchange POKéMON?", self.game.save.player.name),
function(yes)
if not yes then return end
local game = self.game
@@ -2930,7 +2954,7 @@ function BattleState:enemyMonFainted()
-- (AnimateSendingOutMon) with the cry; no POOF -- that animation
-- belongs to the player-side SendOutMon (core.asm:1757-1762)
self.enemySendingOut = true
self:sayNext(("%s sent\nout %s!"):format(self.trainer.name, self.enemy.name))
self:sayNext(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
self:actNext(function()
self.enemySendingOut = false
self:startGrowIn(self.enemy)
@@ -2974,9 +2998,9 @@ function BattleState:enemyMonFainted()
-- prize money
self:actNext(function() self:playVictoryMusic() end)
-- _TrainerDefeatedText: "<PLAYER> defeated\nTRAINER!"
self:sayNext(("%s defeated\n%s!"):format(self.game.save.player.name,
self:sayNext(Strings("%s defeated\n%s!", self.game.save.player.name,
self.trainer.name))
self:sayNext(("%s got ¥%d\nfor winning!"):format(self.game.save.player.name, prize))
self:sayNext(Strings("%s got ¥%d\nfor winning!", self.game.save.player.name, prize))
end
self.result = "win"
self.afterQueue = "finish"
@@ -2992,7 +3016,7 @@ function BattleState:learnMove(mon, moveId)
if #mon.moves < 4 then
table.insert(mon.moves, { id = moveId, pp = mdef.pp })
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
self:sayNext(("%s learned\n%s!"):format(mon.nickname or self.data.pokemon[mon.species].name,
self:sayNext(Strings("%s learned\n%s!", mon.nickname or self.data.pokemon[mon.species].name,
mdef.name))
return
end
@@ -3037,14 +3061,14 @@ function BattleState:playerMonFainted()
if self.oppClass == "OPP_RIVAL1" then
local TextBox = require("src.render.TextBox")
local raw = (self.data.text and self.data.text._Rival1WinText)
or "{RIVAL}: Yeah! Am\nI great or what?"
or Strings("{RIVAL}: Yeah! Am\nI great or what?")
self:sayNext(TextBox.substitute(self.game, raw))
end
-- Oak's Lab starter rival: Rival1WinText only (no blackout lines).
-- Any other wipe, including Route 22 RIVAL1, still blacks out.
if not BattleState.isOaksLabStarterRival(self) then
self:sayNext(("%s is out of\nuseable POKéMON!"):format(self.game.save.player.name))
self:sayNext(("%s blacked\nout!"):format(self.game.save.player.name))
self:sayNext(Strings("%s is out of\nuseable POKéMON!", self.game.save.player.name))
self:sayNext(Strings("%s blacked\nout!", self.game.save.player.name))
end
self.result = "lose"
self.afterQueue = "finish"
@@ -3057,7 +3081,7 @@ function BattleState:playerMonFainted()
-- battles go straight to the party menu (the menu-phase guard).
if self.kind ~= "wild" then return end
local game = self.game
self:say(self.data.text._UseNextMonText or "Use next POKéMON?")
self:say(self.data.text._UseNextMonText or Strings("Use next POKéMON?"))
self:ui(function()
local ChoiceBox = require("src.ui.ChoiceBox")
return ChoiceBox.new(game, function(yes)
@@ -3065,11 +3089,11 @@ function BattleState:playerMonFainted()
local pSpd = (game.save.party[1].stats or { speed = 0 }).speed or 0
if self:runRoll(pSpd, TurnOrder.effectiveSpeed(self.enemy)) then
require("src.core.Sound").play(self.data, "Run")
self:say("Got away safely!")
self:say(Strings("Got away safely!"))
self.result = "run"
self.afterQueue = "finish"
else
self:say("Can't escape!")
self:say(Strings("Can't escape!"))
end
end)
end)
@@ -3089,7 +3113,7 @@ function BattleState:openReplacementMenu()
forceSwitch = true,
onSwitch = function(mon)
if mon.hp <= 0 then
self:say("There's no will\nto fight!")
self:say(Strings("There's no will\nto fight!"))
return -- the menu-phase guard reopens the menu
end
self:restoreMimicked(self.player)
@@ -3133,7 +3157,7 @@ function BattleState:safariAction(choice)
if choice == "run" then
require("src.core.Sound").play(self.data, "Run")
self:say("Got away safely!")
self:say(Strings("Got away safely!"))
self.result = "run"
self.afterQueue = "finish"
return
@@ -3141,7 +3165,7 @@ function BattleState:safariAction(choice)
if choice == "ball" then
st.balls = st.balls - 1
self:say(("%s used\nSAFARI BALL!"):format(playerName))
self:say(Strings("%s used\nSAFARI BALL!", playerName))
self:act(function()
require("src.core.Sound").play(self.data, "Ball_Toss")
self.lastBall = "SAFARI_BALL"
@@ -3158,7 +3182,7 @@ function BattleState:safariAction(choice)
self:actNext(function()
require("src.core.Sound").play(self.data, "Caught_Mon")
end)
self:sayNext(("All right!\n%s was\ncaught!"):format(self.enemy.name))
self:sayNext(Strings("All right!\n%s was\ncaught!", self.enemy.name))
-- same ItemUseBall .captured flow as a regular ball
self:act(function() self:storeCaughtMon() end)
else
@@ -3170,12 +3194,12 @@ function BattleState:safariAction(choice)
end
if choice == "bait" then
self:say(("%s threw some\nBAIT."):format(playerName))
self:say(Strings("%s threw some\nBAIT.", playerName))
self.safariCatchRate = math.floor(self.safariCatchRate / 2)
self.baitFactor = math.min(255, self.baitFactor + self.rng(1, 5))
self.escapeFactor = 0
else -- rock
self:say(("%s threw a\nROCK."):format(playerName))
self:say(Strings("%s threw a\nROCK.", playerName))
self.safariCatchRate = math.min(255, self.safariCatchRate * 2)
self.escapeFactor = math.min(255, self.escapeFactor + self.rng(1, 5))
self.baitFactor = 0
@@ -3191,13 +3215,13 @@ end
function BattleState:safariEnemyTurn()
if self.baitFactor > 0 then
self.baitFactor = self.baitFactor - 1
self:sayNext(("Wild %s\nis eating!"):format(self.enemy.name))
self:sayNext(Strings("Wild %s\nis eating!", self.enemy.name))
elseif self.escapeFactor > 0 then
self.escapeFactor = self.escapeFactor - 1
if self.escapeFactor == 0 then
self.safariCatchRate = self.enemy.def.catchRate
end
self:sayNext(("Wild %s\nis angry!"):format(self.enemy.name))
self:sayNext(Strings("Wild %s\nis angry!", self.enemy.name))
end
self:act(function()
local speed = self.enemy.curStats.speed % 256
@@ -3213,7 +3237,7 @@ function BattleState:safariEnemyTurn()
fled = self.rng(0, 255) < b
end
if fled then
self:sayNext(("Wild %s\nran!"):format(self.enemy.name))
self:sayNext(Strings("Wild %s\nran!", self.enemy.name))
self:actNext(function()
require("src.core.Sound").play(self.data, "Run")
startPicKind(self:picFxFor(self.enemy), "slideOff")
@@ -3266,7 +3290,12 @@ function BattleState:tryRun()
self.phase = "messages"
self.afterQueue = "menu"
if self.kind == "trainer" then
self:say("No! There's no\nrunning from a\ntrainer battle!")
-- _NoRunningText is three lines in a two-line box, so the third arrives
-- on a \v scroll (ContText: ▼ then a button press) rather than a \n.
-- Spelling it with three \n dropped "trainer battle!" and handed the
-- menu straight back (#239).
self:say(self.data.text._NoRunningText
or Strings("No! There's no\nrunning from a\vtrainer battle!"))
return
end
-- modified in-battle speeds (stat stages + paralysis), like the
@@ -3275,11 +3304,11 @@ function BattleState:tryRun()
TurnOrder.effectiveSpeed(self.enemy))
if escaped then
require("src.core.Sound").play(self.data, "Run")
self:say("Got away safely!")
self:say(Strings("Got away safely!"))
self.result = "run"
self.afterQueue = "finish"
else
self:say("Can't escape!")
self:say(Strings("Can't escape!"))
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
@@ -3312,13 +3341,13 @@ end
function BattleState:ballMissMessage(shakes)
local t = self.data.text
if shakes == 0 then
return t._ItemUseBallText01 or "You missed the\nPOKéMON!"
return t._ItemUseBallText01 or Strings("You missed the\nPOKéMON!")
elseif shakes == 1 then
return t._ItemUseBallText02 or "Darn! The POKéMON\nbroke free!"
return t._ItemUseBallText02 or Strings("Darn! The POKéMON\nbroke free!")
elseif shakes == 2 then
return (t._ItemUseBallText03 or "Aww! It appeared\nto be caught!"):gsub("%s+$", "")
return (t._ItemUseBallText03 or Strings("Aww! It appeared\nto be caught!")):gsub("%s+$", "")
end
return t._ItemUseBallText04 or "Shoot! It was so\nclose too!"
return t._ItemUseBallText04 or Strings("Shoot! It was so\nclose too!")
end
-- AskName (engine/menus/naming_screen.asm): ClearSprites, wild field blank,
@@ -3329,7 +3358,7 @@ function BattleState:askNicknameUI(mon, displayName)
self.lockedBall = nil
self.blankForAskName = true
local TextBox = require("src.render.TextBox")
local text = ("Do you want to\ngive a nickname\nto %s?"):format(displayName)
local text = Strings("Do you want to\ngive a nickname\nto %s?", displayName)
local label = game.data.text and game.data.text._DoYouWantToNicknameText
if label then
-- extractor CONT is \t; TextBox scrolls on \n/\v
@@ -3340,7 +3369,7 @@ function BattleState:askNicknameUI(mon, displayName)
self.blankForAskName = false
if not yes then return end
pcall(Screens.push, game, "NamingScreen", {
title = "NICKNAME?", maxLen = 10,
title = Strings("NICKNAME?"), maxLen = 10,
onDone = function(name)
if name and #name > 0 then mon.nickname = name end
end,
@@ -3368,7 +3397,7 @@ function BattleState:storeCaughtMon()
stampOT(game.save, self.enemy.mon)
if isNew then
-- _ItemUseBallText06 + ShowPokedexData
self:sayNext(("New POKéDEX data\nwill be added for\n%s!"):format(self.enemy.name))
self:sayNext(Strings("New POKéDEX data\nwill be added for\n%s!", self.enemy.name))
self:uiNext(function()
return self:buildScreen("DexEntryMenu", species)
end)
@@ -3389,10 +3418,10 @@ function BattleState:storeCaughtMon()
askCaughtNickname()
-- _ItemUseBallText07/08 keyed on EVENT_MET_BILL
local pc = (game.save.flags and game.save.flags.EVENT_MET_BILL)
and "BILL's PC" or "someone's PC"
self:sayNext(("%s was\ntransferred to\n%s!"):format(self.enemy.name, pc))
and "BILL's PC" or Strings("someone's PC")
self:sayNext(Strings("%s was\ntransferred to\n%s!", self.enemy.name, pc))
else
self:sayNext("But every BOX\nis full!")
self:sayNext(Strings("But every BOX\nis full!"))
end
end
Runtime.emit("pokemon.caught", {
@@ -3446,13 +3475,13 @@ end
-- called by BagMenu when a ball is thrown
function BattleState:throwBall(ball)
self:say(("%s used\n%s!"):format(self.game.save.player.name,
self:say(Strings("%s used\n%s!", self.game.save.player.name,
self.data.items[ball].name))
self:act(function()
require("src.core.Sound").play(self.data, "Ball_Toss")
if self.kind ~= "wild" then
self:sayNext("The TRAINER\nblocked the BALL!")
self:sayNext("Don't be a thief!")
self:sayNext(Strings("The TRAINER\nblocked the BALL!"))
self:sayNext(Strings("Don't be a thief!"))
return
end
if self.ghost then
@@ -3462,8 +3491,8 @@ function BattleState:throwBall(ball)
-- ($10 anim data, no wobbles), and the turn is spent like any
-- failed throw
self:animNext(self:tossAnimFor(ball), true, nil, ball)
self:sayNext("It dodged the\nthrown BALL!")
self:sayNext("This POKéMON\ncan't be caught!")
self:sayNext(Strings("It dodged the\nthrown BALL!"))
self:sayNext(Strings("This POKéMON\ncan't be caught!"))
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
@@ -3486,7 +3515,7 @@ function BattleState:throwBall(ball)
self:actNext(function()
require("src.core.Sound").play(self.data, "Caught_Mon")
end)
self:sayNext(("All right!\n%s was\ncaught!"):format(self.enemy.name))
self:sayNext(Strings("All right!\n%s was\ncaught!", self.enemy.name))
self:act(function() self:storeCaughtMon() end)
else
self:sayNext(self:ballMissMessage(shakes))
@@ -3506,9 +3535,9 @@ function BattleState:openParty()
battle = self,
onSwitch = function(mon)
if mon == self.player.mon then
self:say(("%s is\nalready out!"):format(self.player.name))
self:say(Strings("%s is\nalready out!", self.player.name))
elseif mon.hp <= 0 then
self:say("There's no will\nto fight!")
self:say(Strings("There's no will\nto fight!"))
else
self:resolveSwitch(mon)
end
@@ -3534,7 +3563,7 @@ end
function BattleState:finish()
if self.payDay and self.result == "win" then
self.game.save.money = self.game.save.money + self.payDay
self:say(("%s picked up\n¥%d!"):format(self.game.save.player.name, self.payDay))
self:say(Strings("%s picked up\n¥%d!", self.game.save.player.name, self.payDay))
self.payDay = nil
self.afterQueue = "finish"
self.phase = "messages"
@@ -3581,9 +3610,11 @@ local HudTiles = require("src.render.HudTiles")
local hudTile = HudTiles.tile
local drawHPBar = HudTiles.drawHPBar
-- CenterMonName: 1-2 letter names print two tiles right, 3-4 one tile
-- CenterMonName: 1-2 letter names print two tiles right, 3-4 one tile.
-- Counted in glyphs, not bytes: a nickname carrying "é" or "♂" is one
-- charmap sequence per glyph, and byte length would push it a tile left.
local function nameX(tx, name)
local n = #name
local n = #Font.split(name)
return tx * 8 + (n <= 2 and 16 or n <= 4 and 8 or 0)
end
@@ -4343,9 +4374,9 @@ function BattleState:drawTextArea()
-- -- next to FIGHT (9,14) for the first 80 frames, then ITEM (9,16)
Font.drawBox(8, 12, 12, 6)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("FIGHT", 80, 112)
Font.draw(Strings("FIGHT"), 80, 112)
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
Font.draw("ITEM", 80, 128); Font.draw("RUN", 128, 128)
Font.draw(Strings("ITEM"), 80, 128); Font.draw(Strings("RUN"), 128, 128)
Font.drawCode(0xED, 72, (self.demoTimer or 0) <= 80 and 112 or 128)
elseif self.phase == "menu" then
local col = (self.menuIndex - 1) % 2
@@ -4354,16 +4385,16 @@ function BattleState:drawTextArea()
-- SAFARI_BATTLE_MENU_TEMPLATE: full-width box, "BALLx BAIT /
-- THROW ROCK RUN" from (2,14)
Font.drawBox(0, 12, 20, 6)
Font.draw("BALLx", 16, 112); Font.draw("BAIT", 112, 112)
Font.draw("THROW ROCK", 16, 128); Font.draw("RUN", 112, 128)
Font.draw(Strings("BALLx"), 16, 112); Font.draw(Strings("BAIT"), 112, 112)
Font.draw(Strings("THROW ROCK"), 16, 128); Font.draw(Strings("RUN"), 112, 128)
Font.drawCode(0xED, (col == 0 and 8 or 104), 112 + row * 16)
else
-- BATTLE_MENU_TEMPLATE: box (8,12)-(19,17), "FIGHT <PK><MN> /
-- ITEM RUN" from (10,14); cursor columns 9 / 15
Font.drawBox(8, 12, 12, 6)
Font.draw("FIGHT", 80, 112)
Font.draw(Strings("FIGHT"), 80, 112)
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
Font.draw("ITEM", 80, 128); Font.draw("RUN", 128, 128)
Font.draw(Strings("ITEM"), 80, 128); Font.draw(Strings("RUN"), 128, 128)
Font.drawCode(0xED, (col == 0 and 72 or 120), 112 + row * 16)
end
elseif self.phase == "moveSelect" then
@@ -4388,10 +4419,10 @@ function BattleState:drawTextArea()
local sel = self.player.curMoves[self.moveIndex]
if sel then
if self.player.disabledSlot == self.moveIndex then
Font.draw("disabled!", 8, 80)
Font.draw(Strings("disabled!"), 8, 80)
else
local def = self.data.moves[sel.id]
Font.draw("TYPE/", 8, 72)
Font.draw(Strings("TYPE/"), 8, 72)
-- the type record's display name (a mod type shows its name, and
-- PSYCHIC_TYPE prints PSYCHIC like the original)
Font.draw(def.type and TypeChart.displayName(def.type) or "", 16, 80)