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
+7 -6
View File
@@ -9,6 +9,7 @@
local Json = require("src.link.Json")
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local Strings = require("src.core.Strings")
local DiscordPresence = {
APP_ID = "1529183141267374262",
@@ -436,7 +437,7 @@ end
local function buildActivity()
local details, activityState
if state.activity == "battle" then
details = state.battleLabel or "In battle"
details = state.battleLabel or Strings("In battle")
activityState = state.location and (state.location) or "Kanto"
elseif state.activity == "exploring" and state.location then
details = state.location
@@ -550,20 +551,20 @@ local function subscribe(game)
track("battle.started", function(ev)
local battle = ev and ev.battle
local label = "In battle"
local label = Strings("In battle")
if ev and ev.kind == "wild" then
local name = speciesName(game, ev.species)
if name then
label = "Battling wild " .. name
if ev.level then label = label .. " Lv" .. tostring(ev.level) end
else
label = "Wild battle"
label = Strings("Wild battle")
end
elseif ev and ev.kind == "trainer" then
local tname = battle and battle.trainer and battle.trainer.name
label = tname and ("Battling " .. tname) or "Trainer battle"
label = tname and ("Battling " .. tname) or Strings("Trainer battle")
elseif ev and ev.kind == "link" then
label = "Link battle"
label = Strings("Link battle")
end
local mapId = state.mapId
or (game.save and game.save.player and game.save.player.map)
@@ -594,7 +595,7 @@ local function subscribe(game)
state.mapId = nil
setPresence({
activity = "menu",
location = id == "IntroMovie" and "Watching the intro" or "Title screen",
location = id == "IntroMovie" and "Watching the intro" or Strings("Title screen"),
clearBattle = true,
})
end
+3
View File
@@ -53,6 +53,9 @@ function Game:load()
require("src.render.Font").load(Data)
-- menu cursor/border/geometry constants; field.theme restyles them
require("src.ui.Theme").load(Data)
-- the engine's own text, after the merge so a translation mod's catalog
-- is already in Data.strings; empty on a mod-free boot and skipped
require("src.core.Strings").load(Data)
self.stack = StateStack
StateStack:init()
+13 -6
View File
@@ -1155,9 +1155,16 @@ function SaveData.defaultHeal(boot)
end
-- Post-credits home (issue #103). pokered left the player in HALL_OF_FAME
-- after jp Init; this port places CONTINUE at the NewGameWarp bedroom and
-- retargets LAST_MAP exits (Red's house mats) at the heal-point town so
-- leaving the house does not dump the player back at Indigo Plateau.
-- after jp Init; this port relocates CONTINUE instead, and retargets
-- LAST_MAP exits (Red's house mats) at the heal-point town so leaving the
-- house does not dump the player back at Indigo Plateau.
--
-- The landing spot is the blackout point, not the NewGameWarp bedroom:
-- HallOfFameResetEventsAndSaveScript sets wLastBlackoutMap := PALLET_TOWN,
-- so resuming puts the player outside the front door. Spawning them back
-- in the upstairs bedroom was a port-only detour (#253). Non-vanilla
-- spawns are unaffected -- defaultHeal returns boot's own start map for
-- those, which is what the bedroom branch resolved to anyway.
-- Marks postGameHomeOk so a later intentional HoF save is not relocated.
function SaveData.applyPostGameHome(save, boot)
boot = type(boot) == "table" and boot or {}
@@ -1165,9 +1172,9 @@ function SaveData.applyPostGameHome(save, boot)
save.lastHeal = { map = heal.map, x = heal.x, y = heal.y }
save.lastOutdoor = { id = heal.map, x = heal.x, y = heal.y }
save.player = save.player or {}
save.player.map = boot.startMap or "REDS_HOUSE_2F"
save.player.x = boot.startX or 3
save.player.y = boot.startY or 6
save.player.map = heal.map
save.player.x = heal.x
save.player.y = heal.y
save.player.facing = boot.startFacing or "down"
save.postGameHomeOk = true
return heal
+134
View File
@@ -0,0 +1,134 @@
-- The engine's own player-facing text, made overridable.
--
-- Extracted dialogue already had a home: `Data.text`, keyed by pokered's
-- labels, which a mod reaches through `mod.content.text`. The strings the
-- engine *authors* had none. Battle messages, item results, menu labels
-- and the link-play screens were literals in Lua, so a translator could
-- reach two thirds of the game and no more (#186, #245).
--
-- Those literals stay where they are, wrapped in `S(...)`, and the English
-- source doubles as the catalog key:
--
-- self:say(S("But, it failed!"))
-- self:say(S("Wild %s\nappeared!", self.enemy.name))
--
-- Keying on the source rather than on an invented id is deliberate. It
-- keeps the English readable at the point it is used (the alternative
-- scatters a thousand `battle.it_failed` ids that have to be looked up to
-- review a diff), it needs no id registry to stay in sync, and an entry a
-- translation has not covered yet falls through to English instead of
-- rendering a raw id at the player. The cost is that editing an English
-- string orphans its translations; `tools/modkit.py translation --refresh`
-- reports those as changed keys rather than silently dropping them.
--
-- Same-source-different-meaning is the one case source keys cannot hold on
-- their own ("OFF" as a filter setting vs "OFF" as a toggle, which some
-- languages render differently). Those sites pass a context:
--
-- S("OFF", "options.musicFilter") -- key: "options.musicFilter|OFF"
--
-- A mod supplies the catalog through the `strings` registry:
--
-- mod.content.strings:override("But, it failed!", "Echec !")
--
-- With no mod loaded the catalog is empty and `S` is an identity function
-- guarded by one boolean, so a vanilla boot draws byte-identical text.
local Strings = {}
local catalog = nil -- Data.strings once a mod has put something in it
local missing = {} -- format-arity complaints, reported once each
-- Called from Game after the mod merge, and again on dev-mode hot reload.
-- Holding the table (not a copy) means a mod that registers late still
-- takes effect without a second load.
function Strings.load(data)
local t = data and data.strings
catalog = nil
if type(t) ~= "table" then return end
for _ in pairs(t) do
catalog = t
return
end
end
function Strings.active()
return catalog ~= nil
end
-- The lookup itself. `context` is optional and only disambiguates sources
-- that collide; the plain key is tried after it, so a translation that does
-- not care about the distinction can supply one entry for both.
function Strings.lookup(source, context)
if not catalog then return source end
if context then
local hit = catalog[context .. "|" .. source]
if type(hit) == "string" then return hit end
end
local hit = catalog[source]
if type(hit) == "string" then return hit end
return source
end
-- Count `%`-directives so a translation that drops or adds one is caught
-- here rather than as a mid-battle `string.format` error.
local function specifiers(s)
local n = 0
for spec in s:gmatch("%%(.)") do
if spec ~= "%" then n = n + 1 end
end
return n
end
-- S(source) -> translated source
-- S(source, ...) -> translated source, string.format'ed
-- S(source, context) -> context-disambiguated lookup, no formatting
--
-- The two-argument forms are told apart by whether the source carries any
-- format directives: a source with no `%s` cannot be formatting, so a lone
-- string second argument is a context.
function Strings.get(source, ...)
local argc = select("#", ...)
if argc == 0 then return Strings.lookup(source) end
local wants = specifiers(source)
if wants == 0 and argc == 1 and type((...)) == "string" then
return Strings.lookup(source, (...))
end
local text = Strings.lookup(source)
-- A translation with the wrong arity would raise inside string.format,
-- which in a battle means a crash the player cannot escape. Fall back to
-- the English source, which is known to match, and say so once.
if specifiers(text) ~= wants then
if not missing[source] then
missing[source] = true
require("src.core.Logger").warn(
"strings: translation of %q has %d format directives, source has %d"
.. " -- using the source", source, specifiers(text), wants)
end
text = source
end
local ok, out = pcall(string.format, text, ...)
if not ok then return source end
return out
end
-- A marker, not a lookup: returns its argument untouched.
--
-- Some templates are declared in a module-level table and formatted much
-- later (BattleState's charge-move lines, for one). Translating at the
-- declaration would freeze the English, because those tables are built at
-- require time and Strings.load has no catalog yet; the use site therefore
-- calls Strings(template, ...) and looks the source up then. That works at
-- runtime but leaves the literal invisible to the catalog generator, which
-- only sees what is spelled out at a call site. Wrapping the declaration in
-- Strings.source puts it back in the harvest while changing nothing at all
-- about when the lookup happens.
function Strings.source(text)
return text
end
setmetatable(Strings, { __call = function(_, ...) return Strings.get(...) end })
return Strings
+19 -19
View File
@@ -1,4 +1,5 @@
local GameVersion = require("src.core.GameVersion")
local Strings = require("src.core.Strings")
local RomImporter = {}
RomImporter.__index = RomImporter
@@ -356,7 +357,7 @@ end
-- Returns the chosen absolute path or nil. Android uses love.system.pickFile
-- ("mod") instead -- see RomImporter:chooseMod.
local function chooseZip()
local prompt = "Choose a mod .zip"
local prompt = Strings("Choose a mod .zip")
local platform = love.system.getOS()
if platform == "OS X" then
return commandOutput(
@@ -387,7 +388,7 @@ end
-- dialogs). Returns the chosen absolute path or nil. Android uses
-- love.system.pickFile("sav") instead -- see RomImporter:chooseSaveImport.
local function chooseSav()
local prompt = "Choose a .sav save file"
local prompt = Strings("Choose a .sav save file")
local platform = love.system.getOS()
if platform == "OS X" then
return commandOutput(
@@ -1426,7 +1427,7 @@ function RomImporter:draw()
self.updateButton = { x = rect.x, y = rect.y, width = rect.width,
height = rect.height, action = "download" }
message(upLatest and ("Update v" .. upLatest .. " available")
or "An update is available", rect.width)
or Strings("An update is available"), rect.width)
elseif upStatus == "needs_full" then
local rect = actionButton("Open releases")
self.updateButton = { x = rect.x, y = rect.y, width = rect.width,
@@ -1718,7 +1719,7 @@ function RomImporter:_playButton(x, y, w, h, gameName, ready, locked)
col(PAL.disabledInk, 0.3)
love.graphics.rectangle("line", x, y, w, h, r, r)
col(PAL.disabledInk)
local label = locked and "Coming soon" or "Import a ROM to play"
local label = locked and "Coming soon" or Strings("Import a ROM to play")
printfB(label, x, y + (h - self.playFont:getHeight()) / 2, w, "center")
self.playButtonRect = nil
end
@@ -1730,13 +1731,13 @@ function RomImporter:_drawTabBar(x, y, w, h, chip)
local s, pulse = self._s, self.pulse
local tabs = {
{ id = "red", letter = "R", top = PAL.chipRedTop, bot = PAL.chipRedBot,
under = PAL.red, label = "RED", ink = PAL.white },
under = PAL.red, label = Strings("RED"), ink = PAL.white },
{ id = "blue", letter = "B", top = PAL.chipBlueTop, bot = PAL.chipBlueBot,
under = PAL.blue, label = "BLUE", ink = PAL.white },
under = PAL.blue, label = Strings("BLUE"), ink = PAL.white },
{ id = "yellow", letter = "Y", top = PAL.chipGoldTop, bot = PAL.chipGoldBot,
under = PAL.gold, label = "YELLOW", ink = PAL.chipInkGold },
under = PAL.gold, label = Strings("YELLOW"), ink = PAL.chipInkGold },
{ id = "mods", mods = true, top = PAL.chipModTop, bot = PAL.chipModBot,
under = PAL.modDot, label = "MODS" },
under = PAL.modDot, label = Strings("MODS") },
}
local gap = 10 * s
local r = 12 * s
@@ -1795,7 +1796,7 @@ function RomImporter:_drawTabBar(x, y, w, h, chip)
local ready = 0
for _, v in ipairs(GameVersion.ORDER) do if self.ready[v] then ready = ready + 1 end end
love.graphics.setFont(self.readyFont)
local label = ("%d of 3 ready"):format(ready)
local label = Strings("%d of 3 ready", ready)
local lw = self.readyFont:getWidth(label)
if x + w - lw > cursorX + 8 * s then
col(PAL.labelGray)
@@ -1851,7 +1852,7 @@ function RomImporter:_drawGamePanel(version, x, y, w, h)
-- ROM card contents by state (rehomes the existing import flow)
local dropHint = self.android and "Copy the .gb via USB."
or "Or drop the .gb file here."
or Strings("Or drop the .gb file here.")
local accent = locked and PAL.gold or (version == "red" and PAL.red or PAL.blue)
local romState, romDetail, romBtnLabel, romBtnEnabled, romProgress
if locked then
@@ -1866,12 +1867,12 @@ function RomImporter:_drawGamePanel(version, x, y, w, h)
romDetail = self.detail or ""
romProgress = self.progress or 0
elseif ready then
romState = self.romName[version] or "ROM imported"
romState = self.romName[version] or Strings("ROM imported")
romDetail = "Verified."
romBtnLabel, romBtnEnabled = "Re-import ROM", true
elseif erroring then
romState = "Import failed"
romDetail = self.detail or "That ROM could not be imported."
romDetail = self.detail or Strings("That ROM could not be imported.")
romBtnLabel, romBtnEnabled = "Import ROM", true
elseif notice then
romState = "No ROM imported"
@@ -2000,7 +2001,7 @@ function RomImporter:_drawGamePanel(version, x, y, w, h)
if sfNotice and sfNotice.dir then
iy = iy + 4 * s
love.graphics.setFont(self.hintFont)
local label = "Open folder"
local label = Strings("Open folder")
local lw = self.hintFont:getWidth(label)
local frect = { x = ix, y = iy, width = lw, height = self.hintFont:getHeight(),
dir = sfNotice.dir }
@@ -2228,14 +2229,13 @@ function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
love.graphics.setFont(self.slotNameFont)
col(PAL.white)
local name = slot.name or "NEW GAME"
local name = slot.name or Strings("NEW GAME")
printB(ellipsize(self.slotNameFont, name, rw - 24 * s - math.max(pillW, rightReserve)),
rx + 12 * s, ry + rowPadV)
local metaTxt
if slot.exists and slot.meta then
metaTxt = ("%d badges - %s - %d caught"):format(
slot.meta.badges or 0, slot.meta.timeText or "0:00",
metaTxt = Strings("%d badges - %s - %d caught", slot.meta.badges or 0, slot.meta.timeText or "0:00",
slot.meta.dexCount or 0)
else
metaTxt = "empty slot"
@@ -2339,7 +2339,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
for _, m in ipairs(mods) do if m.enabled then enabledCount = enabledCount + 1 end end
love.graphics.setFont(self.hintFont)
col(PAL.warning)
love.graphics.print(("%d of %d enabled"):format(enabledCount, #mods),
love.graphics.print(Strings("%d of %d enabled", enabledCount, #mods),
x + nameW + 14 * s, y + (headerH - self.hintFont:getHeight()) / 2)
local btnLabel = "Import mod .zip"
@@ -2360,7 +2360,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
else
col(PAL.warning)
love.graphics.printf(self.android and "Or copy a mod .zip via USB."
or "Or drop a mod .zip onto the window.", x, top, w, "left")
or Strings("Or drop a mod .zip onto the window."), x, top, w, "left")
end
top = top + self.hintFont:getHeight() + 12 * s
@@ -2376,7 +2376,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
col(PAL.warning)
local emptyHint = self.android
and "No mods installed - tap Import mod .zip to add one."
or "No mods installed - drop a mod .zip here to add one."
or Strings("No mods installed - drop a mod .zip here to add one.")
love.graphics.printf(emptyHint,
x + 16 * s, top + boxH / 2 - self.hintFont:getHeight() / 2, w - 32 * s, "center")
self.modRects = {}
+48 -46
View File
@@ -12,6 +12,7 @@
local Pokemon = require("src.pokemon.Pokemon")
local Flags = require("src.script.Flags")
local Strings = require("src.core.Strings")
local ItemEffects = {}
@@ -119,8 +120,8 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
if battle and (VITAMINS[itemId] or STONES[itemId] or itemId == "PP_UP"
or itemId == "RARE_CANDY" or itemId == "COIN_CASE"
or (itemDef and itemDef.machine)) then
return "failed", { "OAK: " .. save.player.name
.. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!",
save.player.name) }
end
if BALLS[itemId] then
@@ -138,11 +139,12 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
local mapId, npc = adjacentSleepingSnorlax(save, ow)
if npc then
return "flute_wake", { data.text._PlayedFluteHadEffectText
or "{PLAYER} played the\nPOKé FLUTE." }, { mapId = mapId, npc = npc }
or Strings("{PLAYER} played the\nPOKé FLUTE.") },
{ mapId = mapId, npc = npc }
end
-- otherwise: play the tune, nothing happens (ItemUsePokeFlute's
-- PlayedFluteNoEffectText branch)
return "flute_field", { "Played the POKé\nFLUTE.\fNow, that's a\ncatchy tune!" }
return "flute_field", { Strings("Played the POKé\nFLUTE.\fNow, that's a\ncatchy tune!") }
end
local woke = false
local function wake(mon)
@@ -157,24 +159,24 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
-- WakeUpEntireParty runs on the enemy's bench too
for _, mon in ipairs(battle.enemyParty or {}) do wake(mon) end
if not woke then
return "failed", { "Played the POKé\nFLUTE.\fNow, that's a\ncatchy tune!" }
return "failed", { Strings("Played the POKé\nFLUTE.\fNow, that's a\ncatchy tune!") }
end
return "flute", { ("%s played the\nPOKé FLUTE."):format(save.player.name),
"All sleeping\nPOKéMON woke up!" }
return "flute", { Strings("%s played the\nPOKé FLUTE.", save.player.name),
Strings("All sleeping\nPOKéMON woke up!") }
end
-- battle-only items
if X_ITEMS[itemId] or itemId == "DIRE_HIT" or itemId == "GUARD_SPEC"
or itemId == "POKE_DOLL" then
if not battle then
return "failed", { "OAK: " .. save.player.name .. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
local b = battle.player
if itemId == "X_ACCURACY" then
-- ItemUseXAccuracy sets USING_X_ACCURACY: moves never miss
-- (not an accuracy stage)
b.xAccuracy = true
return "consumed", { ("%s's\nhits will never\nmiss!"):format(b.name) }
return "consumed", { Strings("%s's\nhits will never\nmiss!", b.name) }
end
if X_ITEMS[itemId] then
local stat = X_ITEMS[itemId]
@@ -183,28 +185,28 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
-- effect, so at +6 it is still consumed and StatModifierUpEffect
-- just prints "Nothing happened!"
if cur >= 6 then
return "consumed", { "Nothing happened!" }
return "consumed", { Strings("Nothing happened!") }
end
b.stages[stat] = cur + 1
return "consumed", { ("%s's\n%s rose!"):format(b.name, stat:upper()) }
return "consumed", { Strings("%s's\n%s rose!", b.name, stat:upper()) }
end
-- ItemUseDireHit/ItemUseGuardSpec always set the bit and consume
-- the item, even when it is already active
if itemId == "DIRE_HIT" then
b.focusEnergy = true
return "consumed", { ("%s's\ngetting pumped!"):format(b.name) }
return "consumed", { Strings("%s's\ngetting pumped!", b.name) }
end
if itemId == "GUARD_SPEC" then
b.mist = true
return "consumed", { ("%s's\nprotected against\nstat changes!"):format(b.name) }
return "consumed", { Strings("%s's\nprotected against\nstat changes!", b.name) }
end
if itemId == "POKE_DOLL" then
if battle.kind ~= "wild" then
-- ItemUsePokeDoll jumps to ItemUseNotTime in trainer battles
return "failed", { "OAK: " .. save.player.name
.. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings(
"OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
return "consumed_escape", { "The wild POKéMON\nran away!" }
return "consumed_escape", { Strings("The wild POKéMON\nran away!") }
end
end
@@ -213,7 +215,7 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
-- restore every move with no menu.
if itemId == "ETHER" or itemId == "MAX_ETHER"
or itemId == "ELIXER" or itemId == "MAX_ELIXER" then
if not target then return "failed", { "It won't have\nany effect." } end
if not target then return "failed", { Strings("It won't have\nany effect.") } end
local restored = false
local full = itemId == "MAX_ETHER" or itemId == "MAX_ELIXER"
local allMoves = itemId == "ELIXER" or itemId == "MAX_ELIXER"
@@ -235,9 +237,9 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
restored = mv and restore(mv) or false
end
if not restored then
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
return "consumed", { ("%s's PP\nwas restored!"):format(monName(data, target)) }
return "consumed", { Strings("%s's PP\nwas restored!", monName(data, target)) }
end
local heal = HEAL_AMOUNT[itemId]
@@ -250,17 +252,17 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
target.status = nil
cureActiveToxic(battle, target)
require("src.core.Sound").play(data, "Heal_Ailment")
return "consumed", { ("%s's\nstatus returned\nto normal!"):format(monName(data, target)) }
return "consumed", { Strings("%s's\nstatus returned\nto normal!", monName(data, target)) }
end
if not target or target.hp <= 0 or target.hp >= target.stats.hp then
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
if itemId == "MAX_POTION" or itemId == "FULL_RESTORE" then
target.hp = target.stats.hp
else
target.hp = math.min(target.stats.hp, target.hp + heal)
end
local msgs = { ("%s's HP\nwas restored!"):format(monName(data, target)) }
local msgs = { Strings("%s's HP\nwas restored!", monName(data, target)) }
if itemId == "FULL_RESTORE" then
target.status = nil
cureActiveToxic(battle, target)
@@ -272,27 +274,27 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
local cures = STATUS_HEAL[itemId]
if cures then
if not target or not target.status or not cures[target.status] then
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
target.status = nil
cureActiveToxic(battle, target)
require("src.core.Sound").play(data, "Heal_Ailment")
return "consumed", { ("%s's\nstatus returned\nto normal!"):format(monName(data, target)) }
return "consumed", { Strings("%s's\nstatus returned\nto normal!", monName(data, target)) }
end
if itemId == "REVIVE" or itemId == "MAX_REVIVE" then
if not target or target.hp > 0 then
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
target.status = nil
target.hp = itemId == "REVIVE" and math.floor(target.stats.hp / 2) or target.stats.hp
require("src.core.Sound").play(data, "Heal_HP")
return "consumed", { ("%s\nis revitalized!"):format(monName(data, target)) }
return "consumed", { Strings("%s\nis revitalized!", monName(data, target)) }
end
if itemId == "RARE_CANDY" then
if not target or target.level >= 100 then
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
local Growth = require("src.pokemon.Growth")
local Stats = require("src.pokemon.Stats")
@@ -302,56 +304,56 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
local old = target.stats
target.stats = Stats.calc(speciesDef, target.level, target.dvs, target.statExp)
target.hp = math.min(target.stats.hp, target.hp + (target.stats.hp - old.hp))
return "consumed", { ("%s grew\nto level %d!"):format(monName(data, target), target.level) },
return "consumed", { Strings("%s grew\nto level %d!", monName(data, target), target.level) },
{ leveledTo = target.level }
end
if STONES[itemId] then
if not target then return "failed", { "It won't have\nany effect." } end
if not target then return "failed", { Strings("It won't have\nany effect.") } end
local speciesDef = data.pokemon[target.species]
for _, evo in ipairs(speciesDef.evolutions) do
if evo.method == "ITEM" and evo.item == itemId then
return "consumed", nil, { evolveTo = evo.species }
end
end
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
-- vitamins: +2560 stat exp, refused at 25600+ (ItemUseVitamin,
-- engine/items/item_effects.asm)
local vitaminStat = VITAMINS[itemId]
if vitaminStat then
if not target then return "failed", { "It won't have\nany effect." } end
if not target then return "failed", { Strings("It won't have\nany effect.") } end
target.statExp = target.statExp or {}
local cur = target.statExp[vitaminStat] or 0
if cur >= 25600 then
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
target.statExp[vitaminStat] = math.min(65535, cur + 2560)
local Stats = require("src.pokemon.Stats")
target.stats = Stats.calc(data.pokemon[target.species], target.level,
target.dvs, target.statExp)
target.hp = math.min(target.hp, target.stats.hp)
return "consumed", { ("%s's %s\nrose!"):format(monName(data, target),
return "consumed", { Strings("%s's %s\nrose!", monName(data, target),
vitaminStat == "hp" and "HP" or vitaminStat:upper()) }
end
-- PP UP boosts the move the player picked (ItemUsePPUp's move menu)
if itemId == "PP_UP" then
if not target then return "failed", { "It won't have\nany effect." } end
if not target then return "failed", { Strings("It won't have\nany effect.") } end
local mv = target.moves[moveIndex or 1]
local mdef = mv and data.moves[mv.id]
if mdef and (mv.ppUps or 0) < 3 then
mv.ppUps = (mv.ppUps or 0) + 1
-- each PP UP adds maxPP/5 uses on top of the base maximum
mv.pp = mv.pp + math.floor(mdef.pp / 5)
return "consumed", { ("%s's PP\nincreased!"):format(mdef.name) }
return "consumed", { Strings("%s's PP\nincreased!", mdef.name) }
end
return "failed", { "It won't have\nany effect." }
return "failed", { Strings("It won't have\nany effect.") }
end
if itemDef and itemDef.machine then
if not target then return "failed", { "It won't have\nany effect." } end
if not target then return "failed", { Strings("It won't have\nany effect.") } end
local speciesDef = data.pokemon[target.species]
local ok = false
for _, m in ipairs(speciesDef.tmhm) do
@@ -362,11 +364,11 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
-- plays SFX_DENIED before MonCannotLearnMachineMoveText (the generic
-- ItemUseNotTime/NoCyclingAllowedHere paths are silent)
require("src.core.Sound").play(data, "Denied")
return "failed", { ("%s can't\nlearn that move!"):format(monName(data, target)) }
return "failed", { Strings("%s can't\nlearn that move!", monName(data, target)) }
end
for _, mv in ipairs(target.moves) do
if mv.id == itemDef.machine.move then
return "failed", { "It knows that\nmove already!" }
return "failed", { Strings("It knows that\nmove already!") }
end
end
-- HMs are never consumed; TMs are single-use
@@ -375,14 +377,14 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
if itemId == "OLD_ROD" or itemId == "GOOD_ROD" or itemId == "SUPER_ROD" then
if battle then
return "failed", { "OAK: " .. save.player.name .. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
return "fish", itemId
end
if itemId == "BICYCLE" then
if battle then
return "failed", { "OAK: " .. save.player.name .. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
return "bicycle"
end
@@ -392,26 +394,26 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
end
if itemId == "TOWN_MAP" then
if battle then
return "failed", { "OAK: " .. save.player.name .. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
return "townmap"
end
if itemId == "ITEMFINDER" then
if battle then
return "failed", { "OAK: " .. save.player.name .. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
return "itemfinder"
end
if itemId == "COIN_CASE" then
return "failed", { ("Coin count:\n%d"):format(save.coins or 0) }
return "failed", { Strings("Coin count:\n%d", save.coins or 0) }
end
if itemId == "REPEL" or itemId == "SUPER_REPEL" or itemId == "MAX_REPEL" then
local steps = itemId == "REPEL" and 100 or itemId == "SUPER_REPEL" and 200 or 250
save.repelSteps = steps
return "consumed", { ("%s used\n%s!"):format(save.player.name, name) }
return "consumed", { Strings("%s used\n%s!", save.player.name, name) }
end
return "failed", { "OAK: " .. save.player.name .. "!\nThis isn't the\ntime to use that!" }
return "failed", { Strings("OAK: %s!\nThis isn't the\ntime to use that!", save.player.name) }
end
return ItemEffects
+25 -24
View File
@@ -21,6 +21,7 @@ local Logger = require("src.core.Logger")
local Protocol = require("src.link.Protocol")
local Runtime = require("src.mods.Runtime")
local TurnOrder = require("src.battle.TurnOrder")
local Strings = require("src.core.Strings")
local LinkBattle = {}
@@ -177,18 +178,18 @@ function LinkBattle.new(game, net, opts)
local theirName = opts.theirName or "FOE"
if not Handshake.battleAllowed(opts.verdict) then
return nil, "Link battle needs\nthe same mods on\nboth games."
return nil, Strings("Link battle needs\nthe same mods on\nboth games.")
end
-- both parties pass through the same pack->unpack clamp on both
-- machines, so the copies are identical everywhere
local unpackOpts = { strict = opts.strict or false, forceLevel = opts.forceLevel }
local myParty, myErr = unpackParty(game, opts.myParty, unpackOpts, function(p)
return ("Your %s can't\nbattle on the\nother game."):format(tostring(p.species))
return Strings("Your %s can't\nbattle on the\nother game.", tostring(p.species))
end)
if not myParty then return nil, myErr end
local theirParty, theirErr = unpackParty(game, opts.theirParty, unpackOpts, function(p, why)
return ("Their %s isn't\nin this game.\n(%s)"):format(tostring(p.species), tostring(why))
return Strings("Their %s isn't\nin this game.\n(%s)", tostring(p.species), tostring(why))
end)
if not theirParty then return nil, theirErr end
if #myParty == 0 or #theirParty == 0 then
@@ -232,7 +233,7 @@ function LinkBattle.new(game, net, opts)
self.enemyParty = theirParty
self.playerParty = myParty -- intro ball row uses the clamped copies
self.opponentName = theirName
self.introText = ("%s wants\nto battle!"):format(theirName)
self.introText = Strings("%s wants\nto battle!", theirName)
self.remoteHashes = {}
self.localHashes = {}
self.remoteParts = {}
@@ -292,7 +293,7 @@ function LinkBattle.new(game, net, opts)
battle = s, side = s.sides[2], battler = s.enemy, previous = previous,
})
s.enemySendingOut = true
s:sayNext(("%s sent\nout %s!"):format(theirName, s.enemy.name))
s:sayNext(Strings("%s sent\nout %s!", theirName, s.enemy.name))
s:actNext(function()
s.enemySendingOut = false
s:startGrowIn(s.enemy)
@@ -315,8 +316,9 @@ function LinkBattle.new(game, net, opts)
tostring(turn), component, tostring(localH), tostring(remoteH))
Runtime.emit("link.desync", { turn = turn, component = component,
localHash = localH, remoteHash = remoteH })
endAsDraw(s, ("Link desync!\n%s differs.\fAre both games\nrunning the same\nmods?")
:format(component))
endAsDraw(s, Strings(
"Link desync!\n%s differs.\fAre both games\nrunning the same\nmods?",
component))
end
-- a verified turn stays recorded: consuming it here left a finished
@@ -349,7 +351,7 @@ function LinkBattle.new(game, net, opts)
local function resolveLockstep(s, myMsg, theirMsg)
if myMsg.kind == "run" or theirMsg.kind == "run" then
local who = myMsg.kind == "run" and game.save.player.name or theirName
endAsDraw(s, ("%s ran from\nthe battle!"):format(who))
endAsDraw(s, Strings("%s ran from\nthe battle!", who))
return
end
s.phase = "messages"
@@ -479,9 +481,9 @@ function LinkBattle.new(game, net, opts)
party = myParty,
onSwitch = function(mon)
if mon == s.player.mon then
s:say(("%s is\nalready out!"):format(s.player.name))
s:say(Strings("%s is\nalready out!", s.player.name))
elseif mon.hp <= 0 then
s:say("There's no will\nto fight!")
s:say(Strings("There's no will\nto fight!"))
else
s:resolveSwitch(mon)
end
@@ -491,7 +493,7 @@ function LinkBattle.new(game, net, opts)
end
self.openItems = function(s)
s:say("Items can't be\nused in a link\nbattle!")
s:say(Strings("Items can't be\nused in a link\nbattle!"))
s.phase = "messages"
s.afterQueue = "menu"
end
@@ -509,7 +511,7 @@ function LinkBattle.new(game, net, opts)
return
end
end
s:sayNext(("%s is out of\nPOKéMON!\f%s wins!"):format(game.save.player.name,
s:sayNext(Strings("%s is out of\nPOKéMON!\f%s wins!", game.save.player.name,
theirName))
s.result = "lose"
s.afterQueue = "finish"
@@ -522,7 +524,7 @@ function LinkBattle.new(game, net, opts)
return
end
end
s:sayNext(("%s is out of\nPOKéMON!\f%s wins!"):format(theirName,
s:sayNext(Strings("%s is out of\nPOKéMON!\f%s wins!", theirName,
game.save.player.name))
s.result = "win"
s.afterQueue = "finish"
@@ -543,13 +545,13 @@ function LinkBattle.new(game, net, opts)
-- only a draw if our own simulation hasn't already decided
-- (the winner's bye can arrive while we're still animating)
if not s.result then
endAsDraw(s, ("%s left the\nbattle."):format(theirName))
endAsDraw(s, Strings("%s left the\nbattle.", theirName))
end
elseif msg.type == "forfeit" then
-- the peer's own shot clock ran out; unlike a mutual RUN/desync
-- draw, this has a definite winner (us)
if not s.result then
endWithResult(s, "win", ("%s ran out of\ntime!"):format(theirName))
endWithResult(s, "win", Strings("%s ran out of\ntime!", theirName))
end
else
-- a tournament control message (bracket_update, the next
@@ -582,7 +584,7 @@ function LinkBattle.new(game, net, opts)
if s.turnClock <= 0 then
s.turnClockActive = false
send({ type = "forfeit" })
endWithResult(s, "lose", "Time's up! You\nforfeit the match.")
endWithResult(s, "lose", Strings("Time's up! You\nforfeit the match."))
end
elseif opts.turnLimit then
s.turnClockActive = false
@@ -643,17 +645,16 @@ function LinkBattle.newSpectator(game, net, opts)
local guestName = opts.guestName or "GUEST"
if not Handshake.battleAllowed(opts.verdict) then
return nil, "Link battle needs\nthe same mods on\nboth games."
return nil, Strings("Link battle needs\nthe same mods on\nboth games.")
end
local unpackOpts = { strict = opts.strict or false, forceLevel = opts.forceLevel }
local hostParty, hostErr = unpackParty(game, opts.hostParty, unpackOpts, function(p)
return ("%s's %s can't\nbattle on this\ngame."):format(hostName, tostring(p.species))
return Strings("%s's %s can't\nbattle on this\ngame.", hostName, tostring(p.species))
end)
if not hostParty then return nil, hostErr end
local guestParty, guestErr = unpackParty(game, opts.guestParty, unpackOpts, function(p, why)
return ("%s's %s can't\nbattle on this\ngame.\n(%s)"):format(
guestName, tostring(p.species), tostring(why))
return Strings("%s's %s can't\nbattle on this\ngame.\n(%s)", guestName, tostring(p.species), tostring(why))
end)
if not guestParty then return nil, guestErr end
if #hostParty == 0 or #guestParty == 0 then
@@ -672,7 +673,7 @@ function LinkBattle.newSpectator(game, net, opts)
self.enemyParty = guestParty
self.playerParty = hostParty
self.opponentName = guestName
self.introText = ("%s vs %s!"):format(hostName, guestName)
self.introText = Strings("%s vs %s!", hostName, guestName)
local function orderMove(action)
if action and action.id then return game.data.moves[action.id] end
@@ -712,7 +713,7 @@ function LinkBattle.newSpectator(game, net, opts)
battle = s, side = s.sides[2], battler = s.enemy, previous = previous,
})
s.enemySendingOut = true
s:sayNext(("%s sent\nout %s!"):format(guestName, s.enemy.name))
s:sayNext(Strings("%s sent\nout %s!", guestName, s.enemy.name))
s:actNext(function()
s.enemySendingOut = false
s:startGrowIn(s.enemy)
@@ -789,7 +790,7 @@ function LinkBattle.newSpectator(game, net, opts)
return
end
end
s:sayNext(("%s is out of\nPOKéMON!\f%s wins!"):format(hostName, guestName))
s:sayNext(Strings("%s is out of\nPOKéMON!\f%s wins!", hostName, guestName))
s.result = "guestWin"
s.afterQueue = "finish"
end
@@ -801,7 +802,7 @@ function LinkBattle.newSpectator(game, net, opts)
return
end
end
s:sayNext(("%s is out of\nPOKéMON!\f%s wins!"):format(guestName, hostName))
s:sayNext(Strings("%s is out of\nPOKéMON!\f%s wins!", guestName, hostName))
s.result = "hostWin"
s.afterQueue = "finish"
end
+46 -45
View File
@@ -11,6 +11,7 @@ local Protocol = require("src.link.Protocol")
local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local LinkState = {}
LinkState.__index = LinkState
@@ -90,7 +91,7 @@ function LinkState.newJoinOnline(game, code)
self.stage = "onlineJoining"
else
self.stage = "menu" -- exitWith below needs a real stage to unwind from
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
end
return self
end
@@ -174,7 +175,7 @@ function LinkState:update(dt)
if self.net then
self.net:update()
if self.net.error and not PRE_CONNECT_STAGES[self.stage] then
self:exitWith("Link error:\n" .. self.net.error:sub(1, 60))
self:exitWith(Strings("Link error:\n%s", self.net.error:sub(1, 60)))
return
end
-- the peer vanished without a bye (only once the inbox is drained,
@@ -183,7 +184,7 @@ function LinkState:update(dt)
and not PRE_CONNECT_STAGES[self.stage] and self.stage ~= "addrEntry"
and self.stage ~= "codeEntry" and self.stage ~= "notice"
and self.stage ~= "battleRunning" then
self:exitWith("The link was\nbroken.")
self:exitWith(Strings("The link was\nbroken."))
return
end
end
@@ -201,7 +202,7 @@ function LinkState:update(dt)
self.index = 1
elseif self.index == 2 or self.index == 3 then
if not Handshake.onlineAllowed(self.game) then
self:exitWith("Online play needs\nno mods enabled.\fDisable them in\nSTART > MODS.")
self:exitWith(Strings("Online play needs\nno mods enabled.\fDisable them in\nSTART > MODS."))
return
end
if self.index == 2 then
@@ -227,7 +228,7 @@ function LinkState:update(dt)
if self.net:host() then
self.stage = "hosting"
else
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
end
else
self.stage = "addrEntry"
@@ -246,7 +247,7 @@ function LinkState:update(dt)
if self.net:hostOnline() then
self.stage = "onlineHosting"
else
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
end
else
self.stage = "codeEntry"
@@ -284,7 +285,7 @@ function LinkState:update(dt)
if self.net:joinOnline(nil, code) then
self.stage = "onlineJoining"
else
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
end
end
@@ -323,7 +324,7 @@ function LinkState:update(dt)
if self.net:join(table.concat(octets, ".")) then
self.stage = "joining"
else
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
end
end
@@ -433,7 +434,7 @@ function LinkState:update(dt)
end
if not battle then
self.net:send({ type = "bye" })
self:exitWith(why or "Link battle\ncan't start.", "error")
self:exitWith(why or Strings("Link battle\ncan't start."), "error")
return
end
self.game.stack:push(battle)
@@ -491,8 +492,8 @@ function LinkState:updateTrade(input)
local t = self.trade
if t.stage == "cancelled" then
self:exitWith(t.error and ("The trade stopped:\n%s."):format(t.error)
or "The trade was\ncancelled.")
self:exitWith(t.error and Strings("The trade stopped:\n%s.", t.error)
or Strings("The trade was\ncancelled."))
return
end
if t.stage == "done" then
@@ -520,7 +521,7 @@ function LinkState:updateTrade(input)
enemyOtId = received.otId,
onDone = function()
game.stack:push(TextBox.new(game,
("Trade completed!\f%s received\n%s!"):format(game.save.player.name, name),
Strings("Trade completed!\f%s received\n%s!", game.save.player.name, name),
function()
if evoTo then
-- via="TRADE": a trade evolution cannot be B-cancelled
@@ -547,7 +548,7 @@ function LinkState:updateTrade(input)
-- the trade) -- B is dead after that, matching the A branch's own
-- self.confirmed == nil guard
self.net:send({ type = "bye" })
self:exitWith("The trade was\ncancelled.")
self:exitWith(Strings("The trade was\ncancelled."))
elseif t.stage == "picking" and input:wasPressed("a") then
if t:canPick(self.index) then
self.net:send(t:pick(self.index))
@@ -574,30 +575,30 @@ end
function LinkState:draw()
if self.stage == "menu" then
drawTitle("BOIS CLUB LIVE")
Font.draw("LINK CABLE (LAN)", 32, 44)
Font.draw("ONLINE MATCH", 32, 60)
Font.draw("TOURNAMENT", 32, 76)
Font.draw(Strings("LINK CABLE (LAN)"), 32, 44)
Font.draw(Strings("ONLINE MATCH"), 32, 60)
Font.draw(Strings("TOURNAMENT"), 32, 76)
Font.drawCode(CURSOR, 24, 44 + (self.index - 1) * 16)
elseif self.stage == "lanMenu" then
drawTitle("LINK CABLE (LAN)")
Font.draw("HOST A GAME", 32, 48)
Font.draw("JOIN A GAME", 32, 68)
Font.draw(Strings("HOST A GAME"), 32, 48)
Font.draw(Strings("JOIN A GAME"), 32, 68)
Font.drawCode(CURSOR, 24, self.index == 1 and 48 or 68)
Font.draw("UDP port " .. Net.defaultPort(), 8, 128)
Font.draw(Strings("UDP port %s", Net.defaultPort()), 8, 128)
elseif self.stage == "onlineMenu" then
drawTitle("ONLINE MATCH")
Font.draw("HOST ONLINE", 32, 48)
Font.draw("JOIN ONLINE", 32, 68)
Font.draw(Strings("HOST ONLINE"), 32, 48)
Font.draw(Strings("JOIN ONLINE"), 32, 68)
Font.drawCode(CURSOR, 24, self.index == 1 and 48 or 68)
elseif self.stage == "onlineHosting" then
drawTitle("HOSTING ONLINE")
Font.draw("Tell your friend", 16, 40)
Font.draw("the code:", 16, 52)
Font.draw(Strings("Tell your friend"), 16, 40)
Font.draw(Strings("the code:"), 16, 52)
Font.draw(self.net.code or "??????", 32, 68)
Font.draw("Waiting for join...", 8, 96)
Font.draw(Strings("Waiting for join..."), 8, 96)
elseif self.stage == "codeEntry" then
drawTitle("ENTER CODE")
@@ -609,18 +610,18 @@ function LinkState:draw()
Font.drawCode(0xEE, x, 76) -- ▼ under the active slot
end
end
Font.draw("A: connect B: back", 8, 128)
Font.draw(Strings("A: connect B: back"), 8, 128)
elseif self.stage == "onlineJoining" then
drawTitle("CONNECTING...")
Font.draw("Calling...", 8, 56)
Font.draw(Strings("Calling..."), 8, 56)
Font.draw(self.net.target or "", 8, 72)
elseif self.stage == "hosting" then
drawTitle("HOSTING")
Font.draw("Friend joins at:", 16, 48)
Font.draw(Strings("Friend joins at:"), 16, 48)
Font.draw(self.net.address or "?", 16, 64)
Font.draw("Waiting for join...", 8, 96)
Font.draw(Strings("Waiting for join..."), 8, 96)
elseif self.stage == "addrEntry" then
drawTitle("ENTER HOST ADDRESS")
@@ -635,34 +636,34 @@ function LinkState:draw()
for octet = 1, 3 do
Font.draw(".", 16 + octet * 32 - 8, 64)
end
Font.draw("Port: " .. Net.defaultPort(), 16, 96)
Font.draw("A: connect B: back", 8, 128)
Font.draw(Strings("Port: %s", Net.defaultPort()), 16, 96)
Font.draw(Strings("A: connect B: back"), 8, 128)
elseif self.stage == "joining" then
drawTitle("JOINING...")
Font.draw("Calling...", 8, 56)
Font.draw(Strings("Calling..."), 8, 56)
Font.draw(self.net.target or "", 8, 72)
elseif self.stage == "modeSelect" then
drawTitle("CONNECTED!")
Font.draw("TRADE", 32, 48)
Font.draw("BATTLE", 32, 68)
Font.draw(Strings("TRADE"), 32, 48)
Font.draw(Strings("BATTLE"), 32, 68)
Font.drawCode(CURSOR, 24, self.index == 1 and 48 or 68)
elseif self.stage == "battleOptions" then
drawTitle("BATTLE OPTIONS")
Font.draw("LEVELS:", 16, 56)
Font.draw(Strings("LEVELS:"), 16, 56)
Font.draw(forceLevelLabel(self.levelChoice or ANY), 88, 56)
Font.draw("A: continue B: back", 8, 128)
Font.draw(Strings("A: continue B: back"), 8, 128)
elseif self.stage == "waitMode" or self.stage == "waitHello" then
drawTitle("CONNECTED!")
if self.stage == "waitHello" then
Font.draw("Checking the", 16, 56)
Font.draw("other game...", 16, 72)
Font.draw(Strings("Checking the"), 16, 56)
Font.draw(Strings("other game..."), 16, 72)
else
Font.draw("Waiting for the", 16, 56)
Font.draw("host to choose...", 16, 72)
Font.draw(Strings("Waiting for the"), 16, 56)
Font.draw(Strings("host to choose..."), 16, 72)
end
elseif self.stage == "notice" then
@@ -671,12 +672,12 @@ function LinkState:draw()
if i > 8 then break end -- what fits above the prompt row
Font.draw(line, 8, 24 + (i - 1) * 12)
end
Font.draw(self.noticeExits and "A: back" or "A: trade anyway", 8, 128)
Font.draw(self.noticeExits and "A: back" or Strings("A: trade anyway"), 8, 128)
elseif self.stage == "trade" then
drawTitle("TRADE")
local t = self.trade
Font.draw("YOURS", 8, 20)
Font.draw(Strings("YOURS"), 8, 20)
for i, mon in ipairs(self.game.save.party) do
local def = self.game.data.pokemon[mon.species]
local label = (mon.nickname or def.name):sub(1, 8)
@@ -684,7 +685,7 @@ function LinkState:draw()
Font.draw(label, 16, 20 + i * 12)
if i == self.index then Font.drawCode(CURSOR, 8, 20 + i * 12) end
end
Font.draw("THEIRS", 84, 20)
Font.draw(Strings("THEIRS"), 84, 20)
for i, mon in ipairs(t.theirParty or {}) do
local def = self.game.data.pokemon[mon.species]
Font.draw((mon.nickname or def.name):sub(1, 8), 92, 20 + i * 12)
@@ -695,16 +696,16 @@ function LinkState:draw()
elseif t.stage == "waitParty" then hint = "Exchanging data..."
elseif t.stage == "picking" then
hint = t:canPick(self.index) and "Pick one to trade"
or "X: not on theirs"
or Strings("X: not on theirs")
elseif t.stage == "waitPick" then hint = "Waiting for them..."
elseif t.stage == "confirming" then
hint = self.confirmed and "Waiting..." or "A: trade B: cancel"
hint = self.confirmed and "Waiting..." or Strings("A: trade B: cancel")
end
Font.draw(hint or "", 8, 132)
elseif self.stage == "battleWait" or self.stage == "battleRunning" then
drawTitle("LINK BATTLE")
Font.draw("Exchanging data...", 16, 64)
Font.draw(Strings("Exchanging data..."), 16, 64)
end
love.graphics.setColor(1, 1, 1, 1)
end
+13 -9
View File
@@ -26,6 +26,7 @@
local Json = require("src.link.Json")
local Logger = require("src.core.Logger")
local Strings = require("src.core.Strings")
local hasEnet, enet = pcall(require, "enet")
if not hasEnet then enet = nil end
@@ -173,7 +174,7 @@ function Net:connectTCP(addr)
tcp:settimeout(5)
local ok, err = tcp:connect(host, port)
if not ok then
self.error = ("can't reach relay %s:%d\n(%s)"):format(host, port, tostring(err))
self.error = Strings("can't reach relay %s:%d\n(%s)", host, port, tostring(err))
return false
end
tcp:settimeout(0)
@@ -236,11 +237,14 @@ local function handleGenericRelayControl(self, msg)
self.paired = true
return true
elseif msg.type == "join_error" then
self.error = ({
not_found = "That code wasn't\nfound.",
full = "That game already\nhas two players.",
expired = "That code has\nexpired.",
})[msg.reason] or ("Couldn't join:\n%s"):format(tostring(msg.reason))
self.error = Strings(({
not_found = Strings.source("That code wasn't\nfound."),
full = Strings.source("That game already\nhas two players."),
expired = Strings.source("That code has\nexpired."),
})[msg.reason] or "")
if self.error == "" then
self.error = Strings("Couldn't join:\n%s", tostring(msg.reason))
end
self.closed = true
return true
elseif msg.type == "peer_gone" then
@@ -328,7 +332,7 @@ function Net:update()
-- an unreachable join target surfaces as a service error
-- (ICMP unreachable on the connected UDP socket)
if self.mode == "joining" and not self.paired then
self.error = ("no answer from\n%s"):format(self.target or "the host")
self.error = Strings("no answer from\n%s", self.target or "the host")
else
self.error = tostring(event)
end
@@ -358,14 +362,14 @@ function Net:update()
self.closed = true
if not self.paired then
self.error = self.error or
("no answer from\n%s"):format(self.target or "the host")
Strings("no answer from\n%s", self.target or "the host")
end
end
end
end
if self.mode == "joining" and not self.paired
and self.joinDeadline and now() > self.joinDeadline then
self.error = ("no answer from\n%s"):format(self.target or "the host")
self.error = Strings("no answer from\n%s", self.target or "the host")
self.closed = true
pcall(function() self.peer:disconnect_now() end)
end
+35 -35
View File
@@ -18,6 +18,7 @@ local Protocol = require("src.link.Protocol")
local Runtime = require("src.mods.Runtime")
local Sound = require("src.core.Sound")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local Tournament = {}
Tournament.__index = Tournament
@@ -128,7 +129,7 @@ end
function Tournament:startHosting()
self.net = Net.new()
if not self.net:connectTCP(Net.defaultRelayAddress()) then
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
return
end
local size, minL, maxL = partyStats(self.game.save.party)
@@ -151,7 +152,7 @@ end
function Tournament:startJoining(code)
self.net = Net.new()
if not self.net:connectTCP(Net.defaultRelayAddress()) then
self:exitWith("Link error:\n" .. (self.net.error or "?"))
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
return
end
local size, minL, maxL = partyStats(self.game.save.party)
@@ -171,9 +172,9 @@ end
-- -------------------------------------------------------------------
local JOIN_ERROR_TEXT = {
not_found = "That code wasn't\nfound.",
already_started = "That tournament\nhas already begun.",
expired = "That code has\nexpired.",
not_found = Strings.source("That code wasn't\nfound."),
already_started = Strings.source("That tournament\nhas already begun."),
expired = Strings.source("That code has\nexpired."),
}
function Tournament:handleMessage(msg)
@@ -198,17 +199,17 @@ function Tournament:handleMessage(msg)
end
elseif msg.type == "tournament_host_error" then
if msg.reason == "party_ineligible" then
self:exitWith(("Can't host:\nneed %d Pokemon\nLv %s-%s."):format(
msg.requiredPartySize, levelLabel(msg.minLevel), levelLabel(msg.maxLevel)))
self:exitWith(Strings("Can't host:\nneed %d Pokemon\nLv %s-%s.", msg.requiredPartySize, levelLabel(msg.minLevel), levelLabel(msg.maxLevel)))
else
self:exitWith("Couldn't host\nthat tournament.")
self:exitWith(Strings("Couldn't host\nthat tournament."))
end
elseif msg.type == "tournament_join_error" then
if msg.reason == "party_ineligible" then
self:exitWith(("Your party needs\n%d Pokemon, Lv\n%s-%s."):format(
msg.requiredPartySize, levelLabel(msg.minLevel), levelLabel(msg.maxLevel)))
self:exitWith(Strings("Your party needs\n%d Pokemon, Lv\n%s-%s.", msg.requiredPartySize, levelLabel(msg.minLevel), levelLabel(msg.maxLevel)))
else
self:exitWith(JOIN_ERROR_TEXT[msg.reason] or "Couldn't join\nthat tournament.")
self:exitWith(JOIN_ERROR_TEXT[msg.reason]
and Strings(JOIN_ERROR_TEXT[msg.reason])
or Strings("Couldn't join\nthat tournament."))
end
elseif msg.type == "tournament_roster" then
self.roster = msg.players
@@ -281,7 +282,7 @@ function Tournament:beginMatchBattle()
-- shouldn't happen (both sides already passed the online-play mods
-- gate), but a mismatched engine/build is still possible -- bail out
-- of just this match rather than crash the tournament
self:exitWith("Link error:\nversion mismatch\nwith opponent.")
self:exitWith(Strings("Link error:\nversion mismatch\nwith opponent."))
return
end
self.linkSeed = self.isHost and love.math.random(1, 2 ^ 30) or nil
@@ -351,11 +352,11 @@ function Tournament:update(dt)
self.net:update()
if self.net.error and self.stage ~= "menu" and self.stage ~= "hostSettings"
and self.stage ~= "codeEntry" then
self:exitWith("Link error:\n" .. self.net.error:sub(1, 60))
self:exitWith(Strings("Link error:\n%s", self.net.error:sub(1, 60)))
return
end
if self.net.closed and self.stage ~= "done" then
self:exitWith("The tournament\nconnection was\nlost.")
self:exitWith(Strings("The tournament\nconnection was\nlost."))
return
end
end
@@ -381,7 +382,7 @@ function Tournament:update(dt)
and LinkBattle.newHost(self.game, self.net, self.pendingBattleOpts)
or LinkBattle.newGuest(self.game, self.net, self.pendingBattleOpts)
if not battle then
self:exitWith(why or "Link battle\ncan't start.")
self:exitWith(why or Strings("Link battle\ncan't start."))
return
end
-- anything after `party` in this same batch belongs to the
@@ -418,7 +419,7 @@ function Tournament:update(dt)
forceLevel = levelForWire(self.settings.forceLevel),
})
if not battle then
self:exitWith(why or "Can't watch this\nmatch.")
self:exitWith(why or Strings("Can't watch this\nmatch."))
return
end
for j = #msgs, i + 1, -1 do
@@ -541,8 +542,8 @@ local SETTINGS_LABELS = { "POKEMON", "MIN LV", "MAX LV", "TIMER", "LEVELS", "PLA
function Tournament:draw()
if self.stage == "menu" then
drawTitle("TOURNAMENT")
Font.draw("HOST", 32, 48)
Font.draw("JOIN", 32, 68)
Font.draw(Strings("HOST"), 32, 48)
Font.draw(Strings("JOIN"), 32, 68)
Font.drawCode(CURSOR, 24, self.index == 1 and 48 or 68)
elseif self.stage == "hostSettings" then
@@ -561,7 +562,7 @@ function Tournament:draw()
Font.draw(values[i], 96, y)
if i == self.settingsIndex then Font.drawCode(CURSOR, 8, y) end
end
Font.draw("START: create", 8, 128)
Font.draw(Strings("START: create"), 8, 128)
elseif self.stage == "codeEntry" then
drawTitle("ENTER CODE")
@@ -573,28 +574,27 @@ function Tournament:draw()
Font.drawCode(0xEE, x, 76)
end
end
Font.draw("A: join B: back", 8, 128)
Font.draw(Strings("A: join B: back"), 8, 128)
elseif self.stage == "registering" then
drawTitle("CONNECTING...")
Font.draw("B: cancel", 8, 128)
Font.draw(Strings("B: cancel"), 8, 128)
elseif self.stage == "bracket" or self.stage == "matchHello"
or self.stage == "matchWaitParty" or self.stage == "spectateWait" then
drawTitle(("TOURNAMENT %s"):format(self.code or "??????"))
drawTitle(Strings("TOURNAMENT %s", self.code or "??????"))
if self.bracket then
local y = 20
for _, round in ipairs(self.bracket.rounds) do
Font.draw(("ROUND %d"):format(round.round), 8, y)
Font.draw(Strings("ROUND %d", round.round), 8, y)
y = y + 10
for _, m in ipairs(round.matches) do
local line
if m.bye then
line = ("%s (bye)"):format(m.a or m.b or "?")
line = Strings("%s (bye)", m.a or m.b or "?")
else
local mark = m.state == "live" and "*" or (m.winner and "" or "")
line = ("%s%s vs %s%s"):format(
m.winner == m.a and ">" or " ", m.a or "?",
line = Strings("%s%s vs %s%s", m.winner == m.a and ">" or " ", m.a or "?",
m.b or "?", m.winner == m.b and "<" or (mark == "*" and " *" or ""))
end
Font.draw(line, 12, y)
@@ -604,11 +604,11 @@ function Tournament:draw()
end
else
if self.participating == false then
Font.draw("(organizing --", 16, 32)
Font.draw("not playing)", 16, 42)
Font.draw(Strings("(organizing --"), 16, 32)
Font.draw(Strings("not playing)"), 16, 42)
end
Font.draw("Waiting for", 16, 48)
Font.draw("players to join:", 16, 60)
Font.draw(Strings("Waiting for"), 16, 48)
Font.draw(Strings("players to join:"), 16, 60)
local y = 60
for i, name in ipairs(self.roster) do
y = 60 + i * 10
@@ -620,18 +620,18 @@ function Tournament:draw()
end
end
if self.isCreator and #self.roster >= 2 and not self.bracket then
Font.draw("A: START B: cancel", 8, 132)
Font.draw(Strings("A: START B: cancel"), 8, 132)
else
Font.draw("B: cancel", 8, 132)
Font.draw(Strings("B: cancel"), 8, 132)
end
elseif self.stage == "done" then
drawTitle("TOURNAMENT OVER")
if self.champion then
Font.draw(("%s is the"):format(self.champion), 16, 56)
Font.draw("champion!", 16, 68)
Font.draw(Strings("%s is the", self.champion), 16, 56)
Font.draw(Strings("champion!"), 16, 68)
end
Font.draw("A: continue", 8, 128)
Font.draw(Strings("A: continue"), 8, 128)
end
love.graphics.setColor(1, 1, 1, 1)
end
+21 -20
View File
@@ -10,6 +10,7 @@ local Semver = require("src.mods.Semver")
local Version = require("src.core.Version")
local Theme = require("src.ui.Theme")
local OptionRows = require("src.ui.OptionRows")
local Strings = require("src.core.Strings")
local ManagerState = {}
ManagerState.__index = ManagerState
@@ -269,7 +270,7 @@ function ManagerState:modRows()
end
end
if #rows == 0 then
rows[1] = { header = true, label = "NO MODS INSTALLED" }
rows[1] = { header = true, label = Strings("NO MODS INSTALLED") }
end
return rows
end
@@ -281,7 +282,7 @@ function ManagerState:profileRows()
rows[#rows + 1] = { profile = p, label = p.name,
glyph = opts.activeProfile == p.name and GLYPH.errored or " " }
end
rows[#rows + 1] = { saveAs = true, label = "SAVE CURRENT AS.." }
rows[#rows + 1] = { saveAs = true, label = Strings("SAVE CURRENT AS..") }
rows[#rows + 1] = { adhoc = true,
label = opts.activeProfile and "[AD-HOC]" or "[AD-HOC] (LIVE)" }
return rows
@@ -316,30 +317,30 @@ function ManagerState:detailRows(m)
rows[#rows + 1] = { label = m.enabled and "DISABLE" or "ENABLE",
action = function() self:beginToggle(m) end }
if self:schemaFor(m) then
rows[#rows + 1] = { label = "OPTIONS..",
rows[#rows + 1] = { label = Strings("OPTIONS.."),
action = function() self:openOptions(m) end }
end
if m.permissions and #m.permissions > 0 then
rows[#rows + 1] = { label = "PERMISSIONS..",
rows[#rows + 1] = { label = Strings("PERMISSIONS.."),
action = function() self:goTo("permissions") end }
end
if m.error then
rows[#rows + 1] = { label = "VIEW ERROR..",
rows[#rows + 1] = { label = Strings("VIEW ERROR.."),
action = function() self:goTo("errors") end }
end
rows[#rows + 1] = { label = "BACK", action = function() self:goBack() end }
rows[#rows + 1] = { label = Strings("BACK"), action = function() self:goBack() end }
return rows
end
function ManagerState:applyRows()
local rows = {}
rows[#rows + 1] = { label = "APPLY & RESTART", action = function()
rows[#rows + 1] = { label = Strings("APPLY & RESTART"), action = function()
self:openConfirm({ "RESTART NOW?" }, function() self:restartGame() end)
end }
rows[#rows + 1] = { label = "DISCARD CHANGES", action = function()
rows[#rows + 1] = { label = Strings("DISCARD CHANGES"), action = function()
self:discardChanges()
end }
rows[#rows + 1] = { label = "BACK", action = function() self:goBack() end }
rows[#rows + 1] = { label = Strings("BACK"), action = function() self:goBack() end }
return rows
end
@@ -352,7 +353,7 @@ function ManagerState:permissionRows(m)
label = info and info.text or name }
end
if #rows == 0 then
rows[1] = { inert = true, label = "DATA & API ONLY" }
rows[1] = { inert = true, label = Strings("DATA & API ONLY") }
end
return rows
end
@@ -564,7 +565,7 @@ function ManagerState:openCascade(r, m, want)
else
local dep = self.byId[r.alsoDisable[1]]
lines[#lines + 1] = (dep and dep.name or r.alsoDisable[1]) .. " NEEDS THIS."
lines[#lines + 1] = #r.alsoDisable > 1 and "DISABLE ALL?" or "DISABLE BOTH?"
lines[#lines + 1] = #r.alsoDisable > 1 and "DISABLE ALL?" or Strings("DISABLE BOTH?")
end
self.overlay = { kind = "confirm", lines = lines, index = 1,
onYes = function() self:commitToggle(r.apply) end }
@@ -700,7 +701,7 @@ end
function ManagerState:saveCurrentAs()
local NamingScreen = require("src.ui.NamingScreen")
self.game.stack:push(NamingScreen.new(self.game, {
title = "PROFILE NAME?",
title = Strings("PROFILE NAME?"),
maxLen = 10,
onDone = function(name)
local opts = self:optionsTable()
@@ -726,7 +727,7 @@ end
function ManagerState:renameProfile(p)
local NamingScreen = require("src.ui.NamingScreen")
self.game.stack:push(NamingScreen.new(self.game, {
title = "RENAME?",
title = Strings("RENAME?"),
maxLen = 10,
default = p.name,
onDone = function(name)
@@ -886,7 +887,7 @@ function ManagerState:buildOptionRows(m, schema)
end }
end
end
rows[#rows + 1] = { id = "__reset", label = "RESET DEFAULTS",
rows[#rows + 1] = { id = "__reset", label = Strings("RESET DEFAULTS"),
value = function() return "" end,
activate = function()
for _, row in ipairs(schema) do
@@ -1047,7 +1048,7 @@ function ManagerState:drawApply()
y = y + 1
end
if #staged == 0 then
Font.draw(Runtime.safeMode and "SAFE MODE" or "NO CHANGES", 16, y * 8)
Font.draw(Runtime.safeMode and "SAFE MODE" or Strings("NO CHANGES"), 16, y * 8)
y = y + 1
end
local rows = self:rowsForScreen()
@@ -1076,12 +1077,12 @@ function ManagerState:drawOverlay()
end
if overlay.kind == "confirm" then
local yesY = ty + #lines + 1
Font.draw("YES", 5 * 8, yesY * 8)
Font.draw("NO", 5 * 8, (yesY + 1) * 8)
Font.draw(Strings("YES"), 5 * 8, yesY * 8)
Font.draw(Strings("NO"), 5 * 8, (yesY + 1) * 8)
Font.drawCode(Theme.cursor, 4 * 8,
(overlay.index == 1 and yesY or yesY + 1) * 8)
else
Font.draw("A:OK", 5 * 8, (ty + #lines + 1) * 8)
Font.draw(Strings("A:OK"), 5 * 8, (ty + #lines + 1) * 8)
end
end
@@ -1090,7 +1091,7 @@ function ManagerState:draw()
OptionRows.draw(self.game, self.optionRows or {}, self.cursor,
self.scroll or 0)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(self.notice or "B:DONE (NO RESTART)", 8, 136)
Font.draw(self.notice or Strings("B:DONE (NO RESTART)"), 8, 136)
love.graphics.setColor(1, 1, 1, 1)
if self.overlay then self:drawOverlay() end
return
@@ -1099,7 +1100,7 @@ function ManagerState:draw()
love.graphics.rectangle("fill", 0, 0, 160, 144)
love.graphics.setColor(1, 1, 1, 1)
Font.drawBox(0, 0, 20, 18)
Font.draw(self.banner or "MOD MANAGER", 16, 8)
Font.draw(self.banner or Strings("MOD MANAGER"), 16, 8)
if self.screen == "list" then
self:drawList()
elseif self.screen == "detail" then
+31 -2
View File
@@ -584,6 +584,17 @@ R.text = {
example = 'mod.content.text:override("_PalletTownText1", "HELLO!")',
}
-- The engine's own authored text, the half of the game `text` does not
-- cover (src/core/Strings.lua). The id is the English source string, so a
-- translation supplies one entry per literal the engine draws and anything
-- it has not reached yet keeps rendering in English. Ids that collide in
-- English but not elsewhere carry a "context|source" prefix.
R.strings = {
semantics = "record", target = "strings",
value = f.str,
example = 'mod.content.strings:override("But, it failed!", "Echec !")',
}
-- the self-contained bytecode blob ChipAsm.song/sfx emit (13.1 shape 2);
-- shared by every namespace that plays a chip program
local chipProgram = f.rec{
@@ -918,8 +929,20 @@ R.battle_sprite_scales = {
-- 1 = native pixels; the drawn size relative to the pic's own pixels
scale = f.numRange(0.25, 4.0),
},
example = 'mod.content.battle_sprite_scales:register("abra_back", ' ..
'{ path = "assets/generated/battle/back/abrab.png", scale = 1.5 })',
example = [[
mod.content.battle_sprite_scales:register("abra_back", {
path = "assets/generated/battle/back/abrab.png",
scale = 1.5,
})]],
notes = [[
Scales one battle pic by its asset path, overriding the species-level
`battleScaleFront`/`battleScaleBack` (see [pokemon](#pokemon)) for that
image. The only way to scale a pic that is not species-keyed, like the
player's trainer back sprite. Resolution order at draw time:
image-level, then species-level, then the defaults (1x front, 2x back).
The pic stays grounded at any scale: player feet stay flush on the
text-box top, the enemy pic stays bottom-pinned in its slot, and the
scale composes with the send-out grow animation.]],
}
-- ------- progression
@@ -1146,6 +1169,12 @@ R.field = {
name = f.opt(f.str) })),
nest = f.opt(f.any) },
flyOrder = f.list(f.str),
-- the player's own trainer art (FieldDefaults.PLAYER_PICS): the battle
-- back pic, the catch tutorial's old man, and the front pic the intro,
-- trainer card and Hall of Fame share. Every key is optional so a
-- conversion can replace one pic and inherit the rest.
playerPics = f.rec{
back = f.opt(f.str), demoBack = f.opt(f.str), front = f.opt(f.str) },
-- the new-game and boot config a total conversion replaces
boot = f.rec{
startMap = f.opt(f.str), startX = f.opt(f.int(0)), startY = f.opt(f.int(0)),
+5 -4
View File
@@ -14,6 +14,7 @@ local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens")
local Stats = require("src.pokemon.Stats")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local Evolution = {}
@@ -23,7 +24,7 @@ Evolution.METHODS = {
return trigger.kind == "levelup" and mon.level >= (evo.level or 0)
end,
describe = function(evo)
return ("Level %d"):format(evo.level or 0)
return Strings("Level %d", evo.level or 0)
end,
},
ITEM = {
@@ -139,7 +140,7 @@ function Evolution.learnEvolutionMoves(game, mon, onDone)
table.insert(mon.moves, { id = moveId, pp = mdef.pp })
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
game.stack:push(TextBox.new(game,
("%s learned\n%s!"):format(name, mdef.name), nextStep))
Strings("%s learned\n%s!", name, mdef.name), nextStep))
else
-- LearnMoveFromLevelUp with a full moveset: the forget UI
Screens.push(game, "MoveLearnMenu", mon, moveId, nextStep)
@@ -160,8 +161,8 @@ function Evolution.evolve(game, mon, newSpecies, onDone, via)
Music.play(game.data, Music.special(game.data, "evolution"))
local oldName = mon.nickname or game.data.pokemon[mon.species].name
Evolution.apply(game, mon, newSpecies, via)
local msg = ("What?\n%s is\nevolving!\fCongratulations!\nYour %s\nevolved into\n%s!")
:format(oldName, oldName, game.data.pokemon[newSpecies].name)
local msg = Strings("What?\n%s is\nevolving!\fCongratulations!\nYour %s\nevolved into\n%s!",
oldName, oldName, game.data.pokemon[newSpecies].name)
game.stack:push(TextBox.new(game, msg, function()
Music.restoreMap(game.data)
-- re-run the evolved species' level-up learn check before onDone
+34
View File
@@ -3,8 +3,12 @@
-- patch pokemon.spriteFront / icons.bySpecies. These helpers are the
-- sanctioned seam: every battle pic and party icon load goes through
-- pokemon.sprite / pokemon.icon, which stay live for the whole process.
--
-- playerPath is the same seam for the player's own trainer art, whose
-- vanilla paths are data (field.playerPics) rather than a species record.
local Runtime = require("src.mods.Runtime")
local FieldDefaults = require("src.world.FieldDefaults")
local Sprites = {}
@@ -36,6 +40,36 @@ function Sprites.path(data, species, side, opts)
return path, ctx.trueColor and true or false
end
-- Resolve the player's own trainer pic path.
-- side: "back" (the battle intro pic) | "front" (intro / card / Hall of Fame)
-- opts.kind: "battle" | "intro" | "trainer_card" | "hof"
-- opts.demo: the catch tutorial, where the old man fights in the player's
-- place and stands in for the back pic
-- opts.battle: the live battle, for kind == "battle"
-- Returns path, trueColor.
function Sprites.playerPath(data, side, opts)
opts = opts or {}
side = side == "back" and "back" or "front"
-- one key per pic, so a conversion can replace the back and inherit the
-- rest; fieldValue folds data.field over FieldDefaults per key
local key = side == "front" and "front"
or (opts.demo and "demoBack" or "back")
local path = FieldDefaults.fieldValue(data, "playerPics", key)
local ctx = {
side = side,
kind = opts.kind or "battle",
demo = opts.demo and true or false,
battle = opts.battle,
trueColor = false,
data = data,
}
if path and Runtime.wantsHook("player.sprite") then
local hooked = Runtime.call("player.sprite", samePath, path, ctx)
if type(hooked) == "string" and hooked ~= "" then path = hooked end
end
return path, ctx.trueColor and true or false
end
-- Resolve a party-menu icon image path for `mon`.
-- vanillaPath is the path PartyMenu already picked from icons.bySpecies /
-- def.icon / icons.byDex; the hook may replace it.
+69 -20
View File
@@ -102,35 +102,84 @@ local function pageFor(code)
return nil
end
local SPACE = 0x7F
-- Segment text into glyph spans: `{ from, to, code }` byte ranges, one per
-- drawn glyph, code nil when the charmap has nothing. A span is a whole
-- charmap sequence, so a multi-byte char ("é", "♂") and an ASCII ligature
-- ("<PK>", "'d") are each one glyph.
--
-- Every caller that *measures* or *cuts* text walks these instead of bytes.
-- "POKéMON" is 8 bytes and 7 glyphs: measuring it as 8 wraps lines that fit
-- (25 vanilla lines did), and cutting at byte 8 splits the é into two
-- invalid bytes that both draw as spaces. That distinction is the whole
-- reason a non-English font can be shipped as a mod (#186, #245).
--
-- Safe before Font.load: with no charmap it falls back to UTF-8 lead-byte
-- boundaries, which is all a headless paginate needs.
function Font.split(text)
local spans = {}
local i, n = 1, #text
while i <= n do
local span
local candidates = state and state.byFirstByte[text:byte(i)]
if candidates then
for _, entry in ipairs(candidates) do
local len = #entry.seq
if text:sub(i, i + len - 1) == entry.seq then
span = { from = i, to = i + len - 1, code = entry.code }
break
end
end
end
if not span then
-- Nothing matched. Still keep a UTF-8 sequence whole, so a cut never
-- lands mid-character even for a glyph we cannot draw.
local last = i
if text:byte(i) >= 0xC0 then
local k = i + 1
while k <= n do
local b = text:byte(k)
if b < 0x80 or b > 0xBF then break end
last, k = k, k + 1
end
end
span = { from = i, to = last }
end
spans[#spans + 1] = span
i = span.to + 1
end
return spans
end
-- How many leading spans fit in `budget` pixels. Advances come from each
-- glyph's own page, so a variable-width page measures correctly.
function Font.spansFitting(spans, budget)
local used, fit = 0, 0
for _, span in ipairs(spans) do
used = used + Font.advanceOf(span.code or SPACE)
if used > budget then break end
fit = fit + 1
end
return fit
end
-- Convert a text string into a list of glyph codes. Unknown characters
-- render as space (and are reported once).
local reported = {}
function Font.encode(text)
local codes = {}
local i = 1
while i <= #text do
local candidates = state.byFirstByte[text:byte(i)]
local matched = false
if candidates then
for _, entry in ipairs(candidates) do
local n = #entry.seq
if text:sub(i, i + n - 1) == entry.seq then
codes[#codes + 1] = entry.code
i = i + n
matched = true
break
end
end
end
if not matched then
local ch = text:sub(i, i)
if not reported[ch] and ch:byte() >= 32 then
for _, span in ipairs(Font.split(text)) do
local code = span.code
if not code then
local ch = text:sub(span.from, span.to)
if not reported[ch] and text:byte(span.from) >= 32 then
reported[ch] = true
require("src.core.Logger").warn("font: no glyph for %q", ch)
end
codes[#codes + 1] = 0x7F -- space
i = i + 1
code = SPACE
end
codes[#codes + 1] = code
end
return codes
end
+31 -4
View File
@@ -92,13 +92,28 @@ end
-- pokered ContText waits for A/B + ▼ before scrolling that line in.
function TextBox.paginate(text, maxCols)
maxCols = maxCols or (Theme.textBox and Theme.textBox.maxCols) or MAX_COLS
-- maxCols is a column count, so the budget is that many vanilla 8px
-- cells. Measuring in pixels rather than columns is what lets a mod's
-- variable-advance page wrap correctly (#186).
local budget = maxCols * 8
local pages = {}
local contBefore = {}
-- Soft-wrap on glyph boundaries, never byte boundaries: a line is over
-- budget by what it *draws*, and the cut falls between glyphs so a
-- multi-byte char is never torn in half.
local function pushLine(lines, conts, line, wait)
while #line > maxCols do
local cut = maxCols
for i = maxCols, 1, -1 do
if line:sub(i, i) == " " then cut = i break end
while true do
local spans = Font.split(line)
local fit = Font.spansFitting(spans, budget)
if fit >= #spans then break end
-- a glyph wider than the whole box still has to advance by one
fit = math.max(fit, 1)
local cut = spans[fit].to
for i = fit, 1, -1 do
if line:sub(spans[i].from, spans[i].to) == " " then
cut = spans[i].to
break
end
end
table.insert(lines, line:sub(1, cut))
table.insert(conts, wait)
@@ -167,6 +182,18 @@ function TextBox:update(dt)
if self.autoSrc and self.autoSrc.isPlaying and self.autoSrc:isPlaying() then
return -- the cry is still sounding (WaitForSoundToFinish)
end
-- auto.wait: the pet-NPC cries (PewterNidoranHouseNidoranText,
-- ViridianNicknameHouseSpearowText) have nothing queued behind the
-- cry, so DisplayTextID's trailing WaitForTextScrollButtonPress still
-- runs once it is over -- their maps enable auto text box drawing,
-- which zeroes wDoNotWaitForButtonPressAfterDisplayingText
-- (home/window.asm AutoTextBoxDrawingCommon). Drop the auto gate and
-- hand the box to the plain A/B path, which also starts the blinking
-- arrow, instead of popping it (#247, #251).
if self.auto.wait then
self.auto = nil
return
end
self.autoTimer = self.autoTimer + 1
local delay = self.auto.delay or 3
-- auto.onOverlap: fired once when the delay elapses but before the
+29 -14
View File
@@ -10,6 +10,7 @@ local Flags = require("src.script.Flags")
local Logger = require("src.core.Logger")
local Screens = require("src.ui.Screens")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local Commands = {}
@@ -74,6 +75,9 @@ end
-- <PROMPT> of its own; the box only closes once WaitForSoundToFinish's poll
-- loop sees the cry channel go quiet, so it's a no-button-wait auto text
-- gated on the cry, not a button-wait one -- see TextBox's opts.auto.
-- play_cry's waitForButton form keeps that cry gate but hands the box back
-- to the A/B path once the cry is over -- see TextBox's opts.auto.wait
-- (#247, #251).
function Commands.show_text(ctx, textId, subs)
local text = ctx.game.data.text[textId]
if not text and ctx.overworld then
@@ -100,10 +104,12 @@ function Commands.show_text(ctx, textId, subs)
local opts
if ctx.pendingCry then
local species = ctx.pendingCry
ctx.pendingCry = nil
local waitForButton = ctx.pendingCryWait
ctx.pendingCry, ctx.pendingCryWait = nil, nil
-- delay 0: WaitForSoundToFinish has no trailing Delay3 of its own
opts = { auto = { sound = function()
return require("src.core.Sound").playCry(ctx.game.data, species)
end, delay = 0 } } -- WaitForSoundToFinish has no trailing Delay3 of its own
end, delay = 0, wait = waitForButton } }
end
-- text_opts armed the next box: auto = true is the plain no-button-wait
-- form, overlap folds under auto, everything else passes through
@@ -189,7 +195,7 @@ function Commands.give_item(ctx, itemId, count, gotText)
-- skips the received text entirely when AddItemToInventory refuses)
if not require("src.inventory.Bag").add(ctx.save, itemId, count or 1) then
Commands.show_text(ctx, ctx.game.data.text
and ctx.game.data.text._BagFullText or "You can't carry\nany more items!")
and ctx.game.data.text._BagFullText or Strings("You can't carry\nany more items!"))
return math.huge
end
local def = ctx.game.data.items[itemId]
@@ -202,7 +208,7 @@ function Commands.give_item(ctx, itemId, count, gotText)
(def and def.keyItem) and "Get_Key_Item" or "Get_Item1")
if gotText ~= false then
Commands.show_text(ctx, gotText
or "{PLAYER} got\n" .. ctx.game.stringBuffer .. "!")
or Strings("{PLAYER} got\n%s!", ctx.game.stringBuffer))
end
end
@@ -469,18 +475,26 @@ function Commands.play_once(ctx, songId)
runner:yield()
end
-- play_cry <species>: PlayCry (home/audio.asm). The text_asm bodies that
-- use it run text_far (a no-button-wait "...@" string) -> PlayCry ->
-- WaitForSoundToFinish, all within the same text ID -- the cry only starts
-- once the box has finished typing, and the box then auto-closes (no A
-- press) the instant the cry finishes, rather than firing immediately
-- play_cry <species> [waitForButton]: PlayCry (home/audio.asm). The
-- text_asm bodies that use it run text_far (a no-button-wait "...@" string)
-- -> PlayCry -> WaitForSoundToFinish, all within the same text ID -- the cry
-- only starts once the box has finished typing, and the box then auto-closes
-- (no A press) the instant the cry finishes, rather than firing immediately
-- alongside the typewriter effect. Script rows run strictly in order, so
-- this stashes the species on ctx for the show_text row that always
-- immediately follows it (Power Plant Zapdos, Seafoam Articuno, Victory
-- Road Moltres, Cerulean Cave Mewtwo battle text) to play once its box is
-- done typing (see show_text's opts.auto). Headless-safe no-op there.
function Commands.play_cry(ctx, species)
--
-- waitForButton is the pet-NPC form (scripts/PewterNidoranHouse.asm and
-- scripts/ViridianNicknameHouse.asm: the text prints, then PlayCry,
-- WaitForSoundToFinish, TextScriptEnd). Nothing is queued behind the cry
-- there, so DisplayTextID's trailing WaitForTextScrollButtonPress still
-- runs and the box holds until A/B like any other NPC line instead of
-- popping itself into a static battle (#247, #251).
function Commands.play_cry(ctx, species, waitForButton)
ctx.pendingCry = species
ctx.pendingCryWait = waitForButton or nil
end
-- check_battle_result <r1> [r2 ...]: lastCheck = the last scripted
@@ -517,7 +531,7 @@ local function askNickname(ctx, mon)
if ctx.game.data.text and ctx.game.data.text._DoYouWantToNicknameText then
Commands.show_text(ctx, "_DoYouWantToNicknameText", { RAM = name })
else
Commands.show_text(ctx, ("Do you want to\ngive a nickname\nto %s?"):format(name))
Commands.show_text(ctx, Strings("Do you want to\ngive a nickname\nto %s?", name))
end
local ChoiceBox = require("src.ui.ChoiceBox")
ctx.game.stack:push(ChoiceBox.new(ctx.game, function(yes)
@@ -527,7 +541,7 @@ local function askNickname(ctx, mon)
return
end
Screens.push(ctx.game, "NamingScreen", {
title = "NICKNAME?", maxLen = 10,
title = Strings("NICKNAME?"), maxLen = 10,
onDone = function(nick)
if nick and #nick > 0 then mon.nickname = nick end
ctx.lastCheck = success
@@ -598,8 +612,9 @@ function Commands.give_pokemon(ctx, species, level)
local TextBox = require("src.render.TextBox")
local raw = ctx.game.data.text and ctx.game.data.text._SentToBoxText
ctx.game.stack:push(TextBox.new(ctx.game, raw or (
"There's no more\nroom for POKéMON!\v" .. name
.. " was\vsent to POKéMON\vBOX " .. boxNum .. " on PC!"),
Strings(
"There's no more\nroom for POKéMON!\v%s was\vsent to POKéMON\vBOX %s on PC!",
name, boxNum)),
function() end))
end
end
+2 -1
View File
@@ -15,6 +15,7 @@
local Data = require("src.core.Data")
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local Strings = require("src.core.Strings")
local MapScripts = {}
@@ -268,7 +269,7 @@ function MapScripts.validateContribution(contribution, lookup)
end
end
if type(contribution) ~= "table" then
return { "contribution is not a table" }
return { Strings("contribution is not a table") }
end
for textConst, script in pairs(contribution.talk or {}) do
if type(script) == "table" then collect("talk." .. textConst, script) end
+2 -1
View File
@@ -13,6 +13,7 @@
local Commands = require("src.script.Commands")
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local Strings = require("src.core.Strings")
local unpack = table.unpack or unpack -- LuaJIT (LÖVE) compatibility
@@ -195,7 +196,7 @@ function ScriptRunner:resume(...)
if not ok then
local source = self.ctx and self.ctx.source
local where = source
and (" [%s %s.%s]"):format(tostring(source.modId or "engine"),
and Strings(" [%s %s.%s]", tostring(source.modId or "engine"),
tostring(source.mapId or "?"), tostring(source.hook or "?"))
or ""
Logger.error("script error%s: %s", where, tostring(err))
+29 -20
View File
@@ -9,6 +9,7 @@ local TextBox = require("src.render.TextBox")
local BagMenu = {}
local Bag = require("src.inventory.Bag")
local Strings = require("src.core.Strings")
-- acquisition order like wBagItems (Bag.order), not alphabetical
local function buildItems(game)
@@ -108,13 +109,13 @@ local function useOn(game, battle, id, target, list, moveIndex)
if game.save.onBike then
game.save.onBike = false
Music.playMap(game.data, ow and ow.map.id, false)
showMessages(game, { save_name(game) .. " got off\nthe BICYCLE." })
showMessages(game, { Strings("%s got off\nthe BICYCLE.", save_name(game)) })
elseif bikeAllowed() then
game.save.onBike = true
Music.playMap(game.data, ow.map.id, true)
showMessages(game, { save_name(game) .. " got on\nthe BICYCLE!" })
showMessages(game, { Strings("%s got on\nthe BICYCLE!", save_name(game)) })
else
showMessages(game, { "No cycling\nallowed here." })
showMessages(game, { Strings("No cycling\nallowed here.") })
end
return
end
@@ -130,13 +131,14 @@ local function useOn(game, battle, id, target, list, moveIndex)
return
end
end
showMessages(game, { "No good! It's not\neven near water." })
showMessages(game, { Strings("No good! It's not\neven near water.") })
return
end
if result == "ball" then
if not battle then
showMessages(game, { "OAK: " .. game.save.player.name .. "!\nThis isn't the\ntime to use that!" })
showMessages(game, { Strings("OAK: %s!\nThis isn't the\ntime to use that!",
game.save.player.name) })
return
end
consume(game, id)
@@ -151,7 +153,7 @@ local function useOn(game, battle, id, target, list, moveIndex)
local function teach()
if #target.moves < 4 then
table.insert(target.moves, { id = moveId, pp = mdef.pp })
showMessages(game, { ("%s learned\n%s!"):format(target.nickname or
showMessages(game, { Strings("%s learned\n%s!", target.nickname or
game.data.pokemon[target.species].name, mdef.name) })
if result == "learn" then consume(game, id) end
else
@@ -172,7 +174,7 @@ local function useOn(game, battle, id, target, list, moveIndex)
require("src.ui.Screens").push(game, "TownMap")
end)
if not ok then
showMessages(game, { "The TOWN MAP is\nunreadable here." })
showMessages(game, { Strings("The TOWN MAP is\nunreadable here.") })
end
return
end
@@ -184,10 +186,10 @@ local function useOn(game, battle, id, target, list, moveIndex)
local t = game.data.text
if ow and ow:hasHiddenItemLeft() then
showMessages(game, { t._ItemfinderFoundItemText
or "Yes! ITEMFINDER\nindicates there's\nan item nearby." })
or Strings("Yes! ITEMFINDER\nindicates there's\nan item nearby.") })
else
showMessages(game, { t._ItemfinderFoundNothingText
or "Nope! ITEMFINDER\nisn't responding." })
or Strings("Nope! ITEMFINDER\nisn't responding.") })
end
return
end
@@ -219,8 +221,9 @@ local function useOn(game, battle, id, target, list, moveIndex)
-- departure helper -- the same path Dig/Teleport take from the party menu
ow:beginTeleportOut()
else
showMessages(game, { "OAK: " .. game.save.player.name
.. "!\nThis isn't the\ntime to use that!" })
showMessages(game, { Strings(
"OAK: %s!\nThis isn't the\ntime to use that!",
game.save.player.name) })
end
return
end
@@ -264,7 +267,7 @@ local function useOn(game, battle, id, target, list, moveIndex)
if #target.moves < 4 then
table.insert(target.moves, { id = moveId, pp = mdef.pp })
local name = target.nickname or def.name
showMessages(game, { ("%s learned\n%s!"):format(name, mdef.name) },
showMessages(game, { Strings("%s learned\n%s!", name, mdef.name) },
nextStep)
else
require("src.ui.Screens").push(game, "MoveLearnMenu",
@@ -354,8 +357,8 @@ local function useItem(game, battle, id, list)
local moveDef = game.data.moves[def.machine.move]
local moveName = moveDef and moveDef.name or def.machine.move
local booted = def.machine.kind == "HM"
and "Booted up an HM!" or "Booted up a TM!"
showMessages(game, { booted, ("It contained\n%s!"):format(moveName) },
and "Booted up an HM!" or Strings("Booted up a TM!")
showMessages(game, { booted, Strings("It contained\n%s!", moveName) },
function() pickTargetAndUse(game, battle, id, list) end)
return
end
@@ -402,17 +405,23 @@ function BagMenu.new(game, opts)
useItem(game, battle, id, list)
return
end
-- USE / TOSS submenu (the original's item options)
-- USE / TOSS submenu (the original's item options).
-- data/text_boxes.asm USE_TOSS_MENU_TEMPLATE: box (13,10)-(19,14),
-- text at (15,11); start_sub_menus.asm then sets wTopMenuItemY/X to
-- 11/14 for the cursor. Menu's own geometry reproduces all of that
-- from the box alone, so this needs opts rather than a change to the
-- shared Menu. The old 12/10/8/6 box was a column too wide and a row
-- too tall, which left the labels stranded near its top edge (#284).
local Menu = require("src.ui.Menu")
game.stack:push(Menu.new(game, {
{ label = "USE", onSelect = function()
{ label = Strings("USE"), onSelect = function()
useItem(game, battle, id, list)
end },
{ label = "TOSS", onSelect = function()
{ label = Strings("TOSS"), onSelect = function()
-- KeyItemFlags + HMs decide tossability (not price:
-- MOON STONE is price 0 but tossable)
if not def or def.keyItem or id:find("^HM_") then
showMessages(game, { "That's too impor-\ntant to toss!" })
showMessages(game, { Strings("That's too impor-\ntant to toss!") })
return
end
local QuantityBox = require("src.ui.QuantityBox")
@@ -426,12 +435,12 @@ function BagMenu.new(game, opts)
Bag.remove(game.save, id, qty)
list.items = buildItems(game)
list.index = math.min(list.index, math.max(1, #list.items))
showMessages(game, { ("Threw away\n%s."):format(def and def.name or id) })
showMessages(game, { Strings("Threw away\n%s.", def and def.name or id) })
end))
end,
}))
end },
}, { tx = 12, ty = 10, tw = 8, th = 6 }))
}, { tx = 13, ty = 10, tw = 7, th = 5 }))
end,
})
return list
+5 -2
View File
@@ -7,6 +7,7 @@
local Font = require("src.render.Font")
local ListMenu = require("src.ui.ListMenu")
local Input = require("src.core.Input")
local Strings = require("src.core.Strings")
local BindingsMenu = setmetatable({}, { __index = ListMenu })
BindingsMenu.__index = BindingsMenu
@@ -55,7 +56,9 @@ function BindingsMenu.new(game)
and game.save.options.bindings
local items = {}
for i, def in ipairs(BUTTONS) do
items[i] = { label = def.label,
-- translated here, not in ROWS: that table is built at require
-- time, before Strings.load has a catalog to look in
items[i] = { label = Strings(def.label),
right = boundRight(overlay, def), button = def }
end
local self = setmetatable(ListMenu.new(game, "CONTROLS", items, {}),
@@ -111,7 +114,7 @@ function BindingsMenu:draw()
if self.capture then
Font.drawBox(1, 6, 18, 4)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("PRESS A BUTTON", 24, 60)
Font.draw(Strings("PRESS A BUTTON"), 24, 60)
love.graphics.setColor(1, 1, 1, 1)
end
end
+26 -25
View File
@@ -8,12 +8,13 @@ local ListMenu = require("src.ui.ListMenu")
local Menu = require("src.ui.Menu")
local Party = require("src.pokemon.Party")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local BoxMenu = {}
local function monLabel(game, mon)
local def = game.data.pokemon[mon.species]
return ("%s :L%d"):format(mon.nickname or def.name, mon.level)
return Strings("%s :L%d", mon.nickname or def.name, mon.level)
end
local function monName(game, mon)
@@ -28,13 +29,13 @@ local function monSubmenu(game, action, mon, onAction)
game.stack:push(Menu.new(game, {
{ label = action, onSelect = onAction },
{
label = "STATS",
label = Strings("STATS"),
keepOpen = true,
onSelect = function()
require("src.ui.Screens").push(game, "SummaryMenu", mon)
end,
},
{ label = "CANCEL" },
{ label = Strings("CANCEL") },
}, { tx = 9, ty = 10, tw = 11, th = 8, noSound = true }))
end
@@ -50,12 +51,12 @@ local function withdraw(game)
local t = game.data.text
if #box == 0 then
game.stack:push(TextBox.new(game, t._NoMonText
or "What? There are\nno POKéMON here!"))
or Strings("What? There are\nno POKéMON here!")))
return
end
if #game.save.party >= Party.MAX then
game.stack:push(TextBox.new(game, t._CantTakeMonText
or "You can't take\nany more POKéMON.\fDeposit POKéMON\nfirst."))
or Strings("You can't take\nany more POKéMON.\fDeposit POKéMON\nfirst.")))
return
end
local items = {}
@@ -63,7 +64,7 @@ local function withdraw(game)
table.insert(items, { label = monLabel(game, mon), value = i })
end
game.stack:push(ListMenu.new(game,
("BOX %d (WITHDRAW)"):format(game.save.currentBox), items, {
Strings("BOX %d (WITHDRAW)", game.save.currentBox), items, {
onChoose = function(item, list)
local mon = box[item.value]
if not mon then return end
@@ -78,7 +79,7 @@ local function withdraw(game)
game.stringBuffer = name
require("src.core.Sound").playCry(game.data, mon.species)
afterTransfer(game, list, t._MonIsTakenOutText
or (name .. " is\ntaken out.\vGot " .. name .. "."))
or Strings("%s is\ntaken out.\vGot %s.", name, name))
end)
end,
}))
@@ -88,13 +89,13 @@ local function deposit(game)
local t = game.data.text
if #game.save.party <= 1 then
game.stack:push(TextBox.new(game, t._CantDepositLastMonText
or "You can't deposit\nthe last POKéMON!"))
or Strings("You can't deposit\nthe last POKéMON!")))
return
end
local box = Boxes.active(game.save)
if #box >= Boxes.CAPACITY then
game.stack:push(TextBox.new(game, t._BoxFullText
or "Oops! This Box is\nfull of POKéMON."))
or Strings("Oops! This Box is\nfull of POKéMON.")))
return
end
local items = {}
@@ -107,12 +108,12 @@ local function deposit(game)
if not mon then return end
monSubmenu(game, "DEPOSIT", mon, function()
if #game.save.party <= 1 then
list.footer = "You need at least\none POKéMON!"
list.footer = Strings("You need at least\none POKéMON!")
return
end
local active = Boxes.active(game.save)
if #active >= Boxes.CAPACITY then
list.footer = ("BOX %d is full!"):format(game.save.currentBox)
list.footer = Strings("BOX %d is full!", game.save.currentBox)
return
end
table.remove(game.save.party, item.value)
@@ -122,7 +123,7 @@ local function deposit(game)
game.boxNumString = tostring(game.save.currentBox)
require("src.core.Sound").playCry(game.data, mon.species)
afterTransfer(game, list, t._MonWasStoredText
or (name .. " was\nstored in Box " .. game.boxNumString .. "."))
or Strings("%s was\nstored in Box %s.", name, game.boxNumString))
end)
end,
}))
@@ -135,7 +136,7 @@ local function release(game)
local t = game.data.text
if #box == 0 then
game.stack:push(TextBox.new(game, t._NoMonText
or "What? There are\nno POKéMON here!"))
or Strings("What? There are\nno POKéMON here!")))
return
end
local items = {}
@@ -143,20 +144,20 @@ local function release(game)
table.insert(items, { label = monLabel(game, mon), value = i })
end
game.stack:push(ListMenu.new(game,
("BOX %d (RELEASE)"):format(game.save.currentBox), items, {
Strings("BOX %d (RELEASE)", game.save.currentBox), items, {
onChoose = function(_, list)
local mon = box[list.index]
if not mon then return end
local name = monName(game, mon)
local ChoiceBox = require("src.ui.ChoiceBox")
game.stack:push(TextBox.new(game,
"Once released,\n" .. name .. " is\ngone forever. OK?", function()
Strings("Once released,\n%s is\ngone forever. OK?", name), function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then return end
table.remove(box, list.index)
require("src.core.Sound").playCry(game.data, mon.species)
game.stack:push(TextBox.new(game,
("%s was\nreleased outside.\fBye %s!"):format(name, name)))
Strings("%s was\nreleased outside.\fBye %s!", name, name)))
list:removeCurrent()
end, { defaultNo = true, noSound = true }))
end))
@@ -170,7 +171,7 @@ local function changeBox(game)
for i = 1, Boxes.COUNT do
local mark = i == game.save.currentBox and "*" or " "
table.insert(items, {
label = ("%sBOX %2d"):format(mark, i),
label = Strings("%sBOX %2d", mark, i),
right = ("%d/%d"):format(#boxes[i], Boxes.CAPACITY),
value = i,
})
@@ -181,7 +182,7 @@ local function changeBox(game)
-- BOX, data will be saved. OK?"); declining aborts the change
local ChoiceBox = require("src.ui.ChoiceBox")
game.stack:push(TextBox.new(game,
"When you change a\nPOKéMON BOX, data\nwill be saved. OK?", function()
Strings("When you change a\nPOKéMON BOX, data\nwill be saved. OK?"), function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then return end
game.save.currentBox = item.value
@@ -197,10 +198,10 @@ end
local function drawChrome(game)
Font.drawBox(0, 12, 20, 6)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("What?", 8, 112)
Font.draw(Strings("What?"), 8, 112)
Font.drawBox(9, 14, 11, 4)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("BOX No.", 80, 128)
Font.draw(Strings("BOX No."), 80, 128)
local n = game.save.currentBox or 1
if n >= 10 then
Font.draw(tostring(n), 136, 128)
@@ -217,15 +218,15 @@ function BoxMenu.new(game)
-- full interior (cursor col + label). keepOpen so WITHDRAW/DEPOSIT/
-- RELEASE/CHANGE BOX leave this menu underneath (jp BillsPCMenu).
local menu = Menu.new(game, {
{ label = "WITHDRAW <PK><MN>", keepOpen = true,
{ label = Strings("WITHDRAW <PK><MN>"), keepOpen = true,
onSelect = function() withdraw(game) end },
{ label = "DEPOSIT <PK><MN>", keepOpen = true,
{ label = Strings("DEPOSIT <PK><MN>"), keepOpen = true,
onSelect = function() deposit(game) end },
{ label = "RELEASE <PK><MN>", keepOpen = true,
{ label = Strings("RELEASE <PK><MN>"), keepOpen = true,
onSelect = function() release(game) end },
{ label = "CHANGE BOX", keepOpen = true,
{ label = Strings("CHANGE BOX"), keepOpen = true,
onSelect = function() changeBox(game) end },
{ label = "SEE YA!" },
{ label = Strings("SEE YA!") },
-- Bill's PC runs silent end to end (BIT_NO_MENU_BUTTON_SOUND,
-- engine/menus/pokemon_pc.asm)
}, { tx = 0, ty = 0, tw = 14, th = 12, noSound = true })
+3 -2
View File
@@ -2,6 +2,7 @@
local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local Strings = require("src.core.Strings")
local ChoiceBox = {}
ChoiceBox.__index = ChoiceBox
@@ -46,8 +47,8 @@ function ChoiceBox:draw()
local tx, ty, tw, th = self.tx, self.ty, self.tw, self.th
Font.drawBox(tx, ty, tw, th)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("YES", (tx + 2) * 8, (ty + 1) * 8)
Font.draw("NO", (tx + 2) * 8, (ty + 3) * 8)
Font.draw(Strings("YES"), (tx + 2) * 8, (ty + 1) * 8)
Font.draw(Strings("NO"), (tx + 2) * 8, (ty + 3) * 8)
Font.drawCode(Theme.cursor, (tx + 1) * 8,
(ty + (self.index == 1 and 1 or 3)) * 8)
love.graphics.setColor(1, 1, 1, 1)
+6 -5
View File
@@ -31,6 +31,7 @@
local Font = require("src.render.Font")
local Music = require("src.core.Music")
local Strings = require("src.core.Strings")
local Credits = {}
Credits.__index = Credits
@@ -261,14 +262,14 @@ function Credits:drawCopyright(xoff)
love.graphics.draw(self.gfImg, xoff + 80, 88)
else
love.graphics.setColor(0, 0, 0, 1)
Font.draw("GAME FREAK", xoff + 80, 88)
Font.draw(Strings("GAME FREAK"), xoff + 80, 88)
love.graphics.setColor(1, 1, 1, 1)
end
else
love.graphics.setColor(0, 0, 0, 1)
Font.draw("Nintendo", xoff + 80, 56)
Font.draw("Creatures inc.", xoff + 80, 72)
Font.draw("GAME FREAK inc.", xoff + 16, 88)
Font.draw(Strings("Nintendo"), xoff + 80, 56)
Font.draw(Strings("Creatures inc."), xoff + 80, 72)
Font.draw(Strings("GAME FREAK inc."), xoff + 16, 88)
love.graphics.setColor(1, 1, 1, 1)
end
end
@@ -298,7 +299,7 @@ function Credits:drawTheEnd()
end
else
love.graphics.setColor(0, 0, 0, 1)
Font.draw((te and te.display) or "T H E E N D", 32, 64)
Font.draw((te and te.display) or Strings("T H E E N D"), 32, 64)
love.graphics.setColor(1, 1, 1, 1)
end
end
+4 -3
View File
@@ -8,6 +8,7 @@
-- without permanently marking the mon owned.
local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local DexEntryMenu = {}
DexEntryMenu.__index = DexEntryMenu
@@ -76,8 +77,8 @@ function DexEntryMenu:draw()
-- feet/inches use the dex screen's /″ glyphs ("HT ???″" in
-- pokedex.asm; the tiles come from gfx/pokedex/pokedex.png via
-- engine/gfx/load_pokedex_tiles.asm)
Font.draw(("HT %d%02d″"):format(e.heightFt, e.heightIn or 0), 72, 44)
Font.draw(("WT %.1flb"):format((e.weight or 0) / 10), 72, 54)
Font.draw(Strings("HT %d%02d″", e.heightFt, e.heightIn or 0), 72, 44)
Font.draw(Strings("WT %.1flb", (e.weight or 0) / 10), 72, 54)
end
local text = owned and e.text and self.game.data.text[e.text] or nil
local y = 72
@@ -88,7 +89,7 @@ function DexEntryMenu:draw()
y = y + 10
end
else
Font.draw("Data unknown.", 8, y)
Font.draw(Strings("Data unknown."), 8, y)
end
love.graphics.setColor(1, 1, 1, 1)
end
+19 -13
View File
@@ -1,14 +1,17 @@
-- The evolution movie (engine/movie/evolution.asm): the mon's pic
-- flashes back and forth with the evolved form, speeding up, then the
-- new form appears with its cry and the congratulations text.
-- pokered engine/pokemon/evos_moves.asm (EvolveMon) polls hJoyHeld during
-- the flash: holding B aborts the evolution -- the mon keeps its species
-- and _StoppedEvolvingText ("Huh? MON stopped evolving!") prints. The
-- lone exception is trade evolutions (wLinkState == LINK_STATE_TRADING),
-- which skip that poll and cannot be cancelled (#213).
-- pokered engine/movie/evolution.asm (Evolution_CheckForCancel) polls the
-- joypad during the flash: holding B aborts the evolution -- the mon keeps
-- its species and _StoppedEvolvingText ("Huh? MON stopped evolving!")
-- prints. Two kinds are exempt: trade evolutions, which evos_moves.asm
-- routes past the poll entirely (wLinkState == LINK_STATE_TRADING, #213),
-- and stone evolutions, where the B press is read but thrown away because
-- ItemUseEvoStone left wForceEvolution set (#290).
local Font = require("src.render.Font")
local Music = require("src.core.Music")
local Strings = require("src.core.Strings")
local EvolutionState = {}
EvolutionState.__index = EvolutionState
@@ -43,9 +46,12 @@ function EvolutionState.new(game, mon, newSpecies, onDone, via)
self.newSpecies = newSpecies
self.onDone = onDone
self.via = via
-- evos_moves.asm: only trade evolutions (LINK_STATE_TRADING) skip the
-- B-cancel poll; level-up, stone and rare-candy evos are all cancelable.
self.cancelable = (via ~= "TRADE")
-- evolution.asm Evolution_CheckForCancel: a B press is discarded when
-- wForceEvolution is set, and ItemUseEvoStone sets it before calling
-- TryEvolvingMon, so a stone evolution (via == "ITEM") cannot be
-- cancelled either. Only level-up and rare-candy evolutions run with
-- wForceEvolution clear and so honour B (#290, #213).
self.cancelable = (via ~= "TRADE" and via ~= "ITEM")
self.oldName = mon.nickname or game.data.pokemon[mon.species].name
self.oldSprite = frontSprite(game, mon.species, mon)
self.newSprite = frontSprite(game, newSpecies, mon)
@@ -69,7 +75,7 @@ function EvolutionState:update(dt)
local TextBox = require("src.render.TextBox")
-- mirrors data/generated/text.lua _StoppedEvolvingText
game.stack:push(TextBox.new(game,
("Huh? %s\nstopped evolving!"):format(self.oldName),
Strings("Huh? %s\nstopped evolving!", self.oldName),
function()
Music.restoreMap(game.data)
game.stack:pop() -- the evolution screen itself
@@ -85,8 +91,8 @@ function EvolutionState:update(dt)
local TextBox = require("src.render.TextBox")
local newName = game.data.pokemon[self.newSpecies].name
game.stack:push(TextBox.new(game,
("Congratulations!\nYour %s\nevolved into\n%s!")
:format(self.oldName, newName),
Strings("Congratulations!\nYour %s\nevolved into\n%s!",
self.oldName, newName),
function()
Music.restoreMap(game.data)
game.stack:pop() -- the evolution screen itself
@@ -121,9 +127,9 @@ function EvolutionState:draw()
love.graphics.setColor(0, 0, 0, 1)
if not self.done then
Font.draw("What?", 8, 104)
Font.draw(Strings("What?"), 8, 104)
Font.draw(self.oldName .. " is", 8, 114)
Font.draw("evolving!", 8, 124)
Font.draw(Strings("evolving!"), 8, 124)
end
love.graphics.setColor(1, 1, 1, 1)
end
+16 -11
View File
@@ -7,10 +7,12 @@
-- name/time/money boxes plus the dex rating. Plays Music_HallOfFame
-- when the audio data has it. Calls onDone() after popping itself.
local Assets = require("src.render.Assets")
local Font = require("src.render.Font")
local Music = require("src.core.Music")
local Sound = require("src.core.Sound")
local TypeChart = require("src.battle.TypeChart")
local Strings = require("src.core.Strings")
local HallOfFame = {}
HallOfFame.__index = HallOfFame
@@ -47,9 +49,11 @@ local FADE_FRAMES = 20
-- HoFPrintTextAndDelay after each dex line
local DEX_HOLD = 120
-- through Assets.resolve so an enabled mod's overrides/ shadows these the
-- same way it shadows every other generated asset
local function tryImage(path)
if not path then return nil end
local ok, img = pcall(love.graphics.newImage, path)
local ok, img = pcall(love.graphics.newImage, Assets.resolve(path))
return ok and img or nil
end
@@ -79,7 +83,8 @@ function HallOfFame.new(game, onDone)
self.timer = 0
self.phase = "mons"
self.sprites = {} -- species -> image or false
self.playerPic = tryImage("assets/generated/trainer_card/red.png")
self.playerPic = tryImage(require("src.pokemon.Sprites").playerPath(
game.data, "front", { kind = "hof" }))
self.scrollX = PIC_X
self.showHofBanner = false
self.fade = 0
@@ -206,13 +211,13 @@ function HallOfFame:drawMonInfo(mon)
love.graphics.setColor(0, 0, 0, 1)
local name = mon.nickname or (def and def.name) or mon.species
Font.draw(name, 1 * 8, 4 * 8)
Font.draw("LEVEL/", 2 * 8, 6 * 8)
Font.draw("TYPE1/", 2 * 8, 7 * 8)
Font.draw(Strings("LEVEL/"), 2 * 8, 6 * 8)
Font.draw(Strings("TYPE1/"), 2 * 8, 7 * 8)
local t1 = def and def.types and def.types[1]
local t2 = def and def.types and def.types[2]
local dual = t2 and t2 ~= t1
if dual then
Font.draw("TYPE2/", 2 * 8, 8 * 8)
Font.draw(Strings("TYPE2/"), 2 * 8, 8 * 8)
end
-- PrintLevelCommon at (8,7): bare level digits (no <LV> tile here)
Font.draw(tostring(mon.level), 8 * 8, 7 * 8)
@@ -229,7 +234,7 @@ end
function HallOfFame:drawHofBanner()
Font.drawBox(2, 13, 16, 5)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("HALL OF FAME", 4 * 8, 15 * 8)
Font.draw(Strings("HALL OF FAME"), 4 * 8, 15 * 8)
end
function HallOfFame:drawPic(img)
@@ -249,11 +254,11 @@ function HallOfFame:drawPlayerStats()
-- play time / money box: TextBoxBorder (0,4) b=6,c=10 → drawBox(0,4,12,8)
Font.drawBox(0, 4, 12, 8)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("PLAY TIME", 1 * 8, 6 * 8)
Font.draw(Strings("PLAY TIME"), 1 * 8, 6 * 8)
local t = math.floor(save.playTime or 0)
Font.draw(("%3d:%02d"):format(math.floor(t / 3600), math.floor(t / 60) % 60),
5 * 8, 7 * 8)
Font.draw("MONEY", 1 * 8, 9 * 8)
Font.draw(Strings("MONEY"), 1 * 8, 9 * 8)
-- PrintBCDNumber with MONEY_SIGN; port uses ¥ like TrainerCard
Font.draw(("¥%d"):format(save.money or 0), 4 * 8, 10 * 8)
end
@@ -266,16 +271,16 @@ function HallOfFame:drawDexBox(kind)
if kind == "seen" then
local seen, owned = self:dexSeenOwned()
local seenOwned = text._DexSeenOwnedText
or "POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}"
or Strings("POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}")
seenOwned = seenOwned
:gsub("{NUM:wDexRatingNumMonsSeen[^}]*}", tostring(seen))
:gsub("{NUM:wDexRatingNumMonsOwned[^}]*}", tostring(owned))
drawTextBlock(seenOwned, 1 * 8, 14 * 8, 17 * 8)
else
local _, owned = self:dexSeenOwned()
local ratingHeader = (text._DexRatingText or "POKéDEX Rating{COLON}"):gsub("{COLON}", ":")
local ratingHeader = (text._DexRatingText or Strings("POKéDEX Rating{COLON}")):gsub("{COLON}", ":")
Font.draw(ratingHeader, 1 * 8, 14 * 8)
local rating = text[dexRatingKey(owned)] or "Keep it up!"
local rating = text[dexRatingKey(owned)] or Strings("Keep it up!")
drawTextBlock(rating, 1 * 8, 15 * 8, 17 * 8)
end
end
+4 -3
View File
@@ -27,6 +27,7 @@
local Font = require("src.render.Font")
local Music = require("src.core.Music")
local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local IntroMovie = {}
IntroMovie.__index = IntroMovie
@@ -287,7 +288,7 @@ function IntroMovie:drawSplash()
end
-- custom studio name (replaces the GAME FREAK splash text)
love.graphics.setColor(0, 0, 0, dim and 0.35 or 1)
local card = self.studio.card or "bois club games"
local card = self.studio.card or Strings("bois club games")
Font.draw(card, (160 - #card * 8) / 2, TEXT_Y)
love.graphics.setColor(1, 1, 1, 1)
end
@@ -348,7 +349,7 @@ function IntroMovie:drawFight()
if not gengar and not nido then
love.graphics.setColor(0, 0, 0, 1)
Font.draw("GENGAR VS NIDORINO", (160 - 18 * 8) / 2, 64)
Font.draw(Strings("GENGAR VS NIDORINO"), (160 - 18 * 8) / 2, 64)
love.graphics.setColor(1, 1, 1, 1)
end
drawBars()
@@ -366,7 +367,7 @@ function IntroMovie:draw()
-- custom boot card (replaces the Nintendo / GAME FREAK copyright
-- card; no (c) glyph in the charmap, keep it ASCII-safe)
love.graphics.setColor(0, 0, 0, 1)
local credit = self.studio.credit or "bois club"
local credit = self.studio.credit or Strings("bois club")
Font.draw("2026", (160 - 4 * 8) / 2, 48)
Font.draw(credit, (160 - #credit * 8) / 2, 64)
Font.draw("bryanthaboi", (160 - 11 * 8) / 2, 80)
+6 -2
View File
@@ -5,6 +5,7 @@
local Font = require("src.render.Font")
local Runtime = require("src.mods.Runtime")
local Theme = require("src.ui.Theme")
local Strings = require("src.core.Strings")
local ListMenu = {}
ListMenu.__index = ListMenu
@@ -192,7 +193,7 @@ function ListMenu:draw()
love.graphics.setColor(0, 0, 0, 1)
Font.draw(self.title, 8, 4)
if #self.items == 0 then
Font.draw("Nothing here.", 16, 64)
Font.draw(Strings("Nothing here."), 16, 64)
end
for row = 1, self.rows do
local i = self.scroll + row
@@ -201,7 +202,10 @@ function ListMenu:draw()
local y = 8 + row * 16
Font.draw(item.label, 16, y)
if item.ball then -- the Pokédex owned-ball marker tile
local bx = 16 + (#item.label + 1) * 8 + 3
-- one blank glyph after the name, measured in glyph advances rather
-- than bytes: NIDORAN♂/♀ carry a multi-byte charmap entry, so
-- `#item.label` overcounted by 2 and pushed their ball 16px right (#285)
local bx = 16 + Font.width(item.label) + 8 + 3
local by = y + 3
love.graphics.circle("fill", bx, by, 3.5)
love.graphics.setColor(1, 1, 1, 1)
+12 -11
View File
@@ -4,6 +4,7 @@
-- can't be forgotten; B / CANCEL gives up on the new move.
local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local MoveLearnMenu = {}
MoveLearnMenu.__index = MoveLearnMenu
@@ -43,9 +44,9 @@ function MoveLearnMenu:enter()
local name = self:monName()
self.selecting = false
game.stack:push(TextBox.new(game,
("%s is\ntrying to learn\v%s!\fBut, %s\ncan't learn more\vthan 4 moves!\f")
:format(name, mdef.name, name) ..
("Delete an older\nmove to make room\vfor %s?"):format(mdef.name),
Strings("%s is\ntrying to learn\v%s!\fBut, %s\ncan't learn more\vthan 4 moves!\f",
name, mdef.name, name) ..
Strings("Delete an older\nmove to make room\vfor %s?", mdef.name),
nil, {
choice = function(yes)
if yes then
@@ -76,7 +77,7 @@ function MoveLearnMenu:update(dt)
-- HMCantDeleteText, then back to the forget list
local TextBox = require("src.render.TextBox")
self.game.stack:push(TextBox.new(self.game,
"HM techniques\ncan't be deleted!"))
Strings("HM techniques\ncan't be deleted!")))
return
end
local mdef = self.game.data.moves[self.newMoveId]
@@ -96,7 +97,7 @@ function MoveLearnMenu:confirmAbandon()
local mdef = game.data.moves[self.newMoveId]
self.selecting = false
game.stack:push(TextBox.new(game,
("Abandon learning\n%s?"):format(mdef.name), nil, {
Strings("Abandon learning\n%s?", mdef.name), nil, {
choice = function(yes)
if yes then self:finish(false) else self:enter() end
end,
@@ -113,11 +114,11 @@ function MoveLearnMenu:finish(learned)
local msg
if learned then
-- OneTwoAndText/PoofText/ForgotAndText
msg = ("1, 2 and... Poof!\f%s forgot\n%s!\fAnd...\f%s learned\n%s!")
:format(name, self.forgot, name, mdef.name)
msg = Strings("1, 2 and... Poof!\f%s forgot\n%s!\fAnd...\f%s learned\n%s!",
name, self.forgot, name, mdef.name)
else
-- DidNotLearnText
msg = ("%s\ndid not learn\v%s!"):format(name, mdef.name)
msg = Strings("%s\ndid not learn\v%s!", name, mdef.name)
end
game.stack:push(TextBox.new(game, msg, function()
if self.onDone then self.onDone(learned) end
@@ -133,12 +134,12 @@ function MoveLearnMenu:draw()
for i, mv in ipairs(self.mon.moves) do
Font.draw(self.game.data.moves[mv.id].name, 48, (5 + i) * 8)
end
Font.draw("CANCEL", 48, (6 + #self.mon.moves) * 8)
Font.draw(Strings("CANCEL"), 48, (6 + #self.mon.moves) * 8)
Font.drawCode(CURSOR, 40, (5 + self.index) * 8)
-- WhichMoveToForgetText in the bottom dialogue box
Font.drawBox(0, 12, 20, 6)
Font.draw("Which move should", 8, 14 * 8)
Font.draw("be forgotten?", 8, 16 * 8)
Font.draw(Strings("Which move should"), 8, 14 * 8)
Font.draw(Strings("be forgotten?"), 8, 16 * 8)
love.graphics.setColor(1, 1, 1, 1)
end
+3 -2
View File
@@ -13,6 +13,7 @@ local Font = require("src.render.Font")
local Runtime = require("src.mods.Runtime")
local Sound = require("src.core.Sound")
local Theme = require("src.ui.Theme")
local Strings = require("src.core.Strings")
local NamingScreen = {}
NamingScreen.__index = NamingScreen
@@ -64,7 +65,7 @@ function NamingScreen.new(game, opts)
opts = opts or {}
local self = setmetatable({}, NamingScreen)
self.game = game
self.title = opts.title or "YOUR NAME?"
self.title = opts.title or Strings("YOUR NAME?")
self.presets = opts.presets
self.maxLen = opts.maxLen or 7
self.default = opts.default
@@ -78,7 +79,7 @@ end
function NamingScreen:enter()
if self.presets and #self.presets > 0 then
local Menu = require("src.ui.Menu")
local items = { { label = "NEW NAME" } }
local items = { { label = Strings("NEW NAME") } }
for _, preset in ipairs(self.presets) do
table.insert(items, {
label = preset,
+14 -7
View File
@@ -7,12 +7,14 @@
-- mod can insertBefore("name_player", ...) without counting indices.
-- Calls onDone() after popping itself.
local Assets = require("src.render.Assets")
local Sound = require("src.core.Sound")
local Music = require("src.core.Music")
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local TextBox = require("src.render.TextBox")
local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local OakSpeech = {}
OakSpeech.__index = OakSpeech
@@ -51,9 +53,11 @@ local function textOr(game, key)
return (t and t[key]) or FALLBACKS[key]
end
-- through Assets.resolve so an enabled mod's overrides/ shadows these the
-- same way it shadows every other generated asset
local function tryImage(path)
if not path then return nil end
local ok, img = pcall(love.graphics.newImage, path)
local ok, img = pcall(love.graphics.newImage, Assets.resolve(path))
return ok and img or nil
end
@@ -101,7 +105,9 @@ function OakSpeech.resolvePic(game, desc, speech)
if speech and speech.playerPic and not desc.path then
return speech.playerPic, false
end
return tryImage(desc.path or "assets/generated/trainer_card/red.png"), false
if desc.path then return tryImage(desc.path), false end
return tryImage(require("src.pokemon.Sprites").playerPath(
game.data, "front", { kind = "intro" })), false
elseif t == "image" then
return tryImage(desc.path), desc.flip and true or false
elseif t == "sprite" then
@@ -140,7 +146,7 @@ function OakSpeech.defaultSteps(speech)
id = "name_player",
kind = "name",
who = "player",
title = "YOUR NAME?",
title = Strings("YOUR NAME?"),
presetsWho = "player",
presetsFallback = { "RED", "ASH", "JACK" },
},
@@ -154,7 +160,7 @@ function OakSpeech.defaultSteps(speech)
id = "name_rival",
kind = "name",
who = "rival",
title = "HIS NAME?",
title = Strings("HIS NAME?"),
presetsWho = "rival",
presetsFallback = { "BLUE", "GARY", "JOHN" },
},
@@ -220,7 +226,8 @@ function OakSpeech.new(game, onDone)
self.nameLen = constants.playerNameLength or 7
-- RedPicFront (gfx/player/red.png, shared with the trainer card) and
-- the ShrinkPic1/ShrinkPic2 frames (gfx/player/shrink{1,2}.png)
self.playerPic = tryImage("assets/generated/trainer_card/red.png")
self.playerPic = tryImage(require("src.pokemon.Sprites").playerPath(
game.data, "front", { kind = "intro" }))
self.shrinkPic1 = tryImage(oakGfx.shrink1
or "assets/generated/intro/shrink1.png")
self.shrinkPic2 = tryImage(oakGfx.shrink2
@@ -337,7 +344,7 @@ function OakSpeech:runStep(step)
self.picFlip = true
self:revealPic("wipe", function()
Sound.playCry(self.game.data, self.demoSpecies)
self:say("_OakSpeechText2A", function() self:advance() end)
self:say(Strings("_OakSpeechText2A"), function() self:advance() end)
end)
elseif kind == "name" then
local who = step.who or "player"
@@ -345,7 +352,7 @@ function OakSpeech:runStep(step)
or namePresets(self.game, step.presetsWho or who,
step.presetsFallback or { "RED" })
require("src.ui.Screens").push(self.game, "NamingScreen", {
title = step.title or (who == "rival" and "HIS NAME?" or "YOUR NAME?"),
title = step.title or (who == "rival" and "HIS NAME?" or Strings("YOUR NAME?")),
presets = presets,
maxLen = step.maxLen or self.nameLen,
onDone = function(name)
+19 -18
View File
@@ -22,6 +22,7 @@ local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local OptionRows = require("src.ui.OptionRows")
local Renderer = require("src.render.Renderer")
local Strings = require("src.core.Strings")
local OptionsMenu = {}
OptionsMenu.__index = OptionsMenu
@@ -116,14 +117,14 @@ local function sameRows(_, rows) return rows end
-- ladder's, so the save.options mutations are unchanged
local function buildRows(game)
local rows = {
{ id = "textSpeed", label = "TEXT SPEED",
{ id = "textSpeed", label = Strings("TEXT SPEED"),
value = function(g) return SPEEDS[speedIndex(g)][2] end,
step = function(g)
local i = speedIndex(g) % #SPEEDS + 1
g.save.options.textSpeed = SPEEDS[i][1]
return true
end },
{ id = "animations", label = "BATTLE ANIMATION",
{ id = "animations", label = Strings("BATTLE ANIMATION"),
value = function(g)
return g.save.options.animations == false and "OFF" or "ON"
end,
@@ -132,7 +133,7 @@ local function buildRows(game)
o.animations = o.animations == false and true or false
return true
end },
{ id = "battleStyle", label = "BATTLE STYLE",
{ id = "battleStyle", label = Strings("BATTLE STYLE"),
value = function(g)
return g.save.options.battleStyle == "set" and "SET" or "SHIFT"
end,
@@ -141,7 +142,7 @@ local function buildRows(game)
o.battleStyle = o.battleStyle == "set" and "shift" or "set"
return true
end },
{ id = "ruleset", label = "RULESET",
{ id = "ruleset", label = Strings("RULESET"),
value = function(g) return rulesetName(g) end,
step = function(g, dir)
local ids = rulesetIds(g)
@@ -150,7 +151,7 @@ local function buildRows(game)
g.save.options.ruleset = ids[wrapIndex(i - 1 + dir, #ids) + 1]
return true
end },
{ id = "musicVol", label = "MUSIC VOL",
{ id = "musicVol", label = Strings("MUSIC VOL"),
value = function(g) return volLabel(g.save.options.musicVol) end,
step = function(g, dir)
local o = g.save.options
@@ -158,7 +159,7 @@ local function buildRows(game)
require("src.core.Music").setVolumeLevel(o.musicVol)
return true
end },
{ id = "sfxVol", label = "SFX VOL",
{ id = "sfxVol", label = Strings("SFX VOL"),
value = function(g) return volLabel(g.save.options.sfxVol) end,
step = function(g, dir)
local o = g.save.options
@@ -166,7 +167,7 @@ local function buildRows(game)
require("src.core.Sound").setVolumeLevel(o.sfxVol)
return true
end },
{ id = "musicFilter", label = "MUSIC FILTER",
{ id = "musicFilter", label = Strings("MUSIC FILTER"),
value = function(g)
return FILTERS[(g.save.options.musicFilter or 0) + 1]
end,
@@ -176,7 +177,7 @@ local function buildRows(game)
require("src.core.Music").setFilterLevel(o.musicFilter)
return true
end },
{ id = "colors", label = "COLORS",
{ id = "colors", label = Strings("COLORS"),
value = function(g)
return PaletteFX.modeLabel(g.save.options.colors or "gbc")
end,
@@ -188,7 +189,7 @@ local function buildRows(game)
PaletteFX.setMode(o.colors)
return true
end },
{ id = "tilt", label = "TILT",
{ id = "tilt", label = Strings("TILT"),
value = function(g) return Tilt.levelLabel(g.save.options.tilt or 0) end,
step = function(g, dir)
local o = g.save.options
@@ -205,7 +206,7 @@ local function buildRows(game)
end
return true
end },
{ id = "gbcfx", label = "GBC FX",
{ id = "gbcfx", label = Strings("GBC FX"),
value = function(g)
return GBCFX.levelLabel(g.save.options.gbcfx or 0)
end,
@@ -215,7 +216,7 @@ local function buildRows(game)
GBCFX.setLevel(o.gbcfx)
return true
end },
{ id = "zoom", label = "ZOOM",
{ id = "zoom", label = Strings("ZOOM"),
value = function(g)
return Zoom.offsetLabel(g.save.options.zoom or 0)
end,
@@ -230,7 +231,7 @@ local function buildRows(game)
Zoom.offset = off
return true
end },
{ id = "voidFill", label = "VOID FILL",
{ id = "voidFill", label = Strings("VOID FILL"),
value = function(g)
return TileRenderer.voidFillLabel(g.save.options.voidFill)
end,
@@ -246,7 +247,7 @@ local function buildRows(game)
TileRenderer.setVoidFill(o.voidFill)
return true
end },
{ id = "videoMode", label = "VIDEO MODE",
{ id = "videoMode", label = Strings("VIDEO MODE"),
value = function(g)
return VideoMode.modeLabel(g.save.options.videoMode)
end,
@@ -259,7 +260,7 @@ local function buildRows(game)
-- hard render cap (issue #88): bounds the present rate so a
-- driver-forced vsync-off run cannot spin at thousands of FPS. Logic
-- is fixed-step off dt, so this touches presentation only.
{ id = "fpsCap", label = "MAX FPS",
{ id = "fpsCap", label = Strings("MAX FPS"),
value = function(g)
return FrameCap.label(g.save.options.fpsCap)
end,
@@ -271,7 +272,7 @@ local function buildRows(game)
end },
-- fast-forward the logic clock only; music and sfx keep their tempo
-- (src/core/GameSpeed.lua), so this is safe to leave on
{ id = "speed", label = "GAME SPEED",
{ id = "speed", label = Strings("GAME SPEED"),
value = function(g)
return GameSpeed.levelLabel(g.save.options.speed)
end,
@@ -282,17 +283,17 @@ local function buildRows(game)
end },
-- the manager's discoverable home (18-mod-manager-ux); inert until
-- opened, so the row costs a vanilla install nothing
{ id = "mods", label = "MODS",
{ id = "mods", label = Strings("MODS"),
value = function(g)
local status = g.modStatus or {}
return ("%d INSTALLED"):format(#(status.available or {}))
return Strings("%d INSTALLED", #(status.available or {}))
end,
activate = function(g)
require("src.ui.Screens").push(g, "ManagerState")
end },
-- rebinding UI (gap C2, 12-ui-extensibility 4.4); captured inputs
-- live in options.bindings, so the row costs a vanilla install nothing
{ id = "controls", label = "CONTROLS",
{ id = "controls", label = Strings("CONTROLS"),
activate = function(g)
require("src.ui.Screens").push(g, "BindingsMenu")
end },
+39 -33
View File
@@ -17,6 +17,7 @@ local Screens = require("src.ui.Screens")
local Theme = require("src.ui.Theme")
local FieldDefaults = require("src.world.FieldDefaults")
local Map = require("src.world.Map")
local Strings = require("src.core.Strings")
local PartyMenu = {}
PartyMenu.__index = PartyMenu
@@ -204,7 +205,7 @@ function PartyMenu:update(dt)
self.game.save.flashLit = true
self.game.stack:push(TextBox.new(self.game,
self.game.data.text._FlashLightsAreaText
or "A blinding FLASH\nlights the area!", function()
or Strings("A blinding FLASH\nlights the area!"), function()
self.game.stack:push(Transition.whiteFlash(self.game))
end))
return
@@ -247,7 +248,7 @@ function PartyMenu:update(dt)
current = "_CurrentTooFastText",
no_place = "_SurfingNoPlaceToGetOffText" })[reason]
or "_NoSurfingHereText"
local txt = (self.game.data.text[key] or "No SURFing here!")
local txt = (self.game.data.text[key] or Strings("No SURFing here!"))
:gsub("{RAM:wNameBuffer}", mon.nickname or def.name)
if reason == "no_place" then
-- .cannotStopSurfing prints _SurfingNoPlaceToGetOffText but
@@ -278,7 +279,7 @@ function PartyMenu:update(dt)
local def = self.game.data.pokemon[mon.species]
local key = (reason == "no_badge") and "_NewBadgeRequiredText"
or "_NothingToCutText"
local txt = (self.game.data.text[key] or "Nothing to CUT!")
local txt = (self.game.data.text[key] or Strings("Nothing to CUT!"))
:gsub("{RAM:wNameBuffer}", mon.nickname or def.name)
self.game.stack:push(TextBox.new(self.game, txt))
return -- .loop: submenu stays open behind the message
@@ -300,9 +301,9 @@ function PartyMenu:update(dt)
self.game.stack:pop() -- close the party menu (jp .goBackToMap)
ow.strengthActive = true
local t1 = (self.game.data.text._UsedStrengthText
or "{RAM:wNameBuffer} used\nSTRENGTH."):gsub("{RAM:wNameBuffer}", name)
or Strings("{RAM:wNameBuffer} used\nSTRENGTH.")):gsub("{RAM:wNameBuffer}", name)
local t2 = (self.game.data.text._CanMoveBouldersText
or "{RAM:wNameBuffer} can\nmove boulders."):gsub("{RAM:wNameBuffer}", name)
or Strings("{RAM:wNameBuffer} can\nmove boulders.")):gsub("{RAM:wNameBuffer}", name)
self.game.stack:push(TextBox.new(self.game, t1, function()
self.game.stack:push(TextBox.new(self.game, t2, function()
self.game.stack:push(Transition.whiteFlash(self.game))
@@ -348,7 +349,7 @@ function PartyMenu:update(dt)
or user.hp <= heal then
self.softboiledFrom = nil
local TextBox = require("src.render.TextBox")
self.game.stack:push(TextBox.new(self.game, "It won't have\nany effect."))
self.game.stack:push(TextBox.new(self.game, Strings("It won't have\nany effect.")))
else
user.hp = user.hp - heal
mon.hp = math.min(mon.stats.hp, mon.hp + heal)
@@ -357,7 +358,7 @@ function PartyMenu:update(dt)
local def = self.game.data.pokemon[mon.species]
local TextBox = require("src.render.TextBox")
self.game.stack:push(TextBox.new(self.game,
("%s's HP\nwas restored!"):format(mon.nickname or def.name)))
Strings("%s's HP\nwas restored!", mon.nickname or def.name)))
end
elseif self.swapFrom then
if self.swapFrom ~= self.index then
@@ -375,14 +376,14 @@ function PartyMenu:update(dt)
local ow = self.game.overworld
if self.battle and self.onSwitch then
-- SwitchStatsCancelText (core.asm PartyMenuOrRockOrRun)
items = { { label = "SWITCH", action = "battle_switch" },
{ label = "STATS", action = "stats" },
{ label = "CANCEL", action = "cancel" } }
items = { { label = Strings("SWITCH"), action = "battle_switch" },
{ label = Strings("STATS"), action = "stats" },
{ label = Strings("CANCEL"), action = "cancel" } }
else
-- STATS/SWITCH plus this mon's field moves (start_sub_menus.asm
-- builds the same dynamic list)
items = { { label = "STATS", action = "stats" },
{ label = "SWITCH", action = "switch" } }
items = { { label = Strings("STATS"), action = "stats" },
{ label = Strings("SWITCH"), action = "switch" } }
-- Field moves (HMs/TMs) are usable out of battle even when the mon
-- is fainted -- Gen 1 does not require HP for Cut/Fly/Surf/etc.
-- Battle still excludes this list via `not self.battle`. Softboiled
@@ -395,35 +396,35 @@ function PartyMenu:update(dt)
for _, mv in ipairs(mon.moves) do
if mv.id == "FLY" and outside
and self.game.save.inventory.THUNDERBADGE then
table.insert(items, { label = "FLY", action = "fly" })
table.insert(items, { label = Strings("FLY"), action = "fly" })
elseif mv.id == "FLASH" and ow.dark
and self.game.save.inventory.BOULDERBADGE then
table.insert(items, { label = "FLASH", action = "flash" })
table.insert(items, { label = Strings("FLASH"), action = "flash" })
elseif mv.id == "CUT" and self.game.save.inventory.CASCADEBADGE then
-- CUT/SURF/STRENGTH are party-menu field moves too
-- (start_sub_menus.asm .outOfBattleMovePointers); listed here
-- with the same list-time badge filter this file already uses
-- for FLY/FLASH. The facing-tile/activation check happens on
-- selection (useCutFieldMove/useSurfFieldMove).
table.insert(items, { label = "CUT", action = "cut" })
table.insert(items, { label = Strings("CUT"), action = "cut" })
elseif mv.id == "SURF" and self.game.save.inventory.SOULBADGE then
table.insert(items, { label = "SURF", action = "surf" })
table.insert(items, { label = Strings("SURF"), action = "surf" })
elseif mv.id == "STRENGTH" and self.game.save.inventory.RAINBOWBADGE then
table.insert(items, { label = "STRENGTH", action = "strength" })
table.insert(items, { label = Strings("STRENGTH"), action = "strength" })
elseif mv.id == "SOFTBOILED" then
table.insert(items, { label = "SOFTBOILED", action = "softboiled" })
table.insert(items, { label = Strings("SOFTBOILED"), action = "softboiled" })
elseif mv.id == "TELEPORT" and outside then
-- TELEPORT works only OUTDOORS (start_sub_menus.asm
-- .teleport -> CheckIfInOutsideMap); dark maps don't
-- block it
table.insert(items, { label = "TELEPORT", action = "escape" })
table.insert(items, { label = Strings("TELEPORT"), action = "escape" })
elseif mv.id == "DIG" and DIG_TILESETS[ow.map.def.tileset]
and ow.map.id ~= "AGATHAS_ROOM" then
-- DIG runs ItemUseEscapeRope (.dig sets wCurItem =
-- ESCAPE_ROPE): usable in the dungeon tilesets of
-- escape_rope_tilesets.asm minus Agatha's room, even in
-- the dark (Rock Tunnel)
table.insert(items, { label = "DIG", action = "escape" })
table.insert(items, { label = Strings("DIG"), action = "escape" })
end
end
end
@@ -456,13 +457,13 @@ function PartyMenu:bottomMessage()
return "Use on which one?"
elseif self.tmhm then
return self.game.data.text._PartyMenuUseTMText
or "Use TM on which\nPOKéMON?"
or Strings("Use TM on which\nPOKéMON?")
elseif self.battle then
return self.game.data.text._PartyMenuBattleText
or "Bring out which\nPOKéMON?"
or Strings("Bring out which\nPOKéMON?")
else
return self.game.data.text._PartyMenuNormalText
or "Choose a POKéMON."
or Strings("Choose a POKéMON.")
end
end
@@ -480,7 +481,7 @@ function PartyMenu:draw()
love.graphics.setColor(0, 0, 0, 1)
local party = self.party or self.game.save.party
if #party == 0 then
Font.draw("No POKéMON!", 16, 64)
Font.draw(Strings("No POKéMON!"), 16, 64)
end
local HudTiles = require("src.render.HudTiles")
for i, mon in ipairs(party) do
@@ -512,13 +513,13 @@ function PartyMenu:draw()
end
-- right-aligned so the shorter "ABLE" shares "NOT ABLE"'s right edge
if can then
Font.draw("ABLE", 120, y + 8)
Font.draw(Strings("ABLE"), 120, y + 8)
else
Font.draw("NOT ABLE", 88, y + 8)
Font.draw(Strings("NOT ABLE"), 88, y + 8)
end
else
if mon.hp <= 0 then
Font.draw("FNT", 136, y)
Font.draw(Strings("FNT"), 136, y)
elseif mon.status then
Font.draw(mon.status, 136, y)
end
@@ -528,17 +529,22 @@ function PartyMenu:draw()
love.graphics.setColor(0, 0, 0, 1)
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 104, y + 8)
end
-- home/pokemon.asm PartyMenuInit seeds wTopMenuItemY/X with 1/0, so the
-- cursor sits on the entry's *second* tile row (the level/HP line),
-- level with the middle of the two-row icon -- not on the name row that
-- entryY returns. Drawing it at y put it a tile too high (#278).
local cursorY = y + 8
if i == self.index then
Font.drawCode(Theme.cursor, 0, y)
Font.drawCode(Theme.cursor, 0, cursorY)
end
if i == self.swapFrom or i == self.softboiledFrom then
Font.drawCode(Theme.cursorHollow, 0, y) -- the unfilled swap arrow
Font.drawCode(Theme.cursorHollow, 0, cursorY) -- the unfilled swap arrow
end
end
if self.swapFrom then
Font.draw("Move to where?", 8, 136)
Font.draw(Strings("Move to where?"), 8, 136)
elseif self.softboiledFrom then
Font.draw("Use on which one?", 8, 136)
Font.draw(Strings("Use on which one?"), 8, 136)
elseif self.tmhm then
-- "Use TM on which\nPOKeMON?" in the standard bottom text box
-- (party_menu.asm keeps the message box for the TM/HM menu); box + line
@@ -546,14 +552,14 @@ function PartyMenu:draw()
Font.drawBox(0, 12, 20, 6)
love.graphics.setColor(0, 0, 0, 1)
local prompt = self.game.data.text._PartyMenuUseTMText
or "Use TM on which\nPOKéMON?"
or Strings("Use TM on which\nPOKéMON?")
local ly = 112
for line in (prompt .. "\n"):gmatch("([^\n]*)\n") do
Font.draw(line, 8, ly)
ly = ly + 16
end
elseif self.pickOnly then
Font.draw("Use on which one?", 8, 136)
Font.draw(Strings("Use on which one?"), 8, 136)
else
-- default field party menu (StartMenu) and the battle voluntary-switch
-- (BattleState:openParty): Gen1 prints PartyMenuNormalText / PartyMenuBattleText
+12 -11
View File
@@ -10,6 +10,7 @@ local ChoiceBox = require("src.ui.ChoiceBox")
local ListMenu = require("src.ui.ListMenu")
local Menu = require("src.ui.Menu")
local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local PlayerPC = {}
@@ -76,14 +77,14 @@ local function withdraw(game)
askQuantity(game, list, pc[item.value] or 1, item.value, function(qty)
local Bag = require("src.inventory.Bag")
if not Bag.add(game.save, item.value, qty) then
list.footer = "You can't carry\nany more items."
list.footer = Strings("You can't carry\nany more items.")
return
end
pc[item.value] = pc[item.value] - qty
if pc[item.value] <= 0 then pc[item.value] = nil end
refreshRow(list, pc, item.value)
Sound.play(game.data, "Withdraw_Deposit")
list.footer = ("Withdrew\n%s."):format(itemName(game, item.value))
list.footer = Strings("Withdrew\n%s.", itemName(game, item.value))
end)
end,
}))
@@ -112,14 +113,14 @@ local function deposit(game)
onChoose = function(item, list)
askQuantity(game, list, inv[item.value] or 1, item.value, function(qty)
if pcFull(game, pc, item.value) then
list.footer = "No room left to\nstore items."
list.footer = Strings("No room left to\nstore items.")
return
end
require("src.inventory.Bag").remove(game.save, item.value, qty)
pc[item.value] = (pc[item.value] or 0) + qty
refreshRow(list, inv, item.value)
Sound.play(game.data, "Withdraw_Deposit")
list.footer = ("%s was\nstored via PC."):format(itemName(game, item.value))
list.footer = Strings("%s was\nstored via PC.", itemName(game, item.value))
end)
end,
}))
@@ -132,7 +133,7 @@ local function toss(game)
onChoose = function(item, list)
local def = game.data.items[item.value]
if (def and def.keyItem) or item.value:find("^HM_") then
list.footer = "That's too impor-\ntant to toss!"
list.footer = Strings("That's too impor-\ntant to toss!")
return
end
local QuantityBox = require("src.ui.QuantityBox")
@@ -140,13 +141,13 @@ local function toss(game)
max = pc[item.value] or 1,
onDone = function(qty)
if not qty then return end
list.footer = ("Toss %s?"):format(itemName(game, item.value))
list.footer = Strings("Toss %s?", itemName(game, item.value))
game.stack:push(ChoiceBox.new(game, function(yes)
if yes then
pc[item.value] = pc[item.value] - qty
if pc[item.value] <= 0 then pc[item.value] = nil end
refreshRow(list, pc, item.value)
list.footer = ("Threw away %s."):format(itemName(game, item.value))
list.footer = Strings("Threw away %s.", itemName(game, item.value))
else
list.footer = nil
end
@@ -163,10 +164,10 @@ function PlayerPC.new(game)
-- keepOpen so B in the item lists returns here instead of dropping the
-- whole PC session (players_pc.asm re-shows the PC menu); same pattern
-- as BoxMenu's rows
{ label = "WITHDRAW ITEM", keepOpen = true, onSelect = function() withdraw(game) end },
{ label = "DEPOSIT ITEM", keepOpen = true, onSelect = function() deposit(game) end },
{ label = "TOSS ITEM", keepOpen = true, onSelect = function() toss(game) end },
{ label = "LOG OFF" },
{ label = Strings("WITHDRAW ITEM"), keepOpen = true, onSelect = function() withdraw(game) end },
{ label = Strings("DEPOSIT ITEM"), keepOpen = true, onSelect = function() deposit(game) end },
{ label = Strings("TOSS ITEM"), keepOpen = true, onSelect = function() toss(game) end },
{ label = Strings("LOG OFF") },
-- silent PC session (BIT_NO_MENU_BUTTON_SOUND); players_pc.asm
-- PlayersPCMenu TextBoxBorder (0,0) b=8 c=14 → 16x10
}, { tx = 0, ty = 0, tw = 16, th = 10, noSound = true })
+6 -5
View File
@@ -1,6 +1,7 @@
-- Minimal Pokédex: dex-ordered list with seen/owned markers.
local ListMenu = require("src.ui.ListMenu")
local Strings = require("src.core.Strings")
local PokedexMenu = {}
@@ -46,7 +47,7 @@ function PokedexMenu.new(game, opts)
end
end
local list = ListMenu.new(game, "POKéDEX", items, {
footer = ("SEEN %d OWNED %d"):format(seen, owned),
footer = Strings("SEEN %d OWNED %d", seen, owned),
pageJump = true, -- Left/Right page jumps like the original
onCancel = opts.onCancel, -- B returns to the start menu when opened from it
onChoose = function(item)
@@ -57,16 +58,16 @@ function PokedexMenu.new(game, opts)
local Menu = require("src.ui.Menu")
local Screens = require("src.ui.Screens")
game.stack:push(Menu.new(game, {
{ label = "DATA", onSelect = function()
{ label = Strings("DATA"), onSelect = function()
Screens.push(game, "DexEntryMenu", item.value)
end },
{ label = "CRY", keepOpen = true, onSelect = function()
{ label = Strings("CRY"), keepOpen = true, onSelect = function()
require("src.core.Sound").playCry(game.data, item.value)
end },
{ label = "AREA", onSelect = function()
{ label = Strings("AREA"), onSelect = function()
Screens.push(game, "TownMap", { nestSpecies = item.value })
end },
{ label = "QUIT" },
{ label = Strings("QUIT") },
}, { tx = 12, ty = 8, tw = 8, th = 10 }))
end,
})
+8 -7
View File
@@ -6,6 +6,7 @@
local Font = require("src.render.Font")
local SaveData = require("src.core.SaveData")
local Strings = require("src.core.Strings")
local QuarantineReport = {}
QuarantineReport.__index = QuarantineReport
@@ -38,12 +39,12 @@ local function buildLines(report, meta)
end
local rows = {}
for _, mon in ipairs(report.lostMons or {}) do
rows[#rows + 1] = ("%s (%s)"):format(mon.species or "?", mon.from or "?")
rows[#rows + 1] = Strings("%s (%s)", mon.species or "?", mon.from or "?")
end
section(lines, "Moved to LOST box:", rows)
rows = {}
for _, item in ipairs(report.lostItems or {}) do
rows[#rows + 1] = ("%s x%d"):format(item.id or "?", item.count or 1)
rows[#rows + 1] = Strings("%s x%d", item.id or "?", item.count or 1)
end
section(lines, "Items removed:", rows)
rows = {}
@@ -51,16 +52,16 @@ local function buildLines(report, meta)
if map.to then
rows[#rows + 1] = ("%s>%s"):format(map.id or "?", map.to)
else
rows[#rows + 1] = ("%s (%s)"):format(map.id or "?", map.field or "?")
rows[#rows + 1] = Strings("%s (%s)", map.id or "?", map.field or "?")
end
end
section(lines, "Location reset:", rows)
rows = {}
for _, mon in ipairs(report.restoredMons or {}) do
rows[#rows + 1] = ("%s to box %d"):format(mon.species or "?", mon.box or 0)
rows[#rows + 1] = Strings("%s to box %d", mon.species or "?", mon.box or 0)
end
for _, item in ipairs(report.restoredItems or {}) do
rows[#rows + 1] = ("%s x%d"):format(item.id or "?", item.count or 1)
rows[#rows + 1] = Strings("%s x%d", item.id or "?", item.count or 1)
end
section(lines, "Restored:", rows)
local notice = SaveData.modsDiffNotice(report.modsDiff, meta)
@@ -113,7 +114,7 @@ function QuarantineReport:draw()
love.graphics.rectangle("fill", 0, 0, 160, 144)
Font.drawBox(0, 0, 20, 18)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("LOAD REPORT", 8, 8)
Font.draw(Strings("LOAD REPORT"), 8, 8)
for row = 1, VISIBLE do
local line = self.lines[self.offset + row]
if line then Font.draw(line, 8, 12 + row * 8) end
@@ -121,7 +122,7 @@ function QuarantineReport:draw()
if self.offset < self:maxOffset() then
Font.drawCode(require("src.ui.Theme").moreArrow, 144, 124)
end
Font.draw("A:CONTINUE", 8, 130)
Font.draw(Strings("A:CONTINUE"), 8, 130)
love.graphics.setColor(1, 1, 1, 1)
end
+10 -9
View File
@@ -12,6 +12,7 @@ local ChoiceBox = require("src.ui.ChoiceBox")
local ListMenu = require("src.ui.ListMenu")
local Menu = require("src.ui.Menu")
local QuantityBox = require("src.ui.QuantityBox")
local Strings = require("src.core.Strings")
local ShopMenu = {}
@@ -33,7 +34,7 @@ local function buy(game, stock)
end
local greet = txt(game, "_PokemartBuyingGreetingText", "Take your time.")
local notEnough = txt(game, "_PokemartNotEnoughMoneyText",
"You don't have\nenough money.")
Strings("You don't have\nenough money."))
local list
list = ListMenu.new(game, "BUY", items, {
dialogue = true,
@@ -56,7 +57,7 @@ local function buy(game, stock)
end
local cost = qty * def.price
-- _PokemartTellBuyPriceText + yes/no confirm
list.footer = ("%s?\nThat will be\n¥%d. OK?"):format(def.name, cost)
list.footer = Strings("%s?\nThat will be\n¥%d. OK?", def.name, cost)
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then
list.footer = greet
@@ -68,13 +69,13 @@ local function buy(game, stock)
end
if not Bag.add(game.save, item.value, qty) then
list.footer = txt(game, "_PokemartItemBagFullText",
"You can't carry\nany more items.")
Strings("You can't carry\nany more items."))
return
end
require("src.core.Sound").play(game.data, "Purchase")
game.save.money = game.save.money - cost
list.footer = txt(game, "_PokemartBoughtItemText",
"Here you are!\nThank you!")
Strings("Here you are!\nThank you!"))
end))
end,
}))
@@ -112,7 +113,7 @@ local function sell(game)
-- ITEM_NONE "0" from Blue's House before that pickup was fixed (#11).
if not def or def.keyItem or item.value:find("^HM_") then
list.footer = txt(game, "_PokemartUnsellableItemText",
"I can't put a\nprice on that.")
Strings("I can't put a\nprice on that."))
return
end
local unit = math.floor(def.price / 2)
@@ -125,7 +126,7 @@ local function sell(game)
return
end
-- _PokemartTellSellPriceText + yes/no confirm
list.footer = ("I can pay you\n¥%d for that."):format(unit * qty)
list.footer = Strings("I can pay you\n¥%d for that.", unit * qty)
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then
list.footer = greet
@@ -152,9 +153,9 @@ function ShopMenu.new(game, stock, onQuit)
-- keepOpen: the mart menu stays underneath its list so closing the
-- list lands back here; only QUIT (or B) leaves and fires onQuit
local menu = Menu.new(game, {
{ label = "BUY", keepOpen = true, onSelect = function() buy(game, stock) end },
{ label = "SELL", keepOpen = true, onSelect = function() sell(game) end },
{ label = "QUIT", onSelect = onQuit },
{ label = Strings("BUY"), keepOpen = true, onSelect = function() buy(game, stock) end },
{ label = Strings("SELL"), keepOpen = true, onSelect = function() sell(game) end },
{ label = Strings("QUIT"), onSelect = onQuit },
}, { tx = 0, ty = 0, tw = 8, th = 8 })
menu.onCancel = onQuit
return menu
+8 -7
View File
@@ -45,6 +45,7 @@
local Font = require("src.render.Font")
local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local SlotMachine = {}
SlotMachine.__index = SlotMachine
@@ -356,7 +357,7 @@ function SlotMachine:resolveWin(win)
-- SlotReward300Func prints "Yeah!" (text_pause) before the flash; the port
-- shows it in the box while the screen flashes. LinedUpText follows.
self.yeah = (sym == "7")
self.message = ("%s lined up!\nScored %d coins!"):format(sym, pay)
self.message = Strings("%s lined up!\nScored %d coins!", sym, pay)
-- .flashScreenLoop: flip rBGP, wait 5 frames, b times. The coins are not
-- credited until the player dismisses the "lined up" text (see startPayout).
self.stage = "flash"
@@ -394,7 +395,7 @@ end
-- delay then CloseTextDisplay), otherwise ask "One more go?".
function SlotMachine:afterSpin()
if coins(self) == 0 then
self.message = "Darn!\nRan out of coins!"
self.message = Strings("Darn!\nRan out of coins!")
self.stage = "message"
self.afterMessage = nil
self.exitTimer = 60
@@ -515,7 +516,7 @@ function SlotMachine:update(dt)
self.bet = 3 - self.betIndex
if input:wasPressed("a") then
if coins(self) < self.bet then
self.message = "Not enough\ncoins!"
self.message = Strings("Not enough\ncoins!")
self.afterMessage = "bet"
self.stage = "message"
return
@@ -671,8 +672,8 @@ function SlotMachine:drawBottom()
local by = self.stage == "intro" and 6 or 11
Font.drawBox(13, by, 6, 5)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("YES", 15 * 8, (by + 1) * 8)
Font.draw("NO", 15 * 8, (by + 2) * 8)
Font.draw(Strings("YES"), 15 * 8, (by + 1) * 8)
Font.draw(Strings("NO"), 15 * 8, (by + 2) * 8)
Font.drawCode(0xED, 14 * 8, (by + 1 + (self.yesno == 1 and 0 or 1)) * 8)
end
love.graphics.setColor(1, 1, 1, 1)
@@ -723,8 +724,8 @@ function SlotMachine:drawPlain(art)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("SLOT MACHINE", 32, 4)
Font.draw(("COINS %4d"):format(coins(self)), 8, 16)
Font.draw(Strings("SLOT MACHINE"), 32, 4)
Font.draw(Strings("COINS %4d", coins(self)), 8, 16)
if art then self:drawReels(art) end
love.graphics.setColor(0, 0, 0, 1)
Font.draw(">", 12, 56)
+17 -16
View File
@@ -10,6 +10,7 @@ local Menu = require("src.ui.Menu")
local Renderer = require("src.render.Renderer")
local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens")
local Strings = require("src.core.Strings")
local StartMenu = {}
@@ -26,19 +27,19 @@ function StartMenu.new(game)
-- POKéDEX: only after Oak hands it over
if flags.EVENT_GOT_POKEDEX then
table.insert(items, { label = "POKéDEX", onSelect = function()
table.insert(items, { label = Strings("POKéDEX"), onSelect = function()
Screens.push(game, "PokedexMenu", { onCancel = reopen })
end })
end
-- POKéMON is always listed (draw_start_menu.asm prints it even with
-- an empty party; selecting it then just no-ops)
table.insert(items, { label = "POKéMON", onSelect = function()
table.insert(items, { label = Strings("POKéMON"), onSelect = function()
if #game.save.party == 0 then return end
Screens.push(game, "PartyMenu", { onCancel = reopen })
end })
table.insert(items, { label = "ITEM", onSelect = function()
table.insert(items, { label = Strings("ITEM"), onSelect = function()
Screens.push(game, "BagMenu", { onCancel = reopen })
end })
@@ -50,7 +51,7 @@ function StartMenu.new(game)
-- SAVE shows the player/badges/dex/time panel then asks to confirm
-- (PrintSaveScreenText)
table.insert(items, { label = "SAVE", onSelect = function()
table.insert(items, { label = Strings("SAVE"), onSelect = function()
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
local badges = require("src.inventory.Badges").count(game.data, game.save)
@@ -59,32 +60,32 @@ function StartMenu.new(game)
owned = owned + 1
end
local t = math.floor(game.save.playTime or 0)
local panel = ("PLAYER %s\nBADGES %d\nPOKéDEX %3d\nTIME %6d:%02d")
:format(game.save.player.name or "RED", badges, owned,
math.floor(t / 3600), math.floor(t / 60) % 60)
local panel = Strings("PLAYER %s\nBADGES %d\nPOKéDEX %3d\nTIME %6d:%02d",
game.save.player.name or "RED", badges, owned,
math.floor(t / 3600), math.floor(t / 60) % 60)
game.stack:push(TextBox.new(game,
panel .. "\fWould you like to\nSAVE the game?", function()
panel .. Strings("\fWould you like to\nSAVE the game?"), function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then return end
-- "Now saving..." beat before the write (save.asm
-- NowSavingString), then GameSavedText + SFX_SAVE
game.stack:push(TextBox.new(game, "Now saving...", function()
game.stack:push(TextBox.new(game, Strings("Now saving..."), function()
game:writeSave()
require("src.core.Sound").play(game.data, "Save")
game.stack:push(TextBox.new(game,
(game.save.player.name or "RED") .. " saved\nthe game!"))
Strings("%s saved\nthe game!", game.save.player.name or "RED")))
end))
end))
end))
end })
table.insert(items, { label = "OPTION", onSelect = function()
table.insert(items, { label = Strings("OPTION"), onSelect = function()
Screens.push(game, "OptionsMenu", { onCancel = reopen })
end })
-- LINK needs a party
if #game.save.party > 0 then
table.insert(items, { label = "LINK", onSelect = function()
table.insert(items, { label = Strings("LINK"), onSelect = function()
local LinkState = require("src.link.LinkState")
game.stack:push(LinkState.new(game))
end })
@@ -94,7 +95,7 @@ function StartMenu.new(game)
-- one discovered mod so a vanilla install's menu is unchanged
local status = game.modStatus
if status and #(status.available or {}) > 0 then
table.insert(items, { label = "MODS", onSelect = function()
table.insert(items, { label = Strings("MODS"), onSelect = function()
Screens.push(game, "ManagerState")
end })
end
@@ -102,10 +103,10 @@ function StartMenu.new(game)
-- the original's EXIT just closed the menu (CloseStartMenu); with a
-- window close button covering that, QUIT instead power-cycles back
-- to the title after a confirm (defaultNo guards accidental quits)
table.insert(items, { label = "QUIT", onSelect = function()
table.insert(items, { label = Strings("QUIT"), onSelect = function()
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
game.stack:push(TextBox.new(game, "RETURN TO MAIN\nMENU?", function()
game.stack:push(TextBox.new(game, Strings("RETURN TO MAIN\nMENU?"), function()
game.stack:push(ChoiceBox.new(game, function(yes)
if yes then game:returnToTitle() end
end, { defaultNo = true }))
@@ -155,7 +156,7 @@ function StartMenu.new(game)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(("%3d"):format(math.floor(safari.steps or 0)), 8, 8)
Font.draw("/500", 32, 8)
Font.draw("BALL", 8, 24)
Font.draw(Strings("BALL"), 8, 24)
Font.draw(("%2d"):format(math.floor(safari.balls or 0)), 48, 24)
love.graphics.setColor(1, 1, 1, 1)
end
+9 -8
View File
@@ -12,6 +12,7 @@ local Font = require("src.render.Font")
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
-- battle move-type box already do (#214).
local TypeChart = require("src.battle.TypeChart")
local Strings = require("src.core.Strings")
local SummaryMenu = {}
SummaryMenu.__index = SummaryMenu
@@ -90,7 +91,7 @@ function SummaryMenu:draw()
drawLineBox(19, 1, 6, 10)
HudTiles.drawHPBar(data, 11, 3, mon, 1) -- wHPBarType 1
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 96, 32)
Font.draw("STATUS/", 72, 48)
Font.draw(Strings("STATUS/"), 72, 48)
Font.draw(mon.status or "OK", 128, 48)
-- stats box (0,8) 10x10: names rows 9/11/13/15, values indented
@@ -107,27 +108,27 @@ function SummaryMenu:draw()
-- TYPE1/TYPE2/IDNo/OT column (10,9) with values indented (11,10)
drawLineBox(19, 9, 8, 6)
Font.draw("TYPE1/", 80, 72)
Font.draw(Strings("TYPE1/"), 80, 72)
Font.draw(def.types[1] and TypeChart.displayName(def.types[1]) or "", 88, 80)
if def.types[2] then
Font.draw("TYPE2/", 80, 88)
Font.draw(Strings("TYPE2/"), 80, 88)
Font.draw(TypeChart.displayName(def.types[2]), 88, 96)
end
Font.draw("IDNo/", 80, 104)
Font.draw(Strings("IDNo/"), 80, 104)
-- the trainer ID is rolled at new game (SaveData.newGame) and
-- backfilled on load for old saves
Font.draw(("%05d"):format(mon.otId or game.save.player.id or 0), 96, 112)
Font.draw("OT/", 80, 120)
Font.draw(Strings("OT/"), 80, 120)
Font.draw(mon.ot or game.save.player.name or "RED", 96, 128)
else
-- page 2: EXP + the moves with PP (StatusScreen2)
drawLineBox(19, 1, 6, 10)
Font.draw("EXP POINTS", 72, 24)
Font.draw(Strings("EXP POINTS"), 72, 24)
Font.draw(("%d"):format(mon.exp), 96, 32)
-- StatusScreen2: "LEVEL UP" at (9,5); next-exp PrintNumber 7 cols
-- at (7,6); space at (14,6); PrintLevel at (16,6). The old
-- "%d to L%d" string at x=88 overflowed the DrawLineBox edge.
Font.draw("LEVEL UP", 72, 40)
Font.draw(Strings("LEVEL UP"), 72, 40)
local Growth = require("src.pokemon.Growth")
local nextExp = mon.level < 100
and (Growth.expForLevel(def.growthRate, mon.level + 1) - mon.exp) or 0
@@ -141,7 +142,7 @@ function SummaryMenu:draw()
if mv then
local mdef = data.moves[mv.id]
Font.draw(mdef.name, 16, y)
Font.draw("PP", 88, y + 8)
Font.draw(Strings("PP"), 88, y + 8)
Font.draw(("%2d/%2d"):format(mv.pp, mdef.pp), 112, y + 8)
else
Font.draw("-", 16, y)
+11 -10
View File
@@ -6,6 +6,7 @@
local Font = require("src.render.Font")
local Music = require("src.core.Music")
local GameVersion = require("src.core.GameVersion")
local Strings = require("src.core.Strings")
local TitleState = {}
TitleState.__index = TitleState
@@ -165,19 +166,19 @@ function ContinueInfo:draw()
-- box at (4,7), 8x14 content; labels double-spaced from (5,9)
Font.drawBox(4, 7, 16, 10)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("PLAYER", 40, 72)
Font.draw(Strings("PLAYER"), 40, 72)
Font.draw((save.player and save.player.name) or "RED", 96, 72)
local badges = require("src.inventory.Badges").count(self.game.data, save)
Font.draw("BADGES", 40, 88)
Font.draw(Strings("BADGES"), 40, 88)
Font.draw(("%2d"):format(badges), 128, 88)
local owned = 0
for _ in pairs(save.pokedex and save.pokedex.owned or {}) do
owned = owned + 1
end
Font.draw("POKéDEX", 40, 104)
Font.draw(Strings("POKéDEX"), 40, 104)
Font.draw(("%3d"):format(owned), 120, 104)
local t = math.floor(save.playTime or 0)
Font.draw("TIME", 40, 120)
Font.draw(Strings("TIME"), 40, 120)
Font.draw(("%3d:%02d"):format(math.floor(t / 3600),
math.floor(t / 60) % 60), 104, 120)
love.graphics.setColor(1, 1, 1, 1)
@@ -188,7 +189,7 @@ function TitleState:openMenu()
local game = self.game
local items = {}
if hasSave() then
table.insert(items, { label = "CONTINUE", onSelect = function()
table.insert(items, { label = Strings("CONTINUE"), onSelect = function()
-- peek at the save for the info window; fall through if the
-- file can't be read
local ok, loaded = pcall(require("src.core.SaveData").load)
@@ -199,13 +200,13 @@ function TitleState:openMenu()
end
end })
end
table.insert(items, { label = "NEW GAME", onSelect = function()
table.insert(items, { label = Strings("NEW GAME"), onSelect = function()
if self.onNewGame then self.onNewGame() end
end })
table.insert(items, { label = "OPTION", onSelect = function()
table.insert(items, { label = Strings("OPTION"), onSelect = function()
require("src.ui.Screens").push(game, "OptionsMenu")
end })
table.insert(items, { label = "EXIT GAME", onSelect = function()
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
if love.event and love.event.quit then
love.event.quit()
end
@@ -254,7 +255,7 @@ function TitleState:draw()
love.graphics.draw(self.logo, 16, 8)
else
love.graphics.setColor(0, 0, 0, 1)
Font.draw(self.blue and "POKéMON BLUE" or "POKéMON RED",
Font.draw(self.blue and "POKéMON BLUE" or Strings("POKéMON RED"),
(160 - 12 * 8) / 2, 24)
love.graphics.setColor(1, 1, 1, 1)
end
@@ -290,7 +291,7 @@ function TitleState:draw()
love.graphics.setColor(0, 0, 0, 1)
-- the copyright row (tile 2,17); copyrightText because field.title's
-- copyright key already names the extracted image strip
Font.draw(self.title.copyrightText or "2026 bois club games", 1, 136)
Font.draw(self.title.copyrightText or Strings("2026 bois club games"), 1, 136)
love.graphics.setColor(1, 1, 1, 1)
end
+2 -1
View File
@@ -4,6 +4,7 @@
local Font = require("src.render.Font")
local Sound = require("src.core.Sound")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local TradeAnim = {}
TradeAnim.__index = TradeAnim
@@ -343,7 +344,7 @@ function TradeAnim:drawMonInfo(mon, ot, otId, boxTy)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(no, 56, y0)
Font.draw(speciesName(self.game, mon), 40, y0 + 16)
Font.draw("OT/" .. (ot or "????"), 40, y0 + 32)
Font.draw(Strings("OT/%s", ot or "????"), 40, y0 + 32)
Font.draw(("IDNo.%05d"):format(otId or 0), 40, y0 + 48)
love.graphics.setColor(1, 1, 1, 1)
end
+10 -4
View File
@@ -4,8 +4,10 @@
-- are built from the real trainer_info.png frame tiles (the patterned
-- band + line style).
local Assets = require("src.render.Assets")
local Badges = require("src.inventory.Badges")
local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local TrainerCard = {}
TrainerCard.__index = TrainerCard
@@ -16,8 +18,11 @@ function TrainerCard:sgbPalettes(game)
return require("src.render.PaletteFX").wholeNamed(game.data, "MEWMON")
end
-- through Assets.resolve so an enabled mod's overrides/ shadows these the
-- same way it shadows every other generated asset
local function tryImage(path)
local ok, img = pcall(love.graphics.newImage, path)
if not path then return nil end
local ok, img = pcall(love.graphics.newImage, Assets.resolve(path))
return ok and img or nil
end
@@ -62,7 +67,8 @@ function TrainerCard.new(game, opts)
end
end
self.circle = tryImage("assets/generated/trainer_card/circle_tile.png")
self.pic = tryImage("assets/generated/trainer_card/red.png")
self.pic = tryImage(require("src.pokemon.Sprites").playerPath(
game.data, "front", { kind = "trainer_card" }))
return self
end
@@ -114,7 +120,7 @@ function TrainerCard:draw()
love.graphics.draw(self.pic, 104, 4)
end
love.graphics.setColor(0, 0, 0, 1)
Font.draw("NAME/" .. (save.player.name or "RED"), 16, 16)
Font.draw(Strings("NAME/%s", save.player.name or "RED"), 16, 16)
Font.draw(("MONEY/¥%d"):format(save.money or 0), 16, 32)
local t = math.floor(save.playTime or 0)
Font.draw(("TIME/%3d:%02d"):format(math.floor(t / 3600),
@@ -123,7 +129,7 @@ function TrainerCard:draw()
-- the circle-dotted BADGES banner (TrainerInfo_BadgesText)
self:frameBox(0, 8, 20, 3)
love.graphics.setColor(0, 0, 0, 1)
Font.draw("BADGES", 56, 72)
Font.draw(Strings("BADGES"), 56, 72)
if self.circle then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(self.circle, 48, 72)
+14
View File
@@ -79,6 +79,19 @@ local PLAYER_SPRITES = {
bike = "SPRITE_RED_BIKE", fly = "SPRITE_BIRD",
}
-- The player's own trainer art: RedPicBack (the battle back pic, up until
-- "Go!"), OldManPicBack (the catch tutorial fights in his place), and
-- RedPicFront (gfx/player/red.png, shared by Oak's intro, the trainer card
-- and the Hall of Fame). Asset paths, not sprite ids -- these are pics,
-- unrelated to the walking sheet in PLAYER_SPRITES above. Resolution runs
-- through Sprites.playerPath, so the player.sprite hook can still vary them
-- per save after content freezes.
local PLAYER_PICS = {
back = "assets/generated/battle/redb.png",
demoBack = "assets/generated/battle/oldmanb.png",
front = "assets/generated/trainer_card/red.png",
}
-- Route22Gate_Script rewrites wLastMap from the player's Y every frame, so
-- the north exit leaves onto Route 23 and the south onto Route 22. Rules
-- are ordered, first match wins, the last row is the default.
@@ -106,6 +119,7 @@ FieldDefaults.FIELD = {
fishing = FISHING,
safari = SAFARI,
playerSprites = PLAYER_SPRITES,
playerPics = PLAYER_PICS,
lastMapRewrites = LAST_MAP_REWRITES,
-- CheckIfInOutsideMap: what counts as "outside" for the wLastMap memory
outsideTilesets = { "OVERWORLD", "PLATEAU" },
+103 -65
View File
@@ -22,6 +22,7 @@ local TextBox = require("src.render.TextBox")
local Transition = require("src.render.Transition")
local Warp = require("src.world.Warp")
local Zoom = require("src.render.Zoom")
local Strings = require("src.core.Strings")
-- isOverworld marks the live world state for WorldAPI's stack scan
local OverworldState = { isOpaque = true, isOverworld = true }
@@ -1312,10 +1313,10 @@ function OverworldState:goFishing(rod)
Game.stack:push(TextBox.new(Game, ". . .", function()
self.fishing = nil
if not enc then
Game.stack:push(TextBox.new(Game, "Not even a nibble!"))
Game.stack:push(TextBox.new(Game, Strings("Not even a nibble!")))
return
end
Game.stack:push(TextBox.new(Game, "Oh!\nIt's a bite!", function()
Game.stack:push(TextBox.new(Game, Strings("Oh!\nIt's a bite!"), function()
local BattleState = require("src.battle.BattleState")
local battle = BattleState.newWild(Game, enc.species, enc.level, { hooked = true })
if Game.save.safari and Map.inRegion(self.map.def, "SAFARI", "SAFARI_ZONE") then
@@ -1482,29 +1483,66 @@ function OverworldState:tryBookshelf(fx, fy)
if self.map.def.tileset == "MANSION"
and self.map:tileAt(fx * 2, fy * 2) == 0x38 then
Game.stack:push(TextBox.new(Game, t._DiglettSculptureText
or "It's a sculpture\nof DIGLETT."))
or Strings("It's a sculpture\nof DIGLETT.")))
return true
end
Game.stack:push(TextBox.new(Game, t._PokemonBooksText
or "Crammed full of\nPOKéMON books!"))
or Strings("Crammed full of\nPOKéMON books!")))
elseif kind == "stuff" then
Game.stack:push(TextBox.new(Game, t._PokemonStuffText
or "There's a slew of\nPOKéMON stuff!"))
or Strings("There's a slew of\nPOKéMON stuff!")))
elseif kind == "elevator" then
Game.stack:push(TextBox.new(Game, t._ElevatorText
or "An elevator!"))
or Strings("An elevator!")))
elseif kind == "statues" then
-- IndigoPlateauStatues: the plaque, then one of the two lines
-- keyed by the statue's column (XCoord bit 0)
local line = (self.player.cellX % 2 == 0) and t._IndigoPlateauStatuesText2
or t._IndigoPlateauStatuesText3
Game.stack:push(TextBox.new(Game,
(t._IndigoPlateauStatuesText1 or "INDIGO PLATEAU") .. "\f"
.. (line or "POKéMON LEAGUE HQ")))
(t._IndigoPlateauStatuesText1 or Strings("INDIGO PLATEAU")) .. "\f"
.. (line or Strings("POKéMON LEAGUE HQ"))))
end
return true
end
-- Bench guys are the one hidden-event family whose extracted label is a
-- wrapper rather than the string itself: bench_guys.asm defines e.g.
-- PewterCityPokecenterBenchGuyText:: as `text_far _PewterCityPokecenterGuyText`,
-- so data/generated/field.lua carries the wrapper name while the text sits
-- under the far label. The two only coincide for Mt Moon and the Celadon
-- hotel, which is why every other bench guy answered with silence (#248).
-- pokered's own names are irregular here (Cerulean/Lavender/Vermilion drop
-- "City", Cinnabar drops "Island"), so this is a table, not a transform.
local BENCH_GUY_TEXT = {
ViridianCityPokecenterBenchGuyText = "_ViridianCityPokecenterGuyText",
PewterCityPokecenterBenchGuyText = "_PewterCityPokecenterGuyText",
CeruleanCityPokecenterBenchGuyText = "_CeruleanPokecenterGuyText",
LavenderCityPokecenterBenchGuyText = "_LavenderPokecenterGuyText",
VermilionCityPokecenterBenchGuyText = "_VermilionPokecenterGuyText",
CeladonCityPokecenterBenchGuyText = "_CeladonCityPokecenterGuyText",
FuchsiaCityPokecenterBenchGuyText = "_FuchsiaCityPokecenterGuyText",
CinnabarIslandPokecenterBenchGuyText = "_CinnabarPokecenterGuyText",
RockTunnelPokecenterBenchGuyText = "_RockTunnelPokecenterGuyText",
}
-- SaffronCityPokecenterBenchGuyText is text_asm: he complains about ROCKET
-- until EVENT_BEAT_SILPH_CO_GIOVANNI, then thanks you for clearing them out.
-- Takes data/save rather than reading the Game upvalue so tests can resolve
-- every label in the table without standing a whole overworld up.
function OverworldState.benchGuyText(data, save, label)
if not label then return nil end
if label == "SaffronCityPokecenterBenchGuyText" then
local key = (save and save.flags and save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI)
and "_SaffronCityPokecenterGuyText2"
or "_SaffronCityPokecenterGuyText1"
return data.text[key]
end
-- the direct name first, so a cache whose extractor already resolved the
-- far label keeps working without consulting the table
return data.text["_" .. label] or data.text[BENCH_GUY_TEXT[label] or ""]
end
-- Hidden events at the faced cell (data/events/hidden_events.asm):
-- HiddenItems give their item once, HiddenCoins fill the COIN CASE,
-- StartSlotMachine seats open the minigame. Taken spots persist in
@@ -1519,7 +1557,7 @@ function OverworldState:tryHiddenObject(fx, fy)
save.hiddenTaken = save.hiddenTaken or {}
if save.hiddenTaken[key] then return false end
if not require("src.inventory.Bag").add(save, h.item, 1) then
Game.stack:push(TextBox.new(Game, "You can't carry\nany more items!"))
Game.stack:push(TextBox.new(Game, Strings("You can't carry\nany more items!")))
return true
end
save.hiddenTaken[key] = true
@@ -1527,7 +1565,7 @@ function OverworldState:tryHiddenObject(fx, fy)
-- hidden items always play SFX_GET_ITEM_2 (hidden_items.asm)
require("src.core.Sound").play(Game.data, "Get_Item2")
Game.stack:push(TextBox.new(Game,
("%s found\n%s!"):format(save.player.name, name)))
Strings("%s found\n%s!", save.player.name, name)))
return true
end
end
@@ -1541,7 +1579,7 @@ function OverworldState:tryHiddenObject(fx, fy)
save.coins = math.min(9999, (save.coins or 0) + h.coins)
require("src.core.Sound").play(Game.data, "Get_Item2")
Game.stack:push(TextBox.new(Game,
("%s found\n%d coins!"):format(save.player.name, h.coins)))
Strings("%s found\n%d coins!", save.player.name, h.coins)))
return true
end
end
@@ -1553,20 +1591,20 @@ function OverworldState:tryHiddenObject(fx, fy)
if h.x == fx and h.y == fy then
if h.state == "out_of_order" then
Game.stack:push(TextBox.new(Game, txt._GameCornerOutOfOrderText
or "OUT OF ORDER\nThis is broken."))
or Strings("OUT OF ORDER\nThis is broken.")))
elseif h.state == "out_to_lunch" then
Game.stack:push(TextBox.new(Game, txt._GameCornerOutToLunchText
or "OUT TO LUNCH\nThis is reserved."))
or Strings("OUT TO LUNCH\nThis is reserved.")))
elseif h.state == "keys" then
Game.stack:push(TextBox.new(Game, txt._GameCornerSomeonesKeysText
or "Someone's keys!\nThey'll be back."))
or Strings("Someone's keys!\nThey'll be back.")))
elseif not save.inventory.COIN_CASE then
Game.stack:push(TextBox.new(Game, txt._GameCornerCoinCaseText
or "A COIN CASE is\nrequired!"))
or Strings("A COIN CASE is\nrequired!")))
elseif (save.coins or 0) == 0 then
-- AbleToPlaySlotsCheck: a COIN CASE with no coins can't play
Game.stack:push(TextBox.new(Game, txt._GameCornerNoCoinsText
or "You don't have\nany coins!"))
or Strings("You don't have\nany coins!")))
else
-- one machine per visit is secretly lucky
-- (wLuckySlotHiddenEventIndex, engine/slots/game_corner_slots.asm)
@@ -1609,7 +1647,7 @@ function OverworldState:tryHiddenObject(fx, fy)
-- bench guys (data/events/bench_guys.asm)
for _, h in ipairs(extras.benchGuys[self.map.id] or {}) do
if h.x == fx and h.y == fy and (not h.facing or h.facing == facing) then
local text = h.text and Game.data.text["_" .. h.text]
local text = OverworldState.benchGuyText(Game.data, save, h.text)
if text then
Game.stack:push(TextBox.new(Game, text))
return true
@@ -1626,7 +1664,7 @@ function OverworldState:tryHiddenObject(fx, fy)
if gym then
local key = save.inventory[gym.badge] and "_GymStatueText2" or "_GymStatueText1"
local text = Game.data.text[key]
or "{RAM}\nPOKéMON GYM\nLEADER: {RAM}"
or Strings("{RAM}\nPOKéMON GYM\nLEADER: {RAM}")
text = text:gsub("{RAM:wGymCityName}", gym.city)
:gsub("{RAM:wGymLeaderName}", gym.leader)
Game.stack:push(TextBox.new(Game, text))
@@ -1639,7 +1677,7 @@ function OverworldState:tryHiddenObject(fx, fy)
for _, h in ipairs(extras.printTrash and extras.printTrash[self.map.id] or {}) do
if h.x == fx and h.y == fy then
Game.stack:push(TextBox.new(Game, txt._VermilionGymTrashText
or "Nope, there's\nonly trash here."))
or Strings("Nope, there's\nonly trash here.")))
return true
end
end
@@ -1681,7 +1719,7 @@ function OverworldState:tryCardKeyDoor(fx, fy)
local t = Game.data.text
if not Game.save.inventory.CARD_KEY then
Game.stack:push(TextBox.new(Game,
t._CardKeyFailText or "Darn! It needs a\nCARD KEY!"))
t._CardKeyFailText or Strings("Darn! It needs a\nCARD KEY!")))
return true
end
require("src.core.Sound").play(Game.data, "Go_Inside")
@@ -1698,8 +1736,8 @@ function OverworldState:tryCardKeyDoor(fx, fy)
end
end
Game.stack:push(TextBox.new(Game,
(t._CardKeySuccessText1 or "Bingo!")
.. (t._CardKeySuccessText2 or "\nThe CARD KEY\nopened the door!")))
(t._CardKeySuccessText1 or Strings("Bingo!"))
.. (t._CardKeySuccessText2 or Strings("\nThe CARD KEY\nopened the door!"))))
return true
end
@@ -1716,7 +1754,7 @@ function OverworldState:trashCanSwitch(canIndex)
local t = Game.data.text
local save = Game.save
local tc = Game.data.field.hiddenExtras.trashCans
local trashText = t._VermilionGymTrashText or "Nope, there's\nonly trash here."
local trashText = t._VermilionGymTrashText or Strings("Nope, there's\nonly trash here.")
-- "Don't do the trash can puzzle if it's already been done."
if save.flags.EVENT_2ND_LOCK_OPENED then
Game.stack:push(TextBox.new(Game, trashText))
@@ -1764,7 +1802,7 @@ function OverworldState:trashCanSwitch(canIndex)
-- open.
Game.stack:push(TextBox.new(Game,
t._VermilionGymTrashSuccessText1
or "Hey! There's a\nswitch under the\ntrash!\fThe 1st electric\nlock opened!",
or Strings("Hey! There's a\nswitch under the\ntrash!\fThe 1st electric\nlock opened!"),
function() require("src.core.Sound").play(Game.data, "Switch") end))
return
end
@@ -1781,7 +1819,7 @@ function OverworldState:trashCanSwitch(canIndex)
-- prints, so the beep fires as the box closes, not as it opens.
Game.stack:push(TextBox.new(Game,
t._VermilionGymTrashSuccessText3
or "The 2nd electric\nlock opened!\fThe motorized door\nopened!",
or Strings("The 2nd electric\nlock opened!\fThe motorized door\nopened!"),
function() require("src.core.Sound").play(Game.data, "Go_Inside") end))
else
-- wrong can: ResetEvent EVENT_1ST_LOCK_OPENED and immediately
@@ -1793,7 +1831,7 @@ function OverworldState:trashCanSwitch(canIndex)
-- text prints, so the beep fires as the box closes, not as it opens.
Game.stack:push(TextBox.new(Game,
t._VermilionGymTrashFailText
or "Nope! There's\nonly trash here.\fHey! The electric\nlocks were reset!",
or Strings("Nope! There's\nonly trash here.\fHey! The electric\nlocks were reset!"),
function() require("src.core.Sound").play(Game.data, "Denied") end))
end
end
@@ -1815,12 +1853,12 @@ function OverworldState:billsHousePC()
if flags.EVENT_USED_CELL_SEPARATOR_ON_BILL
or not flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR then
Game.stack:push(TextBox.new(Game, t._BillsHouseMonitorText
or "TELEPORTER is\ndisplayed on the\nPC monitor."))
or Strings("TELEPORTER is\ndisplayed on the\nPC monitor.")))
return
end
require("src.core.Music").stop()
Game.stack:push(TextBox.new(Game, t._BillsHouseInitiatedText
or "{PLAYER} initiated\nTELEPORTER's Cell\nSeparator!", function()
or Strings("{PLAYER} initiated\nTELEPORTER's Cell\nSeparator!"), function()
flags.EVENT_USED_CELL_SEPARATOR_ON_BILL = true
require("src.core.Sound").play(Game.data, "Switch")
self:queueScript({
@@ -1857,13 +1895,13 @@ function OverworldState:billsHousePokemonList()
end,
})
end
table.insert(items, { label = "CANCEL" })
table.insert(items, { label = Strings("CANCEL") })
-- TextBoxBorder b=10,c=9 at (0,0) -> total tw=11, th=12
Game.stack:push(Menu.new(Game, items,
{ tx = 0, ty = 0, tw = 11, th = 12 }))
end
Game.stack:push(TextBox.new(Game, t._BillsHousePokemonListText1
or "BILL's favorite\nPOKéMON list!", openList))
or Strings("BILL's favorite\nPOKéMON list!"), openList))
end
-- BillsHouseBillExitsMachineScript: human Bill appears inside the machine
@@ -1957,7 +1995,7 @@ function OverworldState:trySurf(fx, fy)
local p = self.player
p.surfing = true
require("src.core.Music").setSurfing(Game.data, true)
local text = (Game.data.text._SurfingGotOnText or "{PLAYER} got on\n{RAM:wNameBuffer}!")
local text = (Game.data.text._SurfingGotOnText or Strings("{PLAYER} got on\n{RAM:wNameBuffer}!"))
:gsub("{RAM:wNameBuffer}", name)
Game.stack:push(TextBox.new(Game, text, function()
-- start_sub_menus.asm .surf: UseItem returns (mount + text done),
@@ -2005,7 +2043,7 @@ function OverworldState:tryCut(fx, fy)
-- gen 1 confirms nothing (engine/overworld/cut.asm UsedCut): the
-- _UsedCutText message, then the tree vanishes with dust + SFX_CUT
local name = mon.nickname or Game.data.pokemon[mon.species].name
local text = (Game.data.text._UsedCutText or "{RAM:wNameBuffer} hacked\naway with CUT!")
local text = (Game.data.text._UsedCutText or Strings("{RAM:wNameBuffer} hacked\naway with CUT!"))
:gsub("{RAM:wNameBuffer}", name)
Game.stack:push(TextBox.new(Game, text, function()
self.cutBlocks = self.cutBlocks or {}
@@ -2141,7 +2179,7 @@ function OverworldState:talkTo(npc)
-- the string "0" as truthy, so screen it out and fall through to text.
if d.item and d.item ~= "0" and d.item ~= 0 then
if not require("src.inventory.Bag").add(Game.save, d.item, 1) then
Game.stack:push(TextBox.new(Game, "You can't carry\nany more items!"))
Game.stack:push(TextBox.new(Game, Strings("You can't carry\nany more items!")))
return
end
Game.save.itemsTaken = Game.save.itemsTaken or {}
@@ -2157,7 +2195,7 @@ function OverworldState:talkTo(npc)
require("src.core.Sound").play(Game.data,
(ddef and ddef.keyItem) and "Get_Key_Item" or "Get_Item1")
Game.stack:push(TextBox.new(Game,
("%s found\n%s!"):format(Game.save.player.name, name)))
Strings("%s found\n%s!", Game.save.player.name, name)))
return
end
@@ -2166,7 +2204,7 @@ function OverworldState:talkTo(npc)
if d.pokemon then
npc:facePlayer(self.player)
local text = select(1, Game.data:resolveText(self.map.def.label, d.text))
or "Gyaoo!"
or Strings("Gyaoo!")
local BattleState = require("src.battle.BattleState")
Game.stack:push(TextBox.new(Game, text, function()
local battle = BattleState.newWild(Game, d.pokemon, d.level)
@@ -2209,7 +2247,7 @@ function OverworldState:talkTo(npc)
if entry then
if entry.mart then
npc:facePlayer(self.player)
Game.stack:push(TextBox.new(Game, "Hi there!\nMay I help you?", function()
Game.stack:push(TextBox.new(Game, Strings("Hi there!\nMay I help you?"), function()
Screens.push(Game, "ShopMenu", entry.mart)
unfreeze()
end))
@@ -2253,7 +2291,7 @@ function OverworldState:openPC(onDone)
-- when Bill hands over the SS Ticket)
local metBill = flags.EVENT_MET_BILL or flags.EVENT_GOT_SS_TICKET
table.insert(items, {
label = metBill and "BILL'S PC" or "SOMEONE'S PC",
label = metBill and "BILL'S PC" or Strings("SOMEONE'S PC"),
onSelect = function()
require("src.core.Sound").play(Game.data, "Enter_PC")
Screens.push(Game, "BoxMenu")
@@ -2273,7 +2311,7 @@ function OverworldState:openPC(onDone)
-- Prof. Oak's dex rating only appears once you have the Pokédex
if flags.EVENT_GOT_POKEDEX then
table.insert(items, {
label = "PROF.OAK's PC",
label = Strings("PROF.OAK's PC"),
onSelect = function()
self:dexRating()
done()
@@ -2293,7 +2331,7 @@ function OverworldState:openPC(onDone)
require("src.core.Sound").play(Game.data, "Turn_Off_PC")
done()
end
table.insert(items, { label = "LOG OFF", onSelect = logOff })
table.insert(items, { label = Strings("LOG OFF"), onSelect = logOff })
-- pokered sets BIT_NO_MENU_BUTTON_SOUND for the whole PC session
-- (engine/overworld/pokecenter_pc.asm / player_pc.asm); DisplayPCMainMenu
-- calls TextBoxBorder with c=14 (interior width, +2 for the border), so
@@ -2318,9 +2356,9 @@ function OverworldState:dexRating()
local lo = math.floor(owned / 10) * 10
key = ("_DexRatingText_Own%dTo%d"):format(lo, lo + 9)
end
local rating = Game.data.text[key] or "Keep it up!"
local rating = Game.data.text[key] or Strings("Keep it up!")
local completion = Game.data.text._DexCompletionText
or "POKéDEX comp-\nletion is:\f{NUM:hDexRatingNumMonsSeen} POKéMON seen\n{NUM:hDexRatingNumMonsOwned} POKéMON owned\fPROF.OAK's\nRating:"
or Strings("POKéDEX comp-\nletion is:\f{NUM:hDexRatingNumMonsSeen} POKéMON seen\n{NUM:hDexRatingNumMonsOwned} POKéMON owned\fPROF.OAK's\nRating:")
completion = completion
:gsub("{NUM:hDexRatingNumMonsSeen[^}]*}", tostring(seen))
:gsub("{NUM:hDexRatingNumMonsOwned[^}]*}", tostring(owned))
@@ -2367,20 +2405,20 @@ end
-- POKéMON" and "fighting fit".
function OverworldState:nurseHeal(onDone, npc)
local t = Game.data.text
local bye = t._PokemonCenterFarewellText or "We hope to see\nyou again!"
local bye = t._PokemonCenterFarewellText or Strings("We hope to see\nyou again!")
local hello = t._PokemonCenterWelcomeText
or "Welcome to our\nPOKéMON CENTER!"
or Strings("Welcome to our\nPOKéMON CENTER!")
if not Game.save.usedPokecenter then
Game.save.usedPokecenter = true -- BIT_USED_POKECENTER
hello = hello .. "\f"
.. (t._ShallWeHealYourPokemonText or "Shall we heal your\nPOKéMON?")
.. (t._ShallWeHealYourPokemonText or Strings("Shall we heal your\nPOKéMON?"))
end
Game.stack:push(TextBox.new(Game, hello, nil, { choice = function(yes)
if not yes then
Game.stack:push(TextBox.new(Game, bye, onDone))
return
end
local need = t._NeedYourPokemonText or "OK. We'll need\nyour POKéMON."
local need = t._NeedYourPokemonText or Strings("OK. We'll need\nyour POKéMON.")
Game.stack:push(TextBox.new(Game, need, function()
-- the nurse turns to the machine, the map music stops, and the
-- party heals before the machine runs (predef HealParty)
@@ -2415,7 +2453,7 @@ end
function OverworldState:finishNurseHeal(bye, onDone)
local t = Game.data.text
local fit = t._PokemonFightingFitText or "Your POKéMON are\nfighting fit!"
local fit = t._PokemonFightingFitText or Strings("Your POKéMON are\nfighting fit!")
Game.stack:push(TextBox.new(Game, fit .. "\f" .. bye, onDone))
end
@@ -2427,21 +2465,21 @@ end
-- for the original serial handshake; declining prints "Please come again!"
function OverworldState:cableClubReceptionist(onDone)
local t = Game.data.text
local welcome = t._CableClubNPCWelcomeText or "Welcome to the\nCable Club!"
local welcome = t._CableClubNPCWelcomeText or Strings("Welcome to the\nCable Club!")
if not Game.save.flags.EVENT_GOT_POKEDEX then
-- CableClubNPC .didNotConnect path before the pokedex
Game.stack:push(TextBox.new(Game, welcome .. "\f"
.. (t._CableClubNPCMakingPreparationsText
or "We're making\npreparations.\vPlease wait."), onDone))
or Strings("We're making\npreparations.\vPlease wait.")), onDone))
return
end
local apply = t._CableClubNPCPleaseApplyHereHaveToSaveText
or "Please apply here.\fBefore opening\nthe link, we have\vto save the game."
or Strings("Please apply here.\fBefore opening\nthe link, we have\vto save the game.")
Game.stack:push(TextBox.new(Game, welcome .. "\f" .. apply, nil,
{ choice = function(yes)
if not yes then
Game.stack:push(TextBox.new(Game,
t._CableClubNPCPleaseComeAgainText or "Please come\nagain!", onDone))
t._CableClubNPCPleaseComeAgainText or Strings("Please come\nagain!"), onDone))
return
end
Game:writeSave()
@@ -2476,7 +2514,7 @@ function OverworldState:engageTrainer(npc, onDone)
local battleText = header and header.battle and Game.data.text[header.battle]
if not battleText then
battleText = select(1, Game.data:resolveText(self.map.def.label, d.text))
or "I like shorts!\nThey're comfy and\neasy to wear!"
or Strings("I like shorts!\nThey're comfy and\neasy to wear!")
end
local wonText = header and header.won and Game.data.text[header.won]
@@ -2555,11 +2593,11 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
if reward.badge then
local name = Game.data.items[reward.badge] and Game.data.items[reward.badge].name
or reward.badge
table.insert(lines, ("%s received\nthe %s!"):format(Game.save.player.name, name))
table.insert(lines, Strings("%s received\nthe %s!", Game.save.player.name, name))
end
if reward.item then
local name = Game.stringBuffer or reward.item
table.insert(lines, ("%s received\n%s!"):format(Game.save.player.name, name))
table.insert(lines, Strings("%s received\n%s!", Game.save.player.name, name))
end
end
if #lines > 0 then
@@ -2753,7 +2791,7 @@ function OverworldState:applyFieldPoison()
local queue = {}
for _, mon in ipairs(fainted) do
local name = mon.nickname or Game.data.pokemon[mon.species].name
table.insert(queue, ("%s\nfainted!"):format(name))
table.insert(queue, Strings("%s\nfainted!", name))
end
local alive = false
for _, mon in ipairs(save.party) do
@@ -2767,7 +2805,7 @@ function OverworldState:applyFieldPoison()
end
if not alive then
Game.stack:push(TextBox.new(Game,
("%s blacked\nout!"):format(save.player.name), function()
Strings("%s blacked\nout!", save.player.name), function()
local Pokemon = require("src.pokemon.Pokemon")
for _, mon in ipairs(save.party) do Pokemon.heal(mon) end
save.money = math.floor(save.money
@@ -2903,7 +2941,7 @@ function OverworldState:onStepComplete()
if Game.save.repelSteps == 0 then
-- no encounter on the exact wear-off step (wild_encounters.asm
-- .lastRepelStep returns CantEncounter)
Game.stack:push(TextBox.new(Game, "REPEL's effect\nwore off."))
Game.stack:push(TextBox.new(Game, Strings("REPEL's effect\nwore off.")))
return
end
end
@@ -3037,14 +3075,14 @@ function OverworldState:checkBadgeGate()
-- Route22GateGuardGoRightAheadText plays sound_get_item_1
require("src.core.Sound").play(Game.data, "Get_Item1")
Game.stack:push(TextBox.new(Game,
t["_" .. g.passText] or "Go right ahead!"))
t["_" .. g.passText] or Strings("Go right ahead!")))
end
return false
end
-- Route22GateGuardNoBoulderbadgeText plays SFX_DENIED
require("src.core.Sound").play(Game.data, "Denied")
Game.stack:push(TextBox.new(Game,
(t["_" .. g.failText] or "You don't have the\nBOULDERBADGE yet!")
(t["_" .. g.failText] or Strings("You don't have the\nBOULDERBADGE yet!"))
.. (t._Route22GateGuardICantLetYouPassText or ""), function()
self:scriptMove(p, "down", 1)
end))
@@ -3065,14 +3103,14 @@ function OverworldState:checkBadgeGate()
-- Route23OhThatIsTheBadgeText plays sound_get_item_1
require("src.core.Sound").play(Game.data, "Get_Item1")
local text = (t["_" .. g.passText] or
"Oh! That is the\n{RAM}!"):gsub("{RAM:wNameBuffer}", badgeName)
Strings("Oh! That is the\n{RAM}!")):gsub("{RAM:wNameBuffer}", badgeName)
Game.stack:push(TextBox.new(Game, text))
return false
end
-- Route23YouDontHaveTheBadgeYetText plays SFX_DENIED
require("src.core.Sound").play(Game.data, "Denied")
local text = (t["_" .. g.failText] or
"You don't have the\n{RAM} yet!"):gsub("{RAM:wNameBuffer}", badgeName)
Strings("You don't have the\n{RAM} yet!")):gsub("{RAM:wNameBuffer}", badgeName)
Game.stack:push(TextBox.new(Game, text, function()
self:scriptMove(p, "down", 1)
end))
@@ -3110,7 +3148,7 @@ function OverworldState:checkForcedMovement()
Game.save.forcedBike = true
require("src.core.Music").playMap(Game.data, self.map.id, true)
else
Game.stack:push(TextBox.new(Game, "You need a\nBICYCLE for the\nCycling Road!",
Game.stack:push(TextBox.new(Game, Strings("You need a\nBICYCLE for the\nCycling Road!"),
function()
local back = ({ up = "down", down = "up",
left = "right", right = "left" })[p.facing]
@@ -3238,7 +3276,7 @@ function OverworldState:boulderIntoHole(npc)
for i = #self.entities, 1, -1 do
if self.entities[i] == npc then table.remove(self.entities, i) end
end
Game.stack:push(TextBox.new(Game, "The boulder fell\nthrough the hole!"))
Game.stack:push(TextBox.new(Game, Strings("The boulder fell\nthrough the hole!")))
return true
end
end
@@ -3268,7 +3306,7 @@ function OverworldState:safariStep()
if not st or not self:inSafariStepZone() then return false end
st.steps = st.steps - 1
if st.steps > 0 then return false end
self:safariGameOver("PA: Ding-dong!\nTime's up!")
self:safariGameOver(Strings("PA: Ding-dong!\nTime's up!"))
return true
end
@@ -3277,7 +3315,7 @@ function OverworldState:safariGameOver(text)
Game.save.safari = nil
local t = Game.data.text
Game.stack:push(TextBox.new(Game,
(text or "") .. "\f" .. (t._GameOverText or "PA: Your SAFARI\nGAME is over!"),
(text or "") .. "\f" .. (t._GameOverText or Strings("PA: Your SAFARI\nGAME is over!")),
function()
local exit_ = FieldDefaults.fieldValue(Game.data, "safari", "exitWarp")
self:startWarpTo(exit_.map, exit_.x, exit_.y, exit_.facing or "down")
@@ -3320,7 +3358,7 @@ function OverworldState:afterBattle(result, battle)
else
-- throwing the last SAFARI BALL ends the game
if Game.save.safari and Game.save.safari.balls <= 0 then
self:safariGameOver("PA: You're out of\nSAFARI BALLs!")
self:safariGameOver(Strings("PA: You're out of\nSAFARI BALLs!"))
end
evolutions()
end