Merge pull request #732 from jherediagu/fix/rom-text-move-effects

Extend ROM-text messages to move effects and the overworld
This commit is contained in:
bryanthaboi
2026-08-03 11:58:26 -04:00
committed by GitHub
5 changed files with 157 additions and 145 deletions
+4 -56
View File
@@ -27,70 +27,18 @@ local Timing = require("src.core.Timing")
local TrainerAI = require("src.battle.TrainerAI")
local TurnOrder = require("src.battle.TurnOrder")
local TypeChart = require("src.battle.TypeChart")
local RomText = require("src.core.RomText")
local Strings = require("src.core.Strings")
local WideBattle = require("src.battle.WideBattle")
local romText = RomText
local BattleState = {}
BattleState.__index = BattleState
BattleState.isOpaque = true
-- pokered prints the battle lines itself (engine/battle/core.asm and the
-- move-effect banks), and the importer extracts every one of them, so the
-- port paraphrasing them in Lua meant the screen showed a near-miss of the
-- game's own wording while the cache held the real line -- and on a
-- localized import it showed English over translated data.
--
-- fromRom prefers the extracted text and keeps the literal as the catalog
-- fallback, for a cache built before the label and for the pure-module
-- tests that run without a dataset. The battle text's slots ({USER},
-- {TARGET}, the {RAM:...} buffers) are NOT in the token registry that
-- TextBox.substitute serves -- it only resolves {PLAYER}, {RIVAL} and
-- three string buffers -- so they are spliced here, in argument order,
-- before the box ever sees the string. {PLAYER}/{RIVAL} are left alone
-- for that later pass.
-- {PLAYER}/{RIVAL} are the two slots TextBox.substitute can fill on its
-- own, so they are only consumed here when the caller clearly supplies
-- them: an argument count matching every slot. Matching just the other
-- slots leaves those two for the later pass. Anything else means the
-- extracted line cannot carry what the call has to say -- a few labels
-- stop at a dynamic marker the decoder does not follow, e.g.
-- _EnemysWeakText extracts as "The enemy's weak!\nGet'm! " with nowhere
-- to put the name -- so the engine's own wording stands in rather than
-- printing a sentence with a hole in it.
local function fromRom(data, label, fallback, ...)
local text = data and data.text and data.text[label]
if not text then return Strings(fallback, ...) end
local args = { ... }
if #args == 0 then return text end
local slots, named = 0, 0
for token in text:gmatch("%b{}") do
slots = slots + 1
if token == "{PLAYER}" or token == "{RIVAL}" then named = named + 1 end
end
local fillNamed
if #args == slots then
fillNamed = true
elseif #args == slots - named then
fillNamed = false
else
return Strings(fallback, ...)
end
local index = 0
return (text:gsub("%b{}", function(token)
if not fillNamed and (token == "{PLAYER}" or token == "{RIVAL}") then
return token
end
index = index + 1
local value = args[index]
if value == nil then return token end
return tostring(value)
end))
end
function BattleState:romText(label, fallback, ...)
return fromRom(self.data, label, fallback, ...)
return romText(self.data, label, fallback, ...)
end
-- Letterbox voids around the 160x144 battle canvas fill white so the
-- window reads as one continuous battle screen (no black bars).
+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 romText = require("src.core.RomText")
local Strings = require("src.core.Strings")
local Timing = require("src.core.Timing")
@@ -106,7 +107,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed)
if not (record and record.explode) then battle:cancelMoveAnim() end
missBeat(battle, record)
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
battle:sayNext(romText(battle.data, "_AttackMissedText", "%s's\nattack missed!", displayName(user)))
-- MoveHitTest's INVULNERABLE branch sets the same wMoveMissed as a
-- failed accuracy roll (core.asm:5260), and the miss handler still
-- runs the explode effect ("even if Explosion or Selfdestruct
@@ -135,7 +136,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- Explosion/Selfdestruct still animate on a miss (HandleIfPlayerMoveMissed)
if not (record and record.explode) then battle:cancelMoveAnim() end
missBeat(battle, record)
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
battle:sayNext(romText(battle.data, "_AttackMissedText", "%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
@@ -163,7 +164,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if not counterable or (battle.lastDamage or 0) == 0 then
battle:cancelMoveAnim()
missBeat(battle, record)
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
battle:sayNext(romText(battle.data, "_AttackMissedText", "%s's\nattack missed!", displayName(user)))
return
end
dmg = math.min(65535, battle.lastDamage * 2)
@@ -187,7 +188,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- type immunity zeros damage and sets wMoveMissed in Gen 1, so no anim
if not (record and record.explode) then battle:cancelMoveAnim() end
missBeat(battle, record)
battle:sayNext(Strings("It doesn't affect\n%s!", displayName(target)))
battle:sayNext(romText(battle.data, "_DoesntAffectMonText", "It doesn't affect\n%s!", displayName(target)))
if record and record.onMiss then record.onMiss(ctx, "immune") end
return
end
@@ -195,7 +196,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- 0.25x floored the damage to zero: the original registers a miss
if not (record and record.explode) then battle:cancelMoveAnim() end
missBeat(battle, record)
battle:sayNext(Strings("%s's\nattack missed!", displayName(user)))
battle:sayNext(romText(battle.data, "_AttackMissedText", "%s's\nattack missed!", displayName(user)))
if record and record.onMiss then record.onMiss(ctx, "floored") end
return
end
@@ -247,8 +248,8 @@ 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(Strings("Critical hit!")) end
if info.ohko then battle:sayNext(Strings("One-hit KO!")) end
if info.crit then battle:sayNext(romText(battle.data, "_CriticalHitText", "Critical hit!")) end
if info.ohko then battle:sayNext(romText(battle.data, "_OHKOText", "One-hit KO!")) end
-- PrintCriticalOHKOText closes with `ld c, 20 / jp DelayFrames` at its
-- .done label (core.asm:3812-3814) -- and the no-crit path jumps to that
-- same label (:3799), so this hold is paid on EVERY landed hit, not just
@@ -257,9 +258,9 @@ function EffectRegistry.runDamaging(battle, ctx, record)
-- comes from.
battle:waitNext(Timing.CRIT_OHKO_TEXT)
if info.typeMult > 10 then
battle:sayNext(Strings("It's super\neffective!"))
battle:sayNext(romText(battle.data, "_SuperEffectiveText", "It's super\neffective!"))
elseif info.typeMult < 10 then
battle:sayNext(Strings("It's not very\neffective..."))
battle:sayNext(romText(battle.data, "_NotVeryEffectiveText", "It's not very\neffective..."))
end
if Runtime.wants("battle.damage_dealt") then
Runtime.emit("battle.damage_dealt", {
@@ -277,9 +278,9 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if hits > 1 then
-- player: _MultiHitText; enemy: _HitXTimesText (always plural)
if user.isPlayer then
battle:sayNext(Strings("Hit the enemy\n%d times!", hits))
battle:sayNext(romText(battle.data, "_MultiHitText", "Hit the enemy\n%d times!", hits))
else
battle:sayNext(Strings("Hit %d times!", hits))
battle:sayNext(romText(battle.data, "_HitXTimesText", "Hit %d times!", hits))
end
end
@@ -291,7 +292,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(Strings("%s's\nhit with recoil!", displayName(user)))
battle:sayNext(romText(battle.data, "_HitWithRecoilText", "%s's\nhit with recoil!", displayName(user)))
battle:applyDamage(user, recoil)
end
+54 -52
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 romText = require("src.core.RomText")
local Strings = require("src.core.Strings")
local MoveEffects = {}
@@ -39,12 +40,12 @@ local function changeStage(battle, who, stat, delta, fromEnemy)
if who.mist then
return { Strings("%s is\nprotected by MIST!", displayName(who)) }
end
return { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "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 { Strings("Nothing happened!") }
return { romText(battle.data, "_NothingHappenedText", "Nothing happened!") }
end
who.stages[stat] = new
-- effects.asm:505-506: after any stat-stage change, modified stats are
@@ -89,10 +90,10 @@ end
local function statusMove(status)
return function(battle, user, target, move)
if target.mon.status then
return { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "But, it failed!") }
end
if status == "PSN" and target.substituteHP then
return { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "But, it failed!") }
end
local msgs = inflictStatus(battle, target, status, {
toxic = move and move.id == "TOXIC",
@@ -100,7 +101,7 @@ local function statusMove(status)
source = move and move.id,
})
if #msgs == 0 then
return { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "But, it failed!") }
end
return msgs
end
@@ -112,7 +113,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 { Strings("Fire defrosted\n%s!", displayName(target)) }
return { romText(battle.data, "_FireDefrostedText", "Fire defrosted\n%s!", displayName(target)) }
end
if battle.rng(0, 255) >= chance then return {} end
return inflictStatus(battle, target, status, {
@@ -145,10 +146,10 @@ end
local function confuse(battle, target, pierceSub)
if target.confusedTurns or (target.substituteHP and not pierceSub) then
return { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "But, it failed!") }
end
target.confusedTurns = battle.rng(2, 5)
return { Strings("%s\nbecame confused!", displayName(target)) }
return { romText(battle.data, "_BecameConfusedText", "%s\nbecame confused!", displayName(target)) }
end
-- ---------------------------------------------------------------------
@@ -182,53 +183,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 { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "But, it failed!") }
end
for _, t in ipairs(target.curTypes) do
if t == "GRASS" then return { Strings("But, it failed!") } end
if t == "GRASS" then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
end
target.leechSeeded = true
return { Strings("%s\nwas seeded!", displayName(target)) }
return { romText(battle.data, "_WasSeededText", "%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 { Strings("But, it failed!") } end
if mon.hp == mon.stats.hp then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
mon.hp = mon.stats.hp
mon.status = "SLP"
user.sleepTurns = 2
user.toxicCounter = nil
return { Strings("%s\nstarted sleeping!", displayName(user)) }
return { romText(battle.data, "_StartedSleepingEffect", "%s\nstarted sleeping!", displayName(user)) }
end
if mon.hp == mon.stats.hp then return { Strings("But, it failed!") } end
if mon.hp == mon.stats.hp then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
mon.hp = math.min(mon.stats.hp, mon.hp + math.floor(mon.stats.hp / 2))
return { Strings("%s\nregained health!", displayName(user)) }
return { romText(battle.data, "_RegainedHealthText", "%s\nregained health!", displayName(user)) }
end,
LIGHT_SCREEN_EFFECT = function(battle, user)
if user.lightScreen then return { Strings("But, it failed!") } end
if user.lightScreen then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
user.lightScreen = true
return { Strings("%s's\nprotected against\nspecial attacks!", displayName(user)) }
return { romText(battle.data, "_LightScreenProtectedText", "%s's\nprotected against\nspecial attacks!", displayName(user)) }
end,
REFLECT_EFFECT = function(battle, user)
if user.reflect then return { Strings("But, it failed!") } end
if user.reflect then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
user.reflect = true
return { Strings("%s\ngained armor!", displayName(user)) }
return { romText(battle.data, "_ReflectGainedArmorText", "%s\ngained armor!", displayName(user)) }
end,
MIST_EFFECT = function(battle, user)
if user.mist then return { Strings("But, it failed!") } end
if user.mist then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
user.mist = true
-- _ShroudedInMistText (lowercase "mist")
return { Strings("%s's\nshrouded in mist!", displayName(user)) }
return { romText(battle.data, "_ShroudedInMistText", "%s's\nshrouded in mist!", displayName(user)) }
end,
FOCUS_ENERGY_EFFECT = function(battle, user)
if user.focusEnergy then return { Strings("But, it failed!") } end
if user.focusEnergy then return { romText(battle.data, "_ButItFailedText", "But, it failed!") } end
user.focusEnergy = true
return { Strings("%s's\ngetting pumped!", displayName(user)) }
return { romText(battle.data, "_GettingPumpedText", "%s's\ngetting pumped!", displayName(user)) }
end,
HAZE_EFFECT = function(battle, user, target)
@@ -255,33 +256,33 @@ MoveEffects.primary = {
target.skipMove = true
end
target.mon.status = nil
return { Strings("All STATUS changes\nare eliminated!") }
return { romText(battle.data, "_StatusChangesEliminatedText", "All STATUS changes\nare eliminated!") }
end,
SUBSTITUTE_EFFECT = function(battle, user)
if user.substituteHP then return { Strings("%s\nhas a SUBSTITUTE!", displayName(user)) } end
if user.substituteHP then return { romText(battle.data, "_HasSubstituteText", "%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 { Strings("Too weak to make\na SUBSTITUTE!") }
return { romText(battle.data, "_TooWeakSubstituteText", "Too weak to make\na SUBSTITUTE!") }
end
user.mon.hp = user.mon.hp - cost
user.substituteHP = cost + 1
-- _SubstituteText
return { Strings("It created a\nSUBSTITUTE!") }
return { romText(battle.data, "_SubstituteText", "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 { Strings("But, it failed!") }
return { romText(battle.data, "_ButItFailedText", "But, it failed!") }
end
user.curTypes = { target.curTypes[1], target.curTypes[2] }
-- _ConvertedTypeText
return { Strings("Converted type to\n%s's!", displayName(target)) }
return { romText(battle.data, "_ConvertedTypeText", "Converted type to\n%s's!", displayName(target)) }
end,
-- MIMIC_EFFECT lives in BattleState:resolveMimic: MimicEffect
@@ -312,27 +313,27 @@ MoveEffects.primary = {
table.insert(user.curMoves, { id = mv.id, pp = 5, mimic = true })
end
-- _TransformedText: the copied name prints bare (wNameBuffer)
return { Strings("%s\ntransformed into\n%s!", displayName(user), target.name) }
return { romText(battle.data, "_TransformedText", "%s\ntransformed into\n%s!", displayName(user), target.name) }
end,
DISABLE_EFFECT = function(battle, user, target)
if target.disabledSlot then return { Strings("But, it failed!") } end
if target.disabledSlot then return { romText(battle.data, "_ButItFailedText", "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 { Strings("But, it failed!") } end
if #usable == 0 then return { romText(battle.data, "_ButItFailedText", "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 { Strings("%s's\n%s was\ndisabled!", displayName(target),
return { romText(battle.data, "_MoveWasDisabledText", "%s's\n%s was\ndisabled!", displayName(target),
battle.data.moves[id].name) }
end,
SPLASH_EFFECT = function()
return { Strings("No effect!") }
return { romText(battle.data, "_NoEffectText", "No effect!") }
end,
}
@@ -422,7 +423,7 @@ end
-- drain_hp.asm halves the RAW wDamage IN PLACE (minimum 1) and heals
-- that amount, so Counter would see the halved value
local function drainHalf(text)
local function drainHalf(label, text)
return function(ctx)
local heal = math.max(1, math.floor(ctx.rawDamage / 2))
ctx.battle.lastDamage = heal
@@ -430,8 +431,9 @@ local function drainHalf(text)
mon.hp = math.min(mon.stats.hp, mon.hp + heal)
ctx.drain()
-- `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)))
-- site keeps it in the catalog); the ROM's own line wins when the
-- import carries it, and both are resolved here, at use time
ctx.say(romText(ctx.battle.data, label, text, displayName(ctx.target)))
end
end
@@ -449,7 +451,7 @@ end
-- SUPER_FANG hits Ghosts, and only OHKO_EFFECT calls this (#616).
local function immuneMsg(ctx)
if TypeChart.effectiveness(ctx.move.type, ctx.target.curTypes) == 0 then
return Strings("It doesn't affect\n%s!", displayName(ctx.target))
return romText(ctx.battle.data, "_DoesntAffectMonText", "It doesn't affect\n%s!", displayName(ctx.target))
end
return nil
end
@@ -509,12 +511,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(Strings("%s's\nhit with recoil!", displayName(ctx.user)))
ctx.say(romText(ctx.battle.data, "_HitWithRecoilText", "%s's\nhit with recoil!", displayName(ctx.user)))
ctx.battle:applyDamage(ctx.user, recoil)
end,
},
DRAIN_HP_EFFECT = {
afterDamage = drainHalf(Strings.source("Sucked health from\n%s!")),
afterDamage = drainHalf("_SuckedHealthText", Strings.source("Sucked health from\n%s!")),
},
DREAM_EATER_EFFECT = {
-- only works on sleeping targets (checked before damage)
@@ -522,7 +524,7 @@ MoveEffects.full = {
if ctx.target.mon.status ~= "SLP" then return false, "But, it failed!" end
return true
end,
afterDamage = drainHalf(Strings.source("%s's\ndream was eaten!")),
afterDamage = drainHalf("_DreamWasEatenText", Strings.source("%s's\ndream was eaten!")),
},
-- charge moves: first turn just charges; Fly AND Dig go
@@ -568,7 +570,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(Strings("%s\nbecame confused!", displayName(user)))
ctx.say(romText(ctx.battle.data, "_BecameConfusedText", "%s\nbecame confused!", displayName(user)))
end
end
end
@@ -577,7 +579,7 @@ MoveEffects.full = {
JUMP_KICK_EFFECT = {
onMiss = function(ctx, reason)
if reason ~= "accuracy" then return end
ctx.say(Strings("%s\nkept going and\ncrashed!", displayName(ctx.user)))
ctx.say(romText(ctx.battle.data, "_KeptGoingAndCrashedText", "%s\nkept going and\ncrashed!", displayName(ctx.user)))
ctx.damage(ctx.user, 1)
end,
},
@@ -606,7 +608,7 @@ MoveEffects.full = {
afterDamage = function(ctx)
local battle = ctx.battle
battle.payDay = (battle.payDay or 0) + 2 * ctx.user.mon.level
ctx.say(Strings("Coins scattered\neverywhere!"))
ctx.say(romText(ctx.battle.data, "_CoinsScatteredText", "Coins scattered\neverywhere!"))
end,
},
SWIFT_EFFECT = { neverMiss = true },
@@ -648,27 +650,27 @@ MoveEffects.full = {
end
if ok then
if move.id == "ROAR" then
ctx.say(Strings("%s\nran away scared!", displayName(target)))
ctx.say(romText(ctx.battle.data, "_RanAwayScaredText", "%s\nran away scared!", displayName(target)))
elseif move.id == "WHIRLWIND" then
ctx.say(Strings("%s\nwas blown away!", displayName(target)))
ctx.say(romText(ctx.battle.data, "_WasBlownAwayText", "%s\nwas blown away!", displayName(target)))
else
ctx.say(Strings("%s\nran from battle!", displayName(user)))
ctx.say(romText(ctx.battle.data, "_RanFromBattleText", "%s\nran from battle!", displayName(user)))
end
battle.result = "run"
battle.afterQueue = "finish"
elseif move.id == "TELEPORT" then
battle:cancelMoveAnim()
ctx.say(Strings("But, it failed!"))
ctx.say(romText(ctx.battle.data, "_ButItFailedText", "But, it failed!"))
else
battle:cancelMoveAnim()
ctx.say(Strings("It didn't affect\n%s!", displayName(target)))
ctx.say(romText(ctx.battle.data, "_DidntAffectText", "It didn't affect\n%s!", displayName(target)))
end
elseif move.id == "TELEPORT" then
battle:cancelMoveAnim()
ctx.say(Strings("But, it failed!"))
ctx.say(romText(ctx.battle.data, "_ButItFailedText", "But, it failed!"))
else
battle:cancelMoveAnim()
ctx.say(Strings("%s\nis unaffected!", displayName(target)))
ctx.say(romText(ctx.battle.data, "_IsUnaffectedText", "%s\nis unaffected!", displayName(target)))
end
end,
},
@@ -686,7 +688,7 @@ MoveEffects.full = {
callsMove = function(ctx)
local last = ctx.target.lastMove
if not last then
ctx.say(Strings("The MIRROR MOVE\nfailed!"))
ctx.say(romText(ctx.battle.data, "_MirrorMoveFailedText", "The MIRROR MOVE\nfailed!"))
return nil
end
return last
+60
View File
@@ -0,0 +1,60 @@
-- The line the game itself prints, with the engine's wording as backup.
--
-- pokered prints most of what the player reads -- battle messages, item
-- results, field prompts -- and the importer extracts every one of those
-- labels. Writing the sentence again in Lua meant the screen showed a
-- near-miss of the game's own wording while the cache held the real line,
-- and on a localized import it showed English over translated data.
--
-- Callers pass the pokered label plus the literal they used to print, so
-- the literal stays catalog-backed (Strings) for a cache built before the
-- label, for a total conversion that dropped it, and for the pure-module
-- tests that run without a dataset.
--
-- The slots the extracted text carries ({USER}, {TARGET}, the {RAM:...}
-- buffers) are NOT in the token registry TextBox.substitute serves -- that
-- one only resolves {PLAYER}, {RIVAL} and three string buffers -- so they
-- are filled here, in argument order, before the box ever sees the string.
--
-- {PLAYER}/{RIVAL} are the two the registry CAN fill later, so they are
-- only consumed here when the caller clearly supplies them: an argument
-- count matching every slot. Matching just the other slots leaves those
-- two alone for that pass. Anything else means the extracted line cannot
-- carry what the call has to say -- a few labels stop at a dynamic marker
-- the decoder does not follow, e.g. _EnemysWeakText extracts as "The
-- enemy's weak!\nGet'm! " with nowhere to put the name -- so the engine's
-- own wording stands in rather than printing a sentence with a hole in it.
local Strings = require("src.core.Strings")
return function(data, label, fallback, ...)
local text = data and data.text and data.text[label]
if not text then return Strings(fallback, ...) end
local args = { ... }
if #args == 0 then return text end
local slots, named = 0, 0
for token in text:gmatch("%b{}") do
slots = slots + 1
if token == "{PLAYER}" or token == "{RIVAL}" then named = named + 1 end
end
local fillNamed
if #args == slots then
fillNamed = true
elseif #args == slots - named then
fillNamed = false
else
return Strings(fallback, ...)
end
local index = 0
return (text:gsub("%b{}", function(token)
if not fillNamed and (token == "{PLAYER}" or token == "{RIVAL}") then
return token
end
index = index + 1
local value = args[index]
if value == nil then return token end
return tostring(value)
end))
end
+26 -25
View File
@@ -23,6 +23,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 romText = require("src.core.RomText")
local Strings = require("src.core.Strings")
-- isOverworld marks the live world state for WorldAPI's stack scan
@@ -1570,7 +1571,7 @@ function OverworldState:goFishing(rod)
-- PrintText and only clears it once the verdict box is done, so the rod
-- must NOT vanish with the dots box (#321).
if not enc then
Game.stack:push(TextBox.new(Game, Strings("Not even a nibble!"), function()
Game.stack:push(TextBox.new(Game, romText(Game.data, "_NoNibbleText", "Not even a nibble!"), function()
-- the rod OAM goes out with the verdict box (res BIT_LEDGE_OR_FISHING
-- straight after PrintText) but the player keeps the patched tiles
-- until the overworld reloads them a few frames later
@@ -1581,7 +1582,7 @@ function OverworldState:goFishing(rod)
end))
return
end
Game.stack:push(TextBox.new(Game, Strings("Oh!\nIt's a bite!"), function()
Game.stack:push(TextBox.new(Game, romText(Game.data, "_ItsABiteText", "Oh!\nIt's a bite!"), function()
-- the bite goes straight into battle, which reloads the sprite tiles
self.fishing = nil
self.player.fishing = nil
@@ -1763,7 +1764,7 @@ 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 Strings("It's a sculpture\nof DIGLETT.")))
or romText(Game.data, "_DiglettSculptureText", "It's a sculpture\nof DIGLETT.")))
return true
end
Game.stack:push(TextBox.new(Game, t._PokemonBooksText
@@ -1780,7 +1781,7 @@ function OverworldState:tryBookshelf(fx, fy)
local line = (self.player.cellX % 2 == 0) and t._IndigoPlateauStatuesText2
or t._IndigoPlateauStatuesText3
Game.stack:push(TextBox.new(Game,
(t._IndigoPlateauStatuesText1 or Strings("INDIGO PLATEAU")) .. "\f"
(t._IndigoPlateauStatuesText1 or romText(Game.data, "_IndigoPlateauStatuesText1", "INDIGO PLATEAU")) .. "\f"
.. (line or Strings("POKéMON LEAGUE HQ"))))
end
return true
@@ -1837,7 +1838,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, Game.data) then
Game.stack:push(TextBox.new(Game, Strings("You can't carry\nany more items!")))
Game.stack:push(TextBox.new(Game, romText(Game.data, "_CantCarryMoreText", "You can't carry\nany more items!")))
return true
end
save.hiddenTaken[key] = true
@@ -1871,20 +1872,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 Strings("OUT OF ORDER\nThis is broken.")))
or romText(Game.data, "_GameCornerOutOfOrderText", "OUT OF ORDER\nThis is broken.")))
elseif h.state == "out_to_lunch" then
Game.stack:push(TextBox.new(Game, txt._GameCornerOutToLunchText
or Strings("OUT TO LUNCH\nThis is reserved.")))
or romText(Game.data, "_GameCornerOutToLunchText", "OUT TO LUNCH\nThis is reserved.")))
elseif h.state == "keys" then
Game.stack:push(TextBox.new(Game, txt._GameCornerSomeonesKeysText
or Strings("Someone's keys!\nThey'll be back.")))
or romText(Game.data, "_GameCornerSomeonesKeysText", "Someone's keys!\nThey'll be back.")))
elseif not save.inventory.COIN_CASE then
Game.stack:push(TextBox.new(Game, txt._GameCornerCoinCaseText
or Strings("A COIN CASE is\nrequired!")))
or romText(Game.data, "_GameCornerCoinCaseText", "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 Strings("You don't have\nany coins!")))
or romText(Game.data, "_GameCornerNoCoinsText", "You don't have\nany coins!")))
else
-- one machine per visit is secretly lucky
-- (wLuckySlotHiddenEventIndex, engine/slots/game_corner_slots.asm)
@@ -1968,7 +1969,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 Strings("Nope, there's\nonly trash here.")))
or romText(Game.data, "_VermilionGymTrashText", "Nope, there's\nonly trash here.")))
return true
end
end
@@ -2010,7 +2011,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 Strings("Darn! It needs a\nCARD KEY!")))
t._CardKeyFailText or romText(Game.data, "_CardKeyFailText", "Darn! It needs a\nCARD KEY!")))
return true
end
require("src.core.Sound").play(Game.data, "Go_Inside")
@@ -2028,7 +2029,7 @@ function OverworldState:tryCardKeyDoor(fx, fy)
end
Game.stack:push(TextBox.new(Game,
(t._CardKeySuccessText1 or Strings("Bingo!"))
.. (t._CardKeySuccessText2 or Strings("\nThe CARD KEY\nopened the door!"))))
.. (t._CardKeySuccessText2 or romText(Game.data, "_CardKeySuccessText2", "\nThe CARD KEY\nopened the door!"))))
return true
end
@@ -2045,7 +2046,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 Strings("Nope, there's\nonly trash here.")
local trashText = t._VermilionGymTrashText or romText(Game.data, "_VermilionGymTrashText", "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))
@@ -2144,7 +2145,7 @@ 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 Strings("TELEPORTER is\ndisplayed on the\nPC monitor.")))
or romText(Game.data, "_BillsHouseMonitorText", "TELEPORTER is\ndisplayed on the\nPC monitor.")))
return
end
require("src.core.Music").stop()
@@ -2469,7 +2470,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, Game.data) then
Game.stack:push(TextBox.new(Game, Strings("You can't carry\nany more items!")))
Game.stack:push(TextBox.new(Game, romText(Game.data, "_CantCarryMoreText", "You can't carry\nany more items!")))
return
end
Game.save.itemsTaken = Game.save.itemsTaken or {}
@@ -2537,7 +2538,7 @@ function OverworldState:talkTo(npc)
if entry then
if entry.mart then
npc:facePlayer(self.player)
Game.stack:push(TextBox.new(Game, Strings("Hi there!\nMay I help you?"), function()
Game.stack:push(TextBox.new(Game, romText(Game.data, "_PokemartGreetingText", "Hi there!\nMay I help you?"), function()
Screens.push(Game, "ShopMenu", entry.mart)
unfreeze()
end))
@@ -2739,7 +2740,7 @@ end
-- POKéMON" and "fighting fit".
function OverworldState:nurseHeal(onDone, npc)
local t = Game.data.text
local bye = t._PokemonCenterFarewellText or Strings("We hope to see\nyou again!")
local bye = t._PokemonCenterFarewellText or romText(Game.data, "_PokemonCenterFarewellText", "We hope to see\nyou again!")
local hello = t._PokemonCenterWelcomeText
or Strings("Welcome to our\nPOKéMON CENTER!")
if not Game.save.usedPokecenter then
@@ -2814,21 +2815,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 Strings("Welcome to the\nCable Club!")
local welcome = t._CableClubNPCWelcomeText or romText(Game.data, "_CableClubNPCWelcomeText", "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 Strings("We're making\npreparations.\vPlease wait.")), onDone))
or romText(Game.data, "_CableClubNPCMakingPreparationsText", "We're making\npreparations.\vPlease wait.")), onDone))
return
end
local apply = t._CableClubNPCPleaseApplyHereHaveToSaveText
or Strings("Please apply here.\fBefore opening\nthe link, we have\vto save the game.")
or romText(Game.data, "_CableClubNPCPleaseApplyHereHaveToSaveText", "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 Strings("Please come\nagain!"), onDone))
t._CableClubNPCPleaseComeAgainText or romText(Game.data, "_CableClubNPCPleaseComeAgainText", "Please come\nagain!"), onDone))
return
end
Game:writeSave()
@@ -3310,7 +3311,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, Strings("REPEL's effect\nwore off.")))
Game.stack:push(TextBox.new(Game, romText(Game.data, "_RepelWoreOffText", "REPEL's effect\nwore off.")))
return
end
end
@@ -3676,7 +3677,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(Strings("PA: Ding-dong!\nTime's up!"))
self:safariGameOver(romText(Game.data, "_TimesUpText", "PA: Ding-dong!\nTime's up!"))
return true
end
@@ -3685,7 +3686,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 Strings("PA: Your SAFARI\nGAME is over!")),
(text or "") .. "\f" .. (t._GameOverText or romText(Game.data, "_GameOverText", "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")