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)
+13 -12
View File
@@ -8,6 +8,7 @@
local MoveEffects = require("src.battle.MoveEffects")
local Runtime = require("src.mods.Runtime")
local StatusRegistry = require("src.battle.StatusRegistry")
local Strings = require("src.core.Strings")
local EffectRegistry = {}
@@ -91,7 +92,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if target.invulnerable and not neverMiss then
-- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed)
if not (record and record.explode) then battle:cancelMoveAnim() end
battle:sayNext(("%s's\nattack missed!"):format(displayName(user)))
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
return
end
@@ -113,7 +114,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if not battle:accuracyRoll(move, user, target) then
-- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed)
if not (record and record.explode) then battle:cancelMoveAnim() end
battle:sayNext(("%s's\nattack missed!"):format(displayName(user)))
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
-- Jump Kick crash, Explode self-destruct
if record and record.onMiss then record.onMiss(ctx, "accuracy") end
user.trappingTurns = nil
@@ -140,7 +141,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
end
if not counterable or (battle.lastDamage or 0) == 0 then
battle:cancelMoveAnim()
battle:sayNext(("%s's\nattack missed!"):format(displayName(user)))
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
return
end
dmg = math.min(65535, battle.lastDamage * 2)
@@ -163,14 +164,14 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if info.typeMult == 0 then
-- type immunity zeros damage and sets wMoveMissed in Gen 1, so no anim
if not (record and record.explode) then battle:cancelMoveAnim() end
battle:sayNext(("It doesn't affect\n%s!"):format(displayName(target)))
battle:sayNext(Strings("It doesn't affect\n%s!", displayName(target)))
if record and record.onMiss then record.onMiss(ctx, "immune") end
return
end
if info.missed then
-- 0.25x floored the damage to zero: the original registers a miss
if not (record and record.explode) then battle:cancelMoveAnim() end
battle:sayNext(("%s's\nattack missed!"):format(displayName(user)))
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
if record and record.onMiss then record.onMiss(ctx, "floored") end
return
end
@@ -214,12 +215,12 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- multi-hit loop (core.asm .moveDidNotMiss before the jump back
-- to GetPlayerAnimationType), so crit/effectiveness reprint on
-- every strike -- damage was only rolled once
if info.crit then battle:sayNext("Critical hit!") end
if info.ohko then battle:sayNext("One-hit KO!") end
if info.crit then battle:sayNext(Strings("Critical hit!")) end
if info.ohko then battle:sayNext(Strings("One-hit KO!")) end
if info.typeMult > 10 then
battle:sayNext("It's super\neffective!")
battle:sayNext(Strings("It's super\neffective!"))
elseif info.typeMult < 10 then
battle:sayNext("It's not very\neffective...")
battle:sayNext(Strings("It's not very\neffective..."))
end
if Runtime.wants("battle.damage_dealt") then
Runtime.emit("battle.damage_dealt", {
@@ -237,9 +238,9 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if hits > 1 then
-- player: _MultiHitText; enemy: _HitXTimesText (always plural)
if user.isPlayer then
battle:sayNext(("Hit the enemy\n%d times!"):format(hits))
battle:sayNext(Strings("Hit the enemy\n%d times!", hits))
else
battle:sayNext(("Hit %d times!"):format(hits))
battle:sayNext(Strings("Hit %d times!", hits))
end
end
@@ -251,7 +252,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
elseif moveInst.struggle then
-- struggle recoils even when its effect id resolves to no record
local recoil = math.max(1, math.floor(dmg / 2))
battle:sayNext(("%s's\nhit with recoil!"):format(displayName(user)))
battle:sayNext(Strings("%s's\nhit with recoil!", displayName(user)))
battle:applyDamage(user, recoil)
end
+59 -56
View File
@@ -15,6 +15,7 @@ local Logger = require("src.core.Logger")
local StatusRegistry = require("src.battle.StatusRegistry")
local TurnOrder = require("src.battle.TurnOrder")
local TypeChart = require("src.battle.TypeChart")
local Strings = require("src.core.Strings")
local MoveEffects = {}
@@ -36,14 +37,14 @@ local STAT_LABEL = {
local function changeStage(battle, who, stat, delta, fromEnemy)
if fromEnemy and (who.substituteHP or who.mist) then
if who.mist then
return { displayName(who) .. " is\nprotected by MIST!" }
return { Strings("%s is\nprotected by MIST!", displayName(who)) }
end
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
local cur = who.stages[stat] or 0
local new = math.max(-6, math.min(6, cur + delta))
if new == cur then
return { ("Nothing happened!") }
return { Strings("Nothing happened!") }
end
who.stages[stat] = new
-- effects.asm:505-506: after any stat-stage change, modified stats are
@@ -53,13 +54,13 @@ local function changeStage(battle, who, stat, delta, fromEnemy)
-- _MonsStatsRoseText/_MonsStatsFellText: "X's / STAT rose!"; the
-- two-stage variants scroll "greatly" onto a third line
if delta >= 2 then
return { ("%s's\n%s\ngreatly rose!"):format(displayName(who), STAT_LABEL[stat]) }
return { Strings("%s's\n%s\ngreatly rose!", displayName(who), STAT_LABEL[stat]) }
elseif delta == 1 then
return { ("%s's\n%s rose!"):format(displayName(who), STAT_LABEL[stat]) }
return { Strings("%s's\n%s rose!", displayName(who), STAT_LABEL[stat]) }
elseif delta == -1 then
return { ("%s's\n%s fell!"):format(displayName(who), STAT_LABEL[stat]) }
return { Strings("%s's\n%s fell!", displayName(who), STAT_LABEL[stat]) }
end
return { ("%s's\n%s\ngreatly fell!"):format(displayName(who), STAT_LABEL[stat]) }
return { Strings("%s's\n%s\ngreatly fell!", displayName(who), STAT_LABEL[stat]) }
end
MoveEffects.changeStage = changeStage
@@ -88,10 +89,10 @@ end
local function statusMove(status)
return function(battle, user, target, move)
if target.mon.status then
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
if status == "PSN" and target.substituteHP then
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
local msgs = inflictStatus(battle, target, status, {
toxic = move and move.id == "TOXIC",
@@ -99,7 +100,7 @@ local function statusMove(status)
source = move and move.id,
})
if #msgs == 0 then
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
return msgs
end
@@ -111,7 +112,7 @@ local function statusSide(status, chance)
-- target (regardless of the burn roll)
if move and move.type == "FIRE" and target.mon.status == "FRZ" then
target.mon.status = nil
return { ("Fire defrosted\n%s!"):format(displayName(target)) }
return { Strings("Fire defrosted\n%s!", displayName(target)) }
end
if battle.rng(0, 255) >= chance then return {} end
return inflictStatus(battle, target, status, {
@@ -144,10 +145,10 @@ end
local function confuse(battle, target, pierceSub)
if target.confusedTurns or (target.substituteHP and not pierceSub) then
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
target.confusedTurns = battle.rng(2, 5)
return { ("%s\nbecame confused!"):format(displayName(target)) }
return { Strings("%s\nbecame confused!", displayName(target)) }
end
-- ---------------------------------------------------------------------
@@ -181,53 +182,53 @@ MoveEffects.primary = {
LEECH_SEED_EFFECT = function(battle, user, target)
-- leech_seed.asm has no substitute check: seeding lands through one
if target.leechSeeded then
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
for _, t in ipairs(target.curTypes) do
if t == "GRASS" then return { "But, it failed!" } end
if t == "GRASS" then return { Strings("But, it failed!") } end
end
target.leechSeeded = true
return { ("%s\nwas seeded!"):format(displayName(target)) }
return { Strings("%s\nwas seeded!", displayName(target)) }
end,
HEAL_EFFECT = function(battle, user, target, move)
local mon = user.mon
if move.id == "REST" then
if mon.hp == mon.stats.hp then return { "But, it failed!" } end
if mon.hp == mon.stats.hp then return { Strings("But, it failed!") } end
mon.hp = mon.stats.hp
mon.status = "SLP"
user.sleepTurns = 2
user.toxicCounter = nil
return { ("%s\nstarted sleeping!"):format(displayName(user)) }
return { Strings("%s\nstarted sleeping!", displayName(user)) }
end
if mon.hp == mon.stats.hp then return { "But, it failed!" } end
if mon.hp == mon.stats.hp then return { Strings("But, it failed!") } end
mon.hp = math.min(mon.stats.hp, mon.hp + math.floor(mon.stats.hp / 2))
return { ("%s\nregained health!"):format(displayName(user)) }
return { Strings("%s\nregained health!", displayName(user)) }
end,
LIGHT_SCREEN_EFFECT = function(battle, user)
if user.lightScreen then return { "But, it failed!" } end
if user.lightScreen then return { Strings("But, it failed!") } end
user.lightScreen = true
return { ("%s's\nprotected against\nspecial attacks!"):format(displayName(user)) }
return { Strings("%s's\nprotected against\nspecial attacks!", displayName(user)) }
end,
REFLECT_EFFECT = function(battle, user)
if user.reflect then return { "But, it failed!" } end
if user.reflect then return { Strings("But, it failed!") } end
user.reflect = true
return { ("%s\ngained armor!"):format(displayName(user)) }
return { Strings("%s\ngained armor!", displayName(user)) }
end,
MIST_EFFECT = function(battle, user)
if user.mist then return { "But, it failed!" } end
if user.mist then return { Strings("But, it failed!") } end
user.mist = true
-- _ShroudedInMistText (lowercase "mist")
return { ("%s's\nshrouded in mist!"):format(displayName(user)) }
return { Strings("%s's\nshrouded in mist!", displayName(user)) }
end,
FOCUS_ENERGY_EFFECT = function(battle, user)
if user.focusEnergy then return { "But, it failed!" } end
if user.focusEnergy then return { Strings("But, it failed!") } end
user.focusEnergy = true
return { ("%s's\ngetting pumped!"):format(displayName(user)) }
return { Strings("%s's\ngetting pumped!", displayName(user)) }
end,
HAZE_EFFECT = function(battle, user, target)
@@ -254,33 +255,33 @@ MoveEffects.primary = {
target.skipMove = true
end
target.mon.status = nil
return { "All STATUS changes\nare eliminated!" }
return { Strings("All STATUS changes\nare eliminated!") }
end,
SUBSTITUTE_EFFECT = function(battle, user)
if user.substituteHP then return { ("%s\nhas a SUBSTITUTE!"):format(displayName(user)) } end
if user.substituteHP then return { Strings("%s\nhas a SUBSTITUTE!", displayName(user)) } end
local cost = math.floor(user.mon.stats.hp / 4)
-- substitute.asm only fails on subtraction underflow (current HP
-- strictly below maxHP/4); at equality the substitute is built and
-- the user is left standing on exactly 0 HP (it faints only when
-- the engine next checks HP, not here)
if user.mon.hp < cost then
return { "Too weak to make\na SUBSTITUTE!" }
return { Strings("Too weak to make\na SUBSTITUTE!") }
end
user.mon.hp = user.mon.hp - cost
user.substituteHP = cost + 1
-- _SubstituteText
return { "It created a\nSUBSTITUTE!" }
return { Strings("It created a\nSUBSTITUTE!") }
end,
CONVERSION_EFFECT = function(battle, user, target)
-- conversion.asm fails against a mid-Fly/Dig target (INVULNERABLE)
if target.invulnerable then
return { "But, it failed!" }
return { Strings("But, it failed!") }
end
user.curTypes = { target.curTypes[1], target.curTypes[2] }
-- _ConvertedTypeText
return { ("Converted type to\n%s's!"):format(displayName(target)) }
return { Strings("Converted type to\n%s's!", displayName(target)) }
end,
-- MIMIC_EFFECT lives in BattleState:resolveMimic: MimicEffect
@@ -311,27 +312,27 @@ MoveEffects.primary = {
table.insert(user.curMoves, { id = mv.id, pp = 5, mimic = true })
end
-- _TransformedText: the copied name prints bare (wNameBuffer)
return { ("%s\ntransformed into\n%s!"):format(displayName(user), target.name) }
return { Strings("%s\ntransformed into\n%s!", displayName(user), target.name) }
end,
DISABLE_EFFECT = function(battle, user, target)
if target.disabledSlot then return { "But, it failed!" } end
if target.disabledSlot then return { Strings("But, it failed!") } end
local usable = {}
for i, mv in ipairs(target.curMoves) do
if mv.pp > 0 then table.insert(usable, i) end
end
if #usable == 0 then return { "But, it failed!" } end
if #usable == 0 then return { Strings("But, it failed!") } end
local slot = usable[battle.rng(1, #usable)]
target.disabledSlot = slot
target.disabledTurns = battle.rng(1, 8)
local id = target.curMoves[slot].id
-- _MoveWasDisabledText: "X's / MOVE was / disabled!"
return { ("%s's\n%s was\ndisabled!"):format(displayName(target),
return { Strings("%s's\n%s was\ndisabled!", displayName(target),
battle.data.moves[id].name) }
end,
SPLASH_EFFECT = function()
return { "No effect!" }
return { Strings("No effect!") }
end,
}
@@ -428,7 +429,9 @@ local function drainHalf(text)
local mon = ctx.user.mon
mon.hp = math.min(mon.stats.hp, mon.hp + heal)
ctx.drain()
ctx.say(text:format(displayName(ctx.target)))
-- `text` arrives as a source string (Strings.source at the call
-- site keeps it in the catalog); look it up here, at use time
ctx.say(Strings(text, displayName(ctx.target)))
end
end
@@ -436,7 +439,7 @@ end
-- flags the miss before the special-damage override)
local function immuneMsg(ctx)
if TypeChart.effectiveness(ctx.move.type, ctx.target.curTypes) == 0 then
return ("It doesn't affect\n%s!"):format(displayName(ctx.target))
return Strings("It doesn't affect\n%s!", displayName(ctx.target))
end
return nil
end
@@ -498,12 +501,12 @@ MoveEffects.full = {
-- removed): overkill and substitute hits recoil at full strength
local recoil = math.max(1, math.floor(ctx.rawDamage
/ (ctx.moveInst.struggle and 2 or 4)))
ctx.say(("%s's\nhit with recoil!"):format(displayName(ctx.user)))
ctx.say(Strings("%s's\nhit with recoil!", displayName(ctx.user)))
ctx.battle:applyDamage(ctx.user, recoil)
end,
},
DRAIN_HP_EFFECT = {
afterDamage = drainHalf("Sucked health from\n%s!"),
afterDamage = drainHalf(Strings.source("Sucked health from\n%s!")),
},
DREAM_EATER_EFFECT = {
-- only works on sleeping targets (checked before damage)
@@ -511,7 +514,7 @@ MoveEffects.full = {
if ctx.target.mon.status ~= "SLP" then return false, "But, it failed!" end
return true
end,
afterDamage = drainHalf("%s's\ndream was eaten!"),
afterDamage = drainHalf(Strings.source("%s's\ndream was eaten!")),
},
-- charge moves: first turn just charges; Fly AND Dig go
@@ -557,7 +560,7 @@ MoveEffects.full = {
user.thrashTurns, user.thrashMove, user.thrashAnnounced = nil, nil, nil
if not user.confusedTurns then
user.confusedTurns = ctx.rng(2, 5)
ctx.say(("%s\nbecame confused!"):format(displayName(user)))
ctx.say(Strings("%s\nbecame confused!", displayName(user)))
end
end
end
@@ -566,7 +569,7 @@ MoveEffects.full = {
JUMP_KICK_EFFECT = {
onMiss = function(ctx, reason)
if reason ~= "accuracy" then return end
ctx.say(("%s\nkept going and\ncrashed!"):format(displayName(ctx.user)))
ctx.say(Strings("%s\nkept going and\ncrashed!", displayName(ctx.user)))
ctx.damage(ctx.user, 1)
end,
},
@@ -595,7 +598,7 @@ MoveEffects.full = {
afterDamage = function(ctx)
local battle = ctx.battle
battle.payDay = (battle.payDay or 0) + 2 * ctx.user.mon.level
ctx.say("Coins scattered\neverywhere!")
ctx.say(Strings("Coins scattered\neverywhere!"))
end,
},
SWIFT_EFFECT = { neverMiss = true },
@@ -610,7 +613,7 @@ MoveEffects.full = {
local user = ctx.user
user.bideTurns = ctx.rng(2, 3)
user.bideDamage = 0
ctx.say(("%s\nis storing energy!"):format(displayName(user)))
ctx.say(Strings("%s\nis storing energy!", displayName(user)))
end,
},
SWITCH_AND_TELEPORT_EFFECT = {
@@ -631,27 +634,27 @@ MoveEffects.full = {
end
if ok then
if move.id == "ROAR" then
ctx.say(("%s\nran away scared!"):format(displayName(target)))
ctx.say(Strings("%s\nran away scared!", displayName(target)))
elseif move.id == "WHIRLWIND" then
ctx.say(("%s\nwas blown away!"):format(displayName(target)))
ctx.say(Strings("%s\nwas blown away!", displayName(target)))
else
ctx.say(("%s\nran from battle!"):format(displayName(user)))
ctx.say(Strings("%s\nran from battle!", displayName(user)))
end
battle.result = "run"
battle.afterQueue = "finish"
elseif move.id == "TELEPORT" then
battle:cancelMoveAnim()
ctx.say("But, it failed!")
ctx.say(Strings("But, it failed!"))
else
battle:cancelMoveAnim()
ctx.say(("It didn't affect\n%s!"):format(displayName(target)))
ctx.say(Strings("It didn't affect\n%s!", displayName(target)))
end
elseif move.id == "TELEPORT" then
battle:cancelMoveAnim()
ctx.say("But, it failed!")
ctx.say(Strings("But, it failed!"))
else
battle:cancelMoveAnim()
ctx.say(("%s\nis unaffected!"):format(displayName(target)))
ctx.say(Strings("%s\nis unaffected!", displayName(target)))
end
end,
},
@@ -669,7 +672,7 @@ MoveEffects.full = {
callsMove = function(ctx)
local last = ctx.target.lastMove
if not last then
ctx.say("The MIRROR MOVE\nfailed!")
ctx.say(Strings("The MIRROR MOVE\nfailed!"))
return nil
end
return last
+31 -21
View File
@@ -5,6 +5,8 @@
-- and residual sweep. Callers without a battle (pure-module tests) fall
-- back to the vanilla records, which is bit-identical behavior.
local Strings = require("src.core.Strings")
local Status = {}
-- pokered's <USER>/<TARGET> text macros print "Enemy " before the enemy
@@ -28,8 +30,11 @@ local function hasType(battler, wanted)
end
-- shared PSN/BRN residual: 1/16 max HP, multiplied (and advanced) by the
-- Toxic counter (HandlePoisonBurnLeechSeed)
local function damageOverTime(what)
-- Toxic counter (HandlePoisonBurnLeechSeed). The caller passes the whole
-- sentence rather than the noun: "hurt by poison" and "hurt by the burn"
-- decline differently once translated, so a shared fragment cannot be the
-- translatable unit.
local function damageOverTime(template)
return function(battler)
local mon = battler.mon
local base = math.max(1, math.floor(mon.stats.hp / 16))
@@ -39,10 +44,15 @@ local function damageOverTime(what)
battler.toxicCounter = battler.toxicCounter + 1
end
mon.hp = math.max(0, mon.hp - dmg)
return { ("%s's\nhurt by %s!"):format(name(battler), what) }
return { Strings(template, name(battler)) }
end
end
-- Labels stay plain literals on purpose: this table is built at require
-- time, before Strings.load has a catalog, so a Strings() here would
-- freeze the English. They are already translatable through the
-- statuses registry (mod.content.statuses:patch(id, { label = ... })).
--
-- The five persistent conditions as records: the beforeMove gauntlet, the
-- residual sweep, the inflict text/immunities (StatusRegistry.inflict),
-- the catch/wobble bonuses (Catching.attempt), the HUD label, and the
@@ -57,13 +67,13 @@ Status.RECORDS = {
battler.sleepTurns = (battler.sleepTurns or 1) - 1
if battler.sleepTurns <= 0 then
battler.mon.status = nil
return false, { name(battler) .. "\nwoke up!" } -- wakes but loses the turn
return false, { Strings("%s\nwoke up!", name(battler)) } -- wakes, loses the turn
end
return false, { name(battler) .. "\nis fast asleep!" }
return false, { Strings("%s\nis fast asleep!", name(battler)) }
end,
onInflict = function(battle, target, opts, display)
target.sleepTurns = battle.rng(1, 7)
return { ("%s\nfell asleep!"):format(display) }
return { Strings("%s\nfell asleep!", display) }
end,
},
FRZ = {
@@ -71,35 +81,35 @@ Status.RECORDS = {
catchBonus = 25, shakeBonus = 10,
beforeMovePriority = 30,
beforeMove = function(battler)
return false, { name(battler) .. "\nis frozen solid!" }
return false, { Strings("%s\nis frozen solid!", name(battler)) }
end,
canInflict = function(target) return not hasType(target, "ICE") end,
onInflict = function(_, _, _, display)
return { ("%s\nwas frozen solid!"):format(display) }
return { Strings("%s\nwas frozen solid!", display) }
end,
},
PSN = {
id = "PSN", label = "PSN", hudLabel = "PSN",
catchBonus = 12, shakeBonus = 5,
residual = damageOverTime("poison"),
residual = damageOverTime(Strings.source("%s's\nhurt by poison!")),
canInflict = function(target) return not hasType(target, "POISON") end,
onInflict = function(_, target, opts, display)
if opts.toxic then
target.toxicCounter = 1
-- _BadlyPoisonedText
return { ("%s's\nbadly poisoned!"):format(display) }
return { Strings("%s's\nbadly poisoned!", display) }
end
return { ("%s\nwas poisoned!"):format(display) }
return { Strings("%s\nwas poisoned!", display) }
end,
},
BRN = {
id = "BRN", label = "BRN", hudLabel = "BRN",
catchBonus = 12, shakeBonus = 5,
statPenalty = { stat = "attack", div = 2 },
residual = damageOverTime("the burn"),
residual = damageOverTime(Strings.source("%s's\nhurt by the burn!")),
canInflict = function(target) return not hasType(target, "FIRE") end,
onInflict = function(_, _, _, display)
return { ("%s\nwas burned!"):format(display) }
return { Strings("%s\nwas burned!", display) }
end,
},
PAR = {
@@ -110,7 +120,7 @@ Status.RECORDS = {
beforeMove = function(battler, rng)
-- cp 25 percent / jr nc: fully paralyzed on rand < 63 (63/256)
if rng(0, 255) < 63 then
return false, { name(battler) .. "'s\nfully paralyzed!" }
return false, { Strings("%s's\nfully paralyzed!", name(battler)) }
end
return true, {}
end,
@@ -120,7 +130,7 @@ Status.RECORDS = {
end,
onInflict = function(_, _, _, display)
-- _ParalyzedMayNotAttackText (primary and secondary paralysis)
return { ("%s's\nparalyzed! It may\nnot attack!"):format(display) }
return { Strings("%s's\nparalyzed! It may\nnot attack!", display) }
end,
},
}
@@ -156,7 +166,7 @@ function Status.beforeMove(battler, rng, battle)
end
if battler.flinched then
battler.flinched = false
return false, { name(battler) .. "\nflinched!" }
return false, { Strings("%s\nflinched!", name(battler)) }
end
local record = Status.recordFor(battleStatuses(battle), mon.status)
local handler = record and record.beforeMove
@@ -174,23 +184,23 @@ function Status.beforeMove(battler, rng, battle)
end
if battler.boundTurns and battler.boundTurns > 0 then
battler.boundTurns = battler.boundTurns - 1
msgs[#msgs + 1] = name(battler) .. "\ncan't move!"
msgs[#msgs + 1] = Strings("%s\ncan't move!", name(battler))
return false, msgs
end
if battler.disabledTurns then
battler.disabledTurns = battler.disabledTurns - 1
if battler.disabledTurns <= 0 then
battler.disabledTurns, battler.disabledSlot = nil, nil
table.insert(msgs, name(battler) .. "'s\ndisabled no more!")
table.insert(msgs, Strings("%s's\ndisabled no more!", name(battler)))
end
end
if battler.confusedTurns then
battler.confusedTurns = battler.confusedTurns - 1
if battler.confusedTurns <= 0 then
battler.confusedTurns = nil
table.insert(msgs, name(battler) .. "\nsnapped out of\nconfusion!")
table.insert(msgs, Strings("%s\nsnapped out of\nconfusion!", name(battler)))
else
table.insert(msgs, name(battler) .. "\nis confused!")
table.insert(msgs, Strings("%s\nis confused!", name(battler)))
-- cp 50 percent + 1 / jr c: hurt itself on rand >= 128 (128/256)
if rng(0, 255) < 128 then
return false, msgs, true -- hurt itself
@@ -231,7 +241,7 @@ function Status.residual(battler, opponent, battle)
dmg = math.min(dmg, mon.hp)
mon.hp = mon.hp - dmg
opponent.mon.hp = math.min(opponent.mon.stats.hp, opponent.mon.hp + dmg)
table.insert(msgs, ("LEECH SEED saps\n%s!"):format(name(battler)))
table.insert(msgs, Strings("LEECH SEED saps\n%s!", name(battler)))
end
return msgs
end
+2 -1
View File
@@ -5,6 +5,7 @@
local Runtime = require("src.mods.Runtime")
local Status = require("src.battle.Status")
local Strings = require("src.core.Strings")
local StatusRegistry = {}
@@ -45,7 +46,7 @@ function StatusRegistry.inflict(battle, target, status, opts)
if record and record.onInflict then
msgs = record.onInflict(battle, target, opts, display)
else
msgs = { ("%s\nwas afflicted\nby %s!"):format(display,
msgs = { Strings("%s\nwas afflicted\nby %s!", display,
record and record.label or tostring(status)) }
end
Runtime.emit("battle.status_inflicted", {
+4 -3
View File
@@ -19,6 +19,7 @@
-- enemy PP — Gen 1 AI never reads wEnemyMonPP).
local TypeChart = require("src.battle.TypeChart")
local Strings = require("src.core.Strings")
local TrainerAI = {}
@@ -99,7 +100,7 @@ function TrainerAI.useItem(battle, item)
local enemy = battle.enemy
local trainerName = battle.trainer.name
local itemName = battle.data.items[item] and battle.data.items[item].name or item
local msgs = { ("%s\nused %s!"):format(trainerName, itemName) }
local msgs = { Strings("%s\nused %s!", trainerName, itemName) }
if item == "FULL_HEAL" then
enemy.mon.status = nil
enemy.toxicCounter = nil
@@ -112,10 +113,10 @@ function TrainerAI.useItem(battle, item)
elseif X_STAT[item] then
local stat = X_STAT[item]
enemy.stages[stat] = math.min(6, (enemy.stages[stat] or 0) + 1)
table.insert(msgs, ("%s's\n%s rose!"):format(enemy.name, stat:upper()))
table.insert(msgs, Strings("%s's\n%s rose!", enemy.name, stat:upper()))
elseif item == "GUARD_SPEC" then
enemy.mist = true
table.insert(msgs, ("%s's\nprotected against\nstat changes!"):format(enemy.name))
table.insert(msgs, Strings("%s's\nprotected against\nstat changes!", enemy.name))
end
return msgs
end