mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge branch 'dev' into feat/switch-nx
This commit is contained in:
@@ -14,9 +14,9 @@ return {
|
||||
-- else -> .WhatsLostIsLostText (player has TM_DIG)
|
||||
TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU = {
|
||||
{ "check_item", "TM_DIG" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruTheyStoleATMText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruWhatsLostIsLostText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
-- Fuchsia City (pokered/scripts/FuchsiaCity.asm)
|
||||
--
|
||||
-- The exhibit signs are text_asm bodies: PrintText of a single text_far,
|
||||
-- then DisplayPokedex for the exhibited species. The preview marks the
|
||||
-- species seen but not owned, same as the S.S. Anne passenger's Snorlax.
|
||||
--
|
||||
-- The fossil sign branches on the Mt. Moon fossil events. The exhibit
|
||||
-- holds the fossil the player did NOT take: taking the Dome Fossil puts
|
||||
-- Omanyte on display, taking the Helix Fossil puts Kabuto. With neither
|
||||
-- event set the sign only prints its undetermined line and no dex entry
|
||||
-- opens.
|
||||
--
|
||||
-- The other text pointers (city sign, Safari Game signs, mart/center/gym
|
||||
-- and warden signs, the four NPCs, the exhibited-mon FuchsiaCityPokemonText
|
||||
-- rows) are plain text_far wrappers that resolve through Data:resolveText,
|
||||
-- so they are not ported here.
|
||||
return {
|
||||
FUCHSIA_CITY = {
|
||||
talk = {
|
||||
-- FuchsiaCityChanseySignText: PrintText(_FuchsiaCityChanseySignText),
|
||||
-- then DisplayPokedex CHANSEY.
|
||||
TEXT_FUCHSIACITY_CHANSEY_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityChanseySignText" },
|
||||
{ "mark_seen", "CHANSEY" },
|
||||
{ "push_screen", "DexEntryMenu", "CHANSEY" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityVoltorbSignText: PrintText(_FuchsiaCityVoltorbSignText),
|
||||
-- then DisplayPokedex VOLTORB.
|
||||
TEXT_FUCHSIACITY_VOLTORB_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityVoltorbSignText" },
|
||||
{ "mark_seen", "VOLTORB" },
|
||||
{ "push_screen", "DexEntryMenu", "VOLTORB" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityKangaskhanSignText: PrintText(_FuchsiaCityKangaskhanSignText),
|
||||
-- then DisplayPokedex KANGASKHAN.
|
||||
TEXT_FUCHSIACITY_KANGASKHAN_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityKangaskhanSignText" },
|
||||
{ "mark_seen", "KANGASKHAN" },
|
||||
{ "push_screen", "DexEntryMenu", "KANGASKHAN" },
|
||||
},
|
||||
|
||||
-- FuchsiaCitySlowpokeSignText: PrintText(_FuchsiaCitySlowpokeSignText),
|
||||
-- then DisplayPokedex SLOWPOKE.
|
||||
TEXT_FUCHSIACITY_SLOWPOKE_SIGN = {
|
||||
{ "show_text", "_FuchsiaCitySlowpokeSignText" },
|
||||
{ "mark_seen", "SLOWPOKE" },
|
||||
{ "push_screen", "DexEntryMenu", "SLOWPOKE" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityLaprasSignText: PrintText(_FuchsiaCityLaprasSignText),
|
||||
-- then DisplayPokedex LAPRAS.
|
||||
TEXT_FUCHSIACITY_LAPRAS_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityLaprasSignText" },
|
||||
{ "mark_seen", "LAPRAS" },
|
||||
{ "push_screen", "DexEntryMenu", "LAPRAS" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityFossilSignText: CheckEvent EVENT_GOT_DOME_FOSSIL /
|
||||
-- CheckEventReuseA EVENT_GOT_HELIX_FOSSIL pick the text and the
|
||||
-- displayed entry; with neither set only the undetermined line prints.
|
||||
TEXT_FUCHSIACITY_FOSSIL_SIGN = {
|
||||
{ "check_flag", "EVENT_GOT_DOME_FOSSIL" }, -- 1
|
||||
{ "jump_if_true", 7 }, -- 2
|
||||
{ "check_flag", "EVENT_GOT_HELIX_FOSSIL" }, -- 3
|
||||
{ "jump_if_true", 11 }, -- 4
|
||||
{ "show_text", "_FuchsiaCityFossilSignUndeterminedText" }, -- 5
|
||||
{ "jump", "end" }, -- 6
|
||||
{ "show_text", "_FuchsiaCityFossilSignOmanyteText" }, -- 7
|
||||
{ "mark_seen", "OMANYTE" }, -- 8
|
||||
{ "push_screen", "DexEntryMenu", "OMANYTE" }, -- 9
|
||||
{ "jump", "end" }, -- 10
|
||||
{ "show_text", "_FuchsiaCityFossilSignKabutoText" }, -- 11
|
||||
{ "mark_seen", "KABUTO" }, -- 12
|
||||
{ "push_screen", "DexEntryMenu", "KABUTO" }, -- 13
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -8,9 +8,9 @@ return {
|
||||
TEXT_LAVENDERMART_COOLTRAINER_M = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_LavenderMartCooltrainerMReviveText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_LavenderMartCooltrainerMNuggetText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,9 +13,9 @@ M.MR_FUJIS_HOUSE = {
|
||||
TEXT_MRFUJISHOUSE_SUPER_NERD = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiIsntHereText" }, -- 4
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" }, -- 6
|
||||
},
|
||||
|
||||
@@ -25,9 +25,9 @@ M.MR_FUJIS_HOUSE = {
|
||||
TEXT_MRFUJISHOUSE_LITTLE_GIRL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_MrFujisHouseLittleGirlThisIsMrFujisHouseText" }, -- 4
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
||||
},
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ return {
|
||||
TEXT_ROUTE16GATE1F_GUARD = {
|
||||
{ "face_player" },
|
||||
{ "check_item", "BICYCLE" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_Route16Gate1FGuardNoPedestriansAllowedText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_Route16Gate1FGuardCyclingRoadExplanationText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,9 +14,9 @@ return {
|
||||
TEXT_ROUTE18GATE1F_GUARD = {
|
||||
{ "face_player" },
|
||||
{ "check_item", "BICYCLE" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_Route18Gate1FGuardYouNeedABicycleText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_Route18Gate1FGuardCyclingRoadUphillText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,9 +12,9 @@ return {
|
||||
TEXT_SILPHCO10F_SILPH_WORKER_F = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo10FSilphWorkerFImScaredText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- not set: _SilphCo3FSilphWorkerMWhatShouldIDoText
|
||||
TEXT_SILPHCO3F_SILPH_WORKER_M = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo3FSilphWorkerMWhatShouldIDoText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo3FSilphWorkerMYouSavedUsText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ return {
|
||||
TEXT_SILPHCO4F_SILPH_WORKER_M = {
|
||||
{"face_player"},
|
||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||
{"jump_if_true", 5},
|
||||
{"jump_if_true", 6},
|
||||
{"show_text", "_SilphCo4FSilphWorkerMImHidingText"},
|
||||
{"jump", 6},
|
||||
{"jump", "end"},
|
||||
{"show_text", "_SilphCo4FSilphWorkerMTeamRocketIsGoneText"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ return {
|
||||
TEXT_SILPHCO5F_SILPH_WORKER_M = {
|
||||
{"face_player"},
|
||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||
{"jump_if_true", 5},
|
||||
{"jump_if_true", 6},
|
||||
{"show_text", "_SilphCo5FSilphWorkerMThatsYouRightText"},
|
||||
{"jump", 6},
|
||||
{"jump", "end"},
|
||||
{"show_text", "_SilphCo5FSilphWorkerMYoureOurHeroText"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,9 +11,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM1TookOverTheBuildingText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM1BackToWorkText" },
|
||||
},
|
||||
|
||||
@@ -21,9 +21,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M2 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerMHelpMePleaseText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerMWeGotEngagedText" },
|
||||
},
|
||||
|
||||
@@ -31,9 +31,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_F1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF1SuchACowardText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
|
||||
},
|
||||
|
||||
@@ -41,9 +41,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_F2 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketRanText" },
|
||||
},
|
||||
|
||||
@@ -51,9 +51,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M3 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM3TargetedSilphText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM3WorkForSilphText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM2CancelledMasterBallText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M2 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM2AfterTheMasterBallText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
|
||||
},
|
||||
|
||||
@@ -22,9 +22,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M3 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM3ItWouldBeBadText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
|
||||
},
|
||||
|
||||
@@ -34,9 +34,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM4SafeAtLastText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M4 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM4SafeAtLastText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- set: _SilphCo8FSilphWorkerMThanksForSavingUsText
|
||||
TEXT_SILPHCO8F_SILPH_WORKER_M = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo8FSilphWorkerMSilphIsFinishedText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo8FSilphWorkerMThanksForSavingUsText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ local files = {
|
||||
"data.scripts.flavor.cerulean_trashed_house",
|
||||
"data.scripts.flavor.copycats_house_1f",
|
||||
"data.scripts.flavor.copycats_house_2f",
|
||||
"data.scripts.flavor.fuchsia_city",
|
||||
"data.scripts.flavor.game_corner",
|
||||
"data.scripts.flavor.lavender_cubone_house",
|
||||
"data.scripts.flavor.lavender_mart",
|
||||
|
||||
@@ -176,6 +176,20 @@ M.ROUTE_18_GATE_2F = {
|
||||
{ "face_player" },
|
||||
{ "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- MARC
|
||||
},
|
||||
-- Yellow replaces the youngster with a cook trading SPIKE
|
||||
-- (TANGELA -> PARASECT): pokeyellow/scripts/Route18Gate2F.asm
|
||||
-- Route18Gate2FCookText runs TRADE_FOR_SPIKE, index 6 in the Yellow
|
||||
-- TradeMons table that Data:applyVersionedFieldData swaps in. Red
|
||||
-- maps have no COOK object here and Yellow maps have no YOUNGSTER,
|
||||
-- so each version only ever fires its own row (#651). Both rows
|
||||
-- share the Red-flavoured done flag on purpose: a .sav tracks
|
||||
-- "trade slot 6 completed" in one wCompletedInGameTradeFlags bit
|
||||
-- either version reads, and the save codec maps that bit to this
|
||||
-- flag name (src/save_convert/GenSave.lua EXTRA_FLAG_BITS).
|
||||
TEXT_ROUTE18GATE2F_COOK = {
|
||||
{ "face_player" },
|
||||
{ "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- SPIKE (Yellow)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
+23
-8
@@ -5,8 +5,9 @@
|
||||
-- Three frame-counted phases:
|
||||
-- 1. copyright card, 180 frames (intro.asm:311-312).
|
||||
-- 2. shooting star: 64 frames of empty letterbox (intro.asm:323-324), then
|
||||
-- the big star streaks down-left for 40 frames while the GAME FREAK
|
||||
-- logo + letters sit at (72,56)/(40,80) (splash.asm:27-60, 211-228),
|
||||
-- the big star streaks down-left for 40 frames while the studio logo
|
||||
-- sits centered in the letterbox band (the GAME FREAK logo + letter
|
||||
-- row it replaces sat at (72,56)/(40,80); splash.asm:27-60, 211-228),
|
||||
-- the logo flashes 3x10 frames (splash.asm:72-82), 4 waves of small
|
||||
-- stars rain from the logo -- 6x24 frames, +1px every 3 frames, lower
|
||||
-- star blinking (splash.asm:97-146, 163-209) -- and a 40 frame hold
|
||||
@@ -79,7 +80,12 @@ local SPLASH_FRAMES = WAVES_END + 40 -- ld c, 40 (intro.asm:329-331)
|
||||
-- (GameFreakLogoOAMData, splash.asm:211-228; screen = grid*8, OAM offsets
|
||||
-- cancel)
|
||||
local LOGO_X, LOGO_Y = 72, 56
|
||||
local TEXT_X, TEXT_Y = 40, 80
|
||||
|
||||
-- The studio logo (assets/logo/minilogo.png) stands in for both the
|
||||
-- GAME FREAK logo and its letter row, so it gets the whole band between
|
||||
-- the letterbox bars (y 32..112) down to where the star waves spawn
|
||||
-- (y=88): fit it inside this box, centered, aspect preserved.
|
||||
local STUDIO_BOX = { w = 128, h = 52, cx = 80, cy = 60 }
|
||||
|
||||
-- the 4 waves of small stars: screen X positions, all spawning at y=88
|
||||
-- (OAM $68; SmallStarsWave*Coords, splash.asm:160-183)
|
||||
@@ -150,6 +156,16 @@ function IntroMovie.new(game, onDone)
|
||||
self.skipAll = intro.skip and true or false
|
||||
local function img(e) return tryImage(e and e.path) end
|
||||
self.copyright = tryImage("assets/generated/title/copyright.png")
|
||||
-- studio mark replaces the GAME FREAK logo + splash text entirely; the
|
||||
-- extracted logo stays as the fallback if the asset is missing
|
||||
self.studioLogo = tryImage(self.studio.logo or "assets/logo/minilogo.png")
|
||||
if self.studioLogo then
|
||||
self.studioLogo:setFilter("nearest", "nearest")
|
||||
local iw, ih = self.studioLogo:getDimensions()
|
||||
self.studioScale = math.min(STUDIO_BOX.w / iw, STUDIO_BOX.h / ih)
|
||||
self.studioX = STUDIO_BOX.cx - iw * self.studioScale / 2
|
||||
self.studioY = STUDIO_BOX.cy - ih * self.studioScale / 2
|
||||
end
|
||||
self.logo = img(intro.gamefreakLogo)
|
||||
self.gfText = img(intro.gamefreakText)
|
||||
self.bigStar = img(intro.bigStar)
|
||||
@@ -287,13 +303,12 @@ function IntroMovie:drawSplash()
|
||||
local flashing = t >= FLASH_START and t < FLASH_START + FLASH_FRAMES
|
||||
local dim = flashing and math.floor((t - FLASH_START) / 5) % 2 == 0
|
||||
love.graphics.setColor(1, 1, 1, dim and 0.35 or 1)
|
||||
if self.logo then
|
||||
if self.studioLogo then
|
||||
love.graphics.draw(self.studioLogo, self.studioX, self.studioY,
|
||||
0, self.studioScale, self.studioScale)
|
||||
elseif self.logo then
|
||||
love.graphics.draw(self.logo, LOGO_X, LOGO_Y)
|
||||
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 Strings("bois club games")
|
||||
Font.draw(card, (160 - #card * 8) / 2, TEXT_Y)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
if t >= STAR_START and t < FLASH_START then
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -105,7 +105,14 @@ for path in scripts:lines() do
|
||||
end
|
||||
end
|
||||
scripts:close()
|
||||
check(#TRADE_ROWS == 7, "the nine-row trade table's seven reachable trades are scripted")
|
||||
check(#TRADE_ROWS == 8, "all eight scripted trade rows are found")
|
||||
-- trade slot 6 is scripted twice: Red's youngster and Yellow's gate cook
|
||||
-- share the index and its done flag, one row per version (#651)
|
||||
local slot6 = 0
|
||||
for _, row in ipairs(TRADE_ROWS) do
|
||||
if row.index == 6 then slot6 = slot6 + 1 end
|
||||
end
|
||||
eq(slot6, 2, "trade slot 6 has a row for each version")
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- round trips
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
-- Parity: a branched flavor talk script shows exactly ONE text per branch
|
||||
-- (#719). The buggy layout was
|
||||
--
|
||||
-- check_flag X / jump_if_true <jumpRow> / show before / jump <afterRow> /
|
||||
-- show after
|
||||
--
|
||||
-- where the fall-through "jump" landed ON the after-text row instead of
|
||||
-- ending the script, so talking to the Silph Co. workers (and the other
|
||||
-- NPCs below) before Giovanni showed the worried line and then the
|
||||
-- relieved line right after it. ScriptRunner executes the row a numeric
|
||||
-- jump targets, so the fixed scripts point jump_if_true at the after-text
|
||||
-- row directly and end the before-branch with jump "end".
|
||||
--
|
||||
-- The walker below is a minimal read of ScriptRunner's jump semantics:
|
||||
-- checks are forced to one outcome, every non-control command is a no-op,
|
||||
-- and show_text/ask rows are recorded in order.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local S = require("tests.harness").suite("parity flavor talk branches")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local scripts = require("data.scripts.init")
|
||||
|
||||
local function textsShown(script, checkResult)
|
||||
local texts, pc, steps = {}, 1, 0
|
||||
local last = nil
|
||||
while pc <= #script do
|
||||
steps = steps + 1
|
||||
if steps > 200 then error("script did not terminate", 0) end
|
||||
local row = script[pc]
|
||||
local cmd = row[1]
|
||||
if cmd == "show_text" or cmd == "ask" then
|
||||
texts[#texts + 1] = row[2]
|
||||
pc = pc + 1
|
||||
elseif cmd == "check_flag" or cmd == "check_item" then
|
||||
last = checkResult
|
||||
pc = pc + 1
|
||||
elseif cmd == "jump" then
|
||||
if row[2] == "end" then break end
|
||||
pc = row[2]
|
||||
elseif cmd == "jump_if_true" or cmd == "jump_if_false" then
|
||||
local take = (cmd == "jump_if_true") == (last == true)
|
||||
if take then
|
||||
if row[2] == "end" then break end
|
||||
pc = row[2]
|
||||
else
|
||||
pc = pc + 1
|
||||
end
|
||||
else
|
||||
pc = pc + 1
|
||||
end
|
||||
end
|
||||
return texts
|
||||
end
|
||||
|
||||
-- { mapId, textConst, before-text, after-text }
|
||||
local cases = {
|
||||
{ "SILPH_CO_3F", "TEXT_SILPHCO3F_SILPH_WORKER_M",
|
||||
"_SilphCo3FSilphWorkerMWhatShouldIDoText",
|
||||
"_SilphCo3FSilphWorkerMYouSavedUsText" },
|
||||
{ "SILPH_CO_4F", "TEXT_SILPHCO4F_SILPH_WORKER_M",
|
||||
"_SilphCo4FSilphWorkerMImHidingText",
|
||||
"_SilphCo4FSilphWorkerMTeamRocketIsGoneText" },
|
||||
{ "SILPH_CO_5F", "TEXT_SILPHCO5F_SILPH_WORKER_M",
|
||||
"_SilphCo5FSilphWorkerMThatsYouRightText",
|
||||
"_SilphCo5FSilphWorkerMYoureOurHeroText" },
|
||||
{ "SILPH_CO_6F", "TEXT_SILPHCO6F_SILPH_WORKER_M1",
|
||||
"_SilphCo6FSilphWorkerM1TookOverTheBuildingText",
|
||||
"_SilphCo6FSilphWorkerM1BackToWorkText" },
|
||||
{ "SILPH_CO_6F", "TEXT_SILPHCO6F_SILPH_WORKER_M2",
|
||||
"_SilphCo6FSilphWorkerMHelpMePleaseText",
|
||||
"_SilphCo6FSilphWorkerMWeGotEngagedText" },
|
||||
{ "SILPH_CO_6F", "TEXT_SILPHCO6F_SILPH_WORKER_F1",
|
||||
"_SilphCo6FSilphWorkerF1SuchACowardText",
|
||||
"_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
|
||||
{ "SILPH_CO_6F", "TEXT_SILPHCO6F_SILPH_WORKER_F2",
|
||||
"_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText",
|
||||
"_SilphCo6FSilphWorkerF2TeamRocketRanText" },
|
||||
{ "SILPH_CO_6F", "TEXT_SILPHCO6F_SILPH_WORKER_M3",
|
||||
"_SilphCo6FSilphWorkerM3TargetedSilphText",
|
||||
"_SilphCo6FSilphWorkerM3WorkForSilphText" },
|
||||
{ "SILPH_CO_7F", "TEXT_SILPHCO7F_SILPH_WORKER_M2",
|
||||
"_SilphCo7FSilphWorkerM2AfterTheMasterBallText",
|
||||
"_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
|
||||
{ "SILPH_CO_7F", "TEXT_SILPHCO7F_SILPH_WORKER_M3",
|
||||
"_SilphCo7FSilphWorkerM3ItWouldBeBadText",
|
||||
"_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
|
||||
{ "SILPH_CO_7F", "TEXT_SILPHCO7F_SILPH_WORKER_M4",
|
||||
"_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText",
|
||||
"_SilphCo7FSilphWorkerM4SafeAtLastText" },
|
||||
{ "SILPH_CO_8F", "TEXT_SILPHCO8F_SILPH_WORKER_M",
|
||||
"_SilphCo8FSilphWorkerMSilphIsFinishedText",
|
||||
"_SilphCo8FSilphWorkerMThanksForSavingUsText" },
|
||||
{ "SILPH_CO_10F", "TEXT_SILPHCO10F_SILPH_WORKER_F",
|
||||
"_SilphCo10FSilphWorkerFImScaredText",
|
||||
"_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
|
||||
{ "CERULEAN_TRASHED_HOUSE", "TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU",
|
||||
"_CeruleanTrashedHouseFishingGuruTheyStoleATMText",
|
||||
"_CeruleanTrashedHouseFishingGuruWhatsLostIsLostText" },
|
||||
{ "LAVENDER_MART", "TEXT_LAVENDERMART_COOLTRAINER_M",
|
||||
"_LavenderMartCooltrainerMReviveText",
|
||||
"_LavenderMartCooltrainerMNuggetText" },
|
||||
{ "ROUTE_16_GATE_1F", "TEXT_ROUTE16GATE1F_GUARD",
|
||||
"_Route16Gate1FGuardNoPedestriansAllowedText",
|
||||
"_Route16Gate1FGuardCyclingRoadExplanationText" },
|
||||
{ "ROUTE_18_GATE_1F", "TEXT_ROUTE18GATE1F_GUARD",
|
||||
"_Route18Gate1FGuardYouNeedABicycleText",
|
||||
"_Route18Gate1FGuardCyclingRoadUphillText" },
|
||||
{ "MR_FUJIS_HOUSE", "TEXT_MRFUJISHOUSE_SUPER_NERD",
|
||||
"_MrFujisHouseSuperNerdMrFujiIsntHereText",
|
||||
"_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" },
|
||||
{ "MR_FUJIS_HOUSE", "TEXT_MRFUJISHOUSE_LITTLE_GIRL",
|
||||
"_MrFujisHouseLittleGirlThisIsMrFujisHouseText",
|
||||
"_MrFujisHouseLittleGirlPokemonAreNiceToHugText" },
|
||||
}
|
||||
|
||||
for _, case in ipairs(cases) do
|
||||
local mapId, textConst, beforeText, afterText = case[1], case[2], case[3], case[4]
|
||||
local script = scripts.talkScript(mapId, textConst)
|
||||
check(script ~= nil, mapId .. "/" .. textConst .. " has a talk script")
|
||||
if script then
|
||||
local before = textsShown(script, false)
|
||||
eq(#before, 1, textConst .. " shows exactly one text before the event")
|
||||
eq(before[1], beforeText, textConst .. " shows the before-text")
|
||||
local after = textsShown(script, true)
|
||||
eq(#after, 1, textConst .. " shows exactly one text after the event")
|
||||
eq(after[1], afterText, textConst .. " shows the after-text")
|
||||
end
|
||||
end
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Parity: the Fuchsia City exhibit signs open the exhibited species' Pokédex
|
||||
-- entry and mark it seen (pokered/scripts/FuchsiaCity.asm). The fossil sign
|
||||
-- shows the fossil the player did NOT take at Mt. Moon, and opens no entry
|
||||
-- at all before either fossil is picked up.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local S = require("tests.harness").suite("parity fuchsia exhibit signs")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local scripts = require("data.scripts.init")
|
||||
|
||||
local exhibits = {
|
||||
TEXT_FUCHSIACITY_CHANSEY_SIGN = { "_FuchsiaCityChanseySignText", "CHANSEY" },
|
||||
TEXT_FUCHSIACITY_VOLTORB_SIGN = { "_FuchsiaCityVoltorbSignText", "VOLTORB" },
|
||||
TEXT_FUCHSIACITY_KANGASKHAN_SIGN = { "_FuchsiaCityKangaskhanSignText", "KANGASKHAN" },
|
||||
TEXT_FUCHSIACITY_SLOWPOKE_SIGN = { "_FuchsiaCitySlowpokeSignText", "SLOWPOKE" },
|
||||
TEXT_FUCHSIACITY_LAPRAS_SIGN = { "_FuchsiaCityLaprasSignText", "LAPRAS" },
|
||||
}
|
||||
|
||||
for textConst, exhibit in pairs(exhibits) do
|
||||
local script = scripts.talkScript("FUCHSIA_CITY", textConst)
|
||||
check(script ~= nil, textConst .. " has a talk script")
|
||||
if script then
|
||||
eq(script[1][1], "show_text", textConst .. " prints its line first")
|
||||
eq(script[1][2], exhibit[1], textConst .. " prints its own sign text")
|
||||
eq(script[2][1], "mark_seen", textConst .. " marks the species seen")
|
||||
eq(script[2][2], exhibit[2], textConst .. " marks " .. exhibit[2] .. " seen")
|
||||
eq(script[3][1], "push_screen", textConst .. " opens the Pokédex entry")
|
||||
eq(script[3][2], "DexEntryMenu", textConst .. " uses DexEntryMenu")
|
||||
eq(script[3][3], exhibit[2], textConst .. " shows " .. exhibit[2])
|
||||
end
|
||||
end
|
||||
|
||||
local fossil = scripts.talkScript("FUCHSIA_CITY", "TEXT_FUCHSIACITY_FOSSIL_SIGN")
|
||||
check(fossil ~= nil, "the fossil sign has a talk script")
|
||||
if fossil then
|
||||
eq(fossil[1][1], "check_flag", "the fossil sign checks the Dome Fossil event")
|
||||
eq(fossil[1][2], "EVENT_GOT_DOME_FOSSIL", "Dome Fossil event first")
|
||||
eq(fossil[3][1], "check_flag", "the fossil sign checks the Helix Fossil event")
|
||||
eq(fossil[3][2], "EVENT_GOT_HELIX_FOSSIL", "Helix Fossil event second")
|
||||
|
||||
-- neither fossil taken: only the undetermined line, no dex entry
|
||||
eq(fossil[5][1], "show_text", "the undetermined branch prints its line")
|
||||
eq(fossil[5][2], "_FuchsiaCityFossilSignUndeterminedText",
|
||||
"the undetermined branch uses the undetermined text")
|
||||
eq(fossil[6][1], "jump", "the undetermined branch stops there")
|
||||
check(fossil[6][2] == "end", "the undetermined branch never reaches a dex entry")
|
||||
|
||||
-- Dome Fossil taken: the exhibit holds Omanyte
|
||||
eq(fossil[2][1], "jump_if_true", "the Dome branch jumps when the event is set")
|
||||
eq(fossil[2][2], 7, "the Dome branch lands on the Omanyte rows")
|
||||
eq(fossil[7][2], "_FuchsiaCityFossilSignOmanyteText", "Dome taken: Omanyte text")
|
||||
eq(fossil[8][1], "mark_seen", "Dome taken: marks the exhibited species seen")
|
||||
eq(fossil[8][2], "OMANYTE", "Dome taken: marks Omanyte seen")
|
||||
eq(fossil[9][1], "push_screen", "Dome taken: opens the Pokédex entry")
|
||||
eq(fossil[9][3], "OMANYTE", "Dome taken: shows Omanyte")
|
||||
|
||||
-- Helix Fossil taken: the exhibit holds Kabuto
|
||||
eq(fossil[4][1], "jump_if_true", "the Helix branch jumps when the event is set")
|
||||
eq(fossil[4][2], 11, "the Helix branch lands on the Kabuto rows")
|
||||
eq(fossil[11][2], "_FuchsiaCityFossilSignKabutoText", "Helix taken: Kabuto text")
|
||||
eq(fossil[12][2], "KABUTO", "Helix taken: marks Kabuto seen")
|
||||
eq(fossil[13][3], "KABUTO", "Helix taken: shows Kabuto")
|
||||
end
|
||||
|
||||
local save = { pokedex = { seen = {}, owned = {} } }
|
||||
require("src.script.Commands").mark_seen({ save = save }, "OMANYTE")
|
||||
check(save.pokedex.seen.OMANYTE, "the preview records the species as seen")
|
||||
check(not save.pokedex.owned.OMANYTE, "the preview does not mark it owned")
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Parity: Yellow's Route 18 Gate 2F trader is a cook offering SPIKE
|
||||
-- (TANGELA -> PARASECT), not Red's youngster (pokeyellow/scripts/
|
||||
-- Route18Gate2F.asm Route18Gate2FCookText, TRADE_FOR_SPIKE). The port
|
||||
-- only wired TEXT_ROUTE18GATE2F_YOUNGSTER, so on Yellow the cook had no
|
||||
-- talk script at all and never answered (#651).
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local S = require("tests.harness").suite("parity Yellow route 18 gate trade")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local scripts = require("data.scripts.init")
|
||||
|
||||
local cook = scripts.talkScript("ROUTE_18_GATE_2F", "TEXT_ROUTE18GATE2F_COOK")
|
||||
check(cook ~= nil, "the Yellow cook has a talk script")
|
||||
if cook then
|
||||
eq(cook[2][1], "trade", "the cook runs a trade")
|
||||
eq(cook[2][2], 6, "the cook's trade is TRADE_FOR_SPIKE (index 6)")
|
||||
eq(cook[2][3], "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG",
|
||||
"the cook shares slot 6's done flag, so a converted .sav stays done")
|
||||
end
|
||||
|
||||
-- Red's youngster row stays put: trade 6 is MARC on Red's table and the
|
||||
-- two rows share the index across versions.
|
||||
local youngster = scripts.talkScript("ROUTE_18_GATE_2F", "TEXT_ROUTE18GATE2F_YOUNGSTER")
|
||||
check(youngster ~= nil, "Red's youngster keeps his talk script")
|
||||
if youngster then
|
||||
eq(youngster[2][1], "trade", "the youngster runs a trade")
|
||||
eq(youngster[2][2], 6, "the youngster's trade stays index 6")
|
||||
end
|
||||
|
||||
-- Yellow's map only has the cook, so the new key is the one that fires.
|
||||
local manifestFile = assert(io.open("tools/rom_manifest_yellow.json", "r"))
|
||||
local manifest = manifestFile:read("*a")
|
||||
manifestFile:close()
|
||||
check(manifest:find('"name": "ROUTE18GATE2F_COOK"', 1, true) ~= nil,
|
||||
"Yellow's Route18Gate2F object is the cook")
|
||||
check(manifest:find('"TEXT_ROUTE18GATE2F_COOK"', 1, true) ~= nil,
|
||||
"the cook carries TEXT_ROUTE18GATE2F_COOK")
|
||||
check(manifest:find("ROUTE18GATE2F_YOUNGSTER", 1, true) == nil,
|
||||
"Yellow's Route18Gate2F has no youngster")
|
||||
|
||||
-- The Yellow table makes index 6 the SPIKE row (TANGELA -> PARASECT).
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.field and Data.field.trades) then Data:load() end
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local oldVersion = GameVersion.get()
|
||||
local oldTrades = Data.field.trades
|
||||
GameVersion.set("yellow")
|
||||
Data:applyVersionedFieldData()
|
||||
local spike = Data.field.trades[6]
|
||||
check(spike.give == "TANGELA" and spike.get == "PARASECT"
|
||||
and spike.nickname == "SPIKE",
|
||||
"on Yellow trade 6 is SPIKE (TANGELA -> PARASECT)")
|
||||
Data.field.trades = oldTrades
|
||||
GameVersion.set(oldVersion)
|
||||
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user