diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index a0b138b8..05ae0aa4 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -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). diff --git a/src/battle/EffectRegistry.lua b/src/battle/EffectRegistry.lua index e9d978e4..a6789ade 100644 --- a/src/battle/EffectRegistry.lua +++ b/src/battle/EffectRegistry.lua @@ -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 diff --git a/src/battle/MoveEffects.lua b/src/battle/MoveEffects.lua index 8beb22f3..d04e843c 100644 --- a/src/battle/MoveEffects.lua +++ b/src/battle/MoveEffects.lua @@ -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 diff --git a/src/core/RomText.lua b/src/core/RomText.lua new file mode 100644 index 00000000..ef15eb81 --- /dev/null +++ b/src/core/RomText.lua @@ -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 diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index e075605a..ffb6aa40 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -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)) @@ -2732,7 +2733,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 @@ -2807,21 +2808,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() @@ -3303,7 +3304,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 @@ -3669,7 +3670,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 @@ -3678,7 +3679,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")