Files
gen1recomp/data/scripts/story4.lua
bryanthaboi 15029d811f bug squashing
# Closed issues

CLOSES #17: Incorrect Character Visuals (Only with GBC filter)
CLOSES #23: Could you allow the player to change the order of the moves
CLOSES #24: Evolution music not playing during evolution
CLOSES #26: Standing on door glitch
CLOSES #27: Battle Intro text automatically continues
CLOSES #29: Visual bug when zoomed out
CLOSES #32: Team Rocket recruiter doesn't battle with you unless you speak with him first
CLOSES #33: Developer/Debug Console
CLOSES #35: Professor Oak's introduction Inaccuracies
CLOSES #36: Missing Pokemon Dex entries when picking starter + Rival Pathing issues
CLOSES #39: Guy who stops player from skipping brock doesn't bring you to brock's gym + doesn't leave once you've beaten brock
CLOSES #40: Bill cutscene is broken
CLOSES #41: Ticket guy failing to be a Ticket guy
CLOSES #42: S.S. anne odd behavior + Missing sailing away animation
CLOSES #43: Dig Attack animation appears to be glitched
CLOSES #44: Pokeball flashing doesn't appear to be accurate
CLOSES #45: Inaccurate Cut Animation
CLOSES #46: Dugtrio i caught in diglett cave has two of the same move
CLOSES #47: Rival ignores player in Lavender Tower
CLOSES #48: Healing pad in lavender tower does not function
CLOSES #49: Incorrect dialogue with parched security guard
CLOSES #50: (Game Breaking!) Rocket grunt guarding poster refuses to move
CLOSES #51: Badges showing up as items I can deposit in PC
CLOSES #52: Visual bug on Celadon Department Store roof (Red Filter)
CLOSES #54: Visual issue on route 15 + Fuchsia City
CLOSES #56: Running animation missing
CLOSES #57: Safari Zone does not display steps while you are inside of it
CLOSES #58: (Game Breaking!) Softlock at cycling road gate
CLOSES #59: Bike visual issues
CLOSES #60: Cycling road not forcing you to get on your bike
CLOSES #61: No keycard doors in Silph Co.
CLOSES #63: Missing teleporter animation
CLOSES #64: Inaccurate spinning
CLOSES #65: Reimplement unused Silph Co. Chief and Professor Oak trainer battles
2026-07-22 08:18:13 -04:00

748 lines
30 KiB
Lua

-- Side events that were missing from the port (found by auditing
-- against pokered): Oak's aides, the Magikarp salesman, the Fighting
-- Dojo prize, the Silph LAPRAS, Copycat, Mr. Psychic, the Route 16
-- FLY house, the Celadon rooftop vending machines + thirsty girl, and
-- the Name Rater. Each cites its pokered source.
local M = {}
local function text(game) return game.data.text end
local function push(game, s, done)
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, s, done))
end
local function ask(game, s, cb)
local ChoiceBox = require("src.ui.ChoiceBox")
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
end
-- fill the extracted text placeholders ({NUM:...}, {RAM:...}, {PLAYER})
local function fill(s, subs)
s = s:gsub("{PLAYER}", subs.player or "")
s = s:gsub("{NUM:[^}]*}", function() return tostring(subs.num or "") end)
s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end)
return s
end
-- -------------------------------------------------------------------
-- Oak's aides (engine/events/oaks_aide.asm; Route2Gate / Route11Gate2F
-- / Route15Gate2F pass the requirement + reward)
-- -------------------------------------------------------------------
local function countOwned(save)
local n = 0
for _ in pairs(save.pokedex and save.pokedex.owned or {}) do n = n + 1 end
return n
end
local function oaksAide(threshold, itemId)
return function(game, ow, npc, done)
local t = text(game)
local flags = game.save.flags
local itemName = game.data.items[itemId].name
local flag = "EVENT_GOT_" .. itemId
if flags[flag] then
push(game, fill(t._OaksAideComeBackText or
"I already gave\nyou the {RAM:}!",
{ num = threshold, ram = itemName }), done)
return
end
ask(game, fill(t._OaksAideHiText or
"Have you caught\n{NUM:} kinds of\nPOKéMON?",
{ num = threshold, ram = itemName, player = game.save.player.name }),
function(yes)
if not yes then
push(game, fill(t._OaksAideComeBackText or "Come back later!",
{ num = threshold, ram = itemName }), done)
return
end
local owned = countOwned(game.save)
if owned >= threshold then
if not require("src.inventory.Bag").add(game.save, itemId, 1) then
push(game, fill(t._OaksAideNoRoomText or
"No room for the\n{RAM:}!", { ram = itemName }), done)
return
end
flags[flag] = true
push(game, fill(t._OaksAideHereYouGoText or "Here you go!",
{ num = owned, ram = itemName }),
function()
push(game, fill(t._OaksAideGotItemText or
"{PLAYER} got the\n{RAM:}!",
{ ram = itemName, player = game.save.player.name }), done)
end)
else
push(game, fill(t._OaksAideUhOhText or
"You have only\ncaught {NUM:}!",
{ num = owned, ram = itemName }), done)
end
end)
end
end
M.ROUTE_2_GATE = {
talk = { TEXT_ROUTE2GATE_OAKS_AIDE = oaksAide(10, "HM_FLASH") },
}
M.ROUTE_11_GATE_2F = {
talk = { TEXT_ROUTE11GATE2F_OAKS_AIDE = oaksAide(30, "ITEMFINDER") },
}
M.ROUTE_15_GATE_2F = {
talk = { TEXT_ROUTE15GATE2F_OAKS_AIDE = oaksAide(50, "EXP_ALL") },
}
-- -------------------------------------------------------------------
-- Magikarp salesman (scripts/MtMoonPokecenter.asm): ¥500 for a L5
-- MAGIKARP, once
-- -------------------------------------------------------------------
M.MT_MOON_POKECENTER = {
talk = {
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_BOUGHT_MAGIKARP then
push(game, t._MtMoonPokecenterMagikarpSalesmanNoRefundsText
or "Well, I don't\ngive refunds!", done)
return
end
ask(game, t._MtMoonPokecenterMagikarpSalesmanOfferText
or "MAGIKARP! A\nsteal at ¥500!\nWant one?", function(yes)
if not yes then
push(game, t._MtMoonPokecenterMagikarpSalesmanNoText
or "No? I'm only\nselling today!", done)
return
end
if game.save.money < 500 then
push(game, t._MtMoonPokecenterMagikarpSalesmanNoMoneyText
or "You'll need more\nmoney than that!", done)
return
end
game.save.money = game.save.money - 500
game.save.flags.EVENT_BOUGHT_MAGIKARP = true
local Commands = require("src.script.Commands")
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
"MAGIKARP", 5)
push(game, ("%s got a\nMAGIKARP!"):format(game.save.player.name), done)
end)
end,
},
}
-- -------------------------------------------------------------------
-- Fighting Dojo prize (scripts/FightingDojo.asm): after beating the
-- Karate Master, take HITMONLEE or HITMONCHAN (the other disappears)
-- -------------------------------------------------------------------
local function dojoBall(species, ownBall, otherBall, askKey)
return function(game, ow, npc, done)
local t = text(game)
local flags = game.save.flags
if flags.EVENT_GOT_HITMONLEE or flags.EVENT_GOT_HITMONCHAN then
done()
return
end
if not flags.EVENT_BEAT_KARATE_MASTER then
push(game, "You'll have to\nbeat the master\nfirst!", done)
return
end
ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
if not yes then done() return end
flags["EVENT_GOT_" .. species] = true
flags.EVENT_DEFEATED_FIGHTING_DOJO = true
local Commands = require("src.script.Commands")
local ctx = { save = game.save, game = game, overworld = ow }
Commands.give_pokemon(ctx, species, 30)
Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
Commands.hide_object(ctx, "FIGHTING_DOJO", otherBall)
push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
end)
end
end
M.FIGHTING_DOJO = {
talk = {
TEXT_FIGHTINGDOJO_HITMONLEE_POKE_BALL =
dojoBall("HITMONLEE", "FIGHTINGDOJO_HITMONLEE_POKE_BALL",
"FIGHTINGDOJO_HITMONCHAN_POKE_BALL",
"_FightingDojoHitmonleePokeBallText"),
TEXT_FIGHTINGDOJO_HITMONCHAN_POKE_BALL =
dojoBall("HITMONCHAN", "FIGHTINGDOJO_HITMONCHAN_POKE_BALL",
"FIGHTINGDOJO_HITMONLEE_POKE_BALL",
"_FightingDojoHitmonchanPokeBallText"),
},
}
-- -------------------------------------------------------------------
-- Silph Co. 7F worker's LAPRAS (scripts/SilphCo7F.asm: L15, once,
-- after the rival fight area is reached; gift is unconditional here)
-- -------------------------------------------------------------------
M.SILPH_CO_7F = {
talk = {
TEXT_SILPHCO7F_SILPH_WORKER_M1 = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_GOT_LAPRAS then
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
or "How is LAPRAS\ndoing?", done)
return
end
push(game, t._SilphCo7FSilphWorkerM1ThankYouText
or "Thank you for\nsaving us!\fI want you to\nhave this LAPRAS!",
function()
game.save.flags.EVENT_GOT_LAPRAS = true
local Commands = require("src.script.Commands")
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
"LAPRAS", 15)
push(game, ("%s got\nLAPRAS!"):format(game.save.player.name),
function()
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
or "It's a good\nswimmer!", done)
end)
end)
end,
},
}
-- -------------------------------------------------------------------
-- Copycat (scripts/CopycatsHouse2F.asm): a POKE DOLL buys TM31 MIMIC
-- -------------------------------------------------------------------
M.COPYCATS_HOUSE_2F = {
talk = {
TEXT_COPYCATSHOUSE2F_COPYCAT = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_GOT_TM31 then
push(game, t._CopycatsHouse2FCopycatWhirlingText
or "Huh? Huh? Huh?", done)
return
end
if (game.save.inventory.POKE_DOLL or 0) > 0 then
ask(game, t._CopycatsHouse2FCopycatPokeDollText
or "Oh wow!\nA POKé DOLL!\fFor me?\nCan I have it?", function(yes)
if not yes then done() return end
game.save.inventory.POKE_DOLL = game.save.inventory.POKE_DOLL - 1
if game.save.inventory.POKE_DOLL == 0 then
game.save.inventory.POKE_DOLL = nil
end
if not require("src.inventory.Bag").add(game.save, "TM_MIMIC", 1) then
push(game, "You don't have\nroom for TM31!", done)
return
end
game.save.flags.EVENT_GOT_TM31 = true
push(game, ("%s got\nTM31!"):format(game.save.player.name), done)
end)
return
end
push(game, t._CopycatsHouse2FCopycatText
or "I like to mimic\npeople!", done)
end,
},
}
-- -------------------------------------------------------------------
-- Mr. Psychic (scripts/MrPsychicsHouse.asm): TM29 PSYCHIC, once
-- -------------------------------------------------------------------
M.MR_PSYCHICS_HOUSE = {
talk = {
TEXT_MRPSYCHICSHOUSE_MR_PSYCHIC = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_GOT_TM29 then
push(game, t._MrPsychicsHouseMrPsychicNoMoreText
or "...Hmm...", done)
return
end
push(game, t._MrPsychicsHouseMrPsychicText
or "...Wait!\nDon't say a word!\fYou came to get\nTM29!", function()
if not require("src.inventory.Bag").add(game.save, "TM_PSYCHIC_M", 1) then
push(game, "You don't have\nroom for TM29!", done)
return
end
game.save.flags.EVENT_GOT_TM29 = true
push(game, ("%s got\nTM29!"):format(game.save.player.name), done)
end)
end,
},
}
-- -------------------------------------------------------------------
-- Route 16 hidden house (scripts/Route16FlyHouse.asm): HM02 FLY, once
-- -------------------------------------------------------------------
M.ROUTE_16_FLY_HOUSE = {
talk = {
TEXT_ROUTE16FLYHOUSE_BRUNETTE_GIRL = function(game, ow, npc, done)
local t = text(game)
if game.save.flags.EVENT_GOT_HM02 then
push(game, t._Route16FlyHouseBrunetteGirlHm02ExplanationText
or "HM02 is FLY!\fIt will whisk you\nback to any town!", done)
return
end
push(game, t._Route16FlyHouseBrunetteGirlText
or "Shh! It's a\nsecret!\fMy POKéMON's\nHM02, take it!", function()
if not require("src.inventory.Bag").add(game.save, "HM_FLY", 1) then
push(game, "You don't have\nroom for HM02!", done)
return
end
game.save.flags.EVENT_GOT_HM02 = true
push(game, ("%s got\nHM02!"):format(game.save.player.name), done)
end)
end,
},
}
-- -------------------------------------------------------------------
-- Celadon rooftop (scripts/CeladonMartRoof.asm): vending machines sell
-- the three drinks; the thirsty girl trades drinks for TM13/48/49
-- -------------------------------------------------------------------
local DRINK_PRICES = {
{ id = "FRESH_WATER", price = 200 },
{ id = "SODA_POP", price = 300 },
{ id = "LEMONADE", price = 350 },
}
local function vendingMachine(game, ow, npc, done)
local ListMenu = require("src.ui.ListMenu")
local items = {}
for _, d in ipairs(DRINK_PRICES) do
table.insert(items, {
value = d, label = ("%s ¥%d"):format(game.data.items[d.id].name, d.price),
})
end
game.stack:push(ListMenu.new(game, "VENDING MACHINE", items, {
onChoose = function(item, list)
local d = item.value
if game.save.money < d.price then
push(game, "Not enough\nmoney.")
return
end
if not require("src.inventory.Bag").add(game.save, d.id, 1) then
push(game, "You have no room\nfor it!")
return
end
game.save.money = game.save.money - d.price
push(game, ("%s\npopped out!"):format(game.data.items[d.id].name))
end,
onCancel = done,
}))
end
-- drink -> TM (CeladonMartRoof.asm .gaveFreshWater/.gaveSodaPop/
-- .gaveLemonade branches of CeladonMartRoofScript_GiveDrinkToGirl)
local GIRL_TMS = {
{ drink = "FRESH_WATER", tm = "TM_ICE_BEAM", flag = "EVENT_GOT_TM13",
yay = "_CeladonMartRoofLittleGirlYayFreshWaterText",
received = "_CeladonMartRoofLittleGirlReceivedTM13Text",
explain = "_CeladonMartRoofLittleGirlTM13ExplanationText" },
{ drink = "SODA_POP", tm = "TM_ROCK_SLIDE", flag = "EVENT_GOT_TM48",
yay = "_CeladonMartRoofLittleGirlYaySodaPopText",
received = "_CeladonMartRoofLittleGirlReceivedTM48Text",
explain = "_CeladonMartRoofLittleGirlTM48ExplanationText" },
{ drink = "LEMONADE", tm = "TM_TRI_ATTACK", flag = "EVENT_GOT_TM49",
yay = "_CeladonMartRoofLittleGirlYayLemonadeText",
received = "_CeladonMartRoofLittleGirlReceivedTM49Text",
explain = "_CeladonMartRoofLittleGirlTM49ExplanationText" },
}
M.CELADON_MART_ROOF = {
talk = {
TEXT_CELADONMARTROOF_VENDING_MACHINE1 = vendingMachine,
TEXT_CELADONMARTROOF_VENDING_MACHINE2 = vendingMachine,
TEXT_CELADONMARTROOF_VENDING_MACHINE3 = vendingMachine,
-- CeladonMartRoofLittleGirlText + Script_GiveDrinkToGirl: a menu
-- of the drinks in the bag; the chosen one earns its TM (once per
-- drink kind, EVENT_GOT_TM13/48/49)
TEXT_CELADONMARTROOF_LITTLE_GIRL = function(game, ow, npc, done)
local t = text(game)
local have = {}
for _, g in ipairs(GIRL_TMS) do
if (game.save.inventory[g.drink] or 0) > 0 then
table.insert(have, g)
end
end
if #have == 0 then
push(game, t._CeladonMartRoofLittleGirlImThirstyText
or "I'm thirsty!\nI want something\nto drink!", done)
return
end
ask(game, t._CeladonMartRoofLittleGirlGiveHerADrinkText
or "I'm thirsty!\nI want something\nto drink!\fGive her a drink?",
function(yes)
if not yes then done() return end
push(game, t._CeladonMartRoofLittleGirlGiveHerWhichDrinkText
or "Give her which\ndrink?", function()
local items = {}
for _, g in ipairs(have) do
table.insert(items, {
label = game.data.items[g.drink].name, value = g,
})
end
local ListMenu = require("src.ui.ListMenu")
game.stack:push(ListMenu.new(game, "DRINKS", items, {
onChoose = function(item, list)
list:close()
local g = item.value
if game.save.flags[g.flag] then
push(game, t._CeladonMartRoofLittleGirlImNotThirstyText
or "No thank you!\nI'm not thirsty\nafter all!", done)
return
end
push(game, t[g.yay]
or "Yay!\fThank you!\fYou can have this\nfrom me!", function()
local Bag = require("src.inventory.Bag")
Bag.remove(game.save, g.drink, 1)
if not Bag.add(game.save, g.tm, 1) then
push(game, t._CeladonMartRoofLittleGirlNoRoomText
or "You don't have\nspace for this!", done)
return
end
game.save.flags[g.flag] = true
require("src.core.Sound").play(game.data, "Get_Item1")
local subs = { player = game.save.player.name,
ram = game.data.items[g.tm].name }
local explain = fill(t[g.explain] or "", subs)
:gsub("^\f", "")
push(game, fill(t[g.received]
or "{PLAYER} received\n{RAM:}!", subs), function()
if #explain > 0 then
push(game, explain, done)
else
done()
end
end)
end)
end,
onCancel = done,
}))
end)
end)
end,
},
}
-- -------------------------------------------------------------------
-- Nugget Bridge recruiter (scripts/Route24.asm): the champ prize
-- NUGGET, the TEAM ROCKET pitch, then the battle
-- -------------------------------------------------------------------
M.ROUTE_24 = {
talk = {
TEXT_ROUTE24_COOLTRAINER_M1 = function(game, ow, npc, done)
local flags = game.save.flags
local function battleOrDone()
if ow:trainerDefeated(npc) then
push(game, "I hate this!\nMy dreams of\nTEAM ROCKET...", done)
else
ow:engageTrainer(npc, done)
end
end
if not flags.EVENT_GOT_NUGGET then
push(game, "Congratulations!\nYou beat our 5\ncontest trainers!\f"
.. "You just earned a\nfabulous prize!", function()
flags.EVENT_GOT_NUGGET = true
require("src.inventory.Bag").add(game.save, "NUGGET", 1)
push(game, ("%s received\na NUGGET!"):format(game.save.player.name),
function()
ask(game, "By the way, would\nyou like to join\nTEAM ROCKET?",
function()
push(game, "Arrgh! You are\nnot convinced?\fThen I'll show\n"
.. "you my power!", battleOrDone)
end)
end)
end)
return
end
battleOrDone()
end,
},
-- Route24DefaultScript forces TEXT_ROUTE24_COOLTRAINER_M1 whenever the
-- player stands on (10,15) in front of the recruiter while
-- EVENT_GOT_NUGGET is unset (dbmapcoord 10,15), independent of facing
-- or input -- he has no trainer header, so sight never engages him.
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_GOT_NUGGET then return false end
if not (x == 10 and y == 15) then return false end
if ow.runner:isRunning() or ow.engaging then return false end
local recruiter
for _, npc in ipairs(ow.npcs) do
if npc.def and npc.def.name == "ROUTE24_COOLTRAINER_M1" then
recruiter = npc
break
end
end
if not recruiter then return false end
ow:showMapText("TEXT_ROUTE24_COOLTRAINER_M1", recruiter)
return true
end,
}
-- -------------------------------------------------------------------
-- Cinnabar Lab trade room (scripts/CinnabarLabTradeRoom.asm):
-- Gramps trades DORIS (Raichu -> Electrode), the Beauty CRINKLES
-- (Venonat -> Tangela); indexes into data/events/trades.asm
-- -------------------------------------------------------------------
M.CINNABAR_LAB_TRADE_ROOM = {
talk = {
TEXT_CINNABARLABTRADEROOM_GRAMPS = {
{ "face_player" },
{ "trade", 8, "EVENT_TRADED_RAICHU_FOR_ELECTRODE" }, -- DORIS
},
TEXT_CINNABARLABTRADEROOM_BEAUTY = {
{ "face_player" },
{ "trade", 9, "EVENT_TRADED_VENONAT_FOR_TANGELA" }, -- CRINKLES
},
},
}
-- -------------------------------------------------------------------
-- Name Rater (scripts/NameRatersHouse.asm): rename a party member
-- -------------------------------------------------------------------
M.NAME_RATERS_HOUSE = {
talk = {
TEXT_NAMERATERSHOUSE_NAME_RATER = function(game, ow, npc, done)
local t = text(game)
local function bye()
push(game, t._NameRatersHouseNameRaterComeAnyTimeYouLikeText
or "Fine! Come any\ntime you like!", done)
end
ask(game, t._NameRatersHouseNameRaterWantMeToRateText
or "Hello, hello!\nI am the official\nNAME RATER!\fWant me to rate\nthe nicknames of\nyour POKéMON?",
function(yes)
if not yes then bye() return end
push(game, t._NameRatersHouseNameRaterWhichPokemonText
or "Which POKéMON\nshould I look at?", function()
local PartyMenu = require("src.ui.PartyMenu")
game.stack:push(PartyMenu.new(game, {
pickOnly = true,
-- backing out of the party menu is .did_not_rename:
-- "Fine! Come any time you like!" (NameRatersHouse.asm)
onCancel = bye,
onSwitch = function(mon)
local def = game.data.pokemon[mon.species]
local curName = mon.nickname or def.name or mon.species
-- NameRatersHouseCheckMonOTScript: a mon whose OT name
-- or OT ID isn't the player's can't be renamed
local player = game.save.player
local foreign = mon.traded
or (mon.ot ~= nil and mon.ot ~= player.name)
or (mon.otId ~= nil and player.id ~= nil
and mon.otId ~= player.id)
if foreign then
push(game, fill(t._NameRatersHouseNameRaterATrulyImpeccableNameText
or "{RAM:}, is it?\nThat is a truly\nimpeccable name!\fTake good care of\n{RAM:}!",
{ ram = curName }), done)
return
end
ask(game, fill(t._NameRatersHouseNameRaterGiveItANiceNameText
or "{RAM:}, is it?\nThat is a decent\nnickname!\fBut, would you\nlike me to give\nit a nicer name?\fHow about it?",
{ ram = curName }), function(rename)
if not rename then bye() return end
push(game, t._NameRatersHouseNameRaterWhatShouldWeNameItText
or "Fine! What should\nwe name it?", function()
local NamingScreen = require("src.ui.NamingScreen")
game.stack:push(NamingScreen.new(game, {
title = (def.name or mon.species) .. "'s name?",
maxLen = 10,
default = mon.nickname,
onDone = function(name)
if name and #name > 0 and name ~= def.name then
mon.nickname = name
else
mon.nickname = nil
end
push(game, fill(t._NameRatersHouseNameRaterPokemonHasBeenRenamedText
or "OK! This POKéMON\nhas been renamed\n{RAM:}!\fThat's a better\nname than before!",
{ ram = mon.nickname or def.name }), done)
end,
}))
end)
end)
end,
}))
end)
end)
end,
},
}
-- -------------------------------------------------------------------
-- Saffron City occupation / liberation (scripts/SaffronCity.asm object
-- defaults + scripts/SilphCo11F.asm SilphCo11FTeamRocketLeavesScript +
-- scripts/PokemonTower7F.asm Fuji rescue). The street ROCKETs guard
-- the gym and Silph Co doors; rescuing Mr. Fuji swaps the Silph door
-- guard (ROCKET8 -> ROCKET9), and beating Silph Giovanni clears every
-- rocket and shows the liberated-city NPCs. Synced on map enter so it
-- also repairs saves made before this script existed.
-- -------------------------------------------------------------------
local SAFFRON_ROCKETS = {
"SAFFRONCITY_ROCKET1", "SAFFRONCITY_ROCKET2", "SAFFRONCITY_ROCKET3",
"SAFFRONCITY_ROCKET4", "SAFFRONCITY_ROCKET5", "SAFFRONCITY_ROCKET6",
"SAFFRONCITY_ROCKET7", "SAFFRONCITY_ROCKET8", "SAFFRONCITY_ROCKET9",
}
local SAFFRON_CIVILIANS = {
"SAFFRONCITY_SCIENTIST", "SAFFRONCITY_SILPH_WORKER_M",
"SAFFRONCITY_SILPH_WORKER_F", "SAFFRONCITY_GENTLEMAN",
"SAFFRONCITY_PIDGEOT", "SAFFRONCITY_ROCKER",
}
M.SAFFRON_CITY = {
onEnter = function(game, ow)
local Commands = require("src.script.Commands")
local ctx = { game = game, save = game.save, overworld = ow }
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
for _, name in ipairs(SAFFRON_ROCKETS) do
Commands.hide_object(ctx, "SAFFRON_CITY", name)
end
for _, name in ipairs(SAFFRON_CIVILIANS) do
Commands.show_object(ctx, "SAFFRON_CITY", name)
end
elseif game.save.flags.EVENT_RESCUED_MR_FUJI then
Commands.hide_object(ctx, "SAFFRON_CITY", "SAFFRONCITY_ROCKET8")
Commands.show_object(ctx, "SAFFRON_CITY", "SAFFRONCITY_ROCKET9")
end
end,
}
-- -------------------------------------------------------------------
-- Elite Four room exit doors (scripts/LoreleisRoom.asm
-- LoreleiShowOrHideExitBlock and the Bruno/Agatha equivalents): the
-- block above the exit warp stays solid until the room's trainer is
-- beaten. LORELEIS_ROOM ships closed in the extracted .blk, so
-- without this the league was a dead end after Lorelei.
-- -------------------------------------------------------------------
local function e4ExitSeal(flag, closedBlock, openBlock, dontRunText, autoFlag)
local seal = function(game, ow)
local open = game.save.flags[flag]
ow:replaceBlock(2, 0, open and openBlock or closedBlock)
-- the auto walk-in on first (south) entry (LoreleiScriptWalkIntoRoom)
if autoFlag and not game.save.flags[autoFlag] and ow.player.cellY >= 10 then
game.save.flags[autoFlag] = true
ow:scriptMove(ow.player, "up", 6)
end
end
-- onVictory re-runs the seal so the door opens right after the
-- battle, like pokered's post-battle map reload
return {
onEnter = seal,
onVictory = seal,
-- retreating toward the entrance gets "Don't run away!" and a
-- shove back up (the entrance coords rows in each room script)
onStep = function(game, ow, x, y)
if y < 10 or x < 4 or x > 5 then return false end
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text[dontRunText] or "Don't run away!", function()
ow:scriptMove(ow.player, "up", 1)
end))
return true
end,
}
end
M.LORELEIS_ROOM = e4ExitSeal("EVENT_BEAT_LORELEIS_ROOM_TRAINER_0", 0x24, 0x05,
"_LoreleisRoomLoreleiDontRunAwayText", "EVENT_AUTOWALKED_INTO_LORELEIS_ROOM")
M.BRUNOS_ROOM = e4ExitSeal("EVENT_BEAT_BRUNOS_ROOM_TRAINER_0", 0x24, 0x05,
"_BrunosRoomBrunoDontRunAwayText", "EVENT_AUTOWALKED_INTO_BRUNOS_ROOM")
M.AGATHAS_ROOM = e4ExitSeal("EVENT_BEAT_AGATHAS_ROOM_TRAINER_0", 0x3b, 0x0e,
"_AgathasRoomAgathaDontRunAwayText", "EVENT_AUTOWALKED_INTO_AGATHAS_ROOM")
-- -------------------------------------------------------------------
-- Lance's room (scripts/LancesRoom.asm). Unlike the other three E4
-- rooms this one gates its ENTRANCE, not its exit: the .blk ships with
-- the arena doorway CLOSED (blocks $72/$73 at block (2,6)/(3,6), cells
-- (4-7,12-13)), and LanceShowOrHideEntranceBlocks OPENS it ($31/$32)
-- on every map load while EVENT_LANCES_ROOM_LOCK_DOOR is unset.
-- Without this script the doorway never opened, so the whole arena --
-- Lance AND both CHAMPIONS_ROOM warps at (5,0)/(6,0) -- was sealed off
-- from the entrance hall and the league dead-ended here ("goto (6,11)
-- unreachable on LANCES_ROOM").
--
-- LancesRoomDefaultScript's coordinate triggers, all inert once
-- EVENT_BEAT_LANCE is set:
-- (5,1)/(6,2) beside Lance -> his battle starts (a coordinate
-- trigger, not a talk; victories.lua OPP_LANCE#1 sets
-- EVENT_BEAT_LANCE on the win, so a loss re-arms)
-- (5,11)/(6,11) the doorway -> CheckAndSetEvent
-- EVENT_LANCES_ROOM_LOCK_DOOR: first crossing seals
-- the door behind the player with SFX_GO_INSIDE
-- (24,16) the entrance staircase -> WalkToLance: an auto-walk
-- (up 12, left 12, down 7, left 6) landing on (6,11).
-- It marches straight across the room's water decor:
-- pokered's CollisionCheckOnLand skips collision
-- entirely while simulated joypad states run, and our
-- scriptMove is collision-free the same way.
-- -------------------------------------------------------------------
local function lanceEntranceBlocks(game, ow)
local locked = game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR
ow:replaceBlock(2, 6, locked and 0x72 or 0x31)
ow:replaceBlock(3, 6, locked and 0x73 or 0x32)
end
local function lanceLockDoor(game, ow)
if game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR then return end
game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR = true
require("src.core.Sound").play(game.data, "Go_Inside")
lanceEntranceBlocks(game, ow)
end
local function lanceWalkIn(game, ow)
ow:scriptMove(ow.player, "up", 12, function()
ow:scriptMove(ow.player, "left", 12, function()
ow:scriptMove(ow.player, "down", 7, function()
ow:scriptMove(ow.player, "left", 6, function()
-- the walk lands on (6,11); vanilla's per-frame coord poll
-- then locks the door at once. scriptMove landings do not
-- fire onStep, so lock here.
lanceLockDoor(game, ow)
end)
end)
end)
end)
end
M.LANCES_ROOM = {
onEnter = function(game, ow)
lanceEntranceBlocks(game, ow)
-- the warp arrival lands ON the staircase trigger, and onStep only
-- fires for completed steps -- start the walk-in here, the same
-- way Lorelei's auto walk-in runs from its onEnter
if not game.save.flags.EVENT_BEAT_LANCE
and ow.player.cellX == 24 and ow.player.cellY == 16 then
lanceWalkIn(game, ow)
end
end,
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_BEAT_LANCE then return false end
if (x == 5 and y == 1) or (x == 6 and y == 2) then
local lance
for _, npc in ipairs(ow.npcs) do
if npc.def and npc.def.name == "LANCESROOM_LANCE" then lance = npc break end
end
if not lance or ow:trainerDefeated(lance) then return false end
lance:facePlayer(ow.player)
ow:engageTrainer(lance, function() end)
return true
end
if (x == 5 or x == 6) and y == 11 then
lanceLockDoor(game, ow)
return false
end
if x == 24 and y == 16 then
lanceWalkIn(game, ow)
return true
end
return false
end,
}
M.CELADON_CHIEF_HOUSE = require("data.scripts.celadon_chief_house")
return M