initial commit

This commit is contained in:
bryanthaboi
2026-07-17 20:30:02 -04:00
commit a5d2e77e7d
298 changed files with 100561 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
# Pokemon Gen 1 Recompilation Project
A native LÖVE2D recreation of Pokemon Red. The engine and map behavior are
hand-written Lua; game data and graphics are decoded from a ROM supplied by
the player.
This project does not include a ROM, emulate the Game Boy, transpile assembly,
or download a disassembly. A canonical US Pokemon Red ROM is the only game
content input.
```text
first boot
Pokemon Red ROM -> in-app Lua importer -> private LÖVE save directory
-> generated Lua data and PNGs
-> compact audio channel programs
-> LÖVE2D engine
```
The ROM is verified, used during import, and then released from memory. It is
not copied into the cache. Later launches load the private generated cache and
do not ask for the ROM again.
## Packaged App
Open the desktop app. On first boot, choose your legally obtained `.gb` file
or drop it onto the window. Import takes a few seconds and the game starts
automatically.
Only the canonical 1 MiB US Red ROM is accepted. The importer verifies SHA-1
`ea9bcae617fdf159b045185467ae58b2e4a48b9a` before creating any game data.
The packaged app contains neither a ROM nor pre-extracted game data.
Music, sound effects, and cries are synthesized while the game runs from
compact Game Boy audio channel programs copied out of the verified ROM. No
WAV or OGG library is bundled or generated.
## Source Checkout
The source launchers retain an optional Python workflow for developers. Place
the ROM in the project folder and double-click `Play-Mac.command` or
`Play-Windows.bat`, or run:
```sh
scripts/setup.sh --rom "/path/to/Pokemon Red.gb"
scripts/run.sh
```
Windows PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -Rom C:\path\red.gb
powershell -ExecutionPolicy Bypass -File scripts\run.ps1
```
The setup scripts also accept `ROM_PATH`. With no argument, they use the first
`.gb` file in the project root.
## Developer Data Build
Requirements: Python 3.10+ and Pillow.
```sh
python3 -m pip install pillow
python3 tools/build_data.py --rom "/path/to/Pokemon Red.gb" --clean
```
This command produces the data modules and 495 PNGs in the source tree for
development and parity checks. It is not used by the packaged app.
## Running
Requires LÖVE 11.x:
```sh
love .
```
Controls: arrow keys or WASD move; Z, Enter, or Space is A; X or Backspace is
B; Escape opens START. F1 saves and F2 loads. Controllers are supported.
## Link Play
START > LINK connects two copies directly over UDP. The host chooses HOST A
GAME and shares the shown address; the other player chooses JOIN A GAME.
The default port is 7777 and can be overridden with `POKEPORT_LINK_PORT`.
## Save Editor
Edit party, boxes, items, events, map location, and Pokédex flags without
playing through the game. Close the game first, then from the repo root:
```sh
love . --editor
# or
POKEPORT_EDITOR=1 love .
# open a specific save
love . --editor --save "/path/to/save.lua"
```
By default it loads the game's LÖVE save (`save.lua`):
- macOS: `~/Library/Application Support/LOVE/pokemon-love2d/save.lua (or without the LOVE in a built version)`
- Linux: `~/.local/share/love/pokemon-love2d/save.lua`
- Windows: `%APPDATA%\love\pokemon-love2d\save.lua`
If that file is missing or you want another copy, use **Open...**, drop a
`save.lua` onto the window, or pass `--save`. Each write makes a
`save.lua.bak-YYYYMMDD-HHMMSS` backup first.
See `tools/save-editor/README.md` for headless tests.
## Layout
```text
tools/ ROM decoder, save editor, and developer verification tools
data/generated/ generated Lua game data (gitignored)
data/scripts/ hand-ported map behavior
assets/generated generated graphics and compact audio cache (gitignored)
src/ hand-written LÖVE engine
scripts/ setup, run, and packaging helpers
mobile/ Android and iOS build trees
tests/ headless behavior and parity suites
docs/ architecture, behavior notes, and platform docs
```
See `docs/architecture.md` for runtime details and
`docs/behavior-porting-notes.md` for formula provenance.
## Delete generated files (mac)
```sh
rm -rf data/generated assets/generated \
"$HOME/Library/Application Support/LOVE/pokemon-love2d/data/generated" \
"$HOME/Library/Application Support/LOVE/pokemon-love2d/assets/generated"
rm -f "$HOME/Library/Application Support/LOVE/pokemon-love2d/rom-cache.complete"
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

+44
View File
@@ -0,0 +1,44 @@
function love.conf(t)
local editor = os.getenv("POKEPORT_EDITOR") == "1"
if arg then
for _, a in ipairs(arg) do
if a == "--editor" then editor = true end
end
end
-- main.lua runs in the same Lua state right after conf.lua; stash the
-- decision in a global so it doesn't need to reparse `arg`.
_G.POKEPORT_EDITOR_MODE = editor
if editor then
t.identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d-editor"
t.window.title = "Pokemon Save Editor"
t.window.width = 1280
t.window.height = 800
else
t.identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d"
t.window.title = "Pokemon Red (Gen 1 Recompilation Project)"
t.window.width = 160 * 4
t.window.height = 144 * 4
end
t.version = "11.5"
t.window.vsync = 1
t.modules.joystick = true
t.modules.physics = false
-- love.system is not loaded during love.conf; love._os is set by the
-- engine before conf runs (LÖVE 11.x / 11.5).
local osName = love._os
local mobile = osName == "Android" or osName == "iOS"
if mobile then
-- On Android/iOS, width/height aspect picks portrait vs landscape
-- (fullscreen alone is not enough). Use a tall portrait size; the
-- OS then resizes to the real display. highdpi is required for
-- Retina iOS (Android always behaves as highdpi).
t.window.width = 1080
t.window.height = 1920
t.window.fullscreen = true
t.window.highdpi = true
else
t.window.resizable = true
end
end
+33
View File
@@ -0,0 +1,33 @@
-- Per-trainer-class battle AI: item use and switching.
-- Hand-ported from data/trainers/ai_pointers.asm (uses per Pokémon +
-- routine) and engine/battle/trainer_ai.asm (the routines). Classes
-- not listed use GenericAI (never uses items or switches).
--
-- chance is out of 256 (the routine's `cp X percent` threshold on a
-- random byte). hpBelow means "only when HP < max/N". onStatus means
-- "always, but only when the active mon has a status condition".
return {
OPP_JUGGLER = { uses = 3, chance = 64, switch = true },
OPP_BLACKBELT = { uses = 2, chance = 32, item = "X_ATTACK" },
OPP_GIOVANNI = { uses = 1, chance = 64, item = "GUARD_SPEC" },
OPP_COOLTRAINER_M = { uses = 2, chance = 64, item = "X_ATTACK" },
-- CooltrainerF's 25% roll is dead code in the original (the ret nc is
-- commented out); she heals below 1/10 and switches below 1/5
OPP_COOLTRAINER_F = { uses = 1, item = "HYPER_POTION", hpBelow = 10,
switchBelow = 5 },
OPP_BRUNO = { uses = 2, chance = 64, item = "X_DEFEND" },
OPP_BROCK = { uses = 5, onStatus = true, item = "FULL_HEAL" },
OPP_MISTY = { uses = 1, chance = 64, item = "X_DEFEND" },
OPP_LT_SURGE = { uses = 1, chance = 64, item = "X_SPEED" },
OPP_ERIKA = { uses = 1, chance = 128, item = "SUPER_POTION", hpBelow = 10 },
OPP_KOGA = { uses = 2, chance = 64, item = "X_ATTACK" },
OPP_BLAINE = { uses = 2, chance = 64, item = "SUPER_POTION" },
OPP_SABRINA = { uses = 1, chance = 64, item = "HYPER_POTION", hpBelow = 10 },
OPP_RIVAL2 = { uses = 1, chance = 32, item = "POTION", hpBelow = 5 },
OPP_RIVAL3 = { uses = 1, chance = 32, item = "FULL_RESTORE", hpBelow = 5 },
OPP_LORELEI = { uses = 2, chance = 128, item = "SUPER_POTION", hpBelow = 5 },
OPP_AGATHA = { uses = 2, switchChance = 20, chance = 128,
item = "SUPER_POTION", hpBelow = 4 },
OPP_LANCE = { uses = 1, chance = 128, item = "HYPER_POTION", hpBelow = 5 },
}
+34
View File
@@ -0,0 +1,34 @@
-- Hand-ported from pret/pokered scripts/CeladonMansionRoofHouse.asm
-- (CeladonMansionRoofHouseEeveePokeballText): the poke ball on the table
-- holds an Eevee (level 25). The text_asm calls GivePokemon immediately
-- (no confirm prompt) and, on success, hides the ball object
-- (TOGGLE_CELADON_MANSION_EEVEE_GIFT predef HideObject, persisted in
-- wToggleableObjectFlags) -- the hidden ball is the original's whole
-- re-gift guard. If the party AND box are full GivePokemon fails
-- (BoxIsFullText, .party_full) and the ball stays for later.
--
-- EVENT_GOT_EEVEE is port-internal bookkeeping with no pokered
-- equivalent, kept for save compatibility: rows 1-4 also self-heal
-- older saves (flag set before the port hid the ball) by hiding the
-- leftover ball on the next interaction.
return {
talk = {
TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL = {
{ "check_flag", "EVENT_GOT_EEVEE" }, -- 1
{ "jump_if_false", 5 }, -- 2
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 3 (old saves)
{ "jump", 13 }, -- 4
{ "give_pokemon", "EEVEE", 25 }, -- 5
{ "jump_if_false", 12 }, -- 6 (party+box full)
{ "play_sound", "Get_Item1" }, -- 7 (GotMonText jingle)
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 8
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 9
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 10
{ "jump", 13 }, -- 11
{ "show_text", "_BoxIsFullText" }, -- 12
},
},
}
+33
View File
@@ -0,0 +1,33 @@
-- BikeShop (BIKE_SHOP) flavor dialogue
-- Source: pokered/scripts/BikeShop.asm, pokered/text/BikeShop.asm
--
-- TEXT_BIKESHOP_CLERK is skipped: it drives the actual voucher-for-bicycle
-- exchange (YesNoChoice purchase menu, GiveItem, RemoveItemByID, SetEvent
-- EVENT_GOT_BICYCLE). That is a significant standalone feature outside the
-- scope of these two flavor NPCs and is left unported here.
return {
BIKE_SHOP = {
talk = {
-- BikeShopMiddleAgedWomanText (pokered/scripts/BikeShop.asm):
-- always shows the same flavor line, no branching.
TEXT_BIKESHOP_MIDDLE_AGED_WOMAN = {
{ "face_player" },
{ "show_text", "_BikeShopMiddleAgedWomanText" },
},
-- BikeShopYoungsterText (pokered/scripts/BikeShop.asm):
-- CheckEvent EVENT_GOT_BICYCLE ; jr nz, .gotBike
-- before the player owns a bike -> TheseBikesAreExpensiveText
-- after the player owns a bike -> CoolBikeText
TEXT_BIKESHOP_YOUNGSTER = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_BICYCLE" }, -- 2
{ "jump_if_true", 5 }, -- 3
{ "show_text", "_BikeShopYoungsterTheseBikesAreExpensiveText" }, -- 4
{ "jump", 6 }, -- 5
{ "show_text", "_BikeShopYoungsterCoolBikeText" }, -- 6
},
},
},
}
+15
View File
@@ -0,0 +1,15 @@
-- CeladonCity flavor talk scripts (pokered/scripts/CeladonCity.asm)
return {
CELADON_CITY = {
talk = {
-- CeladonCityPoliwrathText (scripts/CeladonCity.asm): text_far
-- _CeladonCityPoliwrathText, then plays the POLIWRATH cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONCITY_POLIWRATH = {
{"face_player"},
{"show_text", "_CeladonCityPoliwrathText"},
},
},
},
}
@@ -0,0 +1,33 @@
-- CeladonMansion1F flavor talk scripts (pokered/scripts/CeladonMansion1F.asm)
return {
CELADON_MANSION_1F = {
talk = {
-- CeladonMansion1FClefairyText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FClefairyText, then plays the CLEFAIRY cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_CLEFAIRY = {
{"face_player"},
{"show_text", "_CeladonMansion1FClefairyText"},
},
-- CeladonMansion1FMeowthText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FMeowthText, then plays the MEOWTH cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_MEOWTH = {
{"face_player"},
{"show_text", "_CeladonMansion1FMeowthText"},
},
-- CeladonMansion1FNidoranFText (scripts/CeladonMansion1F.asm): text_far
-- _CeladonMansion1FNidoranFText, then plays the NIDORAN_F cry and ends.
-- The cry playback has no port-side equivalent command, so we just
-- show the flavor line.
TEXT_CELADONMANSION1F_NIDORANF = {
{"face_player"},
{"show_text", "_CeladonMansion1FNidoranFText"},
},
},
},
}
@@ -0,0 +1,34 @@
-- Celadon Mansion 3F Game Designer (pokered/scripts/CeladonMansion3F.asm
-- CeladonMansion3FGameDesignerText): text_asm counts set bits in
-- wPokedexOwned and compares against NUM_POKEMON - 1 (discounts Mew).
-- If the player owns >= 150 species, shows the "completed" text
-- (originally followed by DisplayDiploma, which has no equivalent UI
-- in this port, so we just show the congratulatory text); otherwise
-- shows the normal encouragement text.
return {
CELADON_MANSION_3F = {
talk = {
TEXT_CELADONMANSION3F_GAME_DESIGNER = function(game, ow, npc, done)
local t = game.data.text
local dex = game.save.pokedex
local owned = 0
if dex and dex.owned then
for _ in pairs(dex.owned) do
owned = owned + 1
end
end
-- NUM_POKEMON - 1 = 150 (discounts Mew, per pokered)
local label, fallback
if owned >= 150 then
label, fallback = "_CeladonMansion3FGameDesignerCompletedDexText",
"Wow! Excellent!\nYou completed\nyour POKeDEX!\nCongratulations!"
else
label, fallback = "_CeladonMansion3FGameDesignerText",
"Is that right?\nI'm the game\ndesigner!\nFilling up your\nPOKeDEX is tough,\nbut don't quit!\nWhen you finish,\ncome tell me!"
end
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, t[label] or fallback, done))
end,
},
},
}
@@ -0,0 +1,65 @@
-- CeruleanBadgeHouse (pokered/scripts/CeruleanBadgeHouse.asm)
--
-- CeruleanBadgeHouseMiddleAgedManText is a text_asm: it prints a
-- greeting, then loops a badge-description menu (LoadItemList /
-- DisplayListMenuID over CeruleanBadgeHouseBadgeTextPointers) until the
-- player backs out with B, then prints a goodbye line. The badge list
-- is the fixed set of all 8 badges (not filtered by what the player
-- owns) -- it's an explanatory menu, not a real item pick.
local function push(game, s, done)
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, s, done))
end
-- CeruleanBadgeHouseBadgeTextPointers / .BadgeItemList
local BADGE_ORDER = {
"BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE",
"SOULBADGE", "MARSHBADGE", "VOLCANOBADGE", "EARTHBADGE",
}
local BADGE_TEXT = {
BOULDERBADGE = "_CeruleanBadgeHouseBoulderBadgeText",
CASCADEBADGE = "_CeruleanBadgeHouseCascadeBadgeText",
THUNDERBADGE = "_CeruleanBadgeHouseThunderBadgeText",
RAINBOWBADGE = "_CeruleanBadgeHouseRainbowBadgeText",
SOULBADGE = "_CeruleanBadgeHouseSoulBadgeText",
MARSHBADGE = "_CeruleanBadgeHouseMarshBadgeText",
VOLCANOBADGE = "_CeruleanBadgeHouseVolcanoBadgeText",
EARTHBADGE = "_CeruleanBadgeHouseEarthBadgeText",
}
local function middleAgedMan(game, ow, npc, done)
local t = game.data.text
local function loop()
-- .loop: print WhichBadgeText, then show the badge list menu again
push(game, t._CeruleanBadgeHouseMiddleAgedManWhichBadgeText, function()
local ListMenu = require("src.ui.ListMenu")
local items = {}
for _, id in ipairs(BADGE_ORDER) do
local idef = game.data.items[id]
items[#items + 1] = { label = idef and idef.name or id, value = id }
end
local menu = ListMenu.new(game, "", items, {
onChoose = function(item)
push(game, t[BADGE_TEXT[item.value]], loop)
end,
onCancel = function()
-- .done: VisitAnyTimeText, then TextScriptEnd
push(game, t._CeruleanBadgeHouseMiddleAgedManVisitAnyTimeText, done)
end,
})
game.stack:push(menu)
end)
end
push(game, t._CeruleanBadgeHouseMiddleAgedManText, loop)
end
return {
CERULEAN_BADGE_HOUSE = {
talk = {
TEXT_CERULEANBADGEHOUSE_MIDDLE_AGED_MAN = middleAgedMan,
},
},
}
+27
View File
@@ -0,0 +1,27 @@
-- Mewtwo (scripts/CeruleanCaveB1F.asm CeruleanCaveB1FMewtwoText +
-- home/trainers.asm TalkToTrainer; MEWTWO 70 from
-- data/maps/objects/CeruleanCaveB1F.asm).
--
-- The text_asm loads MewtwoTrainerHeader and calls TalkToTrainer:
-- MewtwoBattleText is text_far "Mew!" + text_asm PlayCry MEWTWO +
-- WaitForSoundToFinish, then the wild battle starts -- or, when
-- EVENT_BEAT_MEWTWO is already set, the (identical) after-battle text
-- prints and nothing else happens. EndTrainerBattle sets
-- EVENT_BEAT_MEWTWO and hides the object on any non-blackout result
-- (win, catch or flee) -- static_battle mirrors that.
local M = {}
M.CERULEAN_CAVE_B1F = {
talk = {
TEXT_CERULEANCAVEB1F_MEWTWO = {
{ "play_cry", "MEWTWO" }, -- 1 text_asm PlayCry
{ "show_text", "_MewtwoBattleText" }, -- 2 "Mew!"
{ "check_flag", "EVENT_BEAT_MEWTWO" }, -- 3
{ "jump_if_true", 6 }, -- 4 already beaten: text only
{ "static_battle", "MEWTWO", 70, "EVENT_BEAT_MEWTWO" }, -- 5
},
},
}
return M
+55
View File
@@ -0,0 +1,55 @@
-- Cerulean City flavor dialogue (pokered/scripts/CeruleanCity.asm)
--
-- Both NPCs use text_asm with an hRandomAdd roll to pick one of several
-- flavor lines (no flags, no branching outcome) -- ported as a weighted
-- math.random pick each time the NPC is talked to.
local M = {}
local function push(game, ow, npc, done, text)
local TextBox = require("src.render.TextBox")
npc:facePlayer(ow.player)
game.stack:push(TextBox.new(game, text, done))
end
M.CERULEAN_CITY = {
talk = {
-- CeruleanCityCooltrainerF1Text (scripts/CeruleanCity.asm:362-393)
-- cp 180 -> 76/256 chance of 1st; cp 100 -> 80/256 chance of 2nd;
-- else 100/256 chance of 3rd.
TEXT_CERULEANCITY_COOLTRAINER_F1 = function(game, ow, npc, done)
local t = game.data.text
local roll = math.random(0, 255)
local text
if roll >= 180 then
text = t._CeruleanCityCooltrainerF1SlowbroUseSonicboomText
elseif roll >= 100 then
text = t._CeruleanCityCooltrainerF1SlowbroPunchText
else
text = t._CeruleanCityCooltrainerF1SlowbroWithdrawText
end
push(game, ow, npc, done, text)
end,
-- CeruleanCitySlowbroText (scripts/CeruleanCity.asm:395-436)
-- cp 180 -> 76/256 chance of 1st; cp 120 -> 60/256 chance of 2nd;
-- cp 60 -> 60/256 chance of 3rd; else 60/256 chance of 4th.
TEXT_CERULEANCITY_SLOWBRO = function(game, ow, npc, done)
local t = game.data.text
local roll = math.random(0, 255)
local text
if roll >= 180 then
text = t._CeruleanCitySlowbroTookASnoozeText
elseif roll >= 120 then
text = t._CeruleanCitySlowbroIsLoafingAroundText
elseif roll >= 60 then
text = t._CeruleanCitySlowbroTurnedAwayText
else
text = t._CeruleanCitySlowbroIgnoredOrdersText
end
push(game, ow, npc, done, text)
end,
},
}
return M
@@ -0,0 +1,22 @@
-- CeruleanTradeHouse (pokered/scripts/CeruleanTradeHouse.asm)
--
-- The Gambler NPC (CeruleanTradeHouseGamblerText) is a text_asm that sets
-- wWhichTrade = TRADE_FOR_LOLA and calls the DoInGameTradeDialogue predef
-- (engine/events/in_game_trades.asm). TRADE_FOR_LOLA is entry 7 in
-- data/events/trades.asm (give POLIWHIRL, get JYNX, nickname LOLA), which
-- matches data/generated/field.lua trades[7]. The Granny's text is plain
-- flavor lore (data/scripts/story.lua) -- the Gambler here owns the
-- trade, exactly as in CeruleanTradeHouse.asm.
-- EVENT_TRADED_POLIWHIRL_FOR_JYNX is the port's name for this trade's
-- wCompletedInGameTradeFlags bit (Commands.trade checks it before the
-- offer and sets it on completion).
return {
CERULEAN_TRADE_HOUSE = {
talk = {
TEXT_CERULEANTRADEHOUSE_GAMBLER = {
{ "face_player" },
{ "trade", 7, "EVENT_TRADED_POLIWHIRL_FOR_JYNX" }, -- LOLA (POLIWHIRL -> JYNX)
},
},
},
}
@@ -0,0 +1,24 @@
-- Cerulean Trashed House (pokered/scripts/CeruleanTrashedHouse.asm)
--
-- CeruleanTrashedHouseFishingGuruText (text_asm): checks whether the
-- player is carrying TM_DIG (GetQuantityOfItemInBag) and shows one of
-- two flavor lines depending on the result -- no flags/items are ever
-- changed, it's pure branching flavor text.
return {
CERULEAN_TRASHED_HOUSE = {
talk = {
-- CeruleanTrashedHouseFishingGuruText:
-- ld b, TM_DIG / predef GetQuantityOfItemInBag / and b
-- jr z, .no_dig_tm -> .TheyStoleATMText (player lacks TM_DIG)
-- else -> .WhatsLostIsLostText (player has TM_DIG)
TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU = {
{ "check_item", "TM_DIG" },
{ "jump_if_true", 4 },
{ "show_text", "_CeruleanTrashedHouseFishingGuruTheyStoleATMText" },
{ "jump", 5 },
{ "show_text", "_CeruleanTrashedHouseFishingGuruWhatsLostIsLostText" },
},
},
},
}
+14
View File
@@ -0,0 +1,14 @@
-- Flavor talk scripts for Copycat's House 1F (pokered/scripts/CopycatsHouse1F.asm)
return {
COPYCATS_HOUSE_1F = {
talk = {
-- CopycatsHouse1FChanseyText: text_far _CopycatsHouse1FChanseyText, then
-- text_asm plays the CHANSEY cry (ld a, CHANSEY / call PlayCry) before
-- ending. The port has no cry-playback command exposed to scripts, so
-- only the flavor text is ported.
TEXT_COPYCATSHOUSE1F_CHANSEY = {
{ "show_text", "_CopycatsHouse1FChanseyText" },
},
},
},
}
+26
View File
@@ -0,0 +1,26 @@
-- Flavor talk scripts for CopycatsHouse2F (registry id COPYCATS_HOUSE_2F).
-- Source: pokered/scripts/CopycatsHouse2F.asm, pokered/text/CopycatsHouse2F.asm
--
-- TEXT_COPYCATSHOUSE2F_COPYCAT is already ported (with the POKE DOLL /
-- TM31 MIMIC trade) in data/scripts/story4.lua, so it is intentionally
-- omitted here.
return {
COPYCATS_HOUSE_2F = {
talk = {
-- CopycatsHouse2FPCText (scripts/CopycatsHouse2F.asm):
-- only shows "My Secrets!" when the player is facing UP at the
-- PC (i.e. actually looking at the screen); any other facing
-- gets the generic "Huh? Can't see!" text.
TEXT_COPYCATSHOUSE2F_PC = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local t = game.data.text
local facing = ow and ow.player and ow.player.facing
local label = (facing == "up")
and "_CopycatsHouse2FPCMySecretsText"
or "_CopycatsHouse2FPCCantSeeText"
game.stack:push(TextBox.new(game, t[label], done))
end,
},
},
}
+90
View File
@@ -0,0 +1,90 @@
-- Flavor dialogue for GameCorner (pokered/scripts/GameCorner.asm)
--
-- TEXT_GAMECORNER_CLERK1, TEXT_GAMECORNER_GYM_GUIDE and
-- TEXT_GAMECORNER_POSTER are already ported on data/scripts/story3.lua's
-- and story7.lua's M.GAME_CORNER tables. This file ports the three
-- remaining coin-giveaway NPCs, each a one-shot "give N coins" gated by
-- its own EVENT_GOT_*_COINS flag, the COIN CASE, and Has9990Coins room
-- to receive them (mirroring GameCornerClerk1Text's coin-case/coin-cap
-- checks in story3.lua).
local function coinGiver(opts)
return function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local Sound = require("src.core.Sound")
local t = game.data.text
local function push(label, fallback, onDone)
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done))
end
if game.save.flags[opts.event] then
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
return
end
push(opts.askLabel, opts.askFallback, function()
if not game.save.inventory.COIN_CASE then
push("_GameCornerOopsForgotCoinCaseText", "Oops! Forgot the\nCOIN CASE!")
return
end
if (game.save.coins or 0) >= 9990 then
push(opts.coinCaseFullLabel, opts.coinCaseFullFallback)
return
end
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
game.save.flags[opts.event] = true
Sound.play(game.data, "Get_Item1")
push(opts.receivedLabel,
("{PLAYER} received\n%d coins!"):format(opts.amount))
end)
end
end
return {
GAME_CORNER = {
talk = {
-- GameCornerFishingGuruText (pokered/scripts/GameCorner.asm):
-- gives 10 coins once (EVENT_GOT_10_COINS).
TEXT_GAMECORNER_FISHING_GURU = coinGiver({
event = "EVENT_GOT_10_COINS",
amount = 10,
askLabel = "_GameCornerFishingGuruWantToPlayText",
askFallback = "Kid, do you want\nto play?",
receivedLabel = "_GameCornerFishingGuruReceived10CoinsText",
coinCaseFullLabel = "_GameCornerFishingGuruDontNeedMyCoinsText",
coinCaseFullFallback = "You don't need my\ncoins!",
alreadyGotLabel = "_GameCornerFishingGuruWinsComeAndGoText",
alreadyGotFallback = "Wins seem to come\nand go.",
}),
-- GameCornerClerk2Text (pokered/scripts/GameCorner.asm): gives 20
-- coins once (EVENT_GOT_20_COINS_2).
TEXT_GAMECORNER_CLERK2 = coinGiver({
event = "EVENT_GOT_20_COINS_2",
amount = 20,
askLabel = "_GameCornerClerk2WantSomeCoinsText",
askFallback = "What's up? Want\nsome coins?",
receivedLabel = "_GameCornerClerk2Received20CoinsText",
coinCaseFullLabel = "_GameCornerClerk2YouHaveLotsOfCoinsText",
coinCaseFullFallback = "You have lots of\ncoins!",
alreadyGotLabel = "_GameCornerClerk2INeedMoreCoinsText",
alreadyGotFallback = "Darn! I need more\ncoins for the\vPOKéMON I want!",
}),
-- GameCornerGentlemanText (pokered/scripts/GameCorner.asm): gives 20
-- coins once (EVENT_GOT_20_COINS). Note the original ASM uses
-- Has9990Coins' `jr z` (only the exact-9990 case) here rather than
-- Clerk1/Clerk2's `jr nc` (>=9990); ported as >=9990 to match their
-- "coin case is basically full" intent and avoid a coin-count
-- edge case where 9991-9999 would otherwise slip past the check.
TEXT_GAMECORNER_GENTLEMAN = coinGiver({
event = "EVENT_GOT_20_COINS",
amount = 20,
askLabel = "_GameCornerGentlemanThrowingMeOffText",
askFallback = "Hey, what? You're\nthrowing me off!\vHere are some\vcoins, shoo!",
receivedLabel = "_GameCornerGentlemanReceived20CoinsText",
coinCaseFullLabel = "_GameCornerGentlemanYouGotYourOwnCoinsText",
coinCaseFullFallback = "You've got your\nown coins!",
alreadyGotLabel = "_GameCornerGentlemanCloselyWatchTheReelsText",
alreadyGotFallback = "The trick is to\nwatch the reels\vclosely!",
}),
},
},
}
@@ -0,0 +1,28 @@
-- Lavender Cubone House flavor talk scripts.
-- Source: pokered/scripts/LavenderCuboneHouse.asm
return {
LAVENDER_CUBONE_HOUSE = {
talk = {
-- LavenderCuboneHouseCuboneText: text_far _LavenderCuboneHouseCuboneText,
-- then text_asm plays the CUBONE cry. Cry playback has no Commands
-- equivalent in this port, so just show the line.
TEXT_LAVENDERCUBONEHOUSE_CUBONE = {
{ "face_player" },
{ "show_text", "_LavenderCuboneHouseCuboneText" },
},
-- LavenderCuboneHouseBrunetteGirlText: text_asm branches on
-- EVENT_RESCUED_MR_FUJI -- before the event, she laments Cubone's
-- mother; after, she's relieved the Ghost of Pokemon Tower is gone.
TEXT_LAVENDERCUBONEHOUSE_BRUNETTE_GIRL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
{ "jump_if_true", 6 }, -- 3
{ "show_text", "_LavenderCuboneHouseBrunetteGirlPoorCubonesMotherText" }, -- 4
{ "jump", 7 }, -- 5
{ "show_text", "_LavenderCuboneHouseBrunetteGirlGhostIsGoneText" }, -- 6 (target of jump_if_true)
},
},
},
}
+18
View File
@@ -0,0 +1,18 @@
-- Flavor dialogue for LavenderMart (pokered/scripts/LavenderMart.asm)
return {
LAVENDER_MART = {
talk = {
-- LavenderMartCooltrainerMText (pokered/scripts/LavenderMart.asm):
-- before EVENT_RESCUED_MR_FUJI: talks about REVIVE; after: talks about
-- the NUGGET he found.
TEXT_LAVENDERMART_COOLTRAINER_M = {
{ "face_player" },
{ "check_flag", "EVENT_RESCUED_MR_FUJI" },
{ "jump_if_true", 5 },
{ "show_text", "_LavenderMartCooltrainerMReviveText" },
{ "jump", 6 },
{ "show_text", "_LavenderMartCooltrainerMNuggetText" },
},
},
},
}
+21
View File
@@ -0,0 +1,21 @@
-- Hand-ported text_asm dialogue for LavenderTown.
-- Source: pokered/scripts/LavenderTown.asm, pokered/text/LavenderTown.asm
return {
LAVENDER_TOWN = {
talk = {
-- LavenderTownLittleGirlText (pokered/scripts/LavenderTown.asm):
-- asks "Do you believe in GHOSTs?" via YesNoChoice; on YES shows
-- "Really? So there are believers...", on NO shows
-- "Hahaha, I guess not. That white hand on your shoulder, it's not real."
TEXT_LAVENDERTOWN_LITTLE_GIRL = {
{ "face_player" }, -- [1]
{ "ask", "_LavenderTownLittleGirlDoYouBelieveInGhostsText" }, -- [2]
{ "jump_if_true", 6 }, -- [3] YES -> believers text (row 6)
{ "show_text", "_LavenderTownLittleGirlHaHaGuessNotText" }, -- [4] NO path
{ "jump", 99 }, -- [5] end (skip believers text below)
{ "show_text", "_LavenderTownLittleGirlSoThereAreBelieversText" }, -- [6] YES path
},
},
},
}
+49
View File
@@ -0,0 +1,49 @@
-- Mr. Fuji's House flavor NPCs (pokered/scripts/MrFujisHouse.asm).
-- TEXT_MRFUJISHOUSE_MR_FUJI and TEXT_MRFUJISHOUSE_POKEDEX are already
-- ported in data/scripts/story.lua (M.MR_FUJIS_HOUSE.talk) alongside the
-- EVENT_RESCUED_MR_FUJI onEnter repair, so they're skipped here.
local M = {}
M.MR_FUJIS_HOUSE = {
talk = {
-- scripts/MrFujisHouse.asm MrFujisHouseSuperNerdText: CheckEvent
-- EVENT_RESCUED_MR_FUJI branches between "he's not here" and
-- "he had been praying".
TEXT_MRFUJISHOUSE_SUPER_NERD = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
{ "jump_if_true", 5 }, -- 3
{ "show_text", "_MrFujisHouseSuperNerdMrFujiIsntHereText" }, -- 4
{ "jump", 6 }, -- 5
{ "show_text", "_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" }, -- 6
},
-- scripts/MrFujisHouse.asm MrFujisHouseLittleGirlText: CheckEvent
-- EVENT_RESCUED_MR_FUJI branches between "this is Mr. Fuji's house"
-- and "Pokemon are nice to hug".
TEXT_MRFUJISHOUSE_LITTLE_GIRL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
{ "jump_if_true", 5 }, -- 3
{ "show_text", "_MrFujisHouseLittleGirlThisIsMrFujisHouseText" }, -- 4
{ "jump", 6 }, -- 5
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
},
-- scripts/MrFujisHouse.asm MrFujisHousePsyduckText: text_far then
-- PlayCry(PSYDUCK). Cry playback isn't modeled by Commands, so just
-- show the flavor text.
TEXT_MRFUJISHOUSE_PSYDUCK = {
{ "show_text", "_MrFujisHousePsyduckText" },
},
-- scripts/MrFujisHouse.asm MrFujisHouseNidorinoText: text_far then
-- PlayCry(NIDORINO).
TEXT_MRFUJISHOUSE_NIDORINO = {
{ "show_text", "_MrFujisHouseNidorinoText" },
},
},
}
return M
+54
View File
@@ -0,0 +1,54 @@
-- Museum 1F flavor dialogue (pokered scripts/Museum1F.asm).
-- TEXT_MUSEUM1F_SCIENTIST1 and TEXT_MUSEUM1F_OLD_AMBER are already
-- handled in data/scripts/story2.lua (M.MUSEUM_1F, the ticket-gate
-- onStep + amber-pickup talk handler) -- not re-ported here.
return {
MUSEUM_1F = {
talk = {
-- Museum1FGamblerText: plain text_asm, single text_far line.
TEXT_MUSEUM1F_GAMBLER = {
{ "face_player" },
{ "show_text", "_Museum1FGamblerText" },
},
-- Museum1FScientist2Text: CheckEvent EVENT_GOT_OLD_AMBER branch.
-- Not yet gotten -> pitch text, GiveItem OLD_AMBER (bag-full
-- refusal -> YouDontHaveSpaceText, no flag/received text), on
-- success SetEvent + predef HideObject (TOGGLE_OLD_AMBER, so the
-- OLD_AMBER sprite object on this map also disappears) +
-- ReceivedOldAmberText. Already gotten -> GetTheOldAmberCheckText.
TEXT_MUSEUM1F_SCIENTIST2 = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local Commands = require("src.script.Commands")
local t = game.data.text
local function say(label, cb)
game.stack:push(TextBox.new(game, t[label] or label, cb))
end
if game.save.flags.EVENT_GOT_OLD_AMBER then
say("_Museum1FScientist2GetTheOldAmberCheckText", done)
return
end
say("_Museum1FScientist2TakeThisToAPokemonLabText", function()
if not require("src.inventory.Bag").add(game.save, "OLD_AMBER", 1) then
say("_Museum1FScientist2YouDontHaveSpaceText", done)
return
end
game.save.flags.EVENT_GOT_OLD_AMBER = true
Commands.hide_object({ save = game.save, overworld = ow, game = game },
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
require("src.core.Sound").play(game.data, "Get_Item1")
say("_Museum1FScientist2ReceivedOldAmberText", done)
end)
end,
-- Museum1FScientist3Text: plain text_asm, single text_far line.
TEXT_MUSEUM1F_SCIENTIST3 = {
{ "face_player" },
{ "show_text", "_Museum1FScientist3Text" },
},
},
},
}
+41
View File
@@ -0,0 +1,41 @@
-- Hand-ported flavor text for OaksLab (registry id OAKS_LAB).
-- Source: pokered/scripts/OaksLab.asm. These five text_asm bodies are
-- all simple "PrintText; jp TextScriptEnd" -- no flag branches, no
-- YES/NO menu -- so a one-row talk script showing the real extracted
-- text is a faithful port. (The rest of OaksLab.asm's TEXT_OAKSLAB_*
-- constants -- OAK1, the three starter poke balls, RIVAL -- are already
-- ported with full branching logic in data/scripts/oaks_lab.lua.)
return {
OAKS_LAB = {
talk = {
-- OaksLabGirlText (scripts/OaksLab.asm)
TEXT_OAKSLAB_GIRL = {
{ "face_player" },
{ "show_text", "_OaksLabGirlText" },
},
-- OaksLabPokedexText, used for both the POKEDEX1 and POKEDEX2
-- table objects (scripts/OaksLab.asm OaksLab_TextPointers)
TEXT_OAKSLAB_POKEDEX1 = {
{ "face_player" },
{ "show_text", "_OaksLabPokedexText" },
},
TEXT_OAKSLAB_POKEDEX2 = {
{ "face_player" },
{ "show_text", "_OaksLabPokedexText" },
},
-- OaksLabScientistText, used for both SCIENTIST1 and SCIENTIST2
-- (scripts/OaksLab.asm OaksLab_TextPointers)
TEXT_OAKSLAB_SCIENTIST1 = {
{ "face_player" },
{ "show_text", "_OaksLabScientistText" },
},
TEXT_OAKSLAB_SCIENTIST2 = {
{ "face_player" },
{ "show_text", "_OaksLabScientistText" },
},
},
},
}
+75
View File
@@ -0,0 +1,75 @@
-- Pewter City flavor dialogue (pokered/scripts/PewterCity.asm).
-- PewterCity_TextPointers text_asm bodies for the SUPER_NERD1 museum
-- guide, SUPER_NERD2 garden nerd, and the leaving-east YOUNGSTER.
--
-- The escort choreography (SUPER_NERD1 walking the player to the
-- museum, YOUNGSTER walking the player to the gym) is scripted NPC
-- movement + a wPewterCityCurScript state machine that steers the
-- player off-map; that part is already covered on this map by
-- story5.lua's onStep gate (walks the player back a step at the
-- east exit before EVENT_BEAT_BROCK). Here we only port the real
-- YES/NO-branched flavor text these NPCs speak when talked to.
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
M.PEWTER_CITY = {
talk = {
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm): asks if you
-- checked out the museum; YES -> fossils comment, NO -> "you have
-- to go" (which in pokered also kicks off the escort script).
TEXT_PEWTERCITY_SUPER_NERD1 = function(game, ow, npc, done)
local t = text(game)
ask(game, t._PewterCitySuperNerd1DidYouCheckOutMuseumText
or "Did you check out\nthe MUSEUM?", function(yes)
if yes then
push(game, t._PewterCitySuperNerd1WerentThoseFossilsAmazingText
or "Weren't those\nfossils from MT.\nMOON amazing?", done)
else
push(game, t._PewterCitySuperNerd1YouHaveToGoText
or "Really?\nYou absolutely\nhave to go!", done)
end
end)
end,
-- PewterCitySuperNerd2Text (scripts/PewterCity.asm): asks if you
-- know what he's doing; YES -> "that's right", NO -> reveals he's
-- spraying Repel to keep Pokemon out of his garden.
TEXT_PEWTERCITY_SUPER_NERD2 = function(game, ow, npc, done)
local t = text(game)
ask(game, t._PewterCitySuperNerd2DoYouKnowWhatImDoingText
or "Psssst!\nDo you know what\nI'm doing?", function(yes)
if yes then
push(game, t._PewterCitySuperNerd2ThatsRightText
or "That's right!\nIt's hard work!", done)
else
push(game, t._PewterCitySuperNerd2ImSprayingRepelText
or "I'm spraying REPEL\nto keep POKéMON\nout of my garden!", done)
end
end)
end,
-- PewterCityYoungsterText (scripts/PewterCity.asm): the "follow
-- me" line the youngster says when the player is stopped from
-- leaving Pewter east before beating Brock; the actual gate/step
-- block is handled by story5.lua's onStep for this map.
TEXT_PEWTERCITY_YOUNGSTER = function(game, ow, npc, done)
local t = text(game)
push(game, t._PewterCityYoungsterYoureATrainerFollowMeText
or "You're a trainer\nright? BROCK's\nlooking for new\nchallengers!\nFollow me!", done)
end,
},
}
return M
+19
View File
@@ -0,0 +1,19 @@
-- Flavor talk scripts for PEWTER_MART (pokered/scripts/PewterMart.asm)
return {
PEWTER_MART = {
talk = {
-- PewterMartYoungsterText: text_asm -> single text_far _PewterMartYoungsterText
TEXT_PEWTERMART_YOUNGSTER = {
{ "face_player" },
{ "show_text", "_PewterMartYoungsterText" },
},
-- PewterMartSuperNerdText: text_asm -> single text_far _PewterMartSuperNerdText
TEXT_PEWTERMART_SUPER_NERD = {
{ "face_player" },
{ "show_text", "_PewterMartSuperNerdText" },
},
},
},
}
@@ -0,0 +1,15 @@
-- pokered/scripts/PewterNidoranHouse.asm
-- PewterNidoranHouseNidoranText: text_far _PewterNidoranHouseNidoranText, then
-- text_asm plays the NIDORAN_M cry (PlayCry/WaitForSoundToFinish) before ending.
-- No sound-effect command exists in Commands.lua, so we port the dialogue line
-- only; the cry SFX is cosmetic and has no gameplay effect.
return {
PEWTER_NIDORAN_HOUSE = {
talk = {
TEXT_PEWTERNIDORANHOUSE_NIDORAN = {
{ "face_player" },
{ "show_text", "_PewterNidoranHouseNidoranText" },
},
},
},
}
+54
View File
@@ -0,0 +1,54 @@
-- Flavor talk scripts for POKEMON_FAN_CLUB (pokered/scripts/PokemonFanClub.asm)
--
-- TEXT_POKEMONFANCLUB_CHAIRMAN is already ported in data/scripts/story2.lua
-- (the bike voucher chain), so it is intentionally omitted here.
local M = {}
M.POKEMON_FAN_CLUB = {
talk = {
-- PokemonFanClubPikachuFanText (scripts/PokemonFanClub.asm): brags
-- about her PIKACHU unless she's already "won" the boast war against
-- the SEEL fan (EVENT_PIKACHU_FAN_BOAST set), in which case she gets
-- huffy and resets it. Either way she sets the other fan's boast flag
-- so their next line is the "mine is better" retort.
TEXT_POKEMONFANCLUB_PIKACHU_FAN = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_PIKACHU_FAN_BOAST" }, -- 2
{ "jump_if_true", 7 }, -- 3
{ "show_text", "_PokemonFanClubPikachuFanNormalText" }, -- 4
{ "set_flag", "EVENT_SEEL_FAN_BOAST" }, -- 5
{ "jump", 9 }, -- 6 (skip the "mineisbetter" branch)
{ "show_text", "_PokemonFanClubPikachuFanBetterText" }, -- 7
{ "clear_flag", "EVENT_PIKACHU_FAN_BOAST" }, -- 8
},
-- PokemonFanClubSeelFanText (scripts/PokemonFanClub.asm): mirror of
-- the PIKACHU fan above, keyed off EVENT_SEEL_FAN_BOAST.
TEXT_POKEMONFANCLUB_SEEL_FAN = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_SEEL_FAN_BOAST" }, -- 2
{ "jump_if_true", 7 }, -- 3
{ "show_text", "_PokemonFanClubSeelFanNormalText" }, -- 4
{ "set_flag", "EVENT_PIKACHU_FAN_BOAST" }, -- 5
{ "jump", 9 }, -- 6 (skip the "mineisbetter" branch)
{ "show_text", "_PokemonFanClubSeelFanBetterText" }, -- 7
{ "clear_flag", "EVENT_SEEL_FAN_BOAST" }, -- 8
},
-- PokemonFanClubPikachuText (scripts/PokemonFanClub.asm): the
-- PIKACHU itself, just a flavor line (its cry isn't playable in the
-- port's talk pipeline, so it's dropped like other cry-only lines).
TEXT_POKEMONFANCLUB_PIKACHU = {
{ "show_text", "_PokemonFanClubPikachuText" },
},
-- PokemonFanClubSeelText (scripts/PokemonFanClub.asm): the SEEL
-- itself, flavor line only.
TEXT_POKEMONFANCLUB_SEEL = {
{ "show_text", "_PokemonFanClubSeelText" },
},
},
}
return M
+47
View File
@@ -0,0 +1,47 @@
-- Power Plant static encounters (scripts/PowerPlant.asm + home/trainers.asm
-- TalkToTrainer; species/levels from data/maps/objects/PowerPlant.asm).
--
-- Each "item ball" is a disguised VOLTORB/ELECTRODE: its text_asm loads a
-- trainer header (Voltorb0..7TrainerHeader) and calls TalkToTrainer, which
-- prints PowerPlantVoltorbBattleText ("Bzzzt!") and starts a wild battle --
-- or, when the header's EVENT_BEAT_POWER_PLANT_VOLTORB_n flag is already
-- set, prints the (identical) after-battle text and stops. Zapdos is the
-- same flow with ZapdosTrainerHeader/EVENT_BEAT_ZAPDOS, except its battle
-- text is text_far "Gyaoo!" followed by text_asm PlayCry ZAPDOS +
-- WaitForSoundToFinish. EndTrainerBattle sets the EVENT_BEAT_* flag and
-- hides the object on any non-blackout result (win, catch or flee) --
-- static_battle mirrors that.
-- header order in scripts/PowerPlant.asm: text_asm n uses header n-1
local function ballMon(species, level, flag)
return {
{ "show_text", "_PowerPlantVoltorbBattleText" }, -- 1 "Bzzzt!"
{ "check_flag", flag }, -- 2
{ "jump_if_true", 5 }, -- 3 already beaten: text only
{ "static_battle", species, level, flag }, -- 4
}
end
local M = {}
M.POWER_PLANT = {
talk = {
TEXT_POWERPLANT_VOLTORB1 = ballMon("VOLTORB", 40, "EVENT_BEAT_POWER_PLANT_VOLTORB_0"),
TEXT_POWERPLANT_VOLTORB2 = ballMon("VOLTORB", 40, "EVENT_BEAT_POWER_PLANT_VOLTORB_1"),
TEXT_POWERPLANT_VOLTORB3 = ballMon("VOLTORB", 40, "EVENT_BEAT_POWER_PLANT_VOLTORB_2"),
TEXT_POWERPLANT_ELECTRODE1 = ballMon("ELECTRODE", 43, "EVENT_BEAT_POWER_PLANT_VOLTORB_3"),
TEXT_POWERPLANT_VOLTORB4 = ballMon("VOLTORB", 40, "EVENT_BEAT_POWER_PLANT_VOLTORB_4"),
TEXT_POWERPLANT_VOLTORB5 = ballMon("VOLTORB", 40, "EVENT_BEAT_POWER_PLANT_VOLTORB_5"),
TEXT_POWERPLANT_ELECTRODE2 = ballMon("ELECTRODE", 43, "EVENT_BEAT_POWER_PLANT_VOLTORB_6"),
TEXT_POWERPLANT_VOLTORB6 = ballMon("VOLTORB", 40, "EVENT_BEAT_POWER_PLANT_VOLTORB_7"),
TEXT_POWERPLANT_ZAPDOS = {
{ "play_cry", "ZAPDOS" }, -- 1 text_asm PlayCry
{ "show_text", "_PowerPlantZapdosBattleText" }, -- 2 "Gyaoo!"
{ "check_flag", "EVENT_BEAT_ZAPDOS" }, -- 3
{ "jump_if_true", 6 }, -- 4 already beaten: text only
{ "static_battle", "ZAPDOS", 50, "EVENT_BEAT_ZAPDOS" }, -- 5
},
},
}
return M
+25
View File
@@ -0,0 +1,25 @@
-- Red's House 1F (pokered/scripts/RedsHouse1F.asm).
--
-- TEXT_REDSHOUSE1F_TV (RedsHouse1FTVText): text_asm checks the player's
-- facing direction when interacting with the TV sign -- facing up shows
-- the Stand By Me movie flavor text, any other facing shows the "wrong
-- side" text (you're looking at the back of the TV).
local TextBox = require("src.render.TextBox")
return {
REDS_HOUSE_1F = {
talk = {
TEXT_REDSHOUSE1F_TV = function(game, ow, npc, done)
local t = game.data.text
local text
if ow.player.facing == "up" then
text = t._RedsHouse1FTVStandByMeMovieText
else
text = t._RedsHouse1FTVWrongSideText
end
game.stack:push(TextBox.new(game, text, done))
end,
},
},
}
+43
View File
@@ -0,0 +1,43 @@
-- Route 11 Gate, 2F (pokered/scripts/Route11Gate2F.asm)
--
-- TEXT_ROUTE11GATE2F_YOUNGSTER (in-game trade) and TEXT_ROUTE11GATE2F_OAKS_AIDE
-- (Oak's Aide itemfinder handout) are not ported here; only the two window
-- signs are in scope for this pass.
--
-- Both binocular signs use GateUpstairsScript_PrintIfFacingUp: the sign only
-- shows text when the player is facing UP (looking out the window); reading
-- it from any other direction prints nothing.
return {
ROUTE_11_GATE_2F = {
talk = {
-- Route11Gate2FLeftBinocularsText (scripts/Route11Gate2F.asm): only
-- fires facing up; then branches on EVENT_BEAT_ROUTE12_SNORLAX to show
-- either the "big POKéMON asleep on a road" or the "beautiful view"
-- flavor text.
TEXT_ROUTE11GATE2F_LEFT_BINOCULARS = function(game, ow, npc, done)
if ow.player.facing ~= "up" then
done()
return
end
local TextBox = require("src.render.TextBox")
local label = game.save.flags.EVENT_BEAT_ROUTE12_SNORLAX
and "_Route11Gate2FLeftBinocularsNoSnorlaxText"
or "_Route11Gate2FLeftBinocularsSnorlaxText"
game.stack:push(TextBox.new(game, game.data.text[label], done))
end,
-- Route11Gate2FRightBinocularsText (scripts/Route11Gate2F.asm): only
-- fires facing up; describes the Cerulean-to-Lavender route via Rock
-- Tunnel.
TEXT_ROUTE11GATE2F_RIGHT_BINOCULARS = function(game, ow, npc, done)
if ow.player.facing ~= "up" then
done()
return
end
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, game.data.text["_Route11Gate2FRightBinocularsText"], done))
end,
},
},
}
+34
View File
@@ -0,0 +1,34 @@
-- Route 18 Gate, 2F (pokered/scripts/Route18Gate2F.asm)
--
-- The two binocular signs only show their flavor text when the player
-- is facing up when they interact with them (GateUpstairsScript_
-- PrintIfFacingUp checks wSpritePlayerStateData1FacingDirection ==
-- SPRITE_FACING_UP; if not, it silently ends the text script without
-- printing anything). TEXT_ROUTE18GATE2F_YOUNGSTER (the in-game trade)
-- is skipped here: it's the generic DoInGameTradeDialogue path already
-- covered by the shared `trade` command/example, not map-specific logic.
local M = {}
local function printIfFacingUp(label)
return function(game, ow, npc, done)
if ow.player.facing ~= "up" then
done()
return
end
local TextBox = require("src.render.TextBox")
local t = game.data.text
game.stack:push(TextBox.new(game, t[label] or "", done))
end
end
M.ROUTE_18_GATE_2F = {
talk = {
-- Route18Gate2FLeftBinocularsText: text_far _Route18Gate2FLeftBinocularsText
TEXT_ROUTE18GATE2F_LEFT_BINOCULARS = printIfFacingUp("_Route18Gate2FLeftBinocularsText"),
-- Route18Gate2FRightBinocularsText: text_far _Route18Gate2FRightBinocularsText
TEXT_ROUTE18GATE2F_RIGHT_BINOCULARS = printIfFacingUp("_Route18Gate2FRightBinocularsText"),
},
}
return M
+36
View File
@@ -0,0 +1,36 @@
-- Route 12 Gate 2F binocular signs (pokered/scripts/Route12Gate2F.asm
-- Route12Gate2FLeftBinocularsText / Route12Gate2FRightBinocularsText ->
-- GateUpstairsScript_PrintIfFacingUp): the binoculars only show their
-- text when the player is facing up (looking through them from below);
-- from any other facing the sign is silently a no-op, same as the
-- original (it just clears wDoNotWaitForButtonPressAfterDisplayingText
-- and returns without printing).
--
-- TEXT_ROUTE12GATE2F_BRUNETTE_GIRL (TM39 SWIFT gift) is intentionally
-- skipped here -- omitted, not ported by this file.
local function push(game, s, done)
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, s, done))
end
local function binoculars(label)
return function(game, ow, npc, done)
done = done or function() end
if ow.player.facing ~= "up" then
done()
return
end
local t = game.data.text[label]
push(game, t, done)
end
end
return {
ROUTE_12_GATE_2F = {
talk = {
TEXT_ROUTE12GATE2F_LEFT_BINOCULARS = binoculars("_Route12Gate2FLeftBinocularsText"),
TEXT_ROUTE12GATE2F_RIGHT_BINOCULARS = binoculars("_Route12Gate2FRightBinocularsText"),
},
},
}
+13
View File
@@ -0,0 +1,13 @@
-- pokered/scripts/Route15Gate2F.asm: Route15Gate2FBinocularsText
-- text_asm just calls GateUpstairsScript_PrintIfFacingUp, which prints
-- the binoculars flavor text (no flags/branches involved).
return {
ROUTE_15_GATE_2F = {
talk = {
-- Route15Gate2FBinocularsText -> .Text -> _Route15Gate2FBinocularsText
TEXT_ROUTE15GATE2F_BINOCULARS = {
{ "show_text", "_Route15Gate2FBinocularsText" },
},
},
},
}
@@ -0,0 +1,15 @@
-- Route 16 Fly House flavor dialogue
-- Source: pokered/scripts/Route16FlyHouse.asm, pokered/text/Route16FlyHouse.asm
return {
ROUTE_16_FLY_HOUSE = {
talk = {
-- Route16FlyHouseFearowText: text_asm just prints the one line and
-- plays the FEAROW cry (no cry-playback command exists in this
-- port's Commands vocabulary, so only the text is ported).
TEXT_ROUTE16FLYHOUSE_FEAROW = {
{ "show_text", "_Route16FlyHouseFearowText" },
},
},
},
}
+22
View File
@@ -0,0 +1,22 @@
-- Route16Gate1F (pokered/scripts/Route16Gate1F.asm, Route16Gate1FGuardText)
--
-- TEXT_ROUTE16GATE1F_GUARD: talking to the gate guard directly (as
-- opposed to walking into his blocking coords, which is already
-- handled by story5.lua's ROUTE_16_GATE_1F onStep bikeGateGuard).
-- text_asm calls Route16Gate1FIsBicycleInBagScript (IsItemInBag
-- BICYCLE) and shows the Cycling Road explanation if the player has a
-- BICYCLE, or the "no pedestrians allowed" text otherwise.
return {
ROUTE_16_GATE_1F = {
talk = {
TEXT_ROUTE16GATE1F_GUARD = {
{ "face_player" },
{ "check_item", "BICYCLE" },
{ "jump_if_true", 5 },
{ "show_text", "_Route16Gate1FGuardNoPedestriansAllowedText" },
{ "jump", 6 },
{ "show_text", "_Route16Gate1FGuardCyclingRoadExplanationText" },
},
},
},
}
+34
View File
@@ -0,0 +1,34 @@
-- pokered/scripts/Route16Gate2F.asm
-- Route 16 Gate, 2F (overpass) flavor dialogue: little boy, little girl, and
-- both binocular signs. All four are plain text_far bodies (the binoculars'
-- text_asm just jumps into GateUpstairsScript_PrintIfFacingUp, which is a
-- facing-direction gate handled by the overworld sign-interaction system,
-- not extra dialogue branching), so each ports as a single-row talk script.
return {
ROUTE_16_GATE_2F = {
talk = {
-- Route16Gate2FLittleBoyText: text_far _Route16Gate2FLittleBoyText
TEXT_ROUTE16GATE2F_LITTLE_BOY = {
{ "face_player" },
{ "show_text", "_Route16Gate2FLittleBoyText" },
},
-- Route16Gate2FLittleGirlText: text_far _Route16Gate2FLittleGirlText
TEXT_ROUTE16GATE2F_LITTLE_GIRL = {
{ "face_player" },
{ "show_text", "_Route16Gate2FLittleGirlText" },
},
-- Route16Gate2FLeftBinocularsText: text_far _Route16Gate2FLeftBinocularsText
TEXT_ROUTE16GATE2F_LEFT_BINOCULARS = {
{ "show_text", "_Route16Gate2FLeftBinocularsText" },
},
-- Route16Gate2FRightBinocularsText: text_far _Route16Gate2FRightBinocularsText
TEXT_ROUTE16GATE2F_RIGHT_BINOCULARS = {
{ "show_text", "_Route16Gate2FRightBinocularsText" },
},
},
},
}
+24
View File
@@ -0,0 +1,24 @@
-- Route18Gate1F (pokered/scripts/Route18Gate1F.asm, Route18Gate1FGuardText)
--
-- TEXT_ROUTE18GATE1F_GUARD: talking to the gate guard directly (as
-- opposed to walking into his blocking coords, which triggers
-- TEXT_ROUTE18GATE1F_GUARD_EXCUSE_ME plus scripted movement -- not
-- ported here since that's a separate constant/flow, not a talk).
-- text_asm calls Route16Gate1FIsBicycleInBagScript (IsItemInBag
-- BICYCLE) and shows the "Cycling Road is all uphill from here" text
-- if the player has a BICYCLE, or "You need a BICYCLE for CYCLING
-- ROAD!" otherwise.
return {
ROUTE_18_GATE_1F = {
talk = {
TEXT_ROUTE18GATE1F_GUARD = {
{ "face_player" },
{ "check_item", "BICYCLE" },
{ "jump_if_true", 5 },
{ "show_text", "_Route18Gate1FGuardYouNeedABicycleText" },
{ "jump", 6 },
{ "show_text", "_Route18Gate1FGuardCyclingRoadUphillText" },
},
},
},
}
+25
View File
@@ -0,0 +1,25 @@
-- Route 22 Gate guard (pokered scripts/Route22Gate.asm
-- Route22GateGuardText): text_asm branches on wObtainedBadges
-- BIT_BOULDERBADGE. Without the badge he refuses (plays SFX_DENIED,
-- "no BOULDERBADGE yet" + "the rules are rules") and the original then
-- auto-walks the player back down (Route22GateMovePlayerDownScript);
-- with the badge he waves you through. The auto-walk-back is a
-- movement/collision concern handled outside `talk` (not ported here,
-- per the task's talk-only scope) -- this only ports the guard's real
-- flavor text so the correct branch shows on repeat talks.
local M = {}
M.ROUTE_22_GATE = {
talk = {
TEXT_ROUTE22GATE_GUARD = {
{ "check_flag", "EVENT_BEAT_BROCK" }, -- 1 (BIT_BOULDERBADGE)
{ "jump_if_true", 6 }, -- 2
{ "show_text", "_Route22GateGuardNoBoulderbadgeText" }, -- 3
{ "show_text", "_Route22GateGuardICantLetYouPassText" }, -- 4
{ "jump", 7 }, -- 5 (end, skip go-right-ahead)
{ "show_text", "_Route22GateGuardGoRightAheadText" }, -- 6
},
},
}
return M
+51
View File
@@ -0,0 +1,51 @@
-- Route 23's badge-gate guards (pokered scripts/Route23.asm). Each
-- guard/swimmer stands beside the staircase to Victory Road and, when
-- talked to, checks whether the player holds the badge matching that
-- step; the automatic walk-into-the-guard blocking script
-- (Route23DefaultScript / Route23CheckForBadgeScript) is not ported
-- here -- only the talk-triggered text_asm bodies, which share that
-- same subroutine (Route23CheckForBadgeScript) and text
-- (Route23YouDontHaveTheBadgeYetText / Route23OhThatIsTheBadgeText,
-- pokered/text/Route23.asm _Route23YouDontHaveTheBadgeYetText /
-- _Route23OhThatIsTheBadgeText). Once shown the badge, pokered sets
-- EVENT_PASSED_<BADGE>_CHECK so the walk-through gate (when ported)
-- won't re-ask; we mirror that with set_flag for fidelity even though
-- no onStep gate currently reads it.
local M = {}
-- rows: check_item(badge) -> have it? show "Oh! That is the X!" and
-- set EVENT_PASSED_X_CHECK : show "You don't have the X yet!"
local function badgeGuard(badge, passFlag)
local subs = { RAM = badge }
return {
{ "check_item", badge }, -- 1
{ "jump_if_true", 5 }, -- 2
{ "show_text", "_Route23YouDontHaveTheBadgeYetText", subs }, -- 3
{ "jump", 7 }, -- 4 (end)
{ "show_text", "_Route23OhThatIsTheBadgeText", subs }, -- 5
{ "set_flag", passFlag }, -- 6
}
end
M.ROUTE_23 = {
talk = {
-- Route23Guard1Text: EventFlagBit ..., EVENT_PASSED_EARTHBADGE_CHECK
-- -> wWhichBadge = EARTHBADGE
TEXT_ROUTE23_GUARD1 = badgeGuard("EARTHBADGE", "EVENT_PASSED_EARTHBADGE_CHECK"),
-- Route23Guard2Text: EVENT_PASSED_VOLCANOBADGE_CHECK
TEXT_ROUTE23_GUARD2 = badgeGuard("VOLCANOBADGE", "EVENT_PASSED_VOLCANOBADGE_CHECK"),
-- Route23Guard3Text: EVENT_PASSED_RAINBOWBADGE_CHECK
TEXT_ROUTE23_GUARD3 = badgeGuard("RAINBOWBADGE", "EVENT_PASSED_RAINBOWBADGE_CHECK"),
-- Route23Guard4Text: EVENT_PASSED_THUNDERBADGE_CHECK
TEXT_ROUTE23_GUARD4 = badgeGuard("THUNDERBADGE", "EVENT_PASSED_THUNDERBADGE_CHECK"),
-- Route23Guard5Text: EVENT_PASSED_CASCADEBADGE_CHECK
TEXT_ROUTE23_GUARD5 = badgeGuard("CASCADEBADGE", "EVENT_PASSED_CASCADEBADGE_CHECK"),
-- Route23Swimmer1Text: EVENT_PASSED_MARSHBADGE_CHECK
TEXT_ROUTE23_SWIMMER1 = badgeGuard("MARSHBADGE", "EVENT_PASSED_MARSHBADGE_CHECK"),
-- Route23Swimmer2Text: EVENT_PASSED_SOULBADGE_CHECK
TEXT_ROUTE23_SWIMMER2 = badgeGuard("SOULBADGE", "EVENT_PASSED_SOULBADGE_CHECK"),
},
}
return M
@@ -0,0 +1,24 @@
-- Route 2 Trade House (pokered/scripts/Route2TradeHouse.asm)
--
-- TEXT_ROUTE2TRADEHOUSE_GAMEBOY_KID: text_asm sets wWhichTrade to
-- TRADE_FOR_MARCEL and calls the DoInGameTradeDialogue predef
-- (engine/events/in_game_trades.asm), i.e. the standard offer-then-trade
-- flow. TradeMons entry #2 (data/events/trades.asm) is
-- ABRA -> MR_MIME, nicknamed "MARCEL"; matches field.trades[2] in
-- data/generated/field.lua. The Scientist in this house is plain
-- flavor text (data/scripts/story.lua) -- the Game Boy Kid here owns
-- the trade, exactly as in Route2TradeHouse.asm.
-- EVENT_TRADED_ABRA_FOR_MR_MIME is the port's name for this trade's
-- wCompletedInGameTradeFlags bit (Commands.trade checks it before the
-- offer and sets it on completion).
return {
ROUTE_2_TRADE_HOUSE = {
talk = {
TEXT_ROUTE2TRADEHOUSE_GAMEBOY_KID = {
{ "face_player" },
{ "trade", 2, "EVENT_TRADED_ABRA_FOR_MR_MIME" }, -- MARCEL
},
},
},
}
+23
View File
@@ -0,0 +1,23 @@
-- pokered/scripts/SafariZoneGate.asm SafariZoneGateSafariZoneWorker2Text:
-- the second worker at the gate always asks "Is it your first time
-- here?"; YES prints the SAFARI ZONE rules, NO prints "Sorry, you're a
-- regular here!". No event flag is checked or set -- the answer is
-- purely session (it's a fresh YesNoChoice every time you talk to him).
--
-- worker1's talk/onStep/onEnter handling already lives in
-- data/scripts/safari.lua; this file only adds worker2's flavor text.
return {
SAFARI_ZONE_GATE = {
talk = {
TEXT_SAFARIZONEGATE_SAFARI_ZONE_WORKER2 = {
{"face_player"},
{"ask", "_SafariZoneGateSafariZoneWorker2FirstTimeHereText"},
{"jump_if_true", 6},
{"show_text", "_SafariZoneGateSafariZoneWorker2YoureARegularHereText"},
{"jump", 7},
{"show_text", "_SafariZoneGateSafariZoneWorker2SafariZoneExplanationText"},
},
},
},
}
@@ -0,0 +1,18 @@
-- SaffronPidgeyHouse (pokered/scripts/SaffronPidgeyHouse.asm)
--
-- TEXT_SAFFRONPIDGEYHOUSE_PIDGEY: SaffronPidgeyHousePidgeyText plays the
-- PIDGEY cry after showing its "Kurukkoo!" line (text_asm: ld a, PIDGEY /
-- call PlayCry / jp TextScriptEnd). The command vocabulary has no cry/SFX
-- command, so only the flavor text is ported here; the cry itself has no
-- equivalent to hook into.
return {
SAFFRON_PIDGEY_HOUSE = {
talk = {
TEXT_SAFFRONPIDGEYHOUSE_PIDGEY = {
{ "face_player" },
{ "show_text", "_SaffronPidgeyHousePidgeyText" },
},
},
},
}
@@ -0,0 +1,29 @@
-- Articuno (scripts/SeafoamIslandsB4F.asm SeafoamIslandsB4FArticunoText +
-- home/trainers.asm TalkToTrainer; ARTICUNO 50 from
-- data/maps/objects/SeafoamIslandsB4F.asm).
--
-- The text_asm loads ArticunoTrainerHeader and calls TalkToTrainer:
-- SeafoamIslandsB4FArticunoBattleText is text_far "Gyaoo!" + text_asm
-- PlayCry ARTICUNO + WaitForSoundToFinish, then the wild battle starts --
-- or, when EVENT_BEAT_ARTICUNO is already set, the (identical)
-- after-battle text prints and nothing else happens. The script also
-- switches to SCRIPT_SEAFOAMISLANDSB4F_OBJECT_MOVING3, which just routes
-- the battle end through EndTrainerBattle (set EVENT_BEAT_ARTICUNO + hide
-- the object on any non-blackout result) -- static_battle covers that.
-- The B4F current/boulder puzzle itself is data-driven via field.seafoam.
local M = {}
M.SEAFOAM_ISLANDS_B4F = {
talk = {
TEXT_SEAFOAMISLANDSB4F_ARTICUNO = {
{ "play_cry", "ARTICUNO" }, -- 1 text_asm PlayCry
{ "show_text", "_SeafoamIslandsB4FArticunoBattleText" }, -- 2 "Gyaoo!"
{ "check_flag", "EVENT_BEAT_ARTICUNO" }, -- 3
{ "jump_if_true", 6 }, -- 4 already beaten: text only
{ "static_battle", "ARTICUNO", 50, "EVENT_BEAT_ARTICUNO" }, -- 5
},
},
}
return M
+22
View File
@@ -0,0 +1,22 @@
-- Silph Co. 10F flavor dialogue.
-- Registered standalone (not via init.lua by this agent); returns the
-- talk table for SILPH_CO_10F.
return {
SILPH_CO_10F = {
talk = {
-- pokered/scripts/SilphCo10F.asm SilphCo10FSilphWorkerFText:
-- CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI branches between the
-- "I'm scared" line (Giovanni not yet beaten) and the "please
-- keep quiet about my crying" line (after Giovanni is beaten).
TEXT_SILPHCO10F_SILPH_WORKER_F = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 5 },
{ "show_text", "_SilphCo10FSilphWorkerFImScaredText" },
{ "jump", 6 },
{ "show_text", "_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
},
},
},
}
+20
View File
@@ -0,0 +1,20 @@
-- Flavor talk scripts for Silph Co. 3F.
-- Source: pokered/scripts/SilphCo3F.asm, pokered/text/SilphCo3F.asm
return {
SILPH_CO_3F = {
talk = {
-- SilphCo3FSilphWorkerMText (scripts/SilphCo3F.asm):
-- CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI ->
-- set: _SilphCo3FSilphWorkerMYouSavedUsText
-- not set: _SilphCo3FSilphWorkerMWhatShouldIDoText
TEXT_SILPHCO3F_SILPH_WORKER_M = {
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 4 },
{ "show_text", "_SilphCo3FSilphWorkerMWhatShouldIDoText" },
{ "jump", 5 },
{ "show_text", "_SilphCo3FSilphWorkerMYouSavedUsText" },
},
},
},
}
+17
View File
@@ -0,0 +1,17 @@
-- pokered/scripts/SilphCo4F.asm: SilphCo4FSilphWorkerMText
-- Uses SilphCo6FBeatGiovanniPrintDEOrPrintHLScript: if EVENT_BEAT_SILPH_CO_GIOVANNI
-- is set, show the "Team Rocket is gone?" line; otherwise show the "hiding" line.
return {
SILPH_CO_4F = {
talk = {
TEXT_SILPHCO4F_SILPH_WORKER_M = {
{"face_player"},
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
{"jump_if_true", 5},
{"show_text", "_SilphCo4FSilphWorkerMImHidingText"},
{"jump", 6},
{"show_text", "_SilphCo4FSilphWorkerMTeamRocketIsGoneText"},
},
},
},
}
+17
View File
@@ -0,0 +1,17 @@
-- pokered/scripts/SilphCo5F.asm: SilphCo5FSilphWorkerMText
-- Uses SilphCo6FBeatGiovanniPrintDEOrPrintHLScript: if EVENT_BEAT_SILPH_CO_GIOVANNI
-- is set, show the "You're our hero" line; otherwise show the "That's you right?" line.
return {
SILPH_CO_5F = {
talk = {
TEXT_SILPHCO5F_SILPH_WORKER_M = {
{"face_player"},
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
{"jump_if_true", 5},
{"show_text", "_SilphCo5FSilphWorkerMThatsYouRightText"},
{"jump", 6},
{"show_text", "_SilphCo5FSilphWorkerMYoureOurHeroText"},
},
},
},
}
+61
View File
@@ -0,0 +1,61 @@
-- Silph Co. 6F worker flavor dialogue.
-- pokered/scripts/SilphCo6F.asm: SilphCo6FSilphWorkerM1Text/M2Text/M3Text/
-- F1Text/F2Text all funnel through SilphCo6FBeatGiovanniPrintDEOrPrintHLScript,
-- which checks EVENT_BEAT_SILPH_CO_GIOVANNI and prints the "before" text (hl)
-- if not yet set, or the "after" text (de) once Giovanni has been beaten.
return {
SILPH_CO_6F = {
talk = {
-- SilphCo6FSilphWorkerM1Text
TEXT_SILPHCO6F_SILPH_WORKER_M1 = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 5 },
{ "show_text", "_SilphCo6FSilphWorkerM1TookOverTheBuildingText" },
{ "jump", 6 },
{ "show_text", "_SilphCo6FSilphWorkerM1BackToWorkText" },
},
-- SilphCo6FSilphWorkerM2Text
TEXT_SILPHCO6F_SILPH_WORKER_M2 = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 5 },
{ "show_text", "_SilphCo6FSilphWorkerMHelpMePleaseText" },
{ "jump", 6 },
{ "show_text", "_SilphCo6FSilphWorkerMWeGotEngagedText" },
},
-- SilphCo6FSilphWorkerF1Text
TEXT_SILPHCO6F_SILPH_WORKER_F1 = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 5 },
{ "show_text", "_SilphCo6FSilphWorkerF1SuchACowardText" },
{ "jump", 6 },
{ "show_text", "_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
},
-- SilphCo6FSilphWorkerF2Text
TEXT_SILPHCO6F_SILPH_WORKER_F2 = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 5 },
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText" },
{ "jump", 6 },
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketRanText" },
},
-- SilphCo6FSilphWorkerM3Text
TEXT_SILPHCO6F_SILPH_WORKER_M3 = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 5 },
{ "show_text", "_SilphCo6FSilphWorkerM3TargetedSilphText" },
{ "jump", 6 },
{ "show_text", "_SilphCo6FSilphWorkerM3WorkForSilphText" },
},
},
},
}
+44
View File
@@ -0,0 +1,44 @@
-- Flavor talk scripts for Silph Co. 7F.
-- Source: pokered/scripts/SilphCo7F.asm, pokered/text/SilphCo7F.asm
return {
SILPH_CO_7F = {
talk = {
-- SilphCo7FSilphWorkerM2Text (scripts/SilphCo7F.asm):
-- CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI ->
-- not set: _SilphCo7FSilphWorkerM2AfterTheMasterBallText
-- set: _SilphCo7FSilphWorkerM2CancelledMasterBallText
TEXT_SILPHCO7F_SILPH_WORKER_M2 = {
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 4 },
{ "show_text", "_SilphCo7FSilphWorkerM2AfterTheMasterBallText" },
{ "jump", 5 },
{ "show_text", "_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
},
-- SilphCo7FSilphWorkerM3Text (scripts/SilphCo7F.asm):
-- CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI ->
-- not set: _SilphCo7FSilphWorkerM3ItWouldBeBadText
-- set: _SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText
TEXT_SILPHCO7F_SILPH_WORKER_M3 = {
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 4 },
{ "show_text", "_SilphCo7FSilphWorkerM3ItWouldBeBadText" },
{ "jump", 5 },
{ "show_text", "_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
},
-- SilphCo7FSilphWorkerM4Text (scripts/SilphCo7F.asm):
-- CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI ->
-- not set: _SilphCo7FSilphWorkerM4ItsReallyDangerousHereText
-- set: _SilphCo7FSilphWorkerM4SafeAtLastText
TEXT_SILPHCO7F_SILPH_WORKER_M4 = {
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 4 },
{ "show_text", "_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText" },
{ "jump", 5 },
{ "show_text", "_SilphCo7FSilphWorkerM4SafeAtLastText" },
},
},
},
}
+20
View File
@@ -0,0 +1,20 @@
-- Flavor talk scripts for Silph Co. 8F.
-- Source: pokered/scripts/SilphCo8F.asm, pokered/text/SilphCo8F.asm
return {
SILPH_CO_8F = {
talk = {
-- SilphCo8FSilphWorkerMText (scripts/SilphCo8F.asm):
-- CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI ->
-- not set: _SilphCo8FSilphWorkerMSilphIsFinishedText
-- set: _SilphCo8FSilphWorkerMThanksForSavingUsText
TEXT_SILPHCO8F_SILPH_WORKER_M = {
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 4 },
{ "show_text", "_SilphCo8FSilphWorkerMSilphIsFinishedText" },
{ "jump", 5 },
{ "show_text", "_SilphCo8FSilphWorkerMThanksForSavingUsText" },
},
},
},
}
+24
View File
@@ -0,0 +1,24 @@
-- Silph Co. 9F (registry id: SILPH_CO_9F)
-- Source: pokered/scripts/SilphCo9F.asm, pokered/text/SilphCo9F.asm
return {
SILPH_CO_9F = {
talk = {
-- SilphCo9FNurseText (pokered/scripts/SilphCo9F.asm):
-- before EVENT_BEAT_SILPH_CO_GIOVANNI: heals the party and shows
-- "You look tired..." then "Don't give up!"; after the event, just
-- says thanks. Nurse texts are not in data/generated/text.lua, so
-- the exact pokered/text/SilphCo9F.asm strings are used as literals.
TEXT_SILPHCO9F_NURSE = {
{ "face_player" },
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
{ "jump_if_true", 8 },
{ "show_text", "You look tired!\nYou should take a\nquick nap!" },
{ "heal_party" },
{ "show_text", "Don't give up!" },
{ "jump", 9 },
{ "show_text", "Thank you so\nmuch!" },
},
},
},
}
+12
View File
@@ -0,0 +1,12 @@
-- pokered/scripts/SSAnne1FRooms.asm: SSAnne1FRoomsWigglytuffText
-- text_far _SSAnne1FRoomsWigglytuffText; then ld a, WIGGLYTUFF / call PlayCry (cosmetic cry sound, not ported)
return {
SS_ANNE_1F_ROOMS = {
talk = {
TEXT_SSANNE1FROOMS_WIGGLYTUFF = {
{"face_player"},
{"show_text", "_SSAnne1FRoomsWigglytuffText"},
},
},
},
}
+62
View File
@@ -0,0 +1,62 @@
-- SS Anne, 2F rooms (pokered/scripts/SSAnne2FRooms.asm)
--
-- All ported constants below are plain text_asm bodies: PrintText of a
-- single text_far, no CheckEvent branching, no YES/NO menu. The
-- trainers (Gentleman1/2, Fisher, CooltrainerF) and the PickUpItem
-- rows (MAX_ETHER, RARE_CANDY) are skipped -- trainers are handled by
-- CheckFightingMapTrainers/TalkToTrainer via the existing trainer
-- system, and the item pickups carry no talk text of their own here.
--
-- TEXT_SSANNE2FROOMS_GENTLEMAN3 also opens the POKéDEX entry for
-- SNORLAX after the text box (DisplayPokedex in the original), which
-- has no equivalent talk-script hook in this port (DexEntryMenu has
-- no done-callback the way other pushed UI states do) -- only the
-- flavor line is ported here.
return {
SS_ANNE_2F_ROOMS = {
talk = {
-- SSAnne2FRoomsGentleman3Text: PrintText(_SSAnne2FRoomsGentleman3Text)
-- (then DisplayPokedex SNORLAX -- not ported, see note above)
TEXT_SSANNE2FROOMS_GENTLEMAN3 = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsGentleman3Text" },
},
-- SSAnne2FRoomsGentleman4Text: PrintText(_SSAnne2FRoomsGentleman4Text)
TEXT_SSANNE2FROOMS_GENTLEMAN4 = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsGentleman4Text" },
},
-- SSAnne2FRoomsGentleman5Text: PrintText(_SSAnne2FRoomsGentleman5Text)
TEXT_SSANNE2FROOMS_GENTLEMAN5 = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsGentleman5Text" },
},
-- SSAnne2FRoomsGrampsText: PrintText(_SSAnne2FRoomsGrampsText)
TEXT_SSANNE2FROOMS_GRAMPS = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsGrampsText" },
},
-- SSAnne2FRoomsLittleBoyText: PrintText(_SSAnne2FRoomsLittleBoyText)
TEXT_SSANNE2FROOMS_LITTLE_BOY = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsLittleBoyText" },
},
-- SSAnne2FRoomsBrunetteGirlText: PrintText(_SSAnne2FRoomsBrunetteGirlText)
TEXT_SSANNE2FROOMS_BRUNETTE_GIRL = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsBrunetteGirlText" },
},
-- SSAnne2FRoomsBeautyText: PrintText(_SSAnne2FRoomsBeautyText)
TEXT_SSANNE2FROOMS_BEAUTY = {
{ "face_player" },
{ "show_text", "_SSAnne2FRoomsBeautyText" },
},
},
},
}
+14
View File
@@ -0,0 +1,14 @@
-- pokered/scripts/SSAnneB1FRooms.asm: SSAnneB1FRoomsMachokeText
-- text_far _SSAnneB1FRoomsMachokeText, then `ld a, MACHOKE / call PlayCry`
-- (cry playback has no equivalent Commands.lua verb in this port, so only
-- the flavor text is ported).
return {
SS_ANNE_B1F_ROOMS = {
talk = {
TEXT_SSANNEB1FROOMS_MACHOKE = {
{ "face_player" },
{ "show_text", "_SSAnneB1FRoomsMachokeText" },
},
},
},
}
+43
View File
@@ -0,0 +1,43 @@
-- SS Anne kitchen cook flavor dialogue.
-- pokered/scripts/SSAnneKitchen.asm SSAnneKitchenCook7Text
local function push(game, s, done)
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, s, done))
end
return {
SS_ANNE_KITCHEN = {
talk = {
-- pokered/scripts/SSAnneKitchen.asm SSAnneKitchenCook7Text (text_asm):
-- always shows the "main course is" lead-in
-- (_SSAnneKitchenCook7MainCourseIsText), then rolls hRandomAdd to
-- pick the dish: bit 7 set (~50%) -> Salmon du Salad, else bit 4
-- set (~25%) -> Eels au Barbecue, else (~25%) -> Prime Beef Steak.
-- The three dish texts (SSAnneKitchenCook7SalmonDuSaladText /
-- ...EelsAuBarbecueText / ...PrimeBeefSteakText) aren't extracted
-- into data/generated/text.lua (no leading underscore in
-- pokered/text/SSAnneKitchen.asm), so their literal strings are
-- ported here verbatim.
TEXT_SSANNEKITCHEN_COOK7 = function(game, ow, npc, done)
local t = game.data.text
push(game, t._SSAnneKitchenCook7MainCourseIsText
or "Er-hem! Indeed I\nam le CHEF!\fLe main course is", function()
local roll = math.random(1, 4)
local dish
if roll <= 2 then
-- bit 7 of hRandomAdd set (~50%)
dish = "Salmon du Salad!\fLes guests may\ngripe it's fish\vagain, however!"
elseif roll == 3 then
-- bit 4 set, bit 7 clear (~25%)
dish = "Eels au Barbecue!\fLes guests will\nmutiny, I fear."
else
-- neither bit set (~25%)
dish = "Prime Beef Steak!\fBut, have I enough\nfillets du beef?"
end
push(game, dish, done)
end)
end,
},
},
}
+28
View File
@@ -0,0 +1,28 @@
-- Hand-ported text_asm dialogue for VermilionCity NPCs.
-- pokered/scripts/VermilionCity.asm
return {
VERMILION_CITY = {
talk = {
-- VermilionCityGambler1Text: before the S.S. Anne departs he asks
-- if you saw it moored in the harbor; after it leaves he remarks
-- that it's gone and will return in about a year.
-- (pokered/scripts/VermilionCity.asm)
TEXT_VERMILIONCITY_GAMBLER1 = {
{ "check_flag", "EVENT_SS_ANNE_LEFT" }, -- 1
{ "jump_if_true", 5 }, -- 2
{ "show_text", "_VermilionCityGambler1DidYouSeeText" }, -- 3
{ "jump", 6 }, -- 4
{ "show_text", "_VermilionCityGambler1SSAnneDepartedText" }, -- 5
},
-- VermilionCityMachopText: cries out, then follows up with a
-- second line about stomping the land flat.
-- (pokered/scripts/VermilionCity.asm)
TEXT_VERMILIONCITY_MACHOP = {
{ "show_text", "_VermilionCityMachopText" }, -- 1
{ "show_text", "_VermilionCityMachopStompingTheLandFlatText" }, -- 2
},
},
},
}
@@ -0,0 +1,16 @@
-- pokered/scripts/VermilionPidgeyHouse.asm: VermilionPidgeyHousePidgeyText
-- text_far _VermilionPidgeyHousePidgeyText, then text_asm plays the PIDGEY
-- cry (ld a, PIDGEY / call PlayCry / call WaitForSoundToFinish) before
-- TextScriptEnd. This port has no cry-playback command, so only the
-- flavor line is ported.
return {
VERMILION_PIDGEY_HOUSE = {
talk = {
TEXT_VERMILIONPIDGEYHOUSE_PIDGEY = {
{"face_player"},
{"show_text", "_VermilionPidgeyHousePidgeyText"},
},
},
},
}
+27
View File
@@ -0,0 +1,27 @@
-- Moltres (scripts/VictoryRoad2F.asm VictoryRoad2FMoltresText +
-- home/trainers.asm TalkToTrainer; MOLTRES 50 from
-- data/maps/objects/VictoryRoad2F.asm).
--
-- The text_asm loads MoltresTrainerHeader and calls TalkToTrainer:
-- VictoryRoad2FMoltresBattleText is text_far "Gyaoo!" + text_asm
-- PlayCry MOLTRES + WaitForSoundToFinish, then the wild battle starts --
-- or, when EVENT_BEAT_MOLTRES is already set, the (identical)
-- after-battle text prints and nothing else happens. EndTrainerBattle
-- sets EVENT_BEAT_MOLTRES and hides the object on any non-blackout
-- result (win, catch or flee) -- static_battle mirrors that.
local M = {}
M.VICTORY_ROAD_2F = {
talk = {
TEXT_VICTORYROAD2F_MOLTRES = {
{ "play_cry", "MOLTRES" }, -- 1 text_asm PlayCry
{ "show_text", "_VictoryRoad2FMoltresBattleText" }, -- 2 "Gyaoo!"
{ "check_flag", "EVENT_BEAT_MOLTRES" }, -- 3
{ "jump_if_true", 6 }, -- 4 already beaten: text only
{ "static_battle", "MOLTRES", 50, "EVENT_BEAT_MOLTRES" }, -- 5
},
},
}
return M
+115
View File
@@ -0,0 +1,115 @@
-- Viridian City flavor dialogue (pokered/scripts/ViridianCity.asm).
-- Ports the text_asm bodies for GAMBLER1, YOUNGSTER2, GIRL and OLD_MAN.
--
-- Not ported here (already handled elsewhere / not talk-reachable):
-- * TEXT_VIRIDIANCITY_FISHER (TM42 gift) -- already ported as a
-- `gift()` entry in data/scripts/story5.lua's M.VIRIDIAN_CITY.talk.
-- * TEXT_VIRIDIANCITY_OLD_MAN_SLEEPY / TEXT_VIRIDIANCITY_GYM_LOCKED --
-- these are step-triggered blocking texts (ViridianCityCheckGotPokedexScript /
-- ViridianCityCheckGymOpenScript), not npc talk text_asm bodies; the
-- gates themselves are already implemented via story5.lua's onStep
-- chain (viridianOldManStep / viridianGymLock) for this map.
-- * The old man's catch-training minigame trigger (SCRIPT_VIRIDIANCITY_
-- OLD_MAN_START_CATCH_TRAINING / battle vs. WEEDLE) is a full
-- scripted-battle cutscene outside this task's Commands vocabulary
-- (no static_battle-style "battle a scripted old-man WEEDLE" command
-- exists); we port the real YES/NO branch text he speaks but the
-- "yes" branch here just shows the "I'll show you how" line rather
-- than actually starting the minigame, since that machinery isn't
-- ported to this map yet.
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
M.VIRIDIAN_CITY = {
talk = {
-- ViridianCityGambler1Text (scripts/ViridianCity.asm): normally
-- comments that the gym is "always closed"; once the 7th badge is
-- earned (badges == ~EARTHBADGE) but Giovanni hasn't been beaten
-- yet, he instead says the gym leader returned.
TEXT_VIRIDIANCITY_GAMBLER1 = function(game, ow, npc, done)
local t = text(game)
local sevenBadges = game.save.inventory and
game.save.inventory.BOULDERBADGE and game.save.inventory.CASCADEBADGE
and game.save.inventory.THUNDERBADGE and game.save.inventory.RAINBOWBADGE
and game.save.inventory.SOULBADGE and game.save.inventory.MARSHBADGE
and game.save.inventory.VOLCANOBADGE
-- (pokered checks EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI; the port's flag for
-- that win is EVENT_BEAT_GIOVANNI, set by victories.lua OPP_GIOVANNI#3)
if sevenBadges and not (game.save.flags and game.save.flags.EVENT_BEAT_GIOVANNI) then
push(game, t._ViridianCityGambler1GymLeaderReturnedText
or "VIRIDIAN GYM's\nLEADER returned!", done)
else
push(game, t._ViridianCityGambler1GymAlwaysClosedText
or "This POKéMON GYM\nis always closed.\nI wonder who the\nLEADER is?", done)
end
end,
-- ViridianCityYoungster2Text (scripts/ViridianCity.asm): asks if
-- you want to know about the two kinds of caterpillar Pokemon;
-- YES -> CATERPIE/WEEDLE description, NO -> "Oh, OK then!".
-- ViridianCityYoungster2OkThenText and
-- ViridianCityYoungster2CaterpieAndWeedleDescriptionText are
-- defined without a leading underscore in pokered/text/ViridianCity.asm
-- and aren't present in data/generated/text.lua, so we fall back to
-- the literal strings from pokered.
TEXT_VIRIDIANCITY_YOUNGSTER2 = function(game, ow, npc, done)
local t = text(game)
ask(game, t._ViridianCityYoungster2YouWantToKnowAboutText
or "You want to know\nabout the 2 kinds\nof caterpillar\nPOKéMON?", function(yes)
if yes then
push(game, "CATERPIE has no\npoison, but\nWEEDLE does.\n\nWatch out for its\nPOISON STING!", done)
else
push(game, "Oh, OK then!", done)
end
end)
end,
-- ViridianCityGirlText (scripts/ViridianCity.asm): before the
-- player has the Pokedex she scolds her grandpa for being mean
-- (he hasn't had his coffee yet); after EVENT_GOT_POKEDEX she talks
-- about the winding trail through Viridian Forest to Pewter.
TEXT_VIRIDIANCITY_GIRL = function(game, ow, npc, done)
local t = text(game)
if game.save.flags and game.save.flags.EVENT_GOT_POKEDEX then
push(game, t._ViridianCityGirlWhenIGoShopText
or "When I go shop in\nPEWTER CITY, I\nhave to take the\nwinding trail in\nVIRIDIAN FOREST.", done)
else
push(game, t._ViridianCityGirlHasntHadHisCoffeeYetText
or "Oh Grandpa! Don't\nbe so mean!\nHe hasn't had his\ncoffee yet.", done)
end
end,
-- ViridianCityOldManText (scripts/ViridianCity.asm): once he's had
-- his coffee, he asks (YES/NO) whether you want to learn how to
-- catch Pokemon. YES leads into the catch-training minigame
-- (SCRIPT_VIRIDIANCITY_OLD_MAN_START_CATCH_TRAINING, not ported --
-- see file header); NO just brushes you off ("Time is money...").
TEXT_VIRIDIANCITY_OLD_MAN = function(game, ow, npc, done)
local t = text(game)
ask(game, t._ViridianCityOldManHadMyCoffeeNowText
or "Ahh, I've had my\ncoffee now and I\nfeel great!\nSure you can go\nthrough!\nAre you in a\nhurry?", function(yes)
if yes then
push(game, t._ViridianCityOldManKnowHowToCatchPokemonText
or "I see you're using\na POKéDEX.\nWhen you catch a\nPOKéMON, POKéDEX\nis automatically\nupdated.\nWhat? Don't you\nknow how to catch\nPOKéMON?\nI'll show you\nhow to then.", done)
else
push(game, t._ViridianCityOldManTimeIsMoneyText
or "Time is money...\nGo along then.", done)
end
end)
end,
},
}
return M
@@ -0,0 +1,15 @@
-- Viridian Nickname House (pokered/scripts/ViridianNicknameHouse.asm).
return {
VIRIDIAN_NICKNAME_HOUSE = {
talk = {
-- ViridianNicknameHouseSpearowText: text_asm that shows the text
-- then plays a SPEAROW cry (PlayCry/WaitForSoundToFinish). The
-- cry playback isn't in the talk-script command vocabulary, so
-- only the flavor line is ported here.
TEXT_VIRIDIANNICKNAMEHOUSE_SPEAROW = {
{ "show_text", "_ViridianNicknameHouseSpearowText" },
},
},
},
}
+20
View File
@@ -0,0 +1,20 @@
-- pokered/scripts/WardensHouse.asm: WardensHouseDisplayText branches on
-- hTextID (TEXT_WARDENSHOUSE_DISPLAY_LEFT vs _RIGHT) to pick which of the
-- two display case texts to print. TEXT_WARDENSHOUSE_WARDEN is already
-- ported in data/scripts/story.lua (M.WARDENS_HOUSE); not re-ported here.
return {
WARDENS_HOUSE = {
talk = {
-- left case: photos and fossils
TEXT_WARDENSHOUSE_DISPLAY_LEFT = {
{ "face_player" }, -- 1
{ "show_text", "_WardensHouseDisplayPhotosAndFossilsText" }, -- 2
},
-- right case: old pokemon merchandise
TEXT_WARDENSHOUSE_DISPLAY_RIGHT = {
{ "face_player" }, -- 1
{ "show_text", "_WardensHouseDisplayMerchandiseText" }, -- 2
},
},
},
}
+75
View File
@@ -0,0 +1,75 @@
-- Auto-generated index of the ported text_asm flavor talk scripts
-- (Workstream A backlog). Each data/scripts/flavor/<map>.lua returns
-- { MAP_ID = { talk = {...} } } for one map; this combines them into a
-- single table that data/scripts/init.lua merges into the registry
-- (talk tables merge per TEXT constant, like the story files).
local files = {
"data.scripts.flavor.bike_shop",
"data.scripts.flavor.celadon_city",
"data.scripts.flavor.celadon_mansion_1f",
"data.scripts.flavor.celadon_mansion_3f",
"data.scripts.flavor.cerulean_badge_house",
"data.scripts.flavor.cerulean_cave_b1f",
"data.scripts.flavor.cerulean_city",
"data.scripts.flavor.cerulean_trade_house",
"data.scripts.flavor.cerulean_trashed_house",
"data.scripts.flavor.copycats_house_1f",
"data.scripts.flavor.copycats_house_2f",
"data.scripts.flavor.game_corner",
"data.scripts.flavor.lavender_cubone_house",
"data.scripts.flavor.lavender_mart",
"data.scripts.flavor.lavender_town",
"data.scripts.flavor.mr_fujis_house",
"data.scripts.flavor.museum_1f",
"data.scripts.flavor.oaks_lab",
"data.scripts.flavor.pewter_city",
"data.scripts.flavor.pewter_mart",
"data.scripts.flavor.pewter_nidoran_house",
"data.scripts.flavor.pokemon_fan_club",
"data.scripts.flavor.power_plant",
"data.scripts.flavor.reds_house_1f",
"data.scripts.flavor.route11_gate_2f",
"data.scripts.flavor.route18_gate_2f",
"data.scripts.flavor.route_12_gate_2f",
"data.scripts.flavor.route_15_gate_2f",
"data.scripts.flavor.route_16_fly_house",
"data.scripts.flavor.route_16_gate_1f",
"data.scripts.flavor.route_16_gate_2f",
"data.scripts.flavor.route_18_gate_1f",
"data.scripts.flavor.route_22_gate",
"data.scripts.flavor.route_23",
"data.scripts.flavor.route_2_trade_house",
"data.scripts.flavor.safari_zone_gate",
"data.scripts.flavor.saffron_pidgey_house",
"data.scripts.flavor.seafoam_islands_b4f",
"data.scripts.flavor.silph_co_10f",
"data.scripts.flavor.silph_co_3f",
"data.scripts.flavor.silph_co_4f",
"data.scripts.flavor.silph_co_5f",
"data.scripts.flavor.silph_co_6f",
"data.scripts.flavor.silph_co_7f",
"data.scripts.flavor.silph_co_8f",
"data.scripts.flavor.silph_co_9f",
"data.scripts.flavor.ss_anne_1f_rooms",
"data.scripts.flavor.ss_anne_2f_rooms",
"data.scripts.flavor.ss_anne_b1f_rooms",
"data.scripts.flavor.ss_anne_kitchen",
"data.scripts.flavor.vermilion_city",
"data.scripts.flavor.vermilion_pidgey_house",
"data.scripts.flavor.victory_road_2f",
"data.scripts.flavor.viridian_city",
"data.scripts.flavor.viridian_nickname_house",
"data.scripts.flavor.wardens_house",
}
local M = {}
for _, f in ipairs(files) do
for mapId, mod in pairs(require(f)) do
if M[mapId] and M[mapId].talk and mod.talk then
for k, v in pairs(mod.talk) do M[mapId].talk[k] = v end
else
M[mapId] = mod
end
end
end
return M
+148
View File
@@ -0,0 +1,148 @@
-- Gym metadata for the statue hidden events (data/maps/badge_maps.asm
-- gives map -> badge; each gym's script carries its .CityName /
-- .LeaderName strings, e.g. scripts/VermilionGym.asm
-- LoadGymLeaderAndCityName).
--
-- The module is also merged into the map-script registry (see
-- data/scripts/init.lua), so gym maps can carry hand-ported `talk`
-- scripts alongside the statue metadata.
local M = {
PEWTER_GYM = { city = "PEWTER CITY", leader = "BROCK", badge = "BOULDERBADGE" },
CERULEAN_GYM = { city = "CERULEAN CITY", leader = "MISTY", badge = "CASCADEBADGE" },
VERMILION_GYM = { city = "VERMILION CITY", leader = "LT.SURGE", badge = "THUNDERBADGE" },
CELADON_GYM = { city = "CELADON CITY", leader = "ERIKA", badge = "RAINBOWBADGE" },
FUCHSIA_GYM = { city = "FUCHSIA CITY", leader = "KOGA", badge = "SOULBADGE" },
SAFFRON_GYM = { city = "SAFFRON CITY", leader = "SABRINA", badge = "MARSHBADGE" },
CINNABAR_GYM = { city = "CINNABAR ISLAND", leader = "BLAINE", badge = "VOLCANOBADGE" },
VIRIDIAN_GYM = { city = "VIRIDIAN CITY", leader = "GIOVANNI", badge = "EARTHBADGE" },
}
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
-- _PewterGymBrockPreBattleText and engages the leader battle
-- (engageTrainer shows that same pre-battle text via resolveText; the
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
-- the GYM in CERULEAN..."). The original's middle branch (beat but
-- TM34 not yet handed over, CheckEventReuseA EVENT_GOT_TM34) is
-- unreachable in the port: the TM is granted with the victory.
M.PEWTER_GYM.talk = {
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
if game.save.flags.EVENT_BEAT_BROCK then
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text._PewterGymBrockPostBattleAdviceText
or "Go to the GYM in\nCERULEAN and test\nyour abilities!", done))
else
ow:engageTrainer(npc, done)
end
end,
}
-- The other leaders' text_asm bodies all follow Brock's shape
-- (scripts/CeruleanGym.asm CeruleanGymMistyText ... scripts/ViridianGym.asm
-- ViridianGymGiovanniText): CheckEvent EVENT_BEAT_<LEADER> -- before the
-- badge print the pre-battle text and engage the leader battle
-- (engageTrainer shows that same pre-battle text via resolveText; the
-- badge/TM rewards and the beat flag come from data/scripts/victories.lua)
-- -- and once beaten print the post-battle advice text. As with Brock,
-- the originals' middle branch (beaten but the TM not yet handed over,
-- CheckEventReuseA EVENT_GOT_TM*) is unreachable in the port: the TM is
-- granted with the victory.
-- afterAdvice, when given, takes over `done`: it is handed (game, ow, npc,
-- done) and must call done() itself once whatever it's doing (e.g. a fade
-- around a HideObject) finishes, rather than having it invoked
-- automatically. Only Giovanni's farewell uses this.
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice)
return function(game, ow, npc, done)
if game.save.flags[beatFlag] then
local TextBox = require("src.render.TextBox")
local finish = done
if afterAdvice then
finish = function()
afterAdvice(game, ow, npc, done)
end
end
game.stack:push(TextBox.new(game,
game.data.text[adviceLabel] or fallback, finish))
else
ow:engageTrainer(npc, done)
end
end
end
-- scripts/CeruleanGym.asm CeruleanGymMistyText .afterBeat: Misty has no
-- separate advice label -- her repeat dialogue is the TM11 explanation
-- (.TM11ExplanationText).
M.CERULEAN_GYM.talk = {
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
"_CeruleanGymMistyTM11ExplanationText",
"TM11 teaches\nBUBBLEBEAM!"),
}
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
M.VERMILION_GYM.talk = {
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
"_VermilionGymLTSurgePostBattleAdviceText",
"A little word of\nadvice, kid!"),
}
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
M.CELADON_GYM.talk = {
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
"_CeladonGymErikaPostBattleAdviceText",
"You are cataloging\nPOKéMON? I must\nsay I'm impressed."),
}
-- scripts/FuchsiaGym.asm FuchsiaGymKogaText .afterBeat
M.FUCHSIA_GYM.talk = {
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
"_FuchsiaGymKogaPostBattleAdviceText",
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!"),
}
-- scripts/SaffronGym.asm SaffronGymSabrinaText .afterBeat
M.SAFFRON_GYM.talk = {
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
"_SaffronGymSabrinaPostBattleAdviceText",
"Everyone has\npsychic power!\nPeople just don't\nrealize it!"),
}
-- scripts/CinnabarGym.asm CinnabarGymBlaineText .afterBeat
M.CINNABAR_GYM.talk = {
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
"_CinnabarGymBlainePostBattleAdviceText",
"FIRE BLAST is the\nultimate fire\ntechnique!"),
}
-- scripts/ViridianGym.asm ViridianGymGiovanniText .afterBeat: after the
-- farewell speech Giovanni leaves for good -- the original fades to
-- black (GBFadeOutToBlack), HideObject TOGGLE_VIRIDIAN_GYM_GIOVANNI while
-- the screen is black, then fades back in (GBFadeInFromBlack). The port
-- reuses src/render/Transition.lua (the same fade-out/callback/fade-in
-- primitive warps and PartyMenu field moves push) so HideObject fires at
-- its onMidpoint, between the two fades, instead of as a bare disappearance
-- when the text box closes. The objectToggles entry persists in the save,
-- so he stays gone on re-entry. pokered's beat flag is
-- EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI; the port's equivalent set on winning
-- that battle is EVENT_BEAT_GIOVANNI (data/scripts/victories.lua
-- OPP_GIOVANNI#3).
M.VIRIDIAN_GYM.talk = {
TEXT_VIRIDIANGYM_GIOVANNI = leaderTalk("EVENT_BEAT_GIOVANNI",
"_ViridianGymGiovanniPostBattleAdviceText",
"Let us meet again\nsome day!\nFarewell!",
function(game, ow, npc, done)
local Transition = require("src.render.Transition")
game.stack:push(Transition.new(game, function()
local ok, Commands = pcall(require, "src.script.Commands")
if ok and Commands.hide_object then
Commands.hide_object({ game = game, save = game.save, overworld = ow },
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
end
end, done))
end),
}
return M
+56
View File
@@ -0,0 +1,56 @@
-- Registry of hand-ported map scripts. Map-specific behavior lives HERE,
-- never in engine classes (Critical Engineering Rule #9).
--
-- Each module returns { talk = { [TEXT_CONST] = script }, onEnter = fn,
-- onBoulderMoved = fn } where a script is a list of { "command", args... }
-- rows executed by src/script/ScriptRunner.lua. Every hand-ported script
-- cites the pokered source it was ported from.
local registry = {
PALLET_TOWN = require("data.scripts.pallet_town"),
OAKS_LAB = require("data.scripts.oaks_lab"),
REDS_HOUSE_1F = require("data.scripts.reds_house"),
CELADON_MANSION_ROOF_HOUSE = require("data.scripts.celadon_eevee"),
}
-- story-critical scripts, one table per map. Later files MERGE into
-- earlier ones: talk tables merge per TEXT constant, other hooks
-- (onEnter, onVictory, ...) are replaced, so different files can each
-- add NPCs to the same map.
for _, file in ipairs({ "data.scripts.story", "data.scripts.story2",
"data.scripts.story3", "data.scripts.story4",
"data.scripts.story5", "data.scripts.story6",
"data.scripts.story7", "data.scripts.flavor_all",
"data.scripts.safari", "data.scripts.seafoam",
"data.scripts.gyms" }) do
for mapId, mod in pairs(require(file)) do
local existing = registry[mapId]
if not existing then
registry[mapId] = mod
else
for k, v in pairs(mod) do
if k == "talk" and existing.talk then
for textConst, script in pairs(v) do
existing.talk[textConst] = script
end
else
existing[k] = v
end
end
end
end
end
local M = {}
function M.get(mapId)
return registry[mapId]
end
-- script to run when the player talks to an object with this TEXT_ constant
function M.talkScript(mapId, textConst)
local mod = registry[mapId]
return mod and mod.talk and mod.talk[textConst] or nil
end
return M
+185
View File
@@ -0,0 +1,185 @@
-- Hand-ported from pret/pokered scripts/OaksLab.asm. All text is real
-- extracted text.
--
-- * Starter poke balls (objects 2-4): ask, give the real species, flag,
-- then the rival's counter-pick: he steps to the countering ball,
-- takes it ("I'll take this one, then!") and both balls disappear.
-- Source: scripts/OaksLab.asm OaksLabCharmanderPokeBallText /
-- OaksLabRivalTakePokeBallScript.
-- * Rival (object 1): before starter -> "gramps isn't around"; with
-- starter -> taunt + battle OPP_RIVAL1 with the counter-pick party
-- (player Bulbasaur -> rival Charmander etc., parties 1/2/3 =
-- Squirtle/Bulbasaur/Charmander in data/trainers/parties.asm);
-- afterwards he gloats or sulks and marches out of the lab
-- (OaksLabRivalBattleEndScript).
-- ball objects: CHARMANDER (6,3), SQUIRTLE (7,3), BULBASAUR (8,3);
-- rival = object 1 at (4,3). rivalBallX is the counter-pick's column.
local function starterBall(askText, species, choseFlag, ownBall,
rivalBallX, rivalBall)
return {
{ "check_flag", "EVENT_GOT_STARTER" }, -- 1
{ "jump_if_true", 19 }, -- 2
-- no picking until Oak has walked you in (OaksLabScript gating)
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
{ "jump_if_false", 19 }, -- 4
{ "ask", askText }, -- 5
{ "jump_if_false", 20 }, -- 6
{ "give_pokemon", species, 5 }, -- 7
{ "set_flag", "EVENT_GOT_STARTER" }, -- 8
{ "set_flag", choseFlag }, -- 9
-- POKé BALLs are not handed out here in the original -- Oak gives
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
-- player has beaten the Route 22 rival (see TEXT_OAKSLAB_OAK1 below)
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 10
{ "hide_object", "OAKS_LAB", ownBall }, -- 11
-- the rival walks to the countering ball (around the furniture)
{ "move_npc_to", 1, rivalBallX, 4 }, -- 12
{ "face_object", 1, "up" }, -- 13
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 14
{ "hide_object", "OAKS_LAB", rivalBall }, -- 15
{ "show_text", "_OaksLabRivalReceivedMonText",
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
or "BULBASAUR" } }, -- 16
{ "jump", 20 }, -- 17
{ "jump", 20 }, -- 18 (spacer)
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 19
}
end
return {
talk = {
-- Oak: accepts the parcel and hands over the Pokédex, then (once the
-- player has beaten the Route 22 rival) hands over the real POKé
-- BALLs (scripts/OaksLab.asm OaksLabOak1Text, simplified: no rival
-- recall / dex-rating / "pokemon can fight" / "around the world"
-- branches -- see docs/known-differences.md)
TEXT_OAKSLAB_OAK1 = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_OAKS_PARCEL" }, -- 2
{ "jump_if_false", 12 }, -- 3
{ "check_flag", "EVENT_OAK_GOT_PARCEL" }, -- 4
{ "jump_if_true", 12 }, -- 5
{ "show_text", "_OaksLabOak1DeliverParcelText" }, -- 6
{ "take_item", "OAKS_PARCEL", 1 }, -- 7
{ "set_flag", "EVENT_OAK_GOT_PARCEL" }, -- 8
{ "show_text", "_OaksLabOak1PokemonAroundTheWorldText" }, -- 9
{ "set_flag", "EVENT_GOT_POKEDEX" }, -- 10
{ "jump", 30 }, -- 11
{ "check_flag", "EVENT_GOT_STARTER" }, -- 12
{ "jump_if_false", 27 }, -- 13
{ "check_item", "POKE_BALL" }, -- 14
{ "jump_if_true", 25 }, -- 15
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" }, -- 16
{ "jump_if_false", 29 }, -- 17
{ "check_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" }, -- 18
{ "jump_if_true", 25 }, -- 19
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" }, -- 20
{ "give_item", "POKE_BALL", 5, false }, -- 21
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" }, -- 22
{ "show_text", "_OaksLabGivePokeballsExplanationText" }, -- 23
{ "jump", 30 }, -- 24
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" }, -- 25
{ "jump", 30 }, -- 26
{ "show_text", "_OaksLabOak1WhichPokemonDoYouWantText" }, -- 27
{ "jump", 30 }, -- 28
{ "show_text", "_OaksLabOak1RaiseYourYoungPokemonText" }, -- 29 (30 = end)
},
TEXT_OAKSLAB_CHARMANDER_POKE_BALL =
starterBall("_OaksLabYouWantCharmanderText", "CHARMANDER", "EVENT_CHOSE_CHARMANDER",
"OAKSLAB_CHARMANDER_POKE_BALL", 7, "OAKSLAB_SQUIRTLE_POKE_BALL"),
TEXT_OAKSLAB_SQUIRTLE_POKE_BALL =
starterBall("_OaksLabYouWantSquirtleText", "SQUIRTLE", "EVENT_CHOSE_SQUIRTLE",
"OAKSLAB_SQUIRTLE_POKE_BALL", 8, "OAKSLAB_BULBASAUR_POKE_BALL"),
TEXT_OAKSLAB_BULBASAUR_POKE_BALL =
starterBall("_OaksLabYouWantBulbasaurText", "BULBASAUR", "EVENT_CHOSE_BULBASAUR",
"OAKSLAB_BULBASAUR_POKE_BALL", 6, "OAKSLAB_CHARMANDER_POKE_BALL"),
TEXT_OAKSLAB_RIVAL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_STARTER" }, -- 2
{ "jump_if_false", 20 }, -- 3
{ "check_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" }, -- 4
{ "jump_if_true", 18 }, -- 5
{ "show_text", "_OaksLabRivalMyPokemonLooksStrongerText" }, -- 6
{ "check_flag", "EVENT_CHOSE_BULBASAUR" }, -- 7
{ "jump_if_false", 11 }, -- 8
{ "start_battle", "trainer", "OPP_RIVAL1", 3 }, -- 9 Charmander
{ "jump", 16 }, -- 10
{ "check_flag", "EVENT_CHOSE_SQUIRTLE" }, -- 11
{ "jump_if_false", 15 }, -- 12
{ "start_battle", "trainer", "OPP_RIVAL1", 2 }, -- 13 Bulbasaur
{ "jump", 16 }, -- 14
{ "start_battle", "trainer", "OPP_RIVAL1", 1 }, -- 15 Squirtle
{ "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" }, -- 16
{ "jump", 21 }, -- 17
{ "show_text", "_OaksLabRivalFedUpWithWaitingText" }, -- 18
{ "jump", 25 }, -- 19
{ "show_text", "_OaksLabRivalGrampsIsntAroundText" }, -- 20
-- battle aftermath: on a win the rival sulks and marches out
-- (OaksLabRivalBattleEnd); on a loss the blackout already warped
-- us away, so the script just ends
{ "jump_if_false", 25 }, -- 21
{ "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" }, -- 22
{ "move_npc_to", 1, 4, 11 }, -- 23
{ "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" }, -- 24 (25 = end)
},
},
-- Oak stops you leaving without a starter; the rival stops you on
-- the way out for the first battle (scripts/OaksLab.asm
-- OaksLabScript8 / OaksLabRivalChallenge)
onStep = function(game, ow, x, y)
local flags = game.save.flags
-- Oak blocks the exit mats (4,11)/(5,11) until you take a starter
if flags.EVENT_FOLLOWED_OAK_INTO_LAB and not flags.EVENT_GOT_STARTER
and y == 11 and (x == 4 or x == 5) then
ow.runner:run({
{ "show_text", "_OaksLabOakDontGoAwayYetText" },
{ "move_player", "up", 1 },
}, {})
return true
end
-- the challenge fires as soon as the player steps away from the
-- table (OaksLabRivalChallengesPlayerScript: wYCoord == 6)
if flags.EVENT_GOT_STARTER and not flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB
and y >= 6 then
local rival = ow:npcByIndex(1)
if not rival then return false end
local rows = {
{ "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1
}
-- the rival routes to a free cell beside the player
local target
for _, c in ipairs({ { x, y - 1 }, { x - 1, y }, { x + 1, y },
{ x, y + 1 } }) do
if ow.map:inBounds(c[1], c[2]) and ow.map:isWalkableCell(c[1], c[2]) then
target = c
break
end
end
if target then
table.insert(rows, { "move_npc_to", 1, target[1], target[2] })
end
table.insert(rows, { "face_object", 1,
target and target[2] < y and "down"
or target and target[2] > y and "up"
or target and target[1] < x and "right" or "left" })
local base = #rows
local party = flags.EVENT_CHOSE_BULBASAUR and 3
or flags.EVENT_CHOSE_SQUIRTLE and 2 or 1
table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", party })
table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" })
-- a loss blacks out to another map: end the script there
table.insert(rows, { "jump_if_false", base + 7 })
table.insert(rows, { "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
table.insert(rows, { "move_npc_to", 1, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
ow.runner:run(rows, { npc = rival })
return true
end
return false
end,
}
+24
View File
@@ -0,0 +1,24 @@
-- Hand-ported from pret/pokered scripts/PalletTown.asm.
--
-- Only Oak needs a talk script: PalletTownOakText (scripts/
-- PalletTown.asm:163) is a text_asm branch on wOakWalkedToPlayer showing
-- either _PalletTownOakHeyWaitDontGoOutText or _PalletTownOakItsUnsafeText;
-- we branch on the starter flag, which tracks it. The intro cutscene
-- itself (Oak stopping the player and walking them to the lab) is the
-- PALLET_TOWN onStep in data/scripts/story2.lua.
--
-- The girl, fisher and the four signs resolve automatically through the
-- extracted text pointers (no script needed).
return {
talk = {
TEXT_PALLETTOWN_OAK = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_STARTER" }, -- 2
{ "jump_if_true", 6 }, -- 3
{ "show_text", "_PalletTownOakHeyWaitDontGoOutText" },-- 4
{ "jump", 7 }, -- 5
{ "show_text", "_PalletTownOakItsUnsafeText" }, -- 6
},
},
}
+15
View File
@@ -0,0 +1,15 @@
-- Hand-ported from pret/pokered scripts/RedsHouse1F.asm.
-- Mom (RedsHouse1FMomText, text_asm) heals the party and shows the
-- "you should rest" / "looking great" dialogue. The intro "wake up"
-- branch is tied to the unported intro cutscene, so the heal path is used.
return {
talk = {
TEXT_REDSHOUSE1F_MOM = {
{ "face_player" },
{ "show_text", "_RedsHouse1FMomYouShouldRestText" },
{ "heal_party" },
{ "show_text", "_RedsHouse1FMomLookingGreatText" },
},
},
}
+116
View File
@@ -0,0 +1,116 @@
-- The Safari Zone game entrance (scripts/SafariZoneGate.asm).
--
-- Stepping on (3,2)/(4,2) next to the worker fires the join prompt
-- (.PlayerNextToSafariZoneWorker1CoordsArray). Paying ¥500 hands over
-- 30 SAFARI BALLs and starts the 502-step game
-- (SafariZoneGateWouldYouLikeToJoinScript: wSafariSteps = 502,
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED). Declining walks you back
-- so you can't slip past. Returning to the gate ends the game and the
-- worker takes the leftover balls back.
--
-- Step/ball bookkeeping lives in src/world/OverworldController.lua
-- (safariStep/safariGameOver, from
-- engine/events/hidden_events/safari_game.asm); the in-battle
-- BALL/BAIT/ROCK/RUN game is src/battle/BattleState.lua makeSafari.
local M = {}
local FEE = 500
local BALLS = 30
local STEPS = 502
local function startGame(game, t, done)
game.save.money = game.save.money - FEE
game.save.safari = { balls = BALLS, steps = STEPS }
local TextBox = require("src.render.TextBox")
local paid = (t._SafariZoneGateSafariZoneWorker1ThatllBe500PleaseText
or "That'll be ¥500\nplease!\f{PLAYER} received\n30 SAFARI BALLs!")
:gsub("{PLAYER}", game.save.player.name)
local pa = t._SafariZoneGateSafariZoneWorker1CallYouOnThePAText
or "\fWe'll call you on\nthe PA when you\nrun out of time\nor SAFARI BALLs!"
local luck = t._SafariZoneGateSafariZoneWorker1GoodLuckText or "Good Luck!"
game.stack:push(TextBox.new(game, paid .. pa .. "\f" .. luck, done))
end
local function joinPrompt(game, ow, done)
done = done or function() end
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
local t = game.data.text
local back = function(text)
game.stack:push(TextBox.new(game, text, function()
ow:scriptMove(ow.player, "down", 1, done)
end))
end
game.stack:push(TextBox.new(game,
t._SafariZoneGateSafariZoneWorker1WouldYouLikeToJoinText
or "For just ¥500 you\ncan join the hunt!\fWould you like to\njoin the hunt?",
function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then
back(t._SafariZoneGateSafariZoneWorker1PleaseComeAgainText
or "OK! Please come\nagain!")
elseif game.save.money < FEE then
back(t._SafariZoneGateSafariZoneWorker1NotEnoughMoneyText
or "Oops! Not enough\nmoney!")
else
startGame(game, t, done)
end
end))
end))
end
M.SAFARI_ZONE_GATE = {
talk = {
TEXT_SAFARIZONEGATE_SAFARI_ZONE_WORKER1 = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local t = game.data.text
if game.save.safari then
game.stack:push(TextBox.new(game,
t._SafariZoneGateSafariZoneWorker1GoodLuckText or "Good Luck!", done))
return
end
game.stack:push(TextBox.new(game,
t._SafariZoneGateSafariZoneWorker1Text or "Welcome to the\nSAFARI ZONE!",
function() joinPrompt(game, ow, done) end))
end,
},
-- the join trigger cells in front of the worker
onStep = function(game, ow, x, y)
if y ~= 2 or (x ~= 3 and x ~= 4) then return false end
if game.save.safari then return false end -- paid, walking in
joinPrompt(game, ow, nil)
return true
end,
-- arriving back from the zone (the north warps): the worker asks
-- "Leaving early?" -- yes ends the game and takes the leftover balls,
-- no walks you back into the zone
onEnter = function(game, ow)
if not game.save.safari or ow.player.cellY > 1 then return end
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
local t = game.data.text
game.stack:push(TextBox.new(game,
t._SafariZoneGateSafariZoneWorker1LeavingEarlyText or "Leaving early?",
function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then
-- back into the zone through the entrance warp
local w = game.data.maps.SAFARI_ZONE_CENTER.warps[1]
ow:startWarpTo("SAFARI_ZONE_CENTER", w.x, w.y, "up")
return
end
game.save.safari = nil
game.stack:push(TextBox.new(game,
(t._SafariZoneGateSafariZoneWorker1ReturnSafariBallsText
or "Please return any\nSAFARI BALLs you\nhave left.")
.. "\f" .. (t._SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText
or "Did you get a\ngood haul?\fCome again!")))
end))
end))
end,
}
return M
+26
View File
@@ -0,0 +1,26 @@
-- Seafoam Islands boulder/current puzzle wiring + the Vermilion Gym
-- locked door (map-enter hooks; the mechanics themselves live in
-- src/world/OverworldController.lua driven by field.seafoam and the
-- trash can data).
--
-- Sources: scripts/SeafoamIslands1F.asm / B1F.asm / B3F.asm / B4F.asm
-- (currents, holes), scripts/VermilionGym.asm (VermilionGymSetDoorTile:
-- the door block at (2,2) is $24 until EVENT_2ND_LOCK_OPENED, then $5).
--
-- The 1F->B1F->B2F->B3F->B4F boulder cascade is fully data-driven via
-- field.seafoam (SEAFOAM_ISLANDS_1F/B1F/B3F holes+holeDestination and
-- B3F's pluggedByHolesOn) plus the generic
-- OverworldState:boulderIntoHole in src/world/OverworldController.lua;
-- no per-map onEnter hook is needed here.
local M = {}
M.VERMILION_GYM = {
onEnter = function(game, ow)
if not game.save.flags.EVENT_2ND_LOCK_OPENED then
ow:replaceBlock(2, 2, 36) -- $24: the closed double door
end
end,
}
return M
+609
View File
@@ -0,0 +1,609 @@
-- Hand-ported story-critical scripts, one table per map, all using real
-- extracted text. Each cites its pokered source. Registered in
-- data/scripts/init.lua.
local M = {}
-- -------------------------------------------------------------------
-- Oak's Parcel chain (scripts/ViridianMart.asm, OaksLab.asm,
-- ViridianCity.asm)
-- -------------------------------------------------------------------
M.VIRIDIAN_MART = {
talk = {
TEXT_VIRIDIANMART_CLERK = {
{ "check_flag", "EVENT_OAK_GOT_PARCEL" }, -- 1
{ "jump_if_true", 11 }, -- 2
{ "check_flag", "EVENT_GOT_OAKS_PARCEL" }, -- 3
{ "jump_if_true", 13 }, -- 4
{ "check_flag", "EVENT_GOT_STARTER" }, -- 5
{ "jump_if_false", 11 }, -- 6
{ "show_text", "_ViridianMartClerkYouCameFromPalletTownText" }, -- 7
-- the parcel-quest text's last page is "{PLAYER} got\nOAK's
-- PARCEL!" (scripts/ViridianMart.asm, sound_get_key_item)
{ "give_item", "OAKS_PARCEL", 1, "_ViridianMartClerkParcelQuestText" }, -- 8
{ "set_flag", "EVENT_GOT_OAKS_PARCEL" }, -- 9
{ "jump", 14 }, -- 10
{ "open_mart", "TEXT_VIRIDIANMART_CLERK" }, -- 11
{ "jump", 14 }, -- 12
{ "show_text", "_ViridianMartClerkSayHiToOakText" }, -- 13
},
},
}
M.VIRIDIAN_CITY = {
talk = {
-- the old man napping on the north path (scripts/ViridianCity.asm)
-- after his coffee, the old man offers the catch tutorial: not in a
-- hurry -> he demos catching a wild mon (BATTLE_TYPE_OLD_MAN)
TEXT_VIRIDIANCITY_OLD_MAN_SLEEPY = {
{ "check_flag", "EVENT_OAK_GOT_PARCEL" }, -- 1
{ "jump_if_true", 5 }, -- 2
{ "show_text", "_ViridianCityOldManSleepyPrivatePropertyText" }, -- 3
{ "jump", 14 }, -- 4
{ "face_player" }, -- 5
{ "ask", "_ViridianCityOldManHadMyCoffeeNowText" }, -- 6
{ "jump_if_true", 12 }, -- 7 (in a hurry)
{ "show_text", "_ViridianCityOldManKnowHowToCatchPokemonText" }, -- 8
{ "show_text", "_ViridianCityOldManYouNeedToWeakenTheTargetText" }, -- 9
{ "old_man_demo" }, -- 10
{ "jump", 13 }, -- 11
{ "show_text", "_ViridianCityOldManTimeIsMoneyText" }, -- 12
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" }, -- 13
},
},
-- the sleeping old man lies across the Route 22 path (18,9): until
-- he moves you can't slip past on either side (scripts/ViridianCity
-- blocks the whole corridor, not just his tile)
onStep = function(game, ow, x, y)
local gone = game.save.objectToggles and game.save.objectToggles.VIRIDIAN_CITY
and game.save.objectToggles.VIRIDIAN_CITY.VIRIDIANCITY_OLD_MAN_SLEEPY == false
if gone then return false end
-- crossing north of his row through the 3-wide gap (x 17-19, y<=8)
if y <= 8 and x >= 17 and x <= 19 then
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text._ViridianCityOldManSleepyPrivatePropertyText
or "You can't go\nthrough here!\fThis is private\nproperty!",
function() ow:scriptMove(ow.player, "down", 1) end))
return true
end
return false
end,
}
-- -------------------------------------------------------------------
-- Bill's SS ticket (scripts/BillsHouse.asm; the cell-separation cutscene
-- is compressed into the dialogue)
-- -------------------------------------------------------------------
-- Daisy hands over the TOWN MAP once Oak's errand is under way
-- (scripts/BluesHouse.asm BluesHouseDaisySittingText)
M.BLUES_HOUSE = {
talk = {
TEXT_BLUESHOUSE_DAISY_SITTING = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_TOWN_MAP" }, -- 2
{ "jump_if_true", 10 }, -- 3
{ "check_flag", "EVENT_GOT_STARTER" }, -- 4
{ "jump_if_false", 12 }, -- 5
{ "show_text", "_BluesHouseDaisyOfferMapText" }, -- 6
-- _GotMapText: "{PLAYER} got a\n{RAM:wStringBuffer}!" -- the
-- buffer supplies "TOWN MAP" (scripts/BluesHouse.asm GotMapText)
{ "give_item", "TOWN_MAP", 1, "_GotMapText" }, -- 7
{ "set_flag", "EVENT_GOT_TOWN_MAP" }, -- 8
{ "jump", 13 }, -- 9
{ "show_text", "_BluesHouseDaisyUseMapText" }, -- 10
{ "jump", 13 }, -- 11
{ "show_text", "_BluesHouseDaisyRivalAtLabText" }, -- 12
},
},
}
M.BILLS_HOUSE = {
talk = {
TEXT_BILLSHOUSE_BILL_POKEMON = {
{ "check_flag", "EVENT_GOT_SS_TICKET" }, -- 1
{ "jump_if_true", 11 }, -- 2
{ "show_text", "_BillsHouseBillImNotAPokemonText" }, -- 3
{ "show_text", "_BillsHouseBillNoYouGottaHelpText" }, -- 4
-- the cell-separator PC throws its switch
-- (bills_house_pc.asm BillsHouseInitiatedText: SFX_SWITCH)
{ "play_sound", "Switch" }, -- 5
{ "show_text", "_BillsHouseBillThankYouText" }, -- 6
-- pokered gives first (GiveItem fills wStringBuffer), then prints
-- the received text that reads it (scripts/BillsHouse.asm; the
-- item id is S_S_TICKET in generated items.lua -- keyItem, so the
-- sound_get_key_item jingle plays like BillsHouse.asm:196)
{ "give_item", "S_S_TICKET", 1, false }, -- 7
{ "show_text", "_SSTicketReceivedText" }, -- 8
{ "set_flag", "EVENT_GOT_SS_TICKET" }, -- 9
{ "jump", 12 }, -- 10
{ "show_text", "_BillsHouseBillCheckOutMyRarePokemonText" }, -- 11
},
},
}
-- -------------------------------------------------------------------
-- SS Anne (scripts/VermilionCity.asm, SSAnne2F.asm,
-- SSAnneCaptainsRoom.asm)
-- -------------------------------------------------------------------
M.VERMILION_CITY = {
-- VermilionCity_Script .setFirstLockTrashCanIndex (scripts/
-- VermilionCity.asm): every load of this map re-rolls which Vermilion
-- Gym trash can hides the first-lock switch (Random & $0e: a random
-- EVEN can, 0-14). The roll is unconditional -- pokered does not
-- care whether the first lock is already open, because the index is
-- only read while EVENT_1ST_LOCK_OPENED is unset (the gym is only
-- reachable through this map, so a fresh visit always re-rolls).
onEnter = function(game, ow)
local puz = game.save.trashPuzzle or {}
game.save.trashPuzzle = puz
puz.first = love.math.random(0, 7) * 2
end,
talk = {
-- the sailor guarding the dock gangway
TEXT_VERMILIONCITY_SAILOR1 = {
{ "face_player" }, -- 1
{ "show_text", "_VermilionCitySailor1WelcomeToSSAnneText" }, -- 2
{ "check_item", "S_S_TICKET" }, -- 3
{ "jump_if_false", 8 }, -- 4
{ "show_text", "_VermilionCitySailor1FlashedTicketText" }, -- 5
{ "hide_object", "VERMILION_CITY", "VERMILIONCITY_SAILOR1" }, -- 6
{ "jump", 9 }, -- 7
{ "show_text", "_VermilionCitySailor1YouNeedATicketText" }, -- 8
},
},
}
M.SS_ANNE_2F = {
talk = {
TEXT_SSANNE2F_RIVAL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 2
{ "jump_if_true", 9 }, -- 3
{ "show_text", "_SSAnne2FRivalText" }, -- 4
{ "rival_battle", "OPP_RIVAL2", 1 }, -- 5
{ "jump_if_false", 10 }, -- 6
{ "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7
{ "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8
{ "jump", 10 }, -- 9 (already beaten: silent)
},
},
}
M.SS_ANNE_CAPTAINS_ROOM = {
talk = {
TEXT_SSANNECAPTAINSROOM_CAPTAIN = {
{ "check_flag", "EVENT_GOT_HM01" }, -- 1
{ "jump_if_true", 9 }, -- 2
{ "show_text", "_SSAnneCaptainsRoomRubCaptainsBackText" }, -- 3
{ "show_text", "_SSAnneCaptainsRoomCaptainIFeelMuchBetterText" }, -- 4
-- give-then-print like scripts/SSAnneCaptainsRoom.asm (GiveItem
-- fills wStringBuffer; the received text reads it)
{ "give_item", "HM_CUT", 1, false }, -- 5
{ "show_text", "_SSAnneCaptainsRoomCaptainReceivedHM01Text" }, -- 6
{ "set_flag", "EVENT_GOT_HM01" }, -- 7
{ "jump", 10 }, -- 8
{ "show_text", "_SSAnneCaptainsRoomCaptainNotSickAnymoreText" }, -- 9
},
},
}
-- -------------------------------------------------------------------
-- Pokémon Tower / Poké Flute (scripts/PokemonTower7F.asm,
-- MrFujisHouse.asm; the teleport back to his house is a warp)
-- -------------------------------------------------------------------
M.POKEMON_TOWER_7F = {
talk = {
TEXT_POKEMONTOWER7F_MR_FUJI = {
{ "face_player" }, -- 1
{ "show_text", "_PokemonTower7FMrFujiRescueText" }, -- 2
{ "set_flag", "EVENT_RESCUED_MR_FUJI" }, -- 3
{ "set_flag", "EVENT_RESCUED_MR_FUJI_2" }, -- 4
-- pokered shows Fuji at home and swaps the Silph Co door
-- guard (ROCKET8 on the door tile -> ROCKET9 beside it)
{ "show_object", "MR_FUJIS_HOUSE", "MRFUJISHOUSE_MR_FUJI" }, -- 5
{ "hide_object", "SAFFRON_CITY", "SAFFRONCITY_ROCKET8" }, -- 6
{ "show_object", "SAFFRON_CITY", "SAFFRONCITY_ROCKET9" }, -- 7
{ "warp", "MR_FUJIS_HOUSE", 3, 3, "down" }, -- 8
},
},
}
M.MR_FUJIS_HOUSE = {
-- repair saves from before the rescue toggled him visible
onEnter = function(game, ow)
if game.save.flags.EVENT_RESCUED_MR_FUJI then
local Commands = require("src.script.Commands")
Commands.show_object({ game = game, save = game.save, overworld = ow },
"MR_FUJIS_HOUSE", "MRFUJISHOUSE_MR_FUJI")
end
end,
talk = {
TEXT_MRFUJISHOUSE_MR_FUJI = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_POKE_FLUTE" }, -- 2
{ "jump_if_true", 12 }, -- 3
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 4
{ "jump_if_false", 14 }, -- 5
{ "show_text", "_MrFujisHouseMrFujiIThinkThisMayHelpYourQuestText" }, -- 6
-- give-then-print like scripts/MrFujisHouse.asm
{ "give_item", "POKE_FLUTE", 1, false }, -- 7
{ "show_text", "_MrFujisHouseMrFujiReceivedPokeFluteText" }, -- 8
{ "set_flag", "EVENT_GOT_POKE_FLUTE" }, -- 9
{ "show_text", "_MrFujisHouseMrFujiPokeFluteExplanationText" }, -- 10
{ "jump", 15 }, -- 11
{ "show_text", "_MrFujisHouseMrFujiHasMyFluteHelpedYouText" }, -- 12
{ "jump", 15 }, -- 13
{ "show_text", "_MrFujisHouseMrFujiPokedexText" }, -- 14
},
},
}
-- -------------------------------------------------------------------
-- Snorlax (scripts/Route12.asm, Route16.asm)
-- -------------------------------------------------------------------
-- each route has its own strings (text/Route12.asm, text/Route16.asm;
-- Route 16's sleeping line is the unnamed _Route16Text7). Talking to
-- Snorlax before it's beaten always just shows the sleeping line --
-- Route12DefaultScript/Route16DefaultScript only special-case
-- EVENT_FIGHT_ROUTEnn_SNORLAX, which ItemUsePokeFlute sets when the
-- player USES the POKé FLUTE from the item-use menu while standing next
-- to Snorlax (see ItemEffects.lua's POKE_FLUTE branch); merely talking
-- to it with the flute in the bag does nothing. From the woke-up text
-- on, snorlaxWake below mirrors Route12DefaultScript's fight branch /
-- Route12SnorlaxPostBattleScript (scripts/Route12.asm, Route16.asm):
-- HideObject runs BEFORE the battle (so Snorlax is gone even after a
-- blackout), then the battle, then the calmed-down/returned line only
-- when it was NOT caught (`ld a, [wBattleResult] / cp $2` skips it),
-- and EVENT_BEAT_ROUTEnn_SNORLAX on any non-blackout result.
local function snorlaxWake(mapId, objName, beatFlag, wokeUpText, calmedText)
return {
{ "show_text", wokeUpText }, -- 1
{ "hide_object", mapId, objName }, -- 2 HideObject pre-battle
{ "static_battle", "SNORLAX", 30, beatFlag }, -- 3
{ "check_battle_result", "win", "run" }, -- 4 not caught, not blackout
{ "jump_if_false", 7 }, -- 5 end (skip calmed-down)
{ "show_text", calmedText }, -- 6
}
end
-- snorlaxWake is looked up by ItemEffects.lua/BagMenu.lua (via
-- data/scripts/init.lua's M.get) and run when the flute wakes Snorlax;
-- objName/beatFlag let ItemEffects find the NPC and check whether it's
-- already been beaten before allowing the wake.
M.ROUTE_12 = {
talk = { TEXT_ROUTE12_SNORLAX = { { "show_text", "_Route12SnorlaxText" } } },
snorlaxWake = {
objName = "ROUTE12_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE12_SNORLAX",
script = snorlaxWake("ROUTE_12", "ROUTE12_SNORLAX", "EVENT_BEAT_ROUTE12_SNORLAX",
"_Route12SnorlaxWokeUpText", "_Route12SnorlaxCalmedDownText"),
},
}
M.ROUTE_16 = {
talk = { TEXT_ROUTE16_SNORLAX = { { "show_text", "_Route16Text7" } } },
snorlaxWake = {
objName = "ROUTE16_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE16_SNORLAX",
script = snorlaxWake("ROUTE_16", "ROUTE16_SNORLAX", "EVENT_BEAT_ROUTE16_SNORLAX",
"_Route16SnorlaxWokeUpText", "_Route16SnorlaxReturnedToMountainsText"),
},
}
-- -------------------------------------------------------------------
-- Safari Zone HMs (scripts/SafariZoneSecretHouse.asm, WardensHouse.asm)
-- -------------------------------------------------------------------
M.SAFARI_ZONE_SECRET_HOUSE = {
talk = {
TEXT_SAFARIZONESECRETHOUSE_FISHING_GURU = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_HM03" }, -- 2
{ "jump_if_true", 9 }, -- 3
{ "show_text", "_SafariZoneSecretHouseFishingGuruYouHaveWonText" }, -- 4
-- give-then-print like scripts/SafariZoneSecretHouse.asm
{ "give_item", "HM_SURF", 1, false }, -- 5
{ "show_text", "_SafariZoneSecretHouseFishingGuruReceivedHM03Text" }, -- 6
{ "set_flag", "EVENT_GOT_HM03" }, -- 7
{ "jump", 10 }, -- 8
{ "show_text", "_SafariZoneSecretHouseFishingGuruHM03ExplanationText" }, -- 9
},
},
}
M.WARDENS_HOUSE = {
talk = {
TEXT_WARDENSHOUSE_WARDEN = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_HM04" }, -- 2
{ "jump_if_true", 13 }, -- 3
{ "check_item", "GOLD_TEETH" }, -- 4
{ "jump_if_false", 15 }, -- 5
{ "show_text", "_WardensHouseWardenGaveTheGoldTeethText" }, -- 6
{ "take_item", "GOLD_TEETH", 1 }, -- 7
{ "set_flag", "EVENT_GAVE_GOLD_TEETH" }, -- 8
{ "show_text", "_WardensHouseWardenThanksText" }, -- 9
-- give-then-print like scripts/WardensHouse.asm
{ "give_item", "HM_STRENGTH", 1, false }, -- 10
{ "show_text", "_WardensHouseWardenReceivedHM04Text" }, -- 11
{ "set_flag", "EVENT_GOT_HM04" }, -- 12
{ "jump", 16 }, -- 13 (already got it)
{ "jump", 16 }, -- 14 (unused)
{ "show_text", "_WardensHouseWardenGibberish1Text" }, -- 15
},
},
}
-- -------------------------------------------------------------------
-- Silph Co. president (scripts/SilphCo11F.asm; Giovanni there is the
-- generic OPP_GIOVANNI#2 battle)
-- -------------------------------------------------------------------
M.SILPH_CO_11F = {
talk = {
TEXT_SILPHCO11F_SILPH_PRESIDENT = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" }, -- 2
{ "jump_if_false", 10 }, -- 3
{ "check_flag", "EVENT_GOT_MASTER_BALL" }, -- 4
{ "jump_if_true", 10 }, -- 5
{ "show_text", "_SilphCo11FSilphPresidentText" }, -- 6
-- give-then-print like scripts/SilphCo11F.asm
{ "give_item", "MASTER_BALL", 1, false }, -- 7
{ "show_text", "_SilphCo11FSilphPresidentReceivedMasterBallText" }, -- 8
{ "set_flag", "EVENT_GOT_MASTER_BALL" }, -- 9
{ "jump", 11 }, -- 10
},
},
}
-- -------------------------------------------------------------------
-- Victory Road boulder switches (scripts/VictoryRoad1F/2F/3F.asm):
-- a boulder resting on a switch removes a barrier block; the 3F hole
-- drops a boulder down to the 2F switch.
-- -------------------------------------------------------------------
local function boulderAt(ow, x, y)
local npc = ow:npcAtCell(x, y)
return npc and npc.def.sprite == "SPRITE_BOULDER" and npc or nil
end
M.VICTORY_ROAD_1F = {
onEnter = function(game, ow)
if game.save.flags.EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH then
ow:replaceBlock(4, 6, 0x1D)
end
end,
onBoulderMoved = function(game, ow, npc)
if npc.cellX == 17 and npc.cellY == 13
and not game.save.flags.EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH then
game.save.flags.EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH = true
ow:replaceBlock(4, 6, 0x1D)
end
end,
}
M.VICTORY_ROAD_2F = {
onEnter = function(game, ow)
if game.save.flags.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1 then
ow:replaceBlock(3, 4, 0x15)
end
if game.save.flags.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2 then
ow:replaceBlock(11, 7, 0x1D)
end
end,
onBoulderMoved = function(game, ow, npc)
if npc.cellX == 1 and npc.cellY == 16
and not game.save.flags.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1 then
game.save.flags.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1 = true
ow:replaceBlock(3, 4, 0x15)
end
if npc.cellX == 9 and npc.cellY == 16
and not game.save.flags.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2 then
game.save.flags.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2 = true
ow:replaceBlock(11, 7, 0x1D)
end
end,
}
M.VICTORY_ROAD_3F = {
onEnter = function(game, ow)
if game.save.flags.EVENT_VICTORY_ROAD_3_BOULDER_ON_SWITCH1 then
ow:replaceBlock(3, 5, 0x1D)
end
end,
onBoulderMoved = function(game, ow, npc)
if npc.cellX == 3 and npc.cellY == 5
and not game.save.flags.EVENT_VICTORY_ROAD_3_BOULDER_ON_SWITCH1 then
game.save.flags.EVENT_VICTORY_ROAD_3_BOULDER_ON_SWITCH1 = true
ow:replaceBlock(3, 5, 0x1D)
end
-- the hole at (23,15): the boulder drops to 2F next to switch 2
if npc.cellX == 23 and npc.cellY == 15 then
local Commands = require("src.script.Commands")
local ctx = { save = game.save, overworld = ow, game = game }
Commands.hide_object(ctx, "VICTORY_ROAD_3F", npc.def.name)
Commands.show_object(ctx, "VICTORY_ROAD_2F", "VICTORYROAD2F_BOULDER")
end
end,
}
-- -------------------------------------------------------------------
-- Champion (scripts/ChampionsRoom.asm): rival with the Rival3 party for
-- your starter, then Oak's congratulations (Hall of Fame-lite).
-- -------------------------------------------------------------------
-- The battle gate is the run-scoped EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN,
-- cleared by the Indigo lobby's Elite Four reset (scripts/
-- IndigoPlateauLobby.asm) so the champion is re-fightable on rematches,
-- like pokered's re-entry cutscene. EVENT_BEAT_CHAMPION_RIVAL stays set
-- forever (postgame gates like the Cerulean cave guard read it).
-- scripts/ChampionsRoom.asm ChampionsRoomRivalDefeatedScript ->
-- OakArrivesScript -> OakCongratulatesPlayerScript ->
-- OakDisappointedWithRivalScript -> OakComeWithMeScript -> OakExitsScript,
-- then the simulated walk up-and-left into the HALL_OF_FAME warp
-- (PlayerFollowsOakScript / WalkToHallOfFame_RLEMovement). The induction
-- itself is NOT run here: it belongs to the HALL_OF_FAME room script
-- (scripts/HallOfFame.asm), so we set a one-shot marker and warp; the room
-- onEnter (M.HALL_OF_FAME below) drives the HoF Oak speech + record.
M.CHAMPIONS_ROOM = {
talk = {
TEXT_CHAMPIONSROOM_RIVAL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
{ "jump_if_true", 24 }, -- 3
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
{ "jump_if_false", 24 }, -- 6
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
-- ChampionsRoomOakArrivesScript: Oak's "{PLAYER}!" then reveal + walk in
{ "show_text", "_ChampionsRoomOakText" }, -- 10
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 11
{ "move_npc", 2, "up", 5 }, -- 12 OakEntranceAfterVictoryMovement (3,7)->(3,2)
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
{ "face_object", 1, "left" }, -- 13
{ "face_object", 2, "down" }, -- 14
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 15
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
{ "face_object", 2, "right" }, -- 16
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 17
-- OakComeWithMeScript: Oak faces down again, then exits up
{ "face_object", 2, "down" }, -- 18
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 19
{ "move_npc", 2, "up", 2 }, -- 20 OakExitChampionsRoomMovement (3,2)->(3,0)
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 21
-- hand the induction off to the HALL_OF_FAME room (consumed by its
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
{ "set_field", "pendingHallOfFame", true }, -- 22
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 23
},
},
}
-- -------------------------------------------------------------------
-- Hall of Fame (scripts/HallOfFame.asm): the induction's entry point is
-- the ROOM, not the Champions Room script. HallOfFameDefaultScript walks
-- the player up 5 into Oak (HallOfFameEntryMovement), then
-- HallOfFameOakCongratulationsScript turns them face-to-face, shows
-- _HallOfFameOakText, hides Oak (predef HideObject) and runs
-- HallOfFameResetEventsAndSaveScript's predef HallOfFamePC (the induction +
-- credits, here Commands.record_hall_of_fame).
--
-- onEnter fires DURING the Champions Room warp's setMap, while that warp
-- command's runner is still suspended-alive (yielded at the warp), so we
-- cannot start a runner here directly (ScriptRunner:run asserts the runner
-- is idle). Instead we QUEUE the cutscene: OverworldState:update drains
-- self.pendingScript once the warp's transition has finished and its runner
-- has gone dead, so the room cutscene begins a frame after the warp fully
-- completes. The one-shot save.pendingHallOfFame marker (set by the
-- Champions Room script) is consumed here so re-entering the room later
-- does not replay the induction.
M.HALL_OF_FAME = {
onEnter = function(game, ow)
-- self-heal saves poisoned by a prior version of this script, which
-- wrongly hid Oak here instead of the Cerulean Cave guard (see
-- CERULEAN_CITY onEnter, which handles the real HideObject target)
local toggles = game.save.objectToggles and game.save.objectToggles.HALL_OF_FAME
if toggles then toggles.HALLOFFAME_OAK = nil end
if not game.save.pendingHallOfFame then return end
game.save.pendingHallOfFame = false
ow:queueScript({
{ "move_player", "up", 5 }, -- (4,7)->(4,2) beside Oak (5,2)
{ "face_object", 1, "left" }, -- HALLOFFAME_OAK faces the player
{ "face_player_dir", "right" }, -- player faces Oak (PLAYER_DIR_RIGHT)
{ "show_text", "_HallOfFameOakText" }, -- TEXT_HALLOFFAME_OAK
-- HallOfFameOakCongratulationsScript hides TOGGLE_CERULEAN_CAVE_GUY
-- here, not Oak; that's handled separately by CERULEAN_CITY onEnter
-- gating on EVENT_BEAT_CHAMPION_RIVAL
{ "record_hall_of_fame" }, -- predef HallOfFamePC: induction + credits
})
end,
}
-- -------------------------------------------------------------------
-- Mid-game rival battles (scripts/CeruleanCity.asm, PokemonTower2F.asm);
-- Rival1 parties 7-9 are the Cerulean set, Rival2 parties 4-6 the Tower
-- set (data/trainers/parties.asm)
-- -------------------------------------------------------------------
M.CERULEAN_CITY = {
talk = {
TEXT_CERULEANCITY_RIVAL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 2
{ "jump_if_true", 9 }, -- 3
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
{ "jump_if_false", 10 }, -- 6
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
{ "jump", 10 }, -- 9
},
},
}
M.POKEMON_TOWER_2F = {
talk = {
TEXT_POKEMONTOWER2F_RIVAL = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 2
{ "jump_if_true", 10 }, -- 3
{ "show_text", "_PokemonTower2FRivalWhatBringsYouHereText" }, -- 4
{ "rival_battle", "OPP_RIVAL2", 4 }, -- 5
{ "jump_if_false", 11 }, -- 6
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
{ "jump", 11 }, -- 9
{ "show_text", "_PokemonTower2FRivalHowsYourDexText" }, -- 10
},
},
}
-- -------------------------------------------------------------------
-- In-game trades (data/events/trades.asm; the NPC<->trade mapping is
-- from each map's DoInGameTradeDialogue call)
-- -------------------------------------------------------------------
M.ROUTE_2_TRADE_HOUSE = {
talk = {
-- The Scientist is plain flavor; the Game Boy Kid holds the trade
-- (data/scripts/flavor/route_2_trade_house.lua), per Route2TradeHouse.asm.
TEXT_ROUTE2TRADEHOUSE_SCIENTIST = {
{ "face_player" },
{ "show_text", "_Route2TradeHouseScientistText" },
},
},
}
M.CERULEAN_TRADE_HOUSE = {
talk = {
-- The Granny is plain flavor (points you to her husband); the Gambler
-- holds the trade (data/scripts/flavor/cerulean_trade_house.lua), per
-- CeruleanTradeHouse.asm.
TEXT_CERULEANTRADEHOUSE_GRANNY = {
{ "face_player" },
{ "show_text", "_CeruleanTradeHouseGrannyText" },
},
},
}
M.VERMILION_TRADE_HOUSE = {
talk = {
TEXT_VERMILIONTRADEHOUSE_LITTLE_GIRL = {
{ "face_player" },
{ "trade", 5, "EVENT_TRADED_SPEAROW_FOR_FARFETCHD" }, -- DUX
},
},
}
return M
+720
View File
@@ -0,0 +1,720 @@
-- More hand-ported events: the Pallet Town intro, the thirsty Saffron
-- gate guards, the Bike Voucher chain, fossils and the day-care.
-- Registered via data/scripts/init.lua; each cites its pokered source.
local M = {}
-- -------------------------------------------------------------------
-- Pallet Town intro (scripts/PalletTown.asm PalletTownOakHeyWaitScript):
-- stepping toward the grass without a starter makes Oak stop you and
-- take you to his lab (the walk cutscene is compressed into the warp).
-- -------------------------------------------------------------------
-- Pure escort data, exposed on the map entry as `escort` for
-- tests/parity_intro.lua.
local escort = {}
-- FindPathToPlayer (engine/overworld/pathfinding.asm): each step
-- reduces whichever axis has more distance left; ties step X first
-- (`ld a, e / cp d / jr c, .yDistanceGreater`, e = X left, d = Y left).
function escort.findPath(fromX, fromY, toX, toY)
local xdist, ydist = math.abs(toX - fromX), math.abs(toY - fromY)
local xdir = toX > fromX and "right" or "left"
local ydir = toY > fromY and "down" or "up"
local xprog, yprog = 0, 0
local path = {}
while xprog < xdist or yprog < ydist do
if xdist - xprog >= ydist - yprog and xprog < xdist then
xprog = xprog + 1
path[#path + 1] = xdir
else
yprog = yprog + 1
path[#path + 1] = ydir
end
end
return path
end
-- PalletTownOakWalksToPlayerScript: Oak appears at his object spot
-- (8,5) and zigzags to one tile below the player (hNPCPlayerYDistance
-- is pre-decremented before predef FindPathToPlayer).
function escort.oakApproach(playerX)
return escort.findPath(8, 5, playerX, 2)
end
-- RLEList_ProfOakWalkToLab (engine/overworld/auto_movement.asm):
-- DOWN x5, LEFT, DOWN x5, RIGHT x3, UP -- Oak's last step lands on the
-- lab door (12,11). (The trailing NPC_CHANGE_FACING is a march-in-
-- place beat on the mat; here Oak just stands his final beat.)
escort.oakSteps = {
"down", "down", "down", "down", "down",
"left",
"down", "down", "down", "down", "down",
"right", "right", "right",
"up",
}
-- RLEList_PlayerWalkToLab decodes to UP x2, RIGHT x3, DOWN x5, LEFT,
-- DOWN x6 and plays in REVERSE buffer order (wSimulatedJoypadStatesEnd
-- grows downward): DOWN x6, LEFT, DOWN x5, RIGHT x3, UP x2 -- Oak's
-- exact path one step behind. The 17th press (the second UP) is eaten
-- by the door-warp frame (WarpFound clears hJoyHeld via EnterMap), so
-- only 16 real steps happen; the walk ends on the door at (12,11).
escort.playerSteps = { "down" }
for _, d in ipairs(escort.oakSteps) do
escort.playerSteps[#escort.playerSteps + 1] = d
end
M.PALLET_TOWN = {
talk = require("data.scripts.pallet_town").talk,
escort = escort,
-- Oak stops you at the north row (PalletTownDefaultScript's
-- `wYCoord == 1` check), walks up from (8,5), and leads you to his
-- lab with the player one step behind (scripts/PalletTown.asm +
-- PalletMovementScriptPointerTable in
-- engine/overworld/auto_movement.asm), then the lab walk-in and the
-- choose-mon exchange (scripts/OaksLab.asm OaksLabDefaultScript ..
-- OaksLabOakChooseMonSpeechScript).
onStep = function(game, ow, x, y)
if y ~= 1 or game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB
or game.save.flags.EVENT_GOT_STARTER then
return false
end
local TextBox = require("src.render.TextBox")
local Commands = require("src.script.Commands")
local Music = require("src.core.Music")
local t = game.data.text
local ctx = { save = game.save, game = game, overworld = ow }
-- PalletTownDefaultScript: stop the player, turn them around
-- (wPlayerMovingDirection = PLAYER_DIR_DOWN applies on the very
-- next frame, before the text box opens) and strike up the "oak
-- appears" theme (MUSIC_MEET_PROF_OAK)
ow.player.facing = "down"
Music.play(game.data, "Music_MeetProfOak")
-- DelayFrames-style hold: the world pauses (input stays locked)
-- for `frames` frames, then cb runs. Reuses the emote pause slot;
-- with an `npc` the "!" bubble draws above it (EmotionBubble).
local function hold(frames, npc, cb)
ow.emote = { frames = frames, npc = npc, onDone = cb }
end
-- chain single-tile scriptMoves through a direction list
local function walkList(entity, steps, done)
local i = 0
local function nextStep()
i = i + 1
if not entity or not steps[i] then
if done then done() end
return
end
ow:scriptMove(entity, steps[i], 1, nextStep)
end
nextStep()
end
-- ---- Oak's Lab side (scripts/OaksLab.asm) ----------------------
-- OaksLabOakChooseMonSpeechScript: the fed-up / choose-mon /
-- what-about-me / be-patient exchange, Delay3 between boxes
local function chooseMonSpeech()
local function say(key, fb, next)
game.stack:push(TextBox.new(game, t[key] or fb, next))
end
say("_OaksLabRivalFedUpWithWaitingText",
"{RIVAL}: Gramps!\nI'm fed up with\nwaiting!", function()
hold(3, nil, function()
say("_OaksLabOakChooseMonText",
"OAK: Here, {PLAYER}!\fThere are 3\nPOKéMON here!\fYou can have one!\nChoose!", function()
hold(3, nil, function()
say("_OaksLabRivalWhatAboutMeText",
"{RIVAL}: Hey!\nGramps! What\nabout me?", function()
hold(3, nil, function()
say("_OaksLabOakBePatientText",
"OAK: Be patient!\n{RIVAL}, you can\nhave one too!", function()
game.save.flags.EVENT_OAK_ASKED_TO_CHOOSE_MON = true
end)
end)
end)
end)
end)
end)
end)
end
-- entering the lab: the door Oak (OAKSLAB_OAK2, (5,10)) walks up 3
-- ahead of the player (OaksLabOakEntersLabScript OakEntryMovement),
-- swaps for the desk Oak (OAKSLAB_OAK1, (5,2)), then the player
-- walks up 8 from the mat (PlayerEntryMovementRLE) while the rival
-- and Oak turn with them (OaksLabPlayerEntersLabScript /
-- OaksLabFollowedOakScript)
local function labWalkIn()
local oak2 = ow:npcByIndex(8)
local function swapOaks()
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK1")
hold(3, nil, function() -- Delay3
Commands.face_object(ctx, 1, "down") -- rival watches you pass
ow:scriptMove(ow.player, "up", 8, function()
-- OaksLabFollowedOakScript: flags only after the walk-in, so a
-- stray step on the door mat can't fire the "don't go away"
-- push-up (oaks_lab.lua onStep) mid-cutscene. Outdoor escort
-- still re-arms on F1 mid-escort -- these flags stay clear
-- until the lab walk finishes.
game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB_2 = true
Commands.face_object(ctx, 1, "up")
-- res BIT_NO_MAP_MUSIC + PlayDefaultMusic: the lab theme
-- only starts once the walk-in is done
Music.playMap(game.data, "OAKS_LAB")
chooseMonSpeech()
end)
end)
end
if oak2 then
ow:scriptMove(oak2, "up", 3, swapOaks)
else
swapOaks()
end
end
-- PalletMovementScript_Done + the door warp: Oak is hidden as the
-- player steps into the doorway; PALLET_TOWN warp 3 -> OAKS_LAB
-- warp 2 = (5,11), with the door SFX (WarpFound -> SFX_GO_INSIDE)
local function enterLab()
Commands.hide_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
ow.doorWarp = true
ow:startWarpTo("OAKS_LAB", 5, 11, "up", labWalkIn,
{ keepMusic = true })
end
-- PalletMovementScript_WalkToLab: Oak's NPC movement and the
-- player's simulated joypad run simultaneously, in lockstep; the
-- player retraces Oak's path one step behind and follows him into
-- the doorway on the final beat
local function walkToLab(oak)
local i = 0
local function tick()
i = i + 1
local playerStep = escort.playerSteps[i]
if not playerStep then
enterLab()
return
end
if oak and escort.oakSteps[i] then
ow:scriptMove(oak, escort.oakSteps[i], 1)
elseif oak then
-- RLEList_ProfOakWalkToLab's trailing NPC_CHANGE_FACING beat:
-- Oak marches in place on the door mat while the player takes
-- the final step up behind him (movement.asm ChangeFacingDirection)
ow:marchInPlace(oak)
end
ow:scriptMove(ow.player, playerStep, 1, tick)
end
tick()
end
-- PalletMovementScript_OakMoveLeft/_PlayerMoveLeft: from the right
-- tile (x == 11) Oak sidesteps left first, then the player follows
-- left (wNumStepsToTake = wXCoord - 10), and only then both walk
local function escortToLab(oak)
local numSteps = x - 10
if oak and numSteps > 0 then
ow:scriptMove(oak, "left", numSteps, function()
ow:scriptMove(ow.player, "left", numSteps, function()
walkToLab(oak)
end)
end)
else
walkToLab(oak)
end
end
-- ---- Pallet Town side (scripts/PalletTown.asm) -----------------
-- PalletTownOakWalksToPlayerScript: Oak appears at (8,5), faces up
-- (SetSpriteFacingDirectionAndDelay + Delay3), then zigzags to the
-- player; the "It's unsafe!" text follows and the escort begins
local function oakAppearsAndWalks()
Commands.show_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
local oak = ow:npcByIndex(1)
if oak then oak.facing = "up" end
hold(6, nil, function()
walkList(oak, escort.oakApproach(x), function()
-- PalletTownOakNotSafeComeWithMeScript: the second text waits
-- for a button, then the escort starts
game.stack:push(TextBox.new(game,
t._PalletTownOakItsUnsafeText
or "OAK: It's unsafe!\nWild POKéMON\nlive in tall grass!",
function() escortToLab(oak) end))
end)
end)
end
-- The "Hey! Wait!" box ends without a button wait (auto), then the
-- "!" bubble shows over the player WHILE the box is still on screen
-- (PalletTownOakText: DelayFrames 10 then EmotionBubble, box not yet
-- cleared). onOverlap sets the bubble during the box's last frames;
-- the box pops after `overlap`, and the bubble's 60-frame hold then
-- runs to Oak's appearance (the bubble is static while the box is up,
-- since the overworld pauses under it, so 10 overlap + 50 = 60).
game.stack:push(TextBox.new(game,
t._PalletTownOakHeyWaitDontGoOutText or "OAK: Hey! Wait!\nDon't go out!",
nil, { auto = { delay = 10, overlap = 10, onOverlap = function()
ow.emote = { npc = ow.player, frames = 50, onDone = oakAppearsAndWalks }
end } }))
return true
end,
}
-- -------------------------------------------------------------------
-- Saffron gate guards (scripts/Route5Gate.asm etc.): crossing the gate
-- without having given them a drink gets you turned back; a drink from
-- the bag (bought at Celadon's vending machines... or any mart that
-- stocks them) opens all four gates.
-- -------------------------------------------------------------------
local DRINKS = { "FRESH_WATER", "SODA_POP", "LEMONADE" }
local function saffronGate(guardText, triggers, horizontal)
return {
talk = {
[guardText] = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local t = game.data.text
if game.save.flags.EVENT_GAVE_GUARDS_DRINK then
game.stack:push(TextBox.new(game,
t._SaffronGateGuardThanksForTheDrinkText or "Gee, that was\ntasty!", done))
return
end
for _, drink in ipairs(DRINKS) do
if (game.save.inventory[drink] or 0) > 0 then
game.save.inventory[drink] = game.save.inventory[drink] - 1
if game.save.inventory[drink] == 0 then
game.save.inventory[drink] = nil
end
game.save.flags.EVENT_GAVE_GUARDS_DRINK = true
game.stack:push(TextBox.new(game,
(t._SaffronGateGuardYouCanGoOnThroughText or
"Thanks! You can\ngo on through!"), done))
return
end
end
game.stack:push(TextBox.new(game,
t._SaffronGateGuardGeeImThirstyText or "Gee, I'm thirsty\nthough!", done))
end,
},
-- the gate's trigger cells (each gate's PlayerInCoordsArray):
-- without the drink flag you get walked back the way you came
onStep = function(game, ow, x, y)
local hit = false
for _, c in ipairs(triggers) do
if x == c[1] and y == c[2] then hit = true break end
end
if not hit then return false end
if game.save.flags.EVENT_GAVE_GUARDS_DRINK then return false end
local TextBox = require("src.render.TextBox")
local t = game.data.text
local back
if horizontal then
back = ow.player.facing == "left" and "right" or "left"
else
back = ow.player.facing == "up" and "down" or "up"
end
game.stack:push(TextBox.new(game,
t._SaffronGateGuardImParchedText or "I'm parched...\nNo entry until\nI get a drink!",
function()
ow:scriptMove(ow.player, back, 1)
end))
return true
end,
}
end
M.ROUTE_5_GATE = saffronGate("TEXT_ROUTE5GATE_GUARD", { { 3, 3 }, { 4, 3 } })
M.ROUTE_6_GATE = saffronGate("TEXT_ROUTE6GATE_GUARD", { { 3, 2 }, { 4, 2 } })
M.ROUTE_7_GATE = saffronGate("TEXT_ROUTE7GATE_GUARD", { { 3, 3 }, { 3, 4 } }, true)
M.ROUTE_8_GATE = saffronGate("TEXT_ROUTE8GATE_GUARD", { { 2, 3 }, { 2, 4 } }, true)
-- -------------------------------------------------------------------
-- Bike Voucher chain (scripts/PokemonFanClub.asm, BikeShop.asm)
-- -------------------------------------------------------------------
M.POKEMON_FAN_CLUB = {
talk = {
TEXT_POKEMONFANCLUB_CHAIRMAN = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2
{ "jump_if_true", 9 }, -- 3
{ "show_text", "_PokemonFanClubChairmanIntroText" }, -- 4
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 5
-- give-then-print like scripts/PokemonFanClub.asm (GiveItem
-- fills wStringBuffer; the received text reads it)
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 6
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 7
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 8
{ "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 9
},
},
}
M.BIKE_SHOP = {
talk = {
TEXT_BIKESHOP_CLERK = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
if (game.save.inventory.BICYCLE or 0) > 0 then
game.stack:push(TextBox.new(game, "How's the\nBICYCLE treating\nyou?", done))
elseif (game.save.inventory.BIKE_VOUCHER or 0) > 0 then
game.save.inventory.BIKE_VOUCHER = nil
game.save.inventory.BICYCLE = 1
game.stack:push(TextBox.new(game,
("Oh, that's a\nBIKE VOUCHER!\f%s exchanged\nit for a BICYCLE!")
:format(game.save.player.name), done))
else
game.stack:push(TextBox.new(game,
"A BICYCLE costs\n¥1000000. Sorry,\nno instalments!", done))
end
end,
},
}
-- -------------------------------------------------------------------
-- Fossils (scripts/MtMoonB2F.asm, Museum1F.asm,
-- CinnabarLabFossilRoom.asm): pick one Mt Moon fossil, revive them at
-- the Cinnabar lab (the wait is skipped).
-- -------------------------------------------------------------------
local function mtMoonFossil(itemId, otherName)
return function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
if game.save.flags.EVENT_GOT_A_FOSSIL then
game.stack:push(TextBox.new(game, "You already took\na fossil.", done))
return
end
game.stack:push(TextBox.new(game, "You found a\nfossil! Take it?", function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then done() return end
game.save.inventory[itemId] = 1
game.save.flags.EVENT_GOT_A_FOSSIL = true
local Commands = require("src.script.Commands")
local ctx = { save = game.save, overworld = ow, game = game }
Commands.hide_object(ctx, "MT_MOON_B2F", npc.def.name)
Commands.hide_object(ctx, "MT_MOON_B2F", otherName)
local name = game.data.items[itemId].name
game.stack:push(TextBox.new(game,
("%s got the\n%s!"):format(game.save.player.name, name), done))
end))
end))
end
end
M.MT_MOON_B2F = {
talk = {
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil("DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL"),
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil("HELIX_FOSSIL", "MTMOONB2F_DOME_FOSSIL"),
},
}
-- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text):
-- Y50, once; declining at the rope walks you back out.
local function museumClerk(game, ow, done, onDecline)
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
if game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
game.stack:push(TextBox.new(game,
"Take your time,\nand enjoy it all!", done))
return
end
game.stack:push(TextBox.new(game,
"It's ¥50 for a\nchild's ticket.\fWould you like to\ncome in?", function()
game.stack:push(ChoiceBox.new(game, function(yes)
if yes and game.save.money >= 50 then
game.save.money = game.save.money - 50
game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET = true
game.stack:push(TextBox.new(game,
"Right, ¥50!\nThank you!", done))
elseif yes then
game.stack:push(TextBox.new(game,
"You don't have\nenough money.", onDecline or done))
else
game.stack:push(TextBox.new(game,
"Come again!", onDecline or done))
end
end))
end))
end
M.MUSEUM_1F = {
-- crossing the rope at (9,4)/(10,4) without a ticket calls the clerk
-- over (Museum1FDefaultScript's coordinate check)
onStep = function(game, ow, x, y)
if y == 4 and (x == 9 or x == 10)
and not game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
museumClerk(game, ow, nil, function()
ow:scriptMove(ow.player, "right", 1)
end)
return true
end
return false
end,
talk = {
TEXT_MUSEUM1F_SCIENTIST1 = function(game, ow, npc, done)
museumClerk(game, ow, done)
end,
-- The OLD AMBER display object is plain flavor; the scientist
-- (TEXT_MUSEUM1F_SCIENTIST2, data/scripts/flavor/museum_1f.lua) is who
-- hands it over and hides this object, per scripts/Museum1F.asm.
TEXT_MUSEUM1F_OLD_AMBER = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text._Museum1FOldAmberText or "The OLD AMBER.", done))
end,
},
}
local FOSSIL_MONS = {
HELIX_FOSSIL = "OMANYTE", DOME_FOSSIL = "KABUTO", OLD_AMBER = "AERODACTYL",
}
-- Deterministic scan order mirrors FossilsList (scripts/
-- CinnabarLabFossilRoom.asm lines 43-47): DOME_FOSSIL, HELIX_FOSSIL,
-- OLD_AMBER (the old pairs()-order loop this replaced was undefined).
local FOSSIL_ORDER = { "DOME_FOSSIL", "HELIX_FOSSIL", "OLD_AMBER" }
-- fills the {PLAYER}/{RAM:...} placeholders in the extracted text
-- verbatim (text/CinnabarLabFossilRoom.asm); TextBox itself only knows
-- how to substitute {RAM:wStringBuffer}, so this has to happen first.
-- RAM placeholders resolve by buffer name from subs (SeesFossilText
-- reads both wNameBuffer and wStringBuffer), falling back to subs.ram.
local function fillFossilText(s, subs)
s = s:gsub("{PLAYER}", subs.player or "")
s = s:gsub("{RAM:([^}]*)}", function(name) return subs[name] or subs.ram or "" end)
return s
end
M.CINNABAR_LAB_FOSSIL_ROOM = {
talk = {
-- Fossil revival quest (scripts/CinnabarLabFossilRoom.asm
-- CinnabarLabFossilRoomScientist1Text lines 49-99, deposit flow in
-- engine/events/cinnabar_lab.asm GiveFossilToCinnabarLab): deposit
-- a fossil -> pending for the rest of this visit -> ready once the
-- player leaves and re-enters the CINNABAR_ISLAND overworld map
-- (M.CINNABAR_ISLAND.onEnter in data/scripts/story5.lua clears
-- EVENT_LAB_STILL_REVIVING_FOSSIL there, mirroring CinnabarIsland.
-- asm line 6) -> hand over the mon and reset the whole quest so a
-- second fossil can be deposited later.
--
-- The deposit itself follows GiveFossilToCinnabarLab: a bordered
-- top-left menu of every carried fossil (A/B watched; B backs out),
-- then SeesFossilText with a Yes/No confirm; both cancel paths
-- (B on the menu, NO on the confirm) share ComeAgainText.
TEXT_CINNABARLABFOSSILROOM_SCIENTIST1 = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local t = game.data.text
local f = game.save.flags
local subs = { player = game.save.player.name }
if f.EVENT_GAVE_FOSSIL_TO_LAB then
if f.EVENT_LAB_STILL_REVIVING_FOSSIL then
-- .check_done_reviving -> still pending this visit
game.stack:push(TextBox.new(game,
t._CinnabarLabFossilRoomScientist1GoForAWalkText or
"I take a little\ntime!\fYou go for walk a\nlittle while!", done))
return
end
-- STILL_REVIVING was cleared (CINNABAR_ISLAND was reloaded
-- since the deposit): .done_reviving, lines 72-83
local species = game.save.labFossilMon
subs.ram = species and game.data.pokemon[species]
and game.data.pokemon[species].name or ""
f.EVENT_LAB_HANDING_OVER_FOSSIL_MON = true
game.stack:push(TextBox.new(game,
fillFossilText(
t._CinnabarLabFossilRoomScientist1FossilIsBackToLifeText or
"Where were you?\fYour fossil is\nback to life!\fIt was {RAM:x}\nlike I think!",
subs),
function()
if species then
local Commands = require("src.script.Commands")
local ctx = { save = game.save, game = game, overworld = ow }
Commands.give_pokemon(ctx, species, 30)
if not ctx.lastCheck then
-- GivePokemon failed (party+box full): pokered's
-- `jr nc, .done` leaves the quest pending so the
-- scientist re-offers the mon next visit instead of
-- destroying it.
game.stack:push(TextBox.new(game,
t._BoxIsFullText or "Box is full!", done))
return
end
end
game.save.labFossilMon = nil
f.EVENT_GAVE_FOSSIL_TO_LAB = nil
f.EVENT_LAB_STILL_REVIVING_FOSSIL = nil
f.EVENT_LAB_HANDING_OVER_FOSSIL_MON = nil
done()
end))
return
end
-- No fossil deposited yet: the intro always plays first (.Text),
-- then either the fossil-select menu (GiveFossilToCinnabarLab)
-- or NoFossilsText.
game.stack:push(TextBox.new(game,
t._CinnabarLabFossilRoomScientist1Text or
"Hiya!\fI am important\ndoctor!\fI study here rare\nPOKéMON fossils!\fYou! Have you a\nfossil for me?",
function()
-- Lab4Script_GetFossilsInBag: every carried fossil, in
-- FossilsList order
local carried = {}
for _, fossil in ipairs(FOSSIL_ORDER) do
if (game.save.inventory[fossil] or 0) > 0 then
carried[#carried + 1] = fossil
end
end
if #carried == 0 then
game.stack:push(TextBox.new(game,
t._CinnabarLabFossilRoomScientist1NoFossilsText or
"No! Is too bad!", done))
return
end
-- .cancelledGivingFossil: B on the menu and NO on the
-- confirm both land here
local function comeAgain()
game.stack:push(TextBox.new(game,
t._CinnabarLabFossilRoomScientist1ComeAgainText or
"Aiyah! You come\nagain!", done))
end
local items = {}
for _, fossil in ipairs(carried) do
items[#items + 1] = {
label = game.data.items[fossil].name,
onSelect = function()
-- LoadFossilItemAndMonName: wNameBuffer = item name,
-- wStringBuffer = mon name; then .ScientistSeesFossilText
-- with YesNoChoice (cursor starts on YES)
local species = FOSSIL_MONS[fossil]
local def = game.data.pokemon[species]
subs.wNameBuffer = game.data.items[fossil].name
subs.wStringBuffer = def and def.name or species
game.stack:push(TextBox.new(game,
fillFossilText(
t._CinnabarLabFossilRoomScientist1SeesFossilText or
"Oh! That is\n{RAM:wNameBuffer}!\fIt is fossil of\n{RAM:wStringBuffer}, a\nPOKéMON that is\nalready extinct!\fMy Resurrection\nMachine will make\nthat POKéMON live\nagain!",
subs),
nil, { choice = function(yes)
if not yes then comeAgain() return end
-- YES: TakesFossilText, RemoveItemByID, GoForAWalk2,
-- SetEvents GAVE_FOSSIL_TO_LAB + STILL_REVIVING
require("src.inventory.Bag").remove(game.save, fossil, 1)
game.save.labFossilMon = species
f.EVENT_GAVE_FOSSIL_TO_LAB = true
f.EVENT_LAB_STILL_REVIVING_FOSSIL = true
game.stack:push(TextBox.new(game,
fillFossilText(
t._CinnabarLabFossilRoomScientist1TakesFossilText or
"So! You hurry and\ngive me that!\f{PLAYER} handed\nover {RAM:wNameBuffer}!",
subs),
function()
game.stack:push(TextBox.new(game,
t._CinnabarLabFossilRoomScientist1GoForAWalkText2 or
"I take a little\ntime!\fYou go for walk a\nlittle while!", done))
end))
end }))
end,
}
end
-- GiveFossilToCinnabarLab's menu: TextBoxBorder at 0,0
-- (interior width $d, height 2 per fossil), A|B watched
local Menu = require("src.ui.Menu")
game.stack:push(Menu.new(game, items,
{ tx = 0, ty = 0, tw = 15, onCancel = comeAgain }))
end))
end,
-- the other scientist trades SAILOR: Ponyta -> Seel
-- (scripts/CinnabarLabFossilRoom.asm TRADE_FOR_SAILOR)
TEXT_CINNABARLABFOSSILROOM_SCIENTIST2 = {
{ "face_player" },
{ "trade", 4, "EVENT_TRADED_PONYTA_FOR_SEEL" },
},
},
}
-- -------------------------------------------------------------------
-- Day-care (scripts/Daycare.asm): the boarded Pokémon earns 1 exp per
-- step; the fee is ¥100 plus ¥100 per level gained.
-- -------------------------------------------------------------------
M.DAYCARE = {
talk = {
TEXT_DAYCARE_GENTLEMAN = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
local t = game.data.text
local dc = game.save.daycare
if dc and dc.mon then
local Growth = require("src.pokemon.Growth")
local Stats = require("src.pokemon.Stats")
local mon = dc.mon
local def = game.data.pokemon[mon.species]
mon.exp = mon.exp + (dc.steps or 0)
local newLevel = math.min(100, Growth.levelForExp(def.growthRate, mon.exp))
local fee = 100 + (newLevel - mon.level) * 100
local grew = newLevel > mon.level
mon.level = newLevel
mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp)
mon.hp = mon.stats.hp
local msg = grew and (t._DaycareGentlemanMonHasGrownText or "It's grown a lot!")
or "Back already?"
game.stack:push(TextBox.new(game,
msg .. ("\fThe fee is ¥%d.\nGet it back?"):format(fee), function()
game.stack:push(ChoiceBox.new(game, function(yes)
if yes and game.save.money >= fee then
game.save.money = game.save.money - fee
table.insert(game.save.party, mon)
game.save.daycare = nil
game.stack:push(TextBox.new(game,
t._DaycareGentlemanGotMonBackText or "Here you go!", done))
else
game.stack:push(TextBox.new(game,
yes and (t._DaycareGentlemanOweMoneyText or "You owe me money!")
or "Come again!", done))
end
end))
end))
return
end
if #game.save.party < 2 then
game.stack:push(TextBox.new(game,
"You only have one\nPOKéMON with you!", done))
return
end
game.stack:push(TextBox.new(game,
t._DaycareGentlemanIntroText or "I can raise a\nPOKéMON for you.", function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then done() return end
local PartyMenu = require("src.ui.PartyMenu")
game.stack:push(PartyMenu.new(game, {
pickOnly = true,
onSwitch = function(mon)
for i, m in ipairs(game.save.party) do
if m == mon then table.remove(game.save.party, i) break end
end
game.save.daycare = { mon = mon, steps = 0 }
game.stack:push(TextBox.new(game,
t._DaycareGentlemanWillLookAfterMonText or
"Fine, I'll look\nafter it a while!", done))
end,
}))
end))
end))
end,
},
}
return M
+430
View File
@@ -0,0 +1,430 @@
-- Third batch of hand-ported events: fishing rod givers, the Marowak
-- ghost, elevators, the Game Corner coins/prizes, the SS Anne departure
-- and the Hall of Fame record. Each cites its pokered source.
local M = {}
-- -------------------------------------------------------------------
-- Fishing rod givers (scripts/VermilionOldRodHouse.asm,
-- FuchsiaGoodRodHouse.asm, Route12SuperRodHouse.asm)
-- -------------------------------------------------------------------
local function rodGiver(askText, receivedText, afterText, rodItem, flag)
return {
{ "face_player" }, -- 1
{ "check_flag", flag }, -- 2
{ "jump_if_true", 9 }, -- 3
{ "ask", askText }, -- 4
{ "jump_if_false", 10 }, -- 5
-- give-then-print like the three rod-house scripts (GiveItem fills
-- wStringBuffer; the received texts read OLD/GOOD/SUPER ROD from it)
{ "give_item", rodItem, 1, false }, -- 6
{ "show_text", receivedText }, -- 7
{ "set_flag", flag }, -- 8
{ "jump", 10 }, -- 9 is below
}
end
M.VERMILION_OLD_ROD_HOUSE = {
talk = {
TEXT_VERMILIONOLDRODHOUSE_FISHING_GURU = rodGiver(
"_VermilionOldRodHouseFishingGuruDoYouLikeToFishText",
"_VermilionOldRodHouseFishingGuruTakeThisText",
"_VermilionOldRodHouseFishingGuruHowAreTheFishBitingText",
"OLD_ROD", "EVENT_GOT_OLD_ROD"),
},
}
M.VERMILION_OLD_ROD_HOUSE.talk.TEXT_VERMILIONOLDRODHOUSE_FISHING_GURU[9] =
{ "show_text", "_VermilionOldRodHouseFishingGuruHowAreTheFishBitingText" }
M.FUCHSIA_GOOD_ROD_HOUSE = {
talk = {
TEXT_FUCHSIAGOODRODHOUSE_FISHING_GURU = rodGiver(
"_FuchsiaGoodRodHouseFishingGuruText",
"_FuchsiaGoodRodHouseFishingGuruReceivedGoodRodText",
"_FuchsiaGoodRodHouseFishingGuruHowAreTheFishText",
"GOOD_ROD", "EVENT_GOT_GOOD_ROD"),
},
}
M.FUCHSIA_GOOD_ROD_HOUSE.talk.TEXT_FUCHSIAGOODRODHOUSE_FISHING_GURU[9] =
{ "show_text", "_FuchsiaGoodRodHouseFishingGuruHowAreTheFishText" }
M.ROUTE_12_SUPER_ROD_HOUSE = {
talk = {
TEXT_ROUTE12SUPERRODHOUSE_FISHING_GURU = rodGiver(
"_Route12SuperRodHouseFishingGuruDoYouLikeToFishText",
"_Route12SuperRodHouseFishingGuruReceivedSuperRodText",
"_Route12SuperRodHouseFishingGuruTryFishingText",
"SUPER_ROD", "EVENT_GOT_SUPER_ROD"),
},
}
M.ROUTE_12_SUPER_ROD_HOUSE.talk.TEXT_ROUTE12SUPERRODHOUSE_FISHING_GURU[9] =
{ "show_text", "_Route12SuperRodHouseFishingGuruTryFishingText" }
-- -------------------------------------------------------------------
-- The ghost Marowak (scripts/PokemonTower6F.asm): blocks the stairs at
-- (10,16) until identified with the Silph Scope and defeated.
-- -------------------------------------------------------------------
M.POKEMON_TOWER_6F = {
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_BEAT_GHOST_MAROWAK then return false end
if x ~= 10 or y ~= 16 then return false end
local TextBox = require("src.render.TextBox")
if not game.save.inventory.SILPH_SCOPE then
game.stack:push(TextBox.new(game,
"A GHOST blocks\nthe way...\fDarn! You can't\nidentify it!",
function()
local back = ow.player.facing == "up" and "down" or "up"
ow:scriptMove(ow.player, back, 1)
end))
return true
end
game.stack:push(TextBox.new(game,
"The GHOST was\nMAROWAK!\fThe restless soul\nattacks!", function()
local BattleState = require("src.battle.BattleState")
local battle = BattleState.newWild(game, "MAROWAK", 30)
battle.onFinish = function(result)
if result == "win" then
game.save.flags.EVENT_BEAT_GHOST_MAROWAK = true
game.stack:push(TextBox.new(game,
"The restless soul\ncalmed down and\ndeparted!"))
end
ow:afterBattle(result)
end
game.stack:push(battle)
end))
return true
end,
}
-- -------------------------------------------------------------------
-- Elevators (scripts/SilphCoElevator.asm etc.): a floor menu built from
-- the maps whose warps lead to the elevator (fully data-driven).
--
-- engine/events/elevator.asm DisplayElevatorFloorMenu: prints the floor
-- list (SPECIALLISTMENU -- a plain text list, constants/list_constants
-- .asm:7, not a graphical panel) built from each elevator's fixed
-- FLOOR_* table (e.g. scripts/SilphCoElevator.asm SilphCoElevatorFloors,
-- ascending FLOOR_1F..FLOOR_11F); wCurrentMenuItem is explicitly zeroed
-- so the cursor always rests on the topmost floor -- there is no
-- current-floor marker/wWhichFloor symbol anywhere in Gen1. Floor
-- labels are the short FLOOR_* item-name strings (data/items/names.asm:
-- 87-100 -- '1F'..'11F', 'B1F', 'B2F', 'B4F'), never the room/map name.
-- On B (`ret c`) nothing happens -- no warp, the player just stays put.
-- On A, the map script sees BIT_CUR_MAP_USED_ELEVATOR and runs
-- engine/overworld/elevator.asm ShakeElevator (src/world/ElevatorShake
-- .lua): the music stops, the BG scroll bounces -1/+1px for 100
-- two-frame cycles with SFX_COLLISION each cycle, then
-- SFX_SAFARI_ZONE_PA plays out and the map theme returns, before the
-- player is delivered to the chosen floor.
-- keyGate: the Rocket Hideout panel refuses without the LIFT KEY
-- (scripts/RocketHideoutElevator.asm RocketHideoutElevatorText:
-- "It appears to need a key." and no floor menu)
-- preFrames: the shake's lead-in delays -- ShakeElevator's own Delay3s
-- come to 9 frames; Silph/Rocket's ...ShakeScript prefixes another
-- Delay3 (12) while CeladonMartElevatorShakeScript farjps straight in
-- After the ride the original does NOT jump-cut to the floor: choosing a
-- floor in engine/events/elevator.asm DisplayElevatorFloorMenu rewrites
-- the elevator car's own warp entries (wWarpEntries, via .UpdateWarp) to
-- the chosen floor's exit warp, then the player walks out of the car onto
-- that warp themselves (scripts/SilphCoElevator.asm etc.). Reproduce
-- that here: rewrite the car's exit warps, then drive a short scripted
-- walk-out onto an exit tile and take the (now rewritten) warp, reusing
-- ow:scriptMove / ow:takeWarp (the Oak-escort primitives).
local WALK_DIRVEC = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
local WALK_OPP = { up = "down", down = "up", left = "right", right = "left" }
local WALK_ORDER = { "up", "down", "left", "right" }
local function elevatorWalkOut(ow, floor)
local m, p = ow.map, ow.player
-- .UpdateWarp is run twice, so BOTH car warp entries get the same
-- (warp id, map id): point every exit warp at the picked floor's
-- elevator-door warp (the reciprocal warp found while building the
-- menu). The car map's def is shared generated data, but its own
-- warps are only ever read from inside the car, and this rewrite runs
-- on every ride before the walk-out fires, so it is self-correcting.
for _, w in ipairs(m.def.warps) do
w.destMap = floor.map
w.destWarp = floor.warpIdx
end
-- leave by the exit tile under the player (they warped in onto one),
-- else the nearest
local door
for _, w in ipairs(m.def.warps) do
if w.x == p.cellX and w.y == p.cellY then door = w break end
end
if not door then
local best
for _, w in ipairs(m.def.warps) do
local d = math.abs(w.x - p.cellX) + math.abs(w.y - p.cellY)
if not best or d < best then best, door = d, w end
end
end
-- the car interior sits on the door's walkable side; "out" is the
-- doorway direction (the map edge for Silph/Celadon, the top doorway
-- for the Rocket car). Fixed direction order keeps the step
-- deterministic when a door tile has several walkable neighbours
-- (Silph/Celadon step up into the car, the Rocket car steps down).
local into
for _, dir in ipairs(WALK_ORDER) do
local v = WALK_DIRVEC[dir]
local nx, ny = door.x + v[1], door.y + v[2]
if m:inBounds(nx, ny) and m:isWalkableCell(nx, ny) then into = dir break end
end
into = into or "up"
local out = WALK_OPP[into]
local function leave()
ow:takeWarp(door) -- door SFX + warp to the rewritten floor target
end
-- step one tile into the car, then walk back through the doorway onto
-- the exit tile and take the warp: a visible walk-out on valid tiles
-- for either door orientation, instead of a jump cut
ow:scriptMove(p, into, 1, function()
ow:scriptMove(p, out, 1, leave)
end)
end
local function elevator(elevatorMapId, keyGate, preFrames)
return {
onEnter = function(game, ow)
if keyGate and not game.save.inventory[keyGate.item] then
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text[keyGate.text] or "It appears to\nneed a key."))
return
end
local floors = {}
for mapId, def in pairs(game.data.maps) do
for i, w in ipairs(def.warps) do
if w.destMap == elevatorMapId then
-- short floor token pokered actually prints, e.g.
-- SILPH_CO_10F -> "10F", ROCKET_HIDEOUT_B2F -> "B2F"
local token = mapId:match("_([^_]+)$") or mapId
-- warpIdx: this floor's warp back into the elevator IS the
-- warp the car's rewritten exit lands on (the reciprocal
-- pair), matching wElevatorWarpMaps' (warp id, map id)
table.insert(floors,
{ map = mapId, x = w.x, y = w.y, token = token, warpIdx = i })
break
end
end
end
-- numeric floor order (SilphCoElevatorFloors' FLOOR_1F..FLOOR_11F),
-- not lexicographic -- otherwise 10F/11F sort before 2F..9F
table.sort(floors, function(a, b)
return (tonumber(a.token:match("%d+")) or 0) <
(tonumber(b.token:match("%d+")) or 0)
end)
local items = {}
for _, f in ipairs(floors) do
table.insert(items, { label = f.token, value = f })
end
local ListMenu = require("src.ui.ListMenu")
game.stack:push(ListMenu.new(game, "WHICH FLOOR?", items, {
onChoose = function(item, list)
list:close()
-- the map-entry Transition (startWarpTo) calls onEnter from
-- its OWN midpoint callback, so this menu was pushed on top
-- of that still-active Transition; only the top state
-- updates, so it froze mid-fade instead of finishing. Left
-- alone it would resurface after the ride and play a stray
-- fade at the wrong time -- pop it now, before it can happen.
local Transition = require("src.render.Transition")
if getmetatable(game.stack:top()) == Transition then
game.stack:pop()
end
-- the whole ShakeElevator ride runs in place -- music stop,
-- 100 collision-thud scroll bounces, the PA chime -- and only
-- then does the player walk out of the car onto the chosen
-- floor (elevatorWalkOut rewrites the car's exit warps first)
local ElevatorShake = require("src.world.ElevatorShake")
game.stack:push(ElevatorShake.new(game, ow, {
preFrames = preFrames,
onDone = function()
elevatorWalkOut(ow, item.value)
end,
}))
end,
onCancel = function()
-- DisplayElevatorFloorMenu: `ret c` on B -- no warp, nothing
-- happens, the player just stays in the car
end,
}))
end,
}
end
M.SILPH_CO_ELEVATOR = elevator("SILPH_CO_ELEVATOR")
M.CELADON_MART_ELEVATOR = elevator("CELADON_MART_ELEVATOR", nil, 9)
M.ROCKET_HIDEOUT_ELEVATOR = elevator("ROCKET_HIDEOUT_ELEVATOR",
{ item = "LIFT_KEY", text = "_RocketHideoutElevatorAppearsToNeedKeyText" })
-- -------------------------------------------------------------------
-- Game Corner coins, prizes, and the rocket-poster switch that reveals
-- the hideout stairs (scripts/GameCorner.asm, data/events/prizes.asm +
-- prize_mon_levels.asm)
-- -------------------------------------------------------------------
M.GAME_CORNER = {
-- the hideout stairs hide behind a wall block until the poster
-- switch is found (the block at (8,2) is $2a while
-- EVENT_FOUND_ROCKET_HIDEOUT is unset, $43 after)
onEnter = function(game, ow)
local poster = game.data.field.gameCornerPoster
if not poster then return end
local block = game.save.flags[poster.event] and poster.openBlock
or poster.closedBlock
ow:replaceBlock(poster.x, poster.y, block)
-- pick this visit's lucky slot machine
-- (wLuckySlotHiddenEventIndex, engine/slots/game_corner_slots2.asm)
local seats = game.data.field.slotMachines.GAME_CORNER
ow.luckySlot = love.math.random(1, #seats)
end,
talk = {
-- the poster bg event: pressing A reveals the hidden switch
TEXT_GAMECORNER_POSTER = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local poster = game.data.field.gameCornerPoster
local t = game.data.text
local text = t._GameCornerPosterSwitchBehindPosterText
or "Hey!\fA switch behind\nthe poster!?\nLet's push it!"
if game.save.flags[poster.event] then
game.stack:push(TextBox.new(game, text, done))
return
end
-- GameCornerPosterText: the SwitchBehindPosterText plays
-- SFX_SWITCH as it shows, then SFX_GO_INSIDE opens the stairs
require("src.core.Sound").play(game.data, "Switch")
game.stack:push(TextBox.new(game, text, function()
game.save.flags[poster.event] = true
require("src.core.Sound").play(game.data, "Go_Inside")
ow:replaceBlock(poster.x, poster.y, poster.openBlock)
done()
end))
end,
TEXT_GAMECORNER_CLERK1 = function(game, ow, npc, done)
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")
local t = game.data.text
game.stack:push(TextBox.new(game,
(t._GameCornerClerk1DoYouNeedSomeGameCoinsText
or "Do you need some\ngame coins?\f¥1000 for 50."), function()
game.stack:push(ChoiceBox.new(game, function(yes)
if not yes then
game.stack:push(TextBox.new(game,
t._GameCornerClerk1PleaseComePlaySometimeText
or "No? Please come\nplay sometime!", done))
return
end
-- scripts/GameCorner.asm GameCornerClerk1Text: coins need
-- the COIN CASE and room for at least 9 coins (Has9990Coins)
if not game.save.inventory.COIN_CASE then
game.stack:push(TextBox.new(game,
t._GameCornerClerk1DontHaveCoinCaseText
or "You don't have a\nCOIN CASE!", done))
return
end
if (game.save.coins or 0) >= 9990 then
game.stack:push(TextBox.new(game,
t._GameCornerClerk1CoinCaseIsFullText
or "Oops! Your COIN\nCASE is full.", done))
return
end
if game.save.money < 1000 then
game.stack:push(TextBox.new(game,
t._GameCornerClerk1CantAffordTheCoinsText
or "You can't afford\nthe coins!", done))
return
end
game.save.money = game.save.money - 1000
game.save.coins = math.min(9999, (game.save.coins or 0) + 50)
game.stack:push(TextBox.new(game,
(t._GameCornerClerk1ThanksHereAre50CoinsText
or "Thanks! Here are\nyour 50 coins!")
.. ("\fCOINS: %d"):format(game.save.coins), done))
end))
end))
end,
},
}
-- Red-version prize lists (data/events/prizes.asm, prize_mon_levels.asm)
local PRIZES = {
{ kind = "mon", species = "ABRA", level = 9, cost = 180 },
{ kind = "mon", species = "CLEFAIRY", level = 8, cost = 500 },
{ kind = "mon", species = "NIDORINA", level = 17, cost = 1200 },
{ kind = "mon", species = "DRATINI", level = 18, cost = 2800 },
{ kind = "mon", species = "SCYTHER", level = 25, cost = 5500 },
{ kind = "mon", species = "PORYGON", level = 26, cost = 9999 },
{ kind = "item", item = "TM_DRAGON_RAGE", cost = 3300 },
{ kind = "item", item = "TM_HYPER_BEAM", cost = 5500 },
{ kind = "item", item = "TM_SUBSTITUTE", cost = 7700 },
}
local function prizeCounter(game, ow, npc, done)
local ListMenu = require("src.ui.ListMenu")
local Commands = require("src.script.Commands")
local items = {}
for _, p in ipairs(PRIZES) do
local label
if p.kind == "mon" then
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
else
label = game.data.items[p.item].name
end
table.insert(items, { label = label, right = tostring(p.cost), value = p })
end
local list
list = ListMenu.new(game, "PRIZES (COINS)", items, {
footer = ("COINS %d"):format(game.save.coins or 0),
onChoose = function(item)
local p = item.value
if (game.save.coins or 0) < p.cost then
list.footer = "Not enough coins!"
return
end
game.save.coins = game.save.coins - p.cost
if p.kind == "mon" then
Commands.give_pokemon({ save = game.save, game = game },
p.species, p.level)
else
game.save.inventory[p.item] = (game.save.inventory[p.item] or 0) + 1
end
list.footer = ("Got it! COINS %d"):format(game.save.coins)
end,
onCancel = done,
})
game.stack:push(list)
end
M.GAME_CORNER_PRIZE_ROOM = {
talk = { -- the three prize counters are bg events
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_1 = prizeCounter,
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_2 = prizeCounter,
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_3 = prizeCounter,
},
}
-- -------------------------------------------------------------------
-- SS Anne departure (scripts/VermilionDock.asm): once HM01 is in hand
-- and the player steps off the dock, the ship sets sail.
-- -------------------------------------------------------------------
M.VERMILION_DOCK = {
onEnter = function(game, ow)
if game.save.flags.EVENT_SS_ANNE_LEFT then
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text._VermilionCitySailor1ShipSetSailText
or "The ship set sail.", function()
ow:startWarpTo("VERMILION_CITY", 19, 30, "up")
end))
elseif game.save.flags.EVENT_GOT_HM01 then
game.save.flags.EVENT_SS_ANNE_LEFT = true
require("src.core.Sound").play(game.data, "SS_Anne_Horn")
end
end,
}
return M
+636
View File
@@ -0,0 +1,636 @@
-- 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,
},
}
-- -------------------------------------------------------------------
-- 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")
return M
+518
View File
@@ -0,0 +1,518 @@
-- Gift NPCs and in-game trades. 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
-- fill the extracted text placeholders ({RAM:...}, {PLAYER})
local function fill(s, subs)
s = s:gsub("{PLAYER}", subs.player or "")
s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end)
return s
end
-- One-time item gift, following the original text_asm flow:
-- pre text (optional) -> GiveItem (bag-full refusal keeps the flag
-- unset, talk again after making room) -> received text -> optional
-- explanation; repeat visits get the already text.
local function gift(opts)
return function(game, ow, npc, done)
local t = text(game)
local itemName = game.data.items[opts.item].name
local subs = { ram = itemName, player = game.save.player.name }
local function say(label, fallback, cb)
push(game, fill(t[label] or fallback, subs), cb)
end
if game.save.flags[opts.flag] then
say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done)
return
end
local function give()
if not require("src.inventory.Bag").add(game.save, opts.item, 1) then
say(opts.noRoom, "You have no room\nfor this item!", done)
return
end
game.save.flags[opts.flag] = true
local idef = game.data.items[opts.item]
require("src.core.Sound").play(game.data,
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1")
say(opts.received, "{PLAYER} received\n{RAM:}!", function()
if opts.explain then
say(opts.explain, "", done)
else
done()
end
end)
end
if opts.pre then say(opts.pre, "", give) else give() end
end
end
-- Coin Case (scripts/CeladonDiner.asm, the busted gym guide)
M.CELADON_DINER = {
talk = {
TEXT_CELADONDINER_GYM_GUIDE = gift({
flag = "EVENT_GOT_COIN_CASE", item = "COIN_CASE",
pre = "_CeladonDinerGymGuideImFlatOutBustedText",
received = "_CeladonDinerGymGuideReceivedCoinCaseText",
noRoom = "_CeladonDinerGymGuideCoinCaseNoRoomText",
already = "_CeladonDinerGymGuideWinItBackText",
}),
},
}
-- TM18 Counter (scripts/CeladonMart3F.asm, the TV-game-shop clerk)
M.CELADON_MART_3F = {
talk = {
TEXT_CELADONMART3F_CLERK = gift({
flag = "EVENT_GOT_TM18", item = "TM_COUNTER",
pre = "_CeladonMart3FClerkTM18PreReceiveText",
received = "_CeladonMart3FClerkReceivedTM18Text",
explain = "_CeladonMart3FClerkTM18ExplanationText",
noRoom = "_CeladonMart3FClerkTM18NoRoomText",
}),
},
}
-- TM39 Swift (scripts/Route12Gate2F.asm)
M.ROUTE_12_GATE_2F = {
talk = {
TEXT_ROUTE12GATE2F_BRUNETTE_GIRL = gift({
flag = "EVENT_GOT_TM39", item = "TM_SWIFT",
pre = "_Route12Gate2FBrunetteGirlYouCanHaveThisText",
received = "_Route12Gate2FBrunetteGirlReceivedTM39Text",
explain = "_Route12Gate2FBrunetteGirlTM39ExplanationText",
noRoom = "_Route12Gate2FBrunetteGirlTM39NoRoomText",
}),
},
}
-- TM41 Softboiled (scripts/CeladonCity.asm, Gramps3)
M.CELADON_CITY = {
talk = {
TEXT_CELADONCITY_GRAMPS3 = gift({
flag = "EVENT_GOT_TM41", item = "TM_SOFTBOILED",
pre = "_CeladonCityGramps3Text",
received = "_CeladonCityGramps3ReceivedTM41Text",
explain = "_CeladonCityGramps3TM41ExplanationText",
noRoom = "_CeladonCityGramps3TM41NoRoomText",
}),
},
}
-- TM35 Metronome (scripts/CinnabarLabMetronomeRoom.asm)
M.CINNABAR_LAB_METRONOME_ROOM = {
talk = {
TEXT_CINNABARLABMETRONOMEROOM_SCIENTIST1 = gift({
flag = "EVENT_GOT_TM35", item = "TM_METRONOME",
pre = "_CinnabarLabMetronomeRoomScientist1Text",
received = "_CinnabarLabMetronomeRoomScientist1ReceivedTM35Text",
explain = "_CinnabarLabMetronomeRoomScientist1TM35ExplanationText",
noRoom = "_CinnabarLabMetronomeRoomScientist1TM35NoRoomText",
}),
},
}
-- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher; no pre text)
M.VIRIDIAN_CITY = {
talk = {
TEXT_VIRIDIANCITY_FISHER = gift({
flag = "EVENT_GOT_TM42", item = "TM_DREAM_EATER",
received = "_ViridianCityFisherReceivedTM42Text",
explain = "_ViridianCityFisherTM42ExplanationText",
noRoom = "_ViridianCityFisherTM42NoRoomText",
}),
},
}
-- TM36 Selfdestruct (scripts/SilphCo2F.asm, the rescued worker)
M.SILPH_CO_2F = {
talk = {
TEXT_SILPHCO2F_SILPH_WORKER_F = gift({
flag = "EVENT_GOT_TM36", item = "TM_SELFDESTRUCT",
received = "_SilphCo2FSilphWorkerFReceivedTM36Text",
explain = "_SilphCo2FSilphWorkerFTM36ExplanationText",
noRoom = "_SilphCo2FSilphWorkerFTM36NoRoomText",
}),
},
}
-- Free POTION sample (scripts/Route1.asm; the original burns the flag
-- even on a full bag -- we keep the port's kinder halt-and-retry)
M.ROUTE_1 = {
talk = {
TEXT_ROUTE1_YOUNGSTER1 = gift({
flag = "EVENT_GOT_POTION_SAMPLE", item = "POTION",
pre = "_Route1Youngster1MartSampleText",
received = "_Route1Youngster1GotPotionText",
noRoom = "_Route1Youngster1NoRoomText",
already = "_Route1Youngster1AlsoGotPokeballsText",
}),
},
}
-- The three in-game trades the port was missing (data/events/trades.asm
-- indices are 1-based in data/generated/field.lua trades)
M.ROUTE_11_GATE_2F = {
talk = {
TEXT_ROUTE11GATE2F_YOUNGSTER = {
{ "face_player" },
{ "trade", 1, "EVENT_TRADED_NIDORINO_FOR_NIDORINA" }, -- TERRY
},
},
}
M.ROUTE_18_GATE_2F = {
talk = {
TEXT_ROUTE18GATE2F_YOUNGSTER = {
{ "face_player" },
{ "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- MARC
},
},
}
M.UNDERGROUND_PATH_ROUTE_5 = {
talk = {
TEXT_UNDERGROUNDPATHROUTE5_LITTLE_GIRL = {
{ "face_player" },
{ "trade", 10, "EVENT_TRADED_NIDORAN_M_FOR_NIDORAN_F" }, -- SPOT
},
},
}
-- =====================================================================
-- Progression gates and rival ambushes
-- =====================================================================
local function inCoords(coords, x, y)
for _, c in ipairs(coords) do
if x == c[1] and y == c[2] then return true end
end
return false
end
-- coordinate block: show a line and push the player back one step
local function stepGate(opts)
return function(game, ow, x, y)
if not inCoords(opts.coords, x, y) then return false end
if not opts.blocked(game) then return false end
require("src.core.Sound").play(game.data, "Denied")
push(game, text(game)[opts.text] or opts.fallback,
function() ow:scriptMove(ow.player, opts.push, 1) end)
return true
end
end
-- Viridian Gym stays locked until the seven other badges are earned
-- (scripts/ViridianCity.asm ViridianCityCheckGymOpenScript: wObtained-
-- Badges == ~EARTHBADGE at (32,8) shoves the player off the door)
local SEVEN_BADGES = { "BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE",
"RAINBOWBADGE", "SOULBADGE", "MARSHBADGE",
"VOLCANOBADGE" }
local viridianGymLock = stepGate({
coords = { { 32, 8 } },
blocked = function(game)
for _, b in ipairs(SEVEN_BADGES) do
if not game.save.inventory[b] then return true end
end
return false
end,
text = "_ViridianCityGymLockedText",
fallback = "The GYM's doors\nare locked...",
push = "down",
})
-- story.lua's VIRIDIAN_CITY module owns the sleeping-old-man block;
-- chain it behind the gym lock (the registry keeps one onStep per map)
local viridianOldManStep = require("data.scripts.story").VIRIDIAN_CITY.onStep
M.VIRIDIAN_CITY.onStep = function(game, ow, x, y)
if viridianGymLock(game, ow, x, y) then return true end
return viridianOldManStep(game, ow, x, y)
end
-- Cinnabar Gym needs the SECRET KEY (scripts/CinnabarIsland.asm)
M.CINNABAR_ISLAND = {
onStep = stepGate({
coords = { { 18, 4 } },
blocked = function(game) return not game.save.inventory.SECRET_KEY end,
text = "_CinnabarIslandDoorIsLockedText",
fallback = "The door is\nlocked...",
push = "down",
}),
-- CinnabarIsland_Script line 6: ResetEvent EVENT_LAB_STILL_REVIVING_
-- FOSSIL on every (re)load of this map -- OverworldState:setMap runs
-- onEnter on every entry, not just the first, so this fires both
-- when the player walks out of the fossil lab back onto the island
-- and on any later re-entry (boat, Mansion exit, etc.), matching the
-- oracle's every-load map script. Paired with the deposit/pending/
-- ready state machine in data/scripts/story2.lua's
-- TEXT_CINNABARLABFOSSILROOM_SCIENTIST1.
onEnter = function(game, ow)
if game.save.flags.EVENT_LAB_STILL_REVIVING_FOSSIL then
game.save.flags.EVENT_LAB_STILL_REVIVING_FOSSIL = nil
end
end,
}
-- Pewter's youngster stops you leaving east before Brock is beaten
-- (scripts/PewterCity.asm PewterCityCheckPlayerLeavingEastScript; the
-- original escorts you to the gym, we walk you back a step)
M.PEWTER_CITY = {
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_BEAT_BROCK then return false end
if not inCoords({ { 35, 17 }, { 36, 17 }, { 37, 18 }, { 37, 19 } }, x, y) then
return false
end
local t = text(game)
push(game, t._PewterCityYoungsterYoureATrainerFollowMeText
or "Hey! You're a\ntrainer, right?", function()
push(game, t._PewterCityYoungsterGoTakeOnBrockText
or "Go take on BROCK\nat the GYM first!", function()
ow:scriptMove(ow.player, "left", 1)
end)
end)
return true
end,
}
-- Rival ambush: show the hidden rival, walk him up to the player, run
-- the battle rows, march him back and hide him. On a loss the walk is
-- skipped (the blackout rebuilds the map mid-script).
local function runAmbush(game, ow, rows, playerFacing)
if ow.runner:isRunning() then return false end
ow.player.facing = playerFacing
-- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music
-- takes over and the map theme returns after the victory jingle
require("src.core.Music").play(game.data, "Music_MeetRival")
ow.runner:run(rows)
return true
end
-- Route 22 rival, both visits (scripts/Route22.asm). pokered arms
-- EVENT_ROUTE22_RIVAL_WANTS_BATTLE in Oak's lab (expired by Pewter
-- Gym) and again in Viridian Gym; we derive the same windows from the
-- surrounding story flags so old saves work too.
local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag, py)
return {
{ "show_object", "ROUTE_22", objName }, -- 1
{ "move_npc_to", objIndex, 28, py }, -- 2
{ "face_object", objIndex, "right" }, -- 3
{ "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4
{ "rival_battle", oppClass, baseParty }, -- 5
{ "jump_if_false", 11 }, -- 6
{ "set_flag", beatFlag }, -- 7
{ "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8
{ "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9
{ "move_npc_to", objIndex, 25, 5 }, -- 10
{ "hide_object", "ROUTE_22", objName }, -- 11
}
end
M.ROUTE_22 = {
onStep = function(game, ow, x, y)
if not inCoords({ { 29, 4 }, { 29, 5 } }, x, y) then return false end
local f = game.save.flags
if f.EVENT_GOT_POKEDEX and not f.EVENT_BEAT_BROCK
and not f.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE then
return runAmbush(game, ow,
route22Scene(1, 1, "ROUTE22_RIVAL1", "OPP_RIVAL1", 4,
"EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE", y), "left")
end
if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then
return runAmbush(game, ow,
route22Scene(2, 2, "ROUTE22_RIVAL2", "OPP_RIVAL2", 10,
"EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE", y), "left")
end
return false
end,
}
-- Cerulean City: the Nugget Bridge rival ambush (CeruleanCityCoords2),
-- the TM28 rocket thief with his forced-fight cells (Coords1), and the
-- cave guard who steps aside once you are Champion.
local function ceruleanRivalScene(px, py)
return {
{ "show_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 1
{ "move_npc_to", 1, px, py - 1 }, -- 2
{ "face_object", 1, "down" }, -- 3
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
{ "jump_if_false", 10 }, -- 6
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
{ "move_npc_to", 1, 20, 2 }, -- 9
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 10
}
end
-- scripts/CeruleanCity.asm CeruleanCityRocketText: fight the thief,
-- then he returns TM28 (DIG) and hurries off
local rocketRows = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_GOT_TM28" }, -- 2
{ "jump_if_true", 15 }, -- 3
{ "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4
{ "jump_if_true", 9 }, -- 5
{ "show_text", "_CeruleanCityRocketText" }, -- 6
{ "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 7
{ "jump_if_false", 16 }, -- 8
{ "show_text", "_CeruleanCityRocketIllReturnTheTMText" }, -- 9
{ "set_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 10
{ "give_item", "TM_DIG", 1, false }, -- 11 (row 13 prints)
{ "set_flag", "EVENT_GOT_TM28" }, -- 12
{ "show_text", "_CeruleanCityRocketReceivedTM28Text" }, -- 13
{ "show_text", "_CeruleanCityRocketIBetterGetMovingText" }, -- 14
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_ROCKET" }, -- 15
}
M.CERULEAN_CITY = {
talk = {
TEXT_CERULEANCITY_ROCKET = rocketRows,
},
onEnter = function(game, ow)
if game.save.flags.EVENT_BEAT_CHAMPION_RIVAL then
local Commands = require("src.script.Commands")
Commands.hide_object({ game = game, save = game.save, overworld = ow },
"CERULEAN_CITY", "CERULEANCITY_SUPER_NERD3")
end
end,
onStep = function(game, ow, x, y)
local f = game.save.flags
if not f.EVENT_BEAT_CERULEAN_ROCKET_THIEF
and inCoords({ { 30, 7 }, { 30, 9 } }, x, y) then
if ow.runner:isRunning() then return false end
local rocket = ow:npcByIndex(2)
if rocket then
ow.player.facing = y < 8 and "down" or "up"
ow.runner:run(rocketRows, { npc = rocket })
return true
end
return false
end
if not f.EVENT_BEAT_CERULEAN_RIVAL
and inCoords({ { 20, 6 }, { 21, 6 } }, x, y) then
return runAmbush(game, ow, ceruleanRivalScene(x, y), "up")
end
return false
end,
}
-- The Pewter museum's fossil exhibits (engine/events/hidden_events/
-- museum_fossils.asm: DisplayMonFrontSpriteInBox + the plaque text)
M.MUSEUM_1F = {
onInteract = function(game, ow, fx, fy)
if ow.player.facing ~= "up" then return false end
local PicBox = require("src.ui.PicBox")
local t = text(game)
if fx == 2 and fy == 3 then
game.stack:push(PicBox.new(game,
"assets/generated/battle/front/fossilaerodactyl.png",
t._AerodactylFossilText or "AERODACTYL Fossil"))
return true
end
if fx == 2 and fy == 6 then
game.stack:push(PicBox.new(game,
"assets/generated/battle/front/fossilkabutops.png",
t._KabutopsFossilText or "KABUTOPS Fossil"))
return true
end
return false
end,
}
-- The Pewter Center's singing JIGGLYPUFF (scripts/PewterPokecenter.asm
-- plays MUSIC_JIGGLYPUFF_SONG, then the map theme resumes)
M.PEWTER_POKECENTER = {
talk = {
TEXT_PEWTERPOKECENTER_JIGGLYPUFF = function(game, ow, npc, done)
require("src.core.Music").playOnce(game.data, "Music_JigglypuffSong")
push(game, text(game)._PewterPokecenterJigglypuffText
or "JIGGLYPUFF: Puu\npupuu!", done)
end,
},
}
-- Cycling Road gate guards (scripts/Route16Gate1F.asm /
-- Route18Gate1F.asm): without a BICYCLE in the bag the guard stops you
-- on the west-side cells and walks you back
local function bikeGateGuard(coords, stopText, explainText)
return function(game, ow, x, y)
if game.save.inventory.BICYCLE then return false end
if not inCoords(coords, x, y) then return false end
local t = text(game)
push(game, t[stopText] or "Hey! Wait up!", function()
push(game, t[explainText] or "You need a\nBICYCLE for\nCYCLING ROAD!", function()
ow:scriptMove(ow.player, "up", 1)
end)
end)
return true
end
end
M.ROUTE_16_GATE_1F = {
onStep = bikeGateGuard(
{ { 4, 7 }, { 4, 8 }, { 4, 9 }, { 4, 10 } },
"_Route16Gate1FGuardWaitUpText",
"_Route16Gate1FGuardNoPedestriansAllowedText"),
}
M.ROUTE_18_GATE_1F = {
onStep = bikeGateGuard(
{ { 4, 3 }, { 4, 4 }, { 4, 5 }, { 4, 6 } },
"_Route18Gate1FGuardExcuseMeText",
"_Route18Gate1FGuardYouNeedABicycleText"),
}
-- Silph Co. 7F rival ambush (scripts/SilphCo7F.asm
-- SilphCo7FDefaultScript: coords (3,2)/(3,3), the rival at (3,7) walks
-- up, MUSIC_MEET_RIVAL, OPP_RIVAL2 parties 7-9 by starter, then he
-- wishes you luck, walks off right and disappears; one-time via
-- EVENT_BEAT_SILPH_CO_RIVAL)
M.SILPH_CO_7F = {
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_BEAT_SILPH_CO_RIVAL then return false end
if not inCoords({ { 3, 2 }, { 3, 3 } }, x, y) then return false end
return runAmbush(game, ow, {
{ "show_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 1
{ "show_text", "_SilphCo7FRivalText" }, -- 2
{ "move_npc_to", 9, 3, y + 1 }, -- 3
{ "face_object", 9, "up" }, -- 4
{ "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5
{ "rival_battle", "OPP_RIVAL2", 7 }, -- 6
{ "jump_if_false", 12 }, -- 7
{ "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8
{ "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9
{ "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10
{ "move_npc_to", 9, 5, y + 1 }, -- 11
{ "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 12
}, "down")
end,
}
-- S.S. Anne 2F rival ambush (scripts/SSAnne2F.asm; coords 36/37,8)
M.SS_ANNE_2F = {
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_BEAT_SS_ANNE_RIVAL then return false end
if not inCoords({ { 36, 8 }, { 37, 8 } }, x, y) then return false end
local onLeft = x == 36
return runAmbush(game, ow, {
{ "show_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 1
{ "move_npc_to", 2, 36, onLeft and 7 or 8 }, -- 2
{ "face_object", 2, onLeft and "down" or "right" }, -- 3
{ "show_text", "_SSAnne2FRivalText" }, -- 4
{ "rival_battle", "OPP_RIVAL2", 1 }, -- 5
{ "jump_if_false", 10 }, -- 6
{ "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7
{ "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8
{ "move_npc_to", 2, 36, 4 }, -- 9
{ "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 10
}, onLeft and "up" or "left")
end,
}
return M
+280
View File
@@ -0,0 +1,280 @@
-- Sixth batch of hand-ported map scripts (parity sweep 2026-07-12):
-- the Pokémon Mansion statue switches, the Cinnabar Gym quiz doors,
-- and the Indigo Plateau lobby's Elite Four rematch reset. 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
-- -------------------------------------------------------------------
-- Pokémon Mansion switches (scripts/PokemonMansion1F/2F/3F/B1F.asm):
-- one shared toggle, EVENT_MANSION_SWITCH_ON, flips door/wall blocks
-- on all four floors. Each floor's Mansion*CheckReplaceSwitchDoorBlocks
-- runs on map load, so the state persists purely through the event
-- flag; pressing a switch toggles it (CheckAndSetEvent/ResetEvent) and
-- reapplies the current floor's blocks.
-- Block ids: $2d horizontal gate (1F/B1F "door" wall), $54 horizontal
-- gate, $5f vertical gate, $e open floor. Coords below are
-- ReplaceTileBlock's (X, Y) block coordinates (asm passes b=Y, c=X).
-- -------------------------------------------------------------------
-- per floor: { bx, by, offBlock, onBlock }
local MANSION_BLOCKS = {
POKEMON_MANSION_1F = {
{ 12, 6, 0x0e, 0x2d },
{ 8, 3, 0x2d, 0x0e },
{ 10, 8, 0x2d, 0x0e },
{ 13, 13, 0x2d, 0x0e },
},
POKEMON_MANSION_2F = {
{ 4, 2, 0x0e, 0x5f },
{ 9, 4, 0x54, 0x0e },
{ 3, 11, 0x5f, 0x0e },
},
POKEMON_MANSION_3F = {
{ 7, 2, 0x0e, 0x5f },
{ 7, 5, 0x5f, 0x0e },
},
POKEMON_MANSION_B1F = {
{ 13, 8, 0x0e, 0x2d },
{ 6, 11, 0x0e, 0x5f },
{ 4, 3, 0x5f, 0x0e },
{ 8, 8, 0x54, 0x0e },
},
}
local function applyMansionBlocks(game, ow)
local rows = MANSION_BLOCKS[ow.map.id]
if not rows then return end
local on = game.save.flags.EVENT_MANSION_SWITCH_ON
for _, r in ipairs(rows) do
ow:replaceBlock(r[1], r[2], on and r[4] or r[3])
end
end
-- switch statues (data/events/hidden_events.asm Mansion*Script_Switches,
-- all facing up); 2F/3F/B1F reuse the 2F switch text in pokered
local function mansionFloor(switchCoords, textPrefix)
return {
onEnter = applyMansionBlocks,
onInteract = function(game, ow, fx, fy)
if ow.player.facing ~= "up" then return false end
local hit = false
for _, c in ipairs(switchCoords) do
if fx == c[1] and fy == c[2] then hit = true break end
end
if not hit then return false end
local t = text(game)
ask(game, t[textPrefix .. "SwitchText"] or "A secret switch!\fPress it?",
function(yes)
if not yes then
push(game, t[textPrefix .. "SwitchNotPressedText"]
or "Not quite yet!")
return
end
local f = game.save.flags
if f.EVENT_MANSION_SWITCH_ON then
f.EVENT_MANSION_SWITCH_ON = nil
else
f.EVENT_MANSION_SWITCH_ON = true
end
require("src.core.Sound").play(game.data, "Go_Inside")
applyMansionBlocks(game, ow)
push(game, t[textPrefix .. "SwitchPressedText"] or "Who wouldn't?")
end)
return true
end,
}
end
M.POKEMON_MANSION_1F = mansionFloor({ { 2, 5 } }, "_PokemonMansion1F")
M.POKEMON_MANSION_2F = mansionFloor({ { 2, 11 } }, "_PokemonMansion2F")
M.POKEMON_MANSION_3F = mansionFloor({ { 10, 5 } }, "_PokemonMansion2F")
M.POKEMON_MANSION_B1F = mansionFloor({ { 20, 3 }, { 18, 25 } },
"_PokemonMansion2F")
-- 3F floor holes (PokemonMansion3FDefaultScript.holeCoords +
-- data/maps/special_warps.asm DungeonWarpData): stepping on a hole
-- drops the player -- (16,14)/(17,14) land on 1F at (16,14), the only
-- way into the sealed basement-stairs room; (19,14) lands on 2F at
-- (18,14).
local MANSION_HOLES = {
{ 16, 14, "POKEMON_MANSION_1F", 16, 14 },
{ 17, 14, "POKEMON_MANSION_1F", 16, 14 },
{ 19, 14, "POKEMON_MANSION_2F", 18, 14 },
}
M.POKEMON_MANSION_3F.onStep = function(game, ow, x, y)
for _, h in ipairs(MANSION_HOLES) do
if x == h[1] and y == h[2] then
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
return true
end
end
return false
end
-- -------------------------------------------------------------------
-- Cinnabar Gym quiz doors (engine/events/hidden_events/
-- cinnabar_gym_quiz.asm + scripts/CinnabarGym.asm): six quiz machines;
-- a correct answer opens that room's gate block, a wrong one plays
-- SFX_DENIED and sics the room's trainer on you. Beating the trainer
-- (via the quiz or by talking to him) also opens the gate
-- (CinnabarGymOpenGateScript). Gates persist via per-door event flags.
-- -------------------------------------------------------------------
local GYM_OPEN_BLOCK = 0x0e
-- machine i: quiz tile (x,y), whether YES is correct (answer nibble
-- FALSE = menu item 0 = YES), the gate's block (x,y,closed id) from
-- CinnabarGymGateCoords, and the guarding trainer's object index
-- (wOpponentAfterWrongAnswer = gate index + 2 = SUPER_NERD(i+1))
local GYM_MACHINES = {
{ x = 15, y = 7, yes = true, gate = { 9, 3, 0x54 }, npc = 3 },
{ x = 10, y = 1, yes = false, gate = { 6, 3, 0x54 }, npc = 4 },
{ x = 9, y = 7, yes = false, gate = { 6, 6, 0x54 }, npc = 5 },
{ x = 9, y = 13, yes = false, gate = { 3, 8, 0x5f }, npc = 6 },
{ x = 1, y = 13, yes = true, gate = { 2, 6, 0x54 }, npc = 7 },
{ x = 1, y = 7, yes = false, gate = { 2, 3, 0x54 }, npc = 8 },
}
local function gymGateFlag(i)
return "EVENT_CINNABAR_GYM_GATE" .. (i - 1) .. "_UNLOCKED"
end
local function applyGymGates(game, ow)
for i, m in ipairs(GYM_MACHINES) do
local open = game.save.flags[gymGateFlag(i)]
or game.save.defeatedTrainers["CINNABAR_GYM_obj_" .. m.npc]
ow:replaceBlock(m.gate[1], m.gate[2], open and GYM_OPEN_BLOCK or m.gate[3])
end
end
-- beating a guardian opens his gate like CinnabarGymOpenGateScript
local function syncGymGatesAfterBattle(game, ow)
for i, m in ipairs(GYM_MACHINES) do
if game.save.defeatedTrainers["CINNABAR_GYM_obj_" .. m.npc]
and not game.save.flags[gymGateFlag(i)] then
game.save.flags[gymGateFlag(i)] = true
require("src.core.Sound").play(game.data, "Go_Inside")
end
end
applyGymGates(game, ow)
end
M.CINNABAR_GYM = {
onEnter = applyGymGates,
onVictory = syncGymGatesAfterBattle,
onInteract = function(game, ow, fx, fy)
if ow.player.facing ~= "up" then return false end
local index, machine
for i, m in ipairs(GYM_MACHINES) do
if fx == m.x and fy == m.y then index, machine = i, m break end
end
if not machine then return false end
local t = text(game)
local Sound = require("src.core.Sound")
push(game, t._CinnabarGymQuizIntroText
or "POKéMON Quiz!\fGet it right and\nthe door opens!", function()
ask(game, t["_CinnabarQuizQuestionsText" .. index] or "Well?",
function(yes)
if yes == machine.yes then
-- CinnabarGymQuizCorrectText: item jingle, then the gate
-- slides open (SFX_GO_INSIDE) if it was still locked
Sound.play(game.data, "Get_Item1")
push(game, t._CinnabarGymQuizCorrectText
or "You're absolutely\ncorrect!\fGo on through!", function()
if not game.save.flags[gymGateFlag(index)] then
game.save.flags[gymGateFlag(index)] = true
Sound.play(game.data, "Go_Inside")
end
applyGymGates(game, ow)
end)
return
end
Sound.play(game.data, "Denied")
push(game, t._CinnabarGymQuizIncorrectText or "Sorry! Bad call!",
function()
local npc = ow:npcByIndex(machine.npc)
if npc and not ow:trainerDefeated(npc) then
ow:engageTrainer(npc, function() end)
end
end)
end)
end)
return true
end,
}
-- -------------------------------------------------------------------
-- Indigo Plateau lobby: the Elite Four rematch reset
-- (scripts/IndigoPlateauLobby.asm: on entry, ResetEvent
-- EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH; then, if BIT_STARTED_ELITE_4
-- is set, clear it and ResetEventRange INDIGO_PLATEAU_EVENTS_START ..
-- EVENT_LANCES_ROOM_LOCK_DOOR so the whole league can be re-fought.
-- BIT_STARTED_ELITE_4 is set whenever Lorelei's room loads
-- (LoreleiShowOrHideExitBlock).
-- -------------------------------------------------------------------
-- constants/event_constants.asm $8E0..EVENT_LANCES_ROOM_LOCK_DOOR
local E4_RESET_FLAGS = {
"EVENT_BEAT_LORELEIS_ROOM_TRAINER_0", "EVENT_AUTOWALKED_INTO_LORELEIS_ROOM",
"EVENT_BEAT_BRUNOS_ROOM_TRAINER_0", "EVENT_AUTOWALKED_INTO_BRUNOS_ROOM",
"EVENT_BEAT_AGATHAS_ROOM_TRAINER_0", "EVENT_AUTOWALKED_INTO_AGATHAS_ROOM",
"EVENT_BEAT_LANCES_ROOM_TRAINER_0", "EVENT_BEAT_LANCE",
"EVENT_LANCES_ROOM_LOCK_DOOR",
-- port-side: the run-scoped champion gate (EVENT_BEAT_CHAMPION_RIVAL
-- itself stays set, like pokered's out-of-range flag)
"EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN",
}
local E4_TRAINER_KEYS = {
"LORELEIS_ROOM_obj_1", "BRUNOS_ROOM_obj_1",
"AGATHAS_ROOM_obj_1", "LANCES_ROOM_obj_1",
}
M.INDIGO_PLATEAU_LOBBY = {
onEnter = function(game, ow)
local f = game.save.flags
f.EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH = nil
-- old saves from before EVENT_STARTED_ELITE_4 existed derive it
-- from the run's progress flags
local started = f.EVENT_STARTED_ELITE_4
if not started then
for _, flag in ipairs(E4_RESET_FLAGS) do
if f[flag] then started = true break end
end
end
if not started then return end
f.EVENT_STARTED_ELITE_4 = nil
for _, flag in ipairs(E4_RESET_FLAGS) do f[flag] = nil end
for _, key in ipairs(E4_TRAINER_KEYS) do
game.save.defeatedTrainers[key] = nil
end
end,
}
-- Lorelei's room load marks the challenge as started (the wElite4Flags
-- bit in LoreleiShowOrHideExitBlock); the exit-seal onEnter from
-- story4.lua still runs
local loreleisRoom = require("data.scripts.story4").LORELEIS_ROOM
local loreleiSeal = loreleisRoom.onEnter
M.LORELEIS_ROOM = {
onEnter = function(game, ow)
game.save.flags.EVENT_STARTED_ELITE_4 = true
loreleiSeal(game, ow)
end,
}
return M
+120
View File
@@ -0,0 +1,120 @@
-- Gym-guide NPCs: the helpful trainer stationed near each gym's door.
-- Each pokered <Gym>GymGuideText is a text_asm that branches on whether the
-- leader's badge has already been earned; only Pewter's guide also asks
-- a YES/NO question first (the answer only changes the transitional
-- line -- the advice text that follows is the same either way).
--
-- Row-list rows use the numeric-jump-target style from
-- data/scripts/oaks_lab.lua; see src/script/ScriptRunner.lua for how
-- jump/jump_if_true/jump_if_false select the next row (1-based index).
local M = {}
-- Common "beaten the leader? -> congratulations : champ-in-making advice"
-- shape shared by 7 of the 8 guides (CheckEvent EVENT_BEAT_<LEADER> ...).
local function badgeBranch(beatFlag, champText, beatText)
return {
{ "check_flag", beatFlag }, -- 1
{ "jump_if_true", 5 }, -- 2
{ "show_text", champText }, -- 3
{ "jump", 6 }, -- 4 (skip the beaten-text row)
{ "show_text", beatText }, -- 5
}
end
M.CERULEAN_GYM = {
talk = {
-- scripts/CeruleanGym.asm CeruleanGymGymGuideText
TEXT_CERULEANGYM_GYM_GUIDE = badgeBranch("EVENT_BEAT_MISTY",
"_CeruleanGymGymGuideChampInMakingText",
"_CeruleanGymGymGuideBeatMistyText"),
},
}
M.CINNABAR_GYM = {
talk = {
-- scripts/CinnabarGym.asm CinnabarGymGymGuideText
TEXT_CINNABARGYM_GYM_GUIDE = badgeBranch("EVENT_BEAT_BLAINE",
"_CinnabarGymGymGuideChampInMakingText",
"_CinnabarGymGymGuideBeatBlaineText"),
},
}
M.FUCHSIA_GYM = {
talk = {
-- scripts/FuchsiaGym.asm FuchsiaGymGymGuideText
TEXT_FUCHSIAGYM_GYM_GUIDE = badgeBranch("EVENT_BEAT_KOGA",
"_FuchsiaGymGymGuideChampInMakingText",
"_FuchsiaGymGymGuideBeatKogaText"),
},
}
-- TEXT_GAMECORNER_GYM_GUIDE is displayed on the GAME_CORNER map (the
-- gym guide object_event stands just inside the Game Corner door, next
-- to the stairs down to Celadon Gym) -- scripts/GameCorner.asm
-- GameCornerGymGuideText, gated on Celadon's leader ERIKA.
M.GAME_CORNER = {
talk = {
TEXT_GAMECORNER_GYM_GUIDE = badgeBranch("EVENT_BEAT_ERIKA",
"_GameCornerGymGuideChampInMakingText",
"_GameCornerGymGuideTheyOfferRarePokemonText"),
},
}
M.SAFFRON_GYM = {
talk = {
-- scripts/SaffronGym.asm SaffronGymGymGuideText
TEXT_SAFFRONGYM_GYM_GUIDE = badgeBranch("EVENT_BEAT_SABRINA",
"_SaffronGymGuideChampInMakingText",
"_SaffronGymGuideBeatSabrinaText"),
},
}
M.VERMILION_GYM = {
talk = {
-- scripts/VermilionGym.asm VermilionGymGymGuideText
-- (checks wBeatGymFlags BIT_THUNDERBADGE rather than CheckEvent, but
-- it's the same underlying condition as EVENT_BEAT_LT_SURGE)
TEXT_VERMILIONGYM_GYM_GUIDE = badgeBranch("EVENT_BEAT_LT_SURGE",
"_VermilionGymGymGuideChampInMakingText",
"_VermilionGymGymGuideBeatLTSurgeText"),
},
}
M.VIRIDIAN_GYM = {
talk = {
-- scripts/ViridianGym.asm ViridianGymGymGuideText
-- (checks EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI in pokered; the port's
-- equivalent flag set on winning that battle is EVENT_BEAT_GIOVANNI,
-- see data/scripts/victories.lua OPP_GIOVANNI#3)
TEXT_VIRIDIANGYM_GYM_GUIDE = badgeBranch("EVENT_BEAT_GIOVANNI",
"_ViridianGymGuidePreBattleText",
"_ViridianGymGuidePostBattleText"),
},
}
-- Pewter's guide (scripts/PewterGym.asm PewterGymGuideText) is the one
-- with a real YES/NO branch: before the badge is earned he asks (via
-- PrintText + YesNoChoice on the "I'm no trainer, but I can tell you
-- how to win!" text), and BOTH answers lead into the same advice text --
-- only the one-line lead-in differs ("All right! Let's get happening!"
-- on YES vs. "It's a free service! Let's get happening!" on NO). Once
-- BOULDERBADGE is set he just congratulates you.
M.PEWTER_GYM = {
talk = {
TEXT_PEWTERGYM_GYM_GUIDE = {
{ "check_flag", "EVENT_BEAT_BROCK" }, -- 1
{ "jump_if_true", 10 }, -- 2
{ "ask", "_PewterGymGuidePreAdviceText" }, -- 3
{ "jump_if_false", 7 }, -- 4
{ "show_text", "_PewterGymGuideBeginAdviceText" }, -- 5 (YES)
{ "jump", 8 }, -- 6
{ "show_text", "_PewterGymGuideFreeServiceText" }, -- 7 (NO)
{ "show_text", "_PewterGymGuideAdviceText" }, -- 8 (common)
{ "jump", 11 }, -- 9
{ "show_text", "_PewterGymGuidePostBattleText" }, -- 10 (beaten)
},
},
}
return M
+42
View File
@@ -0,0 +1,42 @@
-- Rewards for winning specific trainer battles, keyed by
-- "OPP_CLASS#partyIndex" (the object_event trainer args). Hand-ported
-- from the leaders'/bosses' text_asm victory scripts:
-- gym badges: scripts/PewterGym.asm ... ViridianGym.asm
-- Rocket Hideout Giovanni: his Silph Scope is an item ball next to him
-- (data/maps/objects/RocketHideoutB4F.asm), so no reward entry needed.
-- The TM each gym leader hands out afterwards is also ported.
return {
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
item = "TM_BIDE" },
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
item = "TM_BUBBLEBEAM" },
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
item = "TM_THUNDERBOLT" },
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
item = "TM_MEGA_DRAIN" },
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
item = "TM_TOXIC" },
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
item = "TM_PSYWAVE" },
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
item = "TM_FIRE_BLAST" },
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
item = "TM_FISSURE" },
-- Silph Co. Giovanni: unlocks the president's Master Ball gift
["OPP_GIOVANNI#2"] = { flag = "EVENT_BEAT_SILPH_CO_GIOVANNI" },
-- Fighting Dojo Karate Master (scripts/FightingDojo.asm
-- FightingDojoKarateMasterPostBattleScript sets EVENT_BEAT_KARATE_MASTER,
-- which gates the HITMONLEE/HITMONCHAN gift). OPP_BLACKBELT party 1 is
-- only him (data/maps/objects/FightingDojo.asm).
["OPP_BLACKBELT#1"] = { flag = "EVENT_BEAT_KARATE_MASTER" },
-- Elite Four progress flags (their rooms' door logic isn't ported, but
-- the flags make the Hall of Fame checkable)
["OPP_LORELEI#1"] = { flag = "EVENT_BEAT_LORELEIS_ROOM_TRAINER_0" },
["OPP_BRUNO#1"] = { flag = "EVENT_BEAT_BRUNOS_ROOM_TRAINER_0" },
["OPP_AGATHA#1"] = { flag = "EVENT_BEAT_AGATHAS_ROOM_TRAINER_0" },
["OPP_LANCE#1"] = { flag = "EVENT_BEAT_LANCE" },
}
+100
View File
@@ -0,0 +1,100 @@
# Architecture
```text
packaged first boot
user-provided Pokemon Red ROM
|
v
RomImporter + RomExtractor (Lua)
|
+--> private data/generated/*.lua
+--> private assets/generated/**/*.png
+--> private assets/generated/audio/programs.bin
|
v
LÖVE2D engine + ChipAudio
```
The importer validates the ROM SHA-1, decodes tables and graphics using
bundled address/name metadata, and writes a private cache. It releases the ROM
after import and does not copy it into the cache. Normal gameplay reads only
the generated files.
`tools/build_data.py` is a separate Python/Pillow developer path that writes
the same core data and graphics into the source tree for verification.
## Runtime layout
| Area | Files | Role |
| --- | --- | --- |
| import | `src/import/RomImporter.lua` | first-boot UI, ROM validation, cache ownership |
| | `src/import/RomExtractor.lua` | ROM tables, text, pictures, PNGs, audio programs |
| core | `src/core/Game.lua` | service owner: data, input, renderer, stack, save |
| | `src/core/Data.lua` | loads `data/generated/*`, resolves TEXT_* pointers |
| | `src/core/ChipAudio.lua` | streams ROM music programs and synthesizes SFX/cries |
| | `src/core/FixedStep.lua` | 60 Hz fixed-step loop |
| | `src/core/Input.lua` | GB button abstraction, per-step edge detection |
| | `src/core/StateStack.lua` | stack of states; top updates, draws bottom-up from the last opaque state |
| | `src/core/SaveData.lua` | Lua-serialized save in the LÖVE save dir |
| render | `src/render/Renderer.lua` | 160x144 canvas, integer nearest scaling |
| | `src/render/TileRenderer.lua` | one SpriteBatch per map (8x8 quads) + border-block ring |
| | `src/render/SpriteRenderer.lua` | 6-frame walker sheets, flipped right facing |
| | `src/render/Font.lua` | glyph rendering via charmap (greedy longest match) |
| | `src/render/TextBox.lua` | dialogue box: typewriter, `\n` line, `\v` scroll, `\f` page |
| | `src/render/Camera.lua`, `Transition.lua` | follow camera, warp fades |
| world | `src/world/Map.lua` | cell queries: walkable/grass/door/warp/sign (bottom-left-tile rule) |
| | `src/world/MapLoader.lua` | generated def -> runtime Map, cached |
| | `src/world/Player.lua`, `NPC.lua` | grid movement, walk animation, wander AI |
| | `src/world/Collision.lua` | tile + entity + bounds checks |
| | `src/world/Warp.lua` | arrive-on-door and walk-off-edge warp rules, LAST_MAP |
| | `src/world/Encounter.lua` | Gen 1 encounter rate + slot buckets |
| | `src/world/OverworldController.lua` | the overworld state: input, interactions, connections, encounters |
| script | `src/script/ScriptRunner.lua` | coroutine executor for command lists |
| | `src/script/Commands.lua` | show_text, flags, battles, warps, movement, objects... |
| | `src/script/Flags.lua` | named event flags in the save |
| pokemon | `src/pokemon/*` | instances, Gen 1 stat calc, growth curves, party |
| battle | `src/battle/BattleState.lua` | battle flow + menus + message queue |
| | `src/battle/Damage.lua` | Gen 1 damage/crit/accuracy formulas |
| | `src/battle/TypeChart.lua`, `TurnOrder.lua`, `Status.lua`, `MoveEffects.lua` | subsystems |
| | `src/battle/Experience.lua`, `Catching.lua`, `TrainerAI.lua` | exp/levels, Gen 1 catch algorithm, AI |
| | `src/battle/rulesets/` | `gen1_faithful` (default) vs `modern_clean` |
| ui | `src/ui/*` | start menu, generic menu, yes/no box, party/bag lists |
| | `tools/save-editor/` | Standalone save editor (`love . --editor`) |
## Map scripts
Map-specific behavior lives in `data/scripts/<map>.lua`, keyed by the
TEXT_* constants from the map's object events. The engine dispatches a
talk interaction to (in order):
1. a hand-ported script in `data/scripts/` (`{ talk = { TEXT_X = {...} } }`),
2. the generic trainer path (object has trainer args from `object_event`),
3. the extracted plain text via the map's text pointer table.
Scripts are arrays of `{ "command", args... }` rows executed by a
coroutine so `show_text`, `ask`, `start_battle`, `warp`, `wait` block
naturally. Every hand-ported script cites its pokered source file.
## Coordinates
- **block**: 32x32 px, the unit of `.blk` layouts (`map.width/height`)
- **cell**: 16x16 px walk grid, the unit of all object/warp coordinates
- **tile**: 8x8 px graphics; a cell is 2x2 tiles, a block 4x4
A cell's behavior (collision, grass, door, warp tile) is decided by its
bottom-left 8x8 tile, matching the original engine's "tile at the
sprite's feet" checks.
## Verification
- `luajit tests/run_tests.lua` - headless behavior suite over real
generated data (collision, warps, text, stats, damage, growth, type
chart, encounters, a full scripted battle, save round-trip) using a
`love` API stub.
- `luajit tests/run_save_editor_tests.lua` (plus the task-specific suites)
- save editor pure logic and panel click tests.
- `POKEPORT_AUTOPILOT=1 love .` - scripted end-to-end run (walk Pallet
Town, read the sign, enter Oak's Lab, take a starter, beat the rival,
exit, cross into Route 1, win a wild battle) that captures screenshots.
- `POKEPORT_DRIVER=tests/drivers/audio_runtime_test.lua love .` - imports
and queues title music, a sound effect, and a Pokemon cry.
+515
View File
@@ -0,0 +1,515 @@
# Behavior porting notes
What was ported from pokered's engine code and where it came from.
## Overworld
- **Collision rule** (`home/overworld.asm` tile-in-front checks): a 16x16
cell is passable when its bottom-left 8x8 tile is in the tileset's
`coll_tiles` list. Verified against Pallet Town's fences/houses/water
and Oak's Lab furniture.
- **Warp activation** (`home/overworld.asm` CheckWarpsNoCollision /
ExtraWarpCheck): a warp fires when arriving on a warp whose standing
tile is in the tileset's door or warp tile list, or when standing on a
warp and walking off the map edge (interior exit mats). Both paths are
data-driven from `door_tile_ids.asm` / `warp_tile_ids.asm`.
- **LAST_MAP warps** return to the remembered outdoor map/position, like
`wLastMap`.
- **Connections** (`map_header` connection directives): crossing an edge
places the player at `destCoord = curCoord - offset*2` cells on the
destination's opposite edge.
- **Movement**: tile-by-tile, 1 px/frame at 60 fps (16 frames per step),
tap-to-turn without stepping, hold-to-walk, input locked mid-step.
- **Wild encounters** (`engine/battle/wild_encounters.asm`): per grass
step, encounter iff `rand(0..255) < rate`; slot picked via the
cumulative buckets 51/102/141/166/191/216/229/242/253/256.
- **Initial object visibility** from `toggleable_objects.asm` (e.g. Oak
hidden in his lab), with `show_object`/`hide_object` script commands
persisting to the save like the missable-object bits.
## Pokémon math (`engine/pokemon/calc_stats.asm`, `experience.asm`)
- `stat = floor(((base + DV)*2 + floor(sqrt(statExp)/4)) * L / 100) + 5`
(HP: `+ L + 10`); HP DV from the low bits of the other four DVs.
- Growth curves use the exact cubic coefficients (MEDIUM_SLOW =
1.2n^3 - 15n^2 + 100n - 140, etc).
- Exp gain = `floor(baseExp * level / 7)` (x1.5 for trainer battles);
defeated species' base stats accumulate as stat experience.
## Battle core (`engine/battle/core.asm`)
- Damage: `floor(floor(2L(x2 crit)/5 + 2) * power * atk / def / 50)`
capped at 997, `+2`, STAB x1.5, per-matchup type multipliers applied
sequentially (x10 fixed point), then `rand(217..255)/255` when
damage > 1.
- Critical hits: `rand(0..255) < baseSpeed/2` (x4 for Karate Chop, Razor
Leaf, Crabhammer, Slash, capped 255); crits double level and ignore
stat stages (gen1_faithful ruleset).
- Accuracy: `rand(0..255) < floor(acc*255/100)` after accuracy/evasion
stages, including the 1/256 miss at 100% accuracy (toggleable via the
`modern_clean` ruleset).
- Stat stages use the 25/28/33/40/50/66/100/150/.../400 multiplier table
(`data/battle/stat_modifiers.asm`).
- Physical/special split by type (special = Water/Grass/Fire/Ice/
Electric/Psychic/Dragon).
- Status: paralysis speed/4 and 25% full para, burn halves physical
attack, poison/burn residual = maxHP/16, sleep 1-7 turns waking on the
lost turn, freeze permanent (as in Gen 1).
- Turn order: effective speed, coin-flip ties; Quick Attack first,
Counter last (Gen 1's only priorities).
- Run formula (`TryRunningFromBattle`): always escape if faster,
otherwise `floor(pSpd*32 / (eSpd/4)) + 30*attempts` vs `rand(0..255)`.
- Catching (`ItemUseBall`): ball-specific rand ranges (255/200/150),
status bonus 25/12, second roll `floor(maxHP*255/ballFactor) /
floor(HP/4)` capped 255.
- Prize money: class base money x last defeated mon's level
(`pic_pointers_money.asm`).
## Battle move effects (engine/battle/core.asm, move_effects/*)
- Mimic via Metronome (effects.asm:1203-1273): MimicEffect's
.letPlayerChooseMove branch snapshots wCurrentMenuItem before the
copy-picker menu opens and restores it afterward as the write index
into wBattleMonMoves. Since SelectMenuItem always writes
wCurrentMenuItem/wPlayerMoveListIndex together at the FIGHT-menu
confirm and nothing (including MetronomePickMove) touches either
variable during mid-move resolution, the reused value is always the
calling move's own slot, BattleState.lua's applyMimic fallback uses
self.moveIndex, frozen the same way, so a called Mimic (e.g. from
METRONOME in slot 3) overwrites the calling move's own slot, keeping
its PP, matching the Gen 1 quirk exactly.
- Multi-hit distribution 2/2/2/3/3/3/4/5 over rand(0..7); all hits reuse
the first damage roll (faithful).
- Recoil = damage/4 (Struggle /2); drain/Dream Eater heal = damage/2;
Dream Eater requires sleep.
- Fixed damage: SonicBoom 20, Dragon Rage 40, Seismic Toss/Night Shade =
level, Psywave rand(1 .. 1.5xlevel-1).
- OHKO deals 65535, fails against faster targets; Swift skips accuracy;
Jump Kick crash = 1 damage on miss; Explosion halves defense and
faints the user even on a miss; Hyper Beam skips recharge if it KOs.
- Charge moves (incl. Fly's invulnerable turn), trapping moves locking
the victim out of its turns, Thrash's 3-4 turn lock ending in
confusion, Bide's 2-3 turn store-and-double, Rage's permanent lock
with attack-up on being hit, Counter/Quick Attack priority.
- Side-effect chances: 26/256 (10%), 77/256 (30%), stat-down side
effects 85/256; Twineedle 20% poison.
- Substitute costs 1/4 max HP, absorbs damage, blocks status/stat/side
effects; screens double effective defense (bypassed by crits); Focus
Energy keeps the Gen 1 quarter-rate bug under gen1_faithful.
- Status: sleep 1-7 turns (wake turn is lost), freeze permanent, burn
halves physical attack, paralysis speed/4 + 25% full para, Toxic's
rising counter, Leech Seed transfer, confusion 2-5 turns with 50%
40-power typeless self-hit.
- Trainer Pokémon use fixed DVs 9/8/8/8 (TrainerAI.asm convention).
## Items (engine/items/item_effects.asm)
- Potion family 20/50/200/full; drinks 50/60/80; status heals per item;
Revive half HP; Rare Candy = exact next-level exp with HP delta kept;
evolution stones use the extracted evos data; TMs single-use / HMs
reusable, gated by the species' real tmhm list; Repel 100/200/250
steps blocking wilds below the lead's level; Escape Rope returns to
the last heal point.
- Snorlax (Route 12/16) only wakes via `ItemUsePokeFlute` (item-use
menu, adjacent to it, not yet beaten), talking to it with the POKé
FLUTE merely in the bag has no effect (`engine/items/item_effects.asm`,
`scripts/Route12.asm`/`Route16.asm`).
- Mart inventories come from the script_mart lists per clerk; selling
pays half price; TM prices from tm_prices.asm.
## Overworld field systems
- Ledges from ledge_tiles.asm (facing + standing tile + ledge tile +
input direction -> two-cell hop).
- Counter talk-through uses the tileset's counter tiles
(tileset_headers.asm), which is how mart clerks and nurses work.
- Trainer sight (`home/trainers.asm` CheckFightingMapTrainers +
`engine/overworld/trainer_sight.asm`): extracted per-trainer range,
inclusive tiles along the facing line; detection runs only on
tile-aligned frames, before input handling, so on detection the d-pad
is dead (wJoyIgnore) and the player freezes on the spotted tile; the
"!" holds 60 frames (EmotionBubble), then the trainer walks
distance1 steps to the adjacent tile (none if already adjacent) and
uses the real battle/won/after dialogue from the trainer headers.
Sight is a pure screen-coordinate comparison with no line-of-sight
obstruction check (TrainerEngage / CheckSpriteCanSeePlayer): an
aligned in-range trainer engages through interposed NPCs and
unwalkable tiles, and the walk-up (TrainerWalkUpToPlayer, a fixed
distance1 MoveSprite_ script) has no collision either, so the
trainer simply walks/overlaps through anything on the line, as OAM
sprites overlap on hardware.
- Elevator rides (`engine/overworld/elevator.asm` ShakeElevator →
`src/world/ElevatorShake.lua`): choosing a floor stops the music,
bounces the BG scroll ±1 px around rest for 100 two-frame cycles with
SFX_COLLISION retriggered every cycle, restores the scroll, plays
SFX_SAFARI_ZONE_PA to completion, and restarts the map theme before
the floor warp. Lead-in delays kept per script: 9 frames of Delay3s
inside ShakeElevator (Celadon farjps in), 12 with the Silph/Rocket
scripts' extra Delay3. The offset applies to the BG layer only,
sprites are OAM and stay put. After the ride the port no longer
jump-cuts: choosing a floor rewrites the car's own exit-warp entries
to that floor (`engine/events/elevator.asm` DisplayElevatorFloorMenu
.UpdateWarp, per scripts/SilphCoElevator.asm /
CeladonMartElevator.asm / RocketHideoutElevator.asm), then the player
is walked out through the doorway onto that warp (ow:scriptMove →
ow:takeWarp), like the original.
- Field-move gates (engine/overworld/field_move_messages.asm +
start_sub_menus.asm): IsSurfingAllowed ported exactly, SURF refuses
with _CyclingIsFunText while the Cycling Road's BIT_ALWAYS_ON_BIKE is
armed (save.forcedBike: set on the Route 16/18 forced-bike tiles,
cleared by the gates, Fly, dungeon/blackout warps; the forced mount
itself is silent, as in CheckForceBikeOrSurf) and with
_CurrentTooFastText on Seafoam B4F's stairs square (7,11) until both
EVENT_SEAFOAM4 boulders are down. Re-selecting SURF while surfing is
ItemUseSurfboard's dismount attempt: steps ashore silently if the
facing tile is land-passable and unoccupied, else "There's no place
to get off!", and the menu closes either way (wActionResult stays 1).
STRENGTH's first page auto-advances after the cry + Delay3 (no
prompt); "can move boulders." prompts. The GBPalWhiteOutWithDelay3
white blink plays on every .goBackToMap closer: Strength, surf
mount/dismount/no-place, Flash (after its text), and Dig/Teleport
(Cut closes without a blink, per the asm).
- Wild slot table + rate per map; water encounter tables used while
surfing.
- Cut-tree block swaps from cut_tree_blocks.asm; surfable tilesets from
water_tilesets.asm (water tile $14, plus $32 on SHIP_PORT).
## Story events (data/scripts/story.lua and friends)
- Every hand-ported script cites its scripts/*.asm source and reuses the
real extracted text and event-flag names.
- Custom flag names (audited equivalent): three port-internal flag
families have no pokered EVENT constant but mirror the original's
state exactly. EVENT_TRADED_* are per-trade names for
wCompletedInGameTradeFlags bits (engine/events/in_game_trades.asm:
FLAG_TEST before the offer → after-trade text, FLAG_SET on completion;
dialogset text families, party-menu pick, the received mon joins the
end of the party, ConnectCable→anim→TradedFor→Thanks all ported).
EVENT_GOT_EEVEE is bookkeeping alongside the real guard, the hidden
ball object (scripts/CeladonMansionRoofHouse.asm HideObject, ≡
save.objectToggles), and self-heals older saves; a full party+box
keeps the ball claimable (_BoxIsFullText). EVENT_BEAT_SS_ANNE_RIVAL
stands in for scripts/SSAnne2F.asm's saved wSSAnne2FCurScript NOOP
progression, including the lose-and-retrigger path (flag only set on
victory). Names are kept for save compatibility. Coverage:
tests/parity_trade_gift.lua.
- The Pallet Town intro follows pokered exactly: the trigger is
PalletTownDefaultScript's wYCoord==1 check, Oak appears at (8,5) and
takes FindPathToPlayer's zigzag to one tile below the player, and the
escort is RLEList_ProfOakWalkToLab against the reverse-order playback
of RLEList_PlayerWalkToLab (the 17th simulated press is eaten by the
door-warp frame), followed by the OaksLab walk-in and choose-mon
exchange with map music deferred like BIT_NO_MAP_MUSIC. Oak's speech
ends with the real shrink: RedPicFront collapses through the extracted
ShrinkPic1/ShrinkPic2 into the overworld walking sprite on
OakSpeech.asm's frame timings (SFX_SHRINK, 4/4/20/50-frame beats, fade
to white), with the closing text box held on screen. The escort's
scripted steps run 16 frames/tile (chained single-tile scriptMoves
start back-to-back, no idle frame); Oak marches in place on the door
mat for RLEList_ProfOakWalkToLab's trailing NPC_CHANGE_FACING beat
(movement.asm ChangeFacingDirection → zero-delta TryWalking); the "!"
EmotionBubble overlaps the still-shown "Hey! Wait!" box
(PalletTownOakText prints without a button wait, then DelayFrames 10 →
EmotionBubble before the box clears); and the shrink beat ramps the
music to silence over ~70 frames (wAudioFadeOutControl = 10;
home/fade_audio.asm FadeOutAudio steps rAUDVOL 7→0) rather than
hard-stopping.
- The 12 disguised static wild battles (Power Plant Voltorb/Electrode +
Zapdos, Articuno, Moltres, Mewtwo) follow TalkToTrainer/
EndTrainerBattle exactly: cry + battle text, after-battle text without
a rematch once EVENT_BEAT_* is set, and the flag/HideObject on any
non-blackout result (fleeing loses the legendary, as in Gen 1).
Snorlax hides before its battle and only shows the calmed-down/
returned line when not caught. Zapdos/Articuno/Moltres/Mewtwo's
battle text is a text_far string ending in a bare "...@" terminator
(no <DONE>/<PROMPT>) followed by text_asm PlayCry + WaitForSoundToFinish:
the box types with no ▼ prompt and auto-closes only once the cry
finishes, never on a button press, ported via `Commands.play_cry`
stashing the pending cry for the following `Commands.show_text` to
consume as the TextBox's auto-close sound. Voltorb/Electrode's battle
text has no PlayCry call in the ROM at all and keeps the ordinary
button-wait close.
- Gym leader repeat dialogue (data/scripts/gyms.lua): each leader's
text_asm branches on EVENT_BEAT_<LEADER>, pre-badge talk prints the
pre-battle text and engages the leader battle (badge/TM via
data/scripts/victories.lua); post-badge talk prints the leader's
post-battle advice text (Misty's is her TM11 explanation). The
originals' middle branch (beaten but TM not handed over) is
unreachable since the TM is granted with the victory. Giovanni's
farewell (`ViridianGymGiovanniText` .afterBeat) hides him inside a
fade-to-black/fade-in Transition matching ViridianGym.asm's
GBFadeOutToBlack → HideObject → GBFadeInFromBlack, persisted
permanently via TOGGLE_VIRIDIAN_GYM_GIOVANNI in save.objectToggles.
- Cable Club receptionists (TX_SCRIPT_CABLE_CLUB_RECEPTIONIST →
CableClubNPC, all 12 Pokémon Centers): welcome, pre-Pokédex "making
preparations" brush-off, and the apply/save YES-NO are ported;
accepting saves the game and opens the link menu, declining prints
"Please come again!".
- Cinnabar fossil deposit follows GiveFossilToCinnabarLab: a menu of
carried fossils (FossilsList order), SeesFossilText with a Yes/No
confirm, ComeAgainText on either cancel.
- Hall of Fame induction: each party mon's front sprite scrolls in from
the left at 4px/frame, matching HoFShowMonOrPlayer's .ScrollPic
front-pic phase (engine/movie/hall_of_fame.asm); the back-pic's
enlarged/blurred pre-wipe is a VRAM-scroll-register trick not
replicated in this sprite-based renderer. The finale
(HoFDisplayPlayerStats) shows trainer name, play time, money, POKéDEX
seen/owned, and Prof. Oak's rating text (engine/events/
pokedex_rating.asm DexRatingsTable) from real save data.
- End credits + post-game reset (engine/movie/credits.asm,
scripts/HallOfFame.asm): screen-by-screen CreditsOrder pages (hlcoord
9,6 + signed columns), FadeInCredits' 4x5-frame ramp, 90/110/120/140-
frame holds, DisplayCreditsMon's 27-frame 8px/frame silhouette wipe,
LoadCopyrightTiles' three-row block, THE END at (4,8). While THE END
is up the HoF script autosaves (wLastBlackoutMap := PALLET_TOWN; the
player is saved in the HALL_OF_FAME room), waits 600 frames, then A/B
triggers `jp Init`, the boot sequence replays into the title screen.
- Victory Road's boulder switches replicate the original's
ReplaceTileBlock data: 1F boulder at (17,13) -> block $1D at (4,6);
2F boulders at (1,16)/(9,16) -> $15 at (3,4) and $1D at (11,7); 3F
boulder at (3,5) -> $1D at (3,5), and the (23,15) hole drops the
boulder to 2F (hide/show toggle). Barriers are re-applied from flags
on map entry, exactly like the originals' map-load scripts.
- Item balls, static legendary encounters and trainer rewards
(badges + gym TMs, the Silph Giovanni flag) are generic systems driven
by the extracted object args and a hand-ported reward table
(data/scripts/victories.lua).
- In-game trades use the real data/events/trades.asm table (species in,
species out, original nickname).
## Safari game (engine/events/hidden_events/safari_game.asm + engine/battle)
- ¥500 buys 30 SAFARI BALLs and 502 steps (scripts/SafariZoneGate.asm
sets `wSafariSteps = 502`); steps count down on the four outdoor zone
maps and hitting 0 (or throwing the last ball) ends the game at the
gate.
- Safari battles offer BALL / BAIT / ROCK / RUN; no player Pokémon
acts. The working catch rate starts at the species rate; BAIT halves
it and adds 1-5 to the bait factor (zeroing the escape factor); ROCK
doubles it (cap 255) and adds 1-5 to the escape factor (zeroing bait)
-- ItemUseBait/ItemUseRock in engine/items/item_effects.asm.
- Each turn one factor decays ("is eating!" / "is angry!"); when the
escape factor decays to 0 the catch rate resets to the species rate
(PrintSafariZoneBattleText, engine/battle/safari_zone.asm).
- Flee check (engine/battle/core.asm): `b = 2 * (speed % 256)`; the mon
always flees when speed > 127; while eating `b /= 4`, while angry
`b = min(255, 2b)`; it flees when `rand(0,255) < b`.
- The SAFARI BALL rolls the ULTRA_BALL rand range (0-150) in the Gen 1
catch formula, against the BAIT/ROCK-modified rate.
## Slot machines (engine/slots/slot_machine.asm)
- The three reels are the extracted 18-symbol wheel sequences
(data/events/slot_machine_wheels.asm); bet 1 plays the middle row,
bet 2 adds top+bottom, bet 3 adds both diagonals.
- Payouts: 7-7-7 = 300, BAR = 100, CHERRY = 8, MOUSE/FISH/BIRD = 15
(SlotRewardPointers).
- Per-wheel stop/slip rules ported exactly: wheel 1 spends up to 4 slip
charges, slipping past a centred CHERRY (in seven-and-bar mode it
always slips all 4 via pokered's `cp HIGH(SLOTS7)` bug); wheel 2 stops
as soon as wheels 1+2 line up any potential match (pairs checked b/b,
b/m, m/m, t/m, t/t) or, in seven-and-bar mode, on 7/BAR; wheel 3 rolls
past forbidden matches free and burns wSlotMachineRerollCounter
charges on winnable no-match spins, animated tile-by-tile. Luck flags
(SetFlags): seven-and-bar mode is sticky across spins; r==0 arms 60
allow-matches charges; a BAR win clears flags; a 300 win zeroes the
counter and clears flags with probability 128/256; 8/15 wins burn one
charge. Lines are checked in asm order with the first match taken;
A-presses are ignored while a prior wheel's slip counter is nonzero.
Machine and COIN CASE texts are byte-identical
(_GameCorner*Text; AbleToPlaySlotsCheck's no-coins gate included).
- Flow brackets: PromptUserToPlaySlots "A slot machine! Want to play?"
(YesNoChoice) and MainSlotMachineLoop's "One more go?" (TwoOptionMenu);
the x3/x2/x1 coin menu (CoinMultiplierSlotMachineText) defaults its
cursor to x3, bet = 3 - menu item. Static frame: the real
SlotMachineMap (gfx/slots/slots.tilemap, 20x12 tile ids < $25) blitted
from red_slots_1.png, extracted as field.slotSymbols.tilemap
(tools/extract/gfx.py extract_slots). Win flash:
SlotMachine_CheckForMatches.flashScreenLoop flips rBGP (shade 3->2) b
times at 5 frames each, b = 20/8/4/2 for the 300/100/15/8 rewards
(SlotReward{300,100,8,15}Func). Payout drip:
SlotMachine_PayCoinsToPlayer credits one coin every 8 frames (4 for a
7/BAR), SFX_SLOTS_REWARD per coin, rOBP0 symbol flicker every 5 coins.
## Spinner arrow tiles (scripts/*.asm arrow movement tables)
- Viridian Gym and Rocket Hideout B2F/B3F keep per-coordinate RLE
movement lists (map_coord_movement); each list executes backwards
from its terminator (DecodeArrowMovementRLE), sliding the player and
chaining onto further arrows.
## Cries (data/pokemon/cries.asm, audio/engine_1.asm)
- Each species = a base cry (one of 38 SFX_CryXX streams) + a frequency
modifier added to every note's frequency register
(Audio1_ApplyFrequencyModifier) + a tempo modifier
(`sfx tempo = $80 + length`, Audio1_SetSfxTempo). All 151 cries are
rendered offline with those modifiers applied and play on battle
entry and Pokédex pages.
## Hidden events & facility puzzles
- Card key doors (engine/events/card_key.asm): door tiles $18/$24
(SILPH_CO_11F: $5e) replaced with block $0e ($03 on 11F).
- Vermilion trash cans
(engine/events/hidden_events/vermilion_gym_trash.asm): the first-lock
can re-rolls on every Vermilion City map load (VermilionCity_Script's
Random & $e, even cans) and after every failed second-can guess; the
second lock uses the GymTrashCans table verbatim, including the
underflow bug that can place it in can 0 regardless of adjacency; a
wrong pick resets EVENT_1ST_LOCK_OPENED and re-rolls immediately; only
SuccessText3 prints on completion; the gym door block at (2,2) is
$24 closed / $5 open (scripts/VermilionGym.asm). SuccessText1/
SuccessText3/FailText play SFX_SWITCH/GO_INSIDE/DENIED from each
text's text_asm tail after the text prints (DisplayTextID's
WaitForTextScrollButtonPress then holds the box), so the port fires
them from an onDone on the TextBox, landing the beep as the box
closes rather than as it opens.
- Menu close-keys follow pokered's per-menu wMenuWatchedKeys mask, not
a single global rule: the shared Menu base (src/ui/Menu.lua) closes
on B only, and START-close is opt-in via opts.startCloses. Only the
start menu sets it, matching engine/menus/draw_start_menu.asm's
PAD_DOWN|PAD_UP|PAD_START|PAD_B|PAD_A; OptionsMenu also closes on
START via its own loop, matching engine/menus/main_menu.asm
DisplayOptionMenu's explicit B_PAD_B/B_PAD_START checks. Every other
menu (bag/PC item lists PAD_A|PAD_B|PAD_SELECT, party menu /
BUY-SELL-QUIT / USE-TOSS submenu / PC menus / Pokedex side menu
PAD_A|PAD_B) leaves PAD_START unwatched, so START does not close
them. START never replays SFX_PRESS_AB (HandleMenuInput_ beeps only
for the PAD_A|PAD_B branch).
- Old man tutorial hollow cursor: the item list is itself scripted in
pokered (DisplayListMenuID's old-man branch, home/list_menu.asm:65-91)
, no input is read; the filled '▶' hovers POKé BALL for 80 frames,
auto-presses A, then PlaceUnfilledArrowMenuCursor leaves the hollow
'▷' on that row until ItemUseBall tears the list down for the throw.
Ported via ListMenu's opts.script hook (src/ui/ListMenu.lua) and
BattleState:openOldManBag driving the same beats. The MissingNo./
wGrassRate side effects of the OLD MAN name swap are not modeled,
see docs/gameboy-hardware-limitations.md.
- Gym statues (gym_statues.asm): plaque with the city/leader from each
gym's script; the player joins WINNING TRAINERS with the badge.
- Route 22 gate / Route 23 guards: real trigger rows, badge order
(EARTH down to CASCADE) and EVENT_PASSED_*_CHECK skip flags.
- Game Corner poster (scripts/GameCorner.asm): block (8,2) $2a -> $43
on EVENT_FOUND_ROCKET_HIDEOUT.
- Seafoam Islands (scripts/SeafoamIslandsB3F/B4F.asm): reversed-RLE
current paths, Seafoam4HolesCoords boulder holes setting the
EVENT_SEAFOAM*_BOULDER*_DOWN_HOLE pairs, the forced pool exit rows.
- Rock Tunnel darkness: wMapPalOffset = 6 on entry, cleared by Flash
(BOULDERBADGE) or leaving (home/overworld.asm).
## Battle extras
- GROWL/ROAR (GetMoveSound/IsCryMove, engine/battle/animations.asm
~2196): the move's own MoveSoundTable tempo byte (Growl $c0, Roar
$40, both pitch $00) layers onto the cry via `Sound.playMoveCry`'s
`Source:setPitch(256/(128+tempoMod))`. Transform (engine/gfx/
palettes.asm DeterminePaletteID, bit TRANSFORMED): the swapped-in pic
is tinted PAL_GRAYMON via `PaletteFX.monPal(data, species,
transformed)`, not the copied species' own palette, in
`BattleState:speciesSprite`. Growl (DoGrowlSpecialEffects,
animations.asm ~928): AnimPlayer's GROWL frame-block branch keeps a
`growlNoteTrail` snapshot so each block's emitted sprites include the
previous block's note copy alongside the current one (GROWL skips
AnimationCleanOAM between blocks per the `cp GROWL` check ~line 145);
ROAR is unaffected since the asm never applies this quirk to it.
- Master/Ultra ball tosses flicker the OBJ palette: DoBallTossSpecial
Effects (engine/battle/animations.asm:685) XORs rOBP0 with %00111100
after every frame block while wCurItem <= ULTRA_BALL, so the 11 toss
blocks alternate the $F0/$CC shade maps starting normal; PlayAnimation
pushes/pops rOBP0 around each subanimation row, so the ambient
palette returns when the toss ends. GREAT/POKE/SAFARI balls never
flicker, and the toss arc always follows wCurItem via
TossBallAnimation, including the ghost-dodge throw.
- Anim-layer OBJ colorization is per 8x8 attribute cell: the SGB's
ATTR_BLK regions color the composited DMG picture per cell, not per
OAM entry, so an anim sprite overlapping a zone boundary takes each
cell's palette on the pixels inside it, AnimPlayer samples the zone
under every cell an 8x8 tile touches and repaints differing cells
through a cell-clipped scissor (aligned tiles stay one draw).
- Ball wobbles (ItemUseBall): Z = X*Y/255 + status2 with
Y = rate*100/ballFactor2; <10/<30/<70 -> 0/1/2 shakes, else 3, with
the matching ItemUseBallText01-04 lines.
- Trainer class AI (data/trainers/ai_pointers.asm +
engine/battle/trainer_ai.asm): per-class item/switch routines with
wAICount uses per Pokémon, ported to data/scripts/ai_classes.lua.
- Exp (engine/battle/experience.asm): baseExp*level/7 divided by the
participant count, x1.5 for trainers, x1.5 for traded mons; stat exp
in full to each participant.
- Move sounds: data/moves/sfx.asm (sound + pitch/tempo per move). The
pitch/tempo modifiers are applied at synthesis time
(Audio2_ApplyFrequencyModifier adds pitch to every frequency write;
Audio2_SetSfxTempo scales tone-channel note lengths, noise skips it),
128 variant WAVs keyed "<sfx>@<pitch><tempo>" that Sound.playMove
selects, exact rather than a playback-rate approximation. Per-row
sounds fire as PlayAnimation does; GROWL/ROAR (IsCryMove) play the
attacker's cry. Hit sounds by effectiveness (Damage/Super/NotVery).
- Screen-effect animations (engine/battle/animations.asm +
engine/gfx/screen_effects.asm): every SE_* is implemented per-routine,
FlashScreen/FlashScreenLong (the FlashScreenLongSGB 12-entry table),
Dark/Light/DarkenMon/Reset palette ops (shade-map permutations of the
SGB zone palettes), all SlideMon variants, ShakeBackAndForth,
BoundUpAndDown, SquishMonPic, Minimize (real MinimizedMonSprite),
spiral/shoot-balls/water-droplets/leaves emitters compiled from the
asm trajectories, per-animation-id frame-block flashes (Explosion,
Rock Slide's rumbles, Blizzard's cadence...), AnimationWavyScreen with
true per-scanline offsets, PredefShakeScreenHorizontally/Vertically
and ShakeEnemyHUD. SE rows carry the faithful blocking durations.
- SGB battle colorization (SetPal_Battle, BlkPacket_Battle,
SetAnimationPalette): the battle screen is colorized by zone, player
HUD, enemy HUD, player mon + message box, enemy mon; trainer front
pics and the player/old-man back pics take PAL_MEWMON (both species
IDs are zero at the intro, so MonsterPalettes[0]); the ghost keeps the
disguised species' palette; attack animation sprites and thrown balls
are colored through the OBJ palettes (wAnimPalette $F0 on SGB, ambient
$E4, OBP1 $6C). Headless/no-shader environments fall back to the flat
pipeline.
- Mimic resolves mid-move (MimicEffect): accuracy first, then the
player's copy menu (enemy/link copy a random slot); the copy
overwrites only the slot's move ID, PP is shared with Mimic's slot,
and reverts on switch/battle end.
- Old man tutorial (DisplayBattleMenu's BATTLE_TYPE_OLD_MAN branch): the
real scripted cursor, ▶ beside FIGHT for 80 frames, beside ITEM for
50, ITEM force-selected into the POKé BALL x50 list; the throw always
catches at full HP (item_effects.asm jumps straight to .captured, 3
shakes, no party/dex add, no ball consumed); backing out of the bag
replays the script. The old man never attacks, the original tutorial
is menu navigation + a guaranteed catch, nothing more.
## Link battles (lockstep)
- Both sides simulate with a shared Park-Miller RNG stream (host deals
the seed), identical pack/unpack-clamped party copies, no badge
boosts, and a mirrored speed-tie roll (the guest inverts it); a
canonical host-side-first state hash is exchanged per turn and any
mismatch ends the match as a draw.
## Music (audio/engine_1.asm)
- Note duration: `frames = length * speed * tempo / 0x100` with
fractional carry, at 60 fps (Audio1_note_length / CalculateDelay).
- Frequency: `reg = pitches[note] asr (octave - 1)` (CalculateFrequency;
the octave byte stores `8 - octave`), `f = 131072/(2048 - reg)` for
squares, halved for channel 3.
- note_type volume/fade renders as an NRx2-style envelope (step every
`fade/64` s); duty_cycle maps to 12.5/25/50/75% pulse widths;
sound_call/sound_loop honor the engine's one-level call stack and
loop counters.
## Text & font
- The Pokédex height row uses the real /″ tiles: gfx/pokedex/pokedex.png
tiles 0/1 are patched over font-extra slots $60/$61 exactly as
engine/gfx/load_pokedex_tiles.asm loads them over vChars2 (they replace
glyphs charmap.asm marks unused); ASCII `"` aliases to the closing-
quote glyph $73 so stray hand-written quotes render.
## Validation against the original
- `tests/run_tests.lua` pins hand-checked values: L5 Bulbasaur 19 HP /
9 Atk at 0 DVs, L100 Mewtwo 415 HP / 406 Spc at max DVs+statExp,
MEDIUM_SLOW(5) = 135, type chart spot checks, deterministic damage
rolls, Route 1 slot 1 = L3 Pidgey.
- The autopilot run reproduces the original's early flow on real map
data: Pallet sign text, lab door warp target (5,11), Oak's Lab exit by
walking off the mat, connection into Route 1 at matching x.
+131
View File
@@ -0,0 +1,131 @@
# Building a Blue Version of the port
pokered builds Red and Blue from one source tree: the Makefile
assembles everything twice with `rgbasm -D _RED` or `-D _BLUE`, and
every in-game difference sits in an `IF DEF(_RED)` / `IF DEF(_BLUE)`
block. Our extraction pipeline resolves those conditionals the same
way (`tools/extract/util.py` `ASM_DEFINES`), so most of a Blue build
falls out of re-running the extractors. Only three things are
hand-ported on the Lua side and need swapping by hand.
## What differs between Red and Blue
| Where (pokered) | What |
| --- | --- |
| `data/wild/maps/*.asm` (34 files) | Version-exclusive encounters (Red: Ekans, Oddish, Growlithe, Mankey, Scyther, Electabuzz, Blue: Sandshrew, Bellsprout, Vulpix, Meowth, Pinsir, Magmar) |
| `data/pokemon/title_mons.asm` | The 16 title-screen Pokémon |
| `engine/movie/title.asm` + `gfx/version.asm` | The "Red Version" / "Blue Version" ribbon |
| `constants/player_constants.asm` | Preset names (Red: RED/ASH/JACK + BLUE/GARY/JOHN) |
| `data/sgb/sgb_palettes.asm`, `sgb_border.asm` | Super Game Boy palettes and border |
| `data/events/prizes.asm`, `prize_mon_levels.asm` | Game Corner prize mons, costs and levels |
| `engine/movie/intro.asm`, `data/credits/credits_text.asm`, `engine/slots/slot_machine.asm`, `engine/battle/animations.asm`, `audio/sfx/save_3.asm` | Small gated tweaks (credits say "BLUE VERSION STAFF", etc.) |
## Step 1, flip the extraction define
`tools/extract/util.py`:
```python
ASM_DEFINES = {"_RED"} # -> {"_BLUE"}
```
Then regenerate everything (same as scripts/setup.sh does):
```sh
cd tools
../.venv/bin/python3 build_data.py \
--pokered /Users/bryanbassett/Documents/development/pokered \
--out ../data/generated --assets ../assets/generated
```
This alone switches the wild encounters, preset names, SGB palettes
and credits text. **Caveat:** `parse_preset_names` sanity-checks in
`tools/extract/field.py:1193` expect "RED" in the player presets,
relax that check for a Blue build (Blue's presets are BLUE/GARY/JOHN
for the player and RED/ASH/JACK for the rival).
`tools/extract/palettes.py` uses its own raw reader with hardcoded
`IF DEF(_BLUE)` skipping (around line 44), invert that too, or port
it to `read_asm` so `ASM_DEFINES` covers it.
## Step 2, title screen (hand-ported)
`src/ui/TitleState.lua`:
1. **Ribbon art.** The extractor writes
`assets/generated/title/red_version.png`; add `blue_version.png`
to `gfx.extract_title` in `tools/extract/gfx.py` (source:
`gfx/title/blue_version.png`, 64×8). In `TitleState:draw()`, the
Red strip needs two quads (tiles 01 "Red", skip, tiles 59
"Version", `title.asm` `VersionOnTitleScreenText`). Blue's strip
prints its tiles contiguously (`db $61..$68`), so draw the whole
64×8 image at px (56, 64) and verify with the title driver
screenshot.
2. **Title mons.** Replace `CYCLE_SPECIES` with Blue's list from
`data/pokemon/title_mons.asm`:
```lua
local CYCLE_SPECIES = {
"SQUIRTLE", "CHARMANDER", "BULBASAUR", "MANKEY", "HITMONLEE",
"VULPIX", "CHANSEY", "AERODACTYL", "JOLTEON", "SNORLAX",
"GLOOM", "POLIWAG", "DODUO", "PORYGON", "GENGAR", "RAICHU",
}
```
## Step 3, Game Corner prizes (hand-ported)
`data/scripts/story3.lua` (~line 209) carries the Red prize tables.
Blue's values (`prizes.asm` + `prize_mon_levels.asm`):
| Prize | Cost | Level |
| --- | --- | --- |
| ABRA | 120 | 6 |
| CLEFAIRY | 750 | 12 |
| NIDORINO | 1200 | 17 |
| PINSIR | 2500 | 20 |
| DRATINI | 4600 | 24 |
| PORYGON | 6500 | 18 |
(TM prizes are identical in both versions.)
## Step 4, verify
```sh
luajit tests/run_tests.lua
```
Plus two spot checks:
```sh
# every grass table must have exactly 10 slots (a conditional-parsing
# regression shows up as 1920 slots)
luajit -e 'local e=dofile("data/generated/encounters.lua")
for m,d in pairs(e) do if type(d)=="table" and d.grass and #d.grass.slots>0
and #d.grass.slots~=10 then print("BAD",m,#d.grass.slots) end end'
# a Blue exclusive should now appear (and Growlithe should not)
grep -c VULPIX data/generated/encounters.lua
grep -c GROWLITHE data/generated/encounters.lua
```
Title screenshot: run the driver in
`tests/drivers/` style (`SHOT_DIR=... POKEPORT_DRIVER=... love .`) and
eyeball the ribbon, title mon, and copyright row.
## What you get for free / what to skip
- Free after re-extraction: encounters (incl. Super Rod groups), SGB
palettes, preset names, credits text, the gated sfx/animation
tweaks.
- Trades, gift Pokémon, story scripts, gym data: identical in Western
Red/Blue, nothing to touch.
- Save files: a Red save loads fine, but dex AREA nests and new
encounters will be Blue's. Trainer parties are identical.
## Making it a runtime toggle instead
If you want one build with both versions, extraction would need to
emit both branches keyed by version (e.g. `slots` / `slotsBlue`) and
the three hand-ported spots would read a `save.version` or
`conf.lua` flag. That is a bigger change than the rebuild above,
the flip-and-regenerate route needs no engine changes at all.
+50
View File
@@ -0,0 +1,50 @@
# ROM Extraction Notes
There are two ROM-only extraction paths:
- The packaged app uses `src/import/RomImporter.lua` and
`src/import/RomExtractor.lua` on first boot.
- Developers can run `tools/build_data.py --rom <path> [--clean]` to generate
data in the source tree for audit and parity work.
Both paths read only the supplied ROM and the checked-in
`tools/rom_manifest.json`. Neither invokes RGBDS, Git, or a disassembly.
## Validation
Only the canonical US Pokemon Red ROM is supported. SHA-1 is checked before
any cached output is removed or written.
## Decoded Data
| Area | ROM data |
| --- | --- |
| world | map headers, block maps, connections, warps, signs, objects |
| tiles | tileset graphics, blocksets, collision, door and warp tile lists |
| text | 2,584 text command streams and RAM/number substitutions |
| Pokemon | names, stats, evolutions, learnsets, Dex data, compressed pictures |
| battle | moves, detailed animations, OAM frames/tiles, effects, type chart, palettes, trainer parties/AI/pictures |
| inventory | item names, prices, key-item flags, TM/HM data |
| encounters | grass and water wild tables |
| UI | fonts, icons, title/intro, trainer card, town map, slots, field effects |
| audio | music, SFX and cry headers, channel programs, wave instruments |
The Python and Lua picture decompressors implement the Gen 1 `pic` format.
Graphics are converted to RGBA PNGs. OAM artwork uses transparent color 0;
battle pictures use edge-connected white matting so white interior details
remain visible.
The in-app importer stores three audio ROM banks as a 48 KiB
`programs.bin`. `src/core/ChipAudio.lua` interprets the channel bytecode and
synthesizes music as a queueable stream; SFX and cries are synthesized on
demand. This avoids shipping or generating a large WAV/OGG tree.
## Metadata Boundary
Names, dimensions, enum ordering, Lua script hooks, and hand-ported field
behavior do not survive compilation in a form the Lua runtime can infer.
Those relationships are bundled in `rom_manifest.json`. The manifest stores
no dialogue strings, images, audio samples, or ROM bytes.
`tools/make_rom_manifest.py` and `tools/verify_rom_data.py` are developer audit
tools. They are not used by the packaged game.
+52
View File
@@ -0,0 +1,52 @@
# Game Boy Hardware Limitations Carried Into This Port
This is a 1:1 parity port of Pokémon Red onto a modern engine (LÖVE2D), so a
lot of the original game's design isn't "design" at all, it's a direct
consequence of what the actual Game Boy hardware could physically do. None
of these constraints apply to a Lua table or a 2026 GPU. They're kept here
purely for faithfulness to the original, and documented below so it's clear
which limits are load-bearing history rather than intentional choices for
this port.
## Kept faithfully (gameplay-visible limits reproduced on purpose)
| # | Mechanic | Value | Why the Game Boy had this limit | Where it lives here |
|---|---|---|---|---|
| 1 | Bag capacity | 20 item slots | `wNumBagItems` save block was a fixed 20-entry id/quantity array in SRAM | `src/inventory/Bag.lua:8` (`Bag.CAPACITY = 20`) |
| 2 | Party size | 6 Pokémon | `wPartyMon1..6` were 6 fixed save-RAM slots | `src/pokemon/Party.lua:5` (`Party.MAX = 6`) |
| 3 | PC storage | 12 boxes × 20 Pokémon | `wBoxDataStart` / Bill's PC allocated a fixed 12×20 SRAM block | `src/pokemon/Boxes.lua:7-8` |
| 4 | Moves per Pokémon | 4 | Fixed 4-move-slot field in the party/box Pokémon struct | `src/pokemon/Pokemon.lua:20`, enforced again in `src/battle/BattleState.lua:1941` |
| 5 | Screen resolution / tile grid | 160×144 px, 8×8 tiles, 20×18 visible tiles | The Game Boy PPU's actual pixel and tile-map dimensions | `src/render/Renderer.lua:13-14`, `src/render/TileRenderer.lua:64`, `src/render/BattleTransition.lua:25` |
| 6 | Name length | Nickname 10 chars, trainer/rival name 7 chars | Fixed-width `wPlayerName` and nickname byte buffers in SRAM | `src/ui/OakSpeech.lua:91,108`, `src/battle/BattleState.lua:2240`, `src/ui/NamingScreen.lua:50` |
| 7 | Text box size / word wrap | 20×6 tile dialogue window, 18-column wrap | Text rendered directly into the tile-map grid | `src/render/TextBox.lua:14,17` |
| 8 | Text print speed | 1/3/5-frame character delay | Text was drawn character-by-character into VRAM on a fixed 60Hz frame budget | `src/render/TextBox.lua:133-136`, `src/core/SaveData.lua` (`textSpeed = 3` default) |
| 9 | Audio channel behavior | 4 channels (2 pulse, 1 wave, 1 noise); fanfares "steal" the music's tone channels | The GB APU only has 4 physical sound channels | `src/core/Sound.lua:14-21`, `docs/behavior-porting-notes.md:225-235` |
| 10 | Stat/damage byte-overflow bugs | Atk/Def quartered when either exceeds 255; Focus Energy crit bug; stat-exp capped at 255 pre-scale | Original math ran on 8-bit registers and overflowed/wrapped exactly this way, these are *bugs*, kept for authenticity | `src/battle/Damage.lua:19-25,143-148`, `src/pokemon/Stats.lua:25-27` (toggleable via `gen1_faithful` ruleset) |
| 11 | Fixed 60Hz update step | `STEP = 1/60` | The Game Boy's actual refresh rate | `src/core/FixedStep.lua` |
## Explicitly NOT carried over (the hardware cause disappeared, so the effect was dropped)
| # | Mechanic | Original GB constraint | Status here |
|---|---|---|---|
| 1 | OAM sprite limits | Max 40 sprites on screen, max 10 per scanline (causes the classic flicker) | Not modeled, LÖVE draws every sprite unconditionally, no scanline budget or cycling exists |
| 2 | Money cap of 999999 | Money was packed as 3 BCD bytes in SRAM, capping at 999,999 | Not enforced, money is only floored at 0, can grow unbounded |
| 3 | DMA transfer / VBlank timing | Sprite/tile updates had to be batched into the VBlank window via DMA | Not applicable, no equivalent constraint in a modern renderer |
| 4 | SRAM save layout | Save data was a precise byte-for-byte struct fit to a small battery-backed SRAM chip | Save file is a plain serialized Lua table (`src/core/SaveData.lua`), not an SRAM layout |
| 5 | Weak/deterministic RNG | The GB's RNG was driven by timer/divider registers, not a real PRNG | Replaced with `love.math.random` / `math.random`, only the *value ranges/thresholds* the original produced are kept. Exception: link battles use a deterministic Park-Miller LCG (`src/link/LinkBattle.lua:22-33`) so both sides can reproduce identical rolls, that's a design choice, not a GB replication |
| 6 | VRAM tile budget | 256 tiles per bank, 2 VRAM banks total | Not applicable, generated tile sheets aren't memory-budget constrained |
| 7 | Old man glitch (MissingNo. / 'M, +128 item duplication, Hall of Fame corruption) | The catch tutorial stashes the 11-byte player name over `wGrassRate`/`wGrassMons` (core.asm:2024-2037) and restores it from there after the throw (item_effects.asm:159-164), leaving name bytes in the grass table; `LoadWildData` skips rewriting `wGrassMons` on maps with grass rate 0 (wild_mons.asm:13-16), so Cinnabar/Route 21 shore tiles read name characters as level/species pairs, out-of-range species IDs render garbage base-stat/sprite memory as MissingNo./'M, the dex-#0 seen-flag write lands 255 bits past `wPokedexSeen` onto bag slot 6's quantity byte (+128 items), and the oversized glitch sprite overflows the decompression buffer into Hall of Fame SRAM | Not modeled, encounters are a pure per-map data lookup (`src/world/Encounter.lua` + `data/generated/encounters.lua`) with no stale copied buffer, the species table is closed (no out-of-range reads to render), and dex flags / bag / HoF data are separate Lua tables with no address adjacency; the tutorial's OLD MAN name swap is kept only in its visible text (`BattleState:oldManThrow`) |
## Notes
- PC Box **overflow handling** was deliberately changed even though the
20×12 box *shape* was kept faithful: instead of Gen 1's "full box discards
or blocks the deposit," this port spills into the next box with room.
- Several battle-related overflow "bugs" (byte overflow, Focus Energy) are
gated behind a `gen1_faithful` vs `modern_clean` ruleset toggle in
`src/battle/rulesets/`, so the faithful-but-buggy behavior can be turned
off without losing the option to replay it exactly as it shipped in 1996.
- Hardware-terminology comments referencing OAM, VRAM, DMA, BCD, SGB
colorization, etc. are scattered across ~14 files for context even where
the constraint itself isn't enforced (e.g. `src/battle/AnimPlayer.lua:310-312`
documents OAM sprite-hiding coordinate quirks used for animation
correctness, without implementing the underlying 40-sprite cap).
+9
View File
@@ -0,0 +1,9 @@
# Known differences from the original game
Only genuine remaining divergences live here: behavior that is still
**missing, wrong, or approximated for convenience** and would need more
work for true parity. Faithfully-ported behavior is documented in
docs/behavior-porting-notes.md; deliberate additions beyond the original
are in docs/new-features.md.
None currently.
+58
View File
@@ -0,0 +1,58 @@
# Native modding
The game has a built-in Lua mod runtime. Mods are installed under the LÖVE
save directory in `mods/<id>/` and are loaded after the verified ROM data has
been imported but before the title screen is created.
## Minimal mod
```text
mods/example_mod/
├── manifest.json
└── main.lua
```
`manifest.json`:
```json
{
"id": "example_mod",
"name": "Example Mod",
"version": "1.0.0",
"entry": "main.lua",
"priority": 0,
"dependencies": [],
"optional_dependencies": [],
"conflicts": []
}
```
`main.lua`:
```lua
return function(mod)
mod.log:info("hello from a native mod")
mod.content.pokemon:override("PIKACHU", {
name = "PIKACHU",
types = { "ELECTRIC" },
base_stats = { hp = 35, attack = 55, defense = 40, speed = 90, special = 50 },
})
mod.events:on("battle.start", function(context)
context.mod_message = "A native mod changed this battle."
end)
end
```
Mods should use registries and events instead of requiring private engine
modules. Registries currently cover Pokémon, moves, items, maps, tilesets,
encounters, trainers, sprites, music, audio, text, scripts, and UI.
Enablement is stored in the normal persistent `options.lua` file alongside
audio, display, and battle settings, so starting a new game does not disable
the selected mods. Changes take effect after restarting the game.
The loader deliberately does not import or execute arbitrary ROM-hack patches.
The supported content source remains the verified base Pokémon Red ROM plus
native mods.
+137
View File
@@ -0,0 +1,137 @@
# New features (deliberate additions beyond the original)
Intentional enhancements this port adds on top of faithful Pokémon Red
behavior. They have no Game Boy equivalent and are kept by design.
Genuine divergences from the original (things still missing, wrong, or
approximated) live in docs/known-differences.md; faithfully-ported
behavior is in docs/behavior-porting-notes.md.
## Survey zoom
The mouse wheel (or `-`/`=`) zooms the overworld between 1 pixel per world
pixel (full survey) and 2× the window fit scale (close-up), in crisp
integer steps. This has no Game Boy equivalent:
- Connected maps render their full bodies, and their NPCs appear as
visual-only "ghosts", they wander but have no sight lines, triggers,
dialogue, or collision until the map is actually entered.
- Menus, text boxes, and battles draw at normal scale on top of the
zoomed world. Zoom input is ignored while a script, menu, or battle is
active; the zoom level persists across warps and is never saved.
- Beyond the border ring the border block repeats indefinitely (interiors
stay black, seaside towns stay water, except OVERWORLD-tileset maps,
whose beyond-edge space fills with the solid tree wall instead of the
per-map border block), and each visible map area is colorized with its
own SGB palette (the original recolored the whole screen per map).
- Neighbor maps load two connection hops out so corner-adjacent maps
don't pop in and out, and ghost NPCs share instances with the real ones
so their wander positions persist across seamless connection crossings
(a warp or fresh map entry still respawns everything at its script
position, like the original's per-entry sprite init).
## Tilt mode
The `3` key (and the Options menu TILT row) cycles a visual-only perspective
tilt of the overworld through **OFF → 15° → 35° → 50° → OFF** for an HD-2D /
diorama look. Like survey zoom this is purely presentational and has no
Game Boy equivalent:
- The entire map tilts as one rigid ground plane, paths, grass, water,
floors, and every background-tile structure (buildings, trees, fences,
signs; in Gen 1 these are baked into the tile layer, not sprites), so
rows above the player recede and rows below come toward the viewer. Only
things that actually *stand* on the ground draw as upright billboards,
unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
and the screen-anchored FX attached to them (heal machine glow, emote
bubbles, the fishing rod, the FLY bird). An earlier revision tried
billboarding buildings/trees/signs too (cutting them out of the ground
per hand-curated per-tileset tables); that chased an endless tail of
special cases, dense tree canopy, fences fused into grass, building
facades with their own baked-in fake perspective, because Gen 1's art
was never drawn with a clean seam between ground and standing scenery. It
wasn't merged; tilting everything but the characters as one plane is the
simpler, shipped tradeoff (buildings recede/foreshorten with the ground
like a photo of a diorama, rather than standing fully upright next to
a full-height character).
- Cycling tweens the angle between levels over ~0.25s rather than snapping;
with tilt fully off the world pass drops back onto the flat blit path, so
flat rendering stays pixel-identical to tilt-off and off costs nothing.
- Tilt input is gated exactly like survey zoom, honored only while
free-roaming, ignored while a script, menu, or battle is active, and it
composes with survey zoom (the zoom scale feeds the projection). The tilt
level is persisted in `save.options.tilt` (default OFF).
- It applies everywhere the overworld draws, interiors and caves included.
Menus, text boxes, and battles render flat on top, unaffected, and the
infinite beyond-the-border-ring fill stays flat by design.
- Collision, movement, sight lines, triggers, encounters, and scripts are
untouched; nothing about the tilt reaches gameplay.
## Colors mode
The `2` key (and the Options menu COLORS row) cycles the global shade-remap
display mode through **GBC → OG → OG INV → GBC INV → CLASSIC → GBC**:
- **GBC** (default): current SGB / GBC zone palettes.
- **OG**: force the four DMG grays (colorization off).
- **OG INV**: inverted DMG grays.
- **GBC INV**: each SGB zone palette with shade order reversed.
- **CLASSIC**: original Game Boy pea-soup greens
(`#9BBC0F` / `#8BAC0F` / `#306230` / `#0F380F`).
The transform is applied centrally in `PaletteFX.sendColors`, so it covers
overworld, menus, battles, and tilt upright billboards. Persisted as
`save.options.colors`.
## GBC FX
The `5` key (and the Options menu GBC FX row) cycles a "played on real
unlit-GBC hardware" post-process through **OFF → 1 → 2 → 3 → 4**. The
levels are a cumulative ladder:
- **1**: reflective-screen backing transparency.
- **2**: + LCD pixel grid.
- **3**: + pixel drop shadows.
- **4**: + sunlight glare and rainbow shimmer with a drifting light.
It runs as a final present pass after world + UI composite in
`Renderer:endFrame`, inspired by the Pixel Transparency RetroArch shader
([github.com/mattakins/Pixel_Transparency](https://github.com/mattakins/Pixel_Transparency)).
Default OFF; persisted as `save.options.gbcfx`.
## Peer-to-peer link play (lua-enet)
Trades and link battles connect two copies of the game directly over
lua-enet (ENet ships inside LÖVE, nothing to install, no server to run)
on a reliable-ordered channel, replacing the original standalone Python
room-code relay (`tools/relay_server.py`, deleted). HOST A GAME shows the
host's LAN address (UDP 7777; `POKEPORT_LINK_PORT` overrides); JOIN A
GAME enters it. Closing performs a graceful ENet disconnect so the final
confirm/bye always lands; a vanished peer exits with "The link was
broken." Internet play needs a forwarded UDP port or a VPN (deliberate
tradeoff vs. the relay). Headless tests drive the protocol over an
in-memory loopback (`Net.loopbackPair`); under LÖVE the same test file
also exercises real UDP pairing.
## Custom boot text
The boot sequence replaces the Nintendo / GAME FREAK identifiers with
"bois club" / "bryanthaboi", a deliberate branding customization. The
rest of the boot beats (copyright splash, "presents" shooting-star, the
Nidorino-vs-Gengar attract scene) mirror the original.
## Custom Options
Options persist in a standalone `options.lua` (separate from the game
progress `save.lua`), so audio/display/battle preferences survive New Game
and aren't wiped when a save slot is cleared. Changing a row in the Options
menu or cycling hotkeys `2`/`3`/`5` writes immediately; an in-game save also
flushes the live options. Old saves that still embed an `options` table are
migrated once into `options.lua` on load.
- Music / SFX volume
- Music Filter
- OG GLITCHES on / off (Gen 1 quirks vs. modern-clean battle rules)
- COLORS (GBC / OG / OG INV / GBC INV / CLASSIC), also hotkey `2`
- TILT (OFF / 15 / 35 / 50), also hotkey `3` while free-roaming
- GBC FX (OFF / 1 / 2 / 3 / 4), also hotkey `5`
+64
View File
@@ -0,0 +1,64 @@
# What This Port Requires
The packaged desktop app requires one user-supplied input on first boot: a
canonical 1 MiB US Pokemon Red ROM.
The importer verifies SHA-1
`ea9bcae617fdf159b045185467ae58b2e4a48b9a`. Other revisions, Virtual
Console releases, and Pokemon Blue are rejected rather than decoded with
incorrect addresses.
After verification, the app generates its private cache in the LÖVE save
directory. It does not keep a copy of the ROM. Later boots use the cache.
Python and Pillow are not required by the packaged app.
## Bundled Metadata
Assembly removes high-level names and some relationships that the Lua port
needs. `tools/rom_manifest.json` therefore contains:
- the 3,268 ROM symbol addresses actually read by the extractor
- symbolic IDs and ordering for maps, species, moves, items, and trainers
- source-erased dimensions, image names, and map object integration names
- hand-ported field/script integration tables
- text labels and runtime substitution markers, but no dialogue payload
- music, sound-effect, and cry header names and addresses
The manifest contains no ROM bytes, graphics, dialogue, audio samples, or
complete symbol file. Dialogue, map blocks, encounters, stats, names, parties,
palettes, artwork, and audio channel programs are read from the user's ROM.
## Generated Output
First boot writes:
- `data/generated/`: constants, maps, tilesets, text and pointer tables,
Pokemon, moves, items, trainers, encounters, field data, palettes, and font
mappings, detailed battle animation programs, plus compact audio metadata
- `assets/generated/`: 495 PNGs covering maps, overworld sprites, fonts,
Pokemon/trainer pictures, title and intro art, menus, field effects, and
battle animation tiles
- `assets/generated/audio/programs.bin`: three 16 KiB ROM banks containing
the music, sound-effect, cry, and waveform programs used by live synthesis
Map behavior remains hand-ported under `data/scripts/`.
## Developer Builder
The optional source-tree builder provides a repeatable audit path:
```sh
python3 tools/build_data.py --rom /path/to/pokemon-red.gb --clean
```
That path requires Python 3.10+ and Pillow. It is not part of a packaged
game's first boot.
## Not Required
- a `pret/pokered` checkout
- the `symbols` branch or a `.sym` file
- Git
- RGBDS
- a separately compiled ROM
- Python or Pillow when running the packaged app
+201
View File
@@ -0,0 +1,201 @@
-- Native LÖVE2D port of Pokemon Red. A packaged build creates its private
-- game-data cache from a user-provided ROM on first boot.
--
-- Set POKEPORT_EDITOR=1 or pass `--editor` to `love .` to boot the save
-- editor tool (tools/save-editor/) instead of the game.
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
local Game, EditorApp, Importer
local autopilot -- optional scripted-input dev tool (tests/autopilot.lua)
local driverCo -- optional frame-driver (POKEPORT_DRIVER=file.lua): a
-- coroutine that receives `Game` and yields once per
-- frame; used headless (xvfb) for scripted screenshots
local function bootGame()
Game = require("src.core.Game")
Game:load()
if os.getenv("POKEPORT_AUTOPILOT") then
autopilot = require("tests.autopilot")
end
local driverPath = os.getenv("POKEPORT_DRIVER")
if driverPath then
local fn = assert(loadfile(driverPath))()
driverCo = coroutine.create(fn)
end
end
function love.load(args)
local savePath
for i, a in ipairs(args or {}) do
if a == "--editor" then
editorMode = true
elseif a == "--save" and args[i + 1] and args[i + 1] ~= "" then
savePath = args[i + 1]
end
end
love.graphics.setDefaultFilter("nearest", "nearest")
if editorMode then
package.path = love.filesystem.getSource() .. "/tools/save-editor/?.lua;"
.. love.filesystem.getSource() .. "/tools/save-editor/panels/?.lua;"
.. package.path
EditorApp = require("App")
EditorApp.load(savePath)
return
end
local RomImporter = require("src.import.RomImporter")
if os.getenv("POKEPORT_FORCE_IMPORT") == "1" or not RomImporter.isReady() then
Importer = RomImporter.new(function()
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
love.event.quit()
return
end
Importer = nil
bootGame()
end)
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
if importPath then Importer:startPath(importPath) end
return
end
bootGame()
end
function love.update(dt)
if editorMode then return EditorApp.update(dt) end
if Importer then return Importer:update(dt) end
if autopilot then
autopilot.update()
Game:update(1 / 60) -- deterministic stepping for the autopilot
return
end
if driverCo then
local ok, err = coroutine.resume(driverCo, Game)
if not ok then
print("driver error: " .. tostring(err))
love.event.quit(1)
return
end
if coroutine.status(driverCo) == "dead" then
love.event.quit()
return
end
Game:update(1 / 60)
return
end
Game:update(dt)
end
function love.draw()
if editorMode then return EditorApp.draw() end
if Importer then return Importer:draw() end
Game:draw()
-- frame capture requested by a driver
if Game.capturePath then
local path = Game.capturePath
Game.capturePath = nil
love.graphics.captureScreenshot(function(imagedata)
local fd = imagedata:encode("png")
local f = io.open(path, "wb")
if f then
f:write(fd:getString())
f:close()
end
end)
end
end
function love.keypressed(key, scancode, isrepeat)
if editorMode then return EditorApp.keypressed(key) end
if Importer then return Importer:keypressed(key) end
Game:keypressed(key)
end
function love.keyreleased(key)
if editorMode then return end
if Importer then return end
Game:keyreleased(key)
end
function love.gamepadpressed(joystick, button)
if editorMode then return end
if Importer then return end
Game:gamepadpressed(joystick, button)
end
function love.gamepadreleased(joystick, button)
if editorMode then return end
if Importer then return end
Game:gamepadreleased(joystick, button)
end
function love.gamepadaxis(joystick, axis, value)
if editorMode then return end
if Importer then return end
Game:gamepadaxis(joystick, axis, value)
end
function love.touchpressed(id, x, y, dx, dy, pressure)
if editorMode then return end
if Importer then return Importer:mousepressed(x, y, 1) end
Game:touchpressed(id, x, y)
end
function love.touchmoved(id, x, y, dx, dy, pressure)
if editorMode then return end
if Importer then return end
Game:touchmoved(id, x, y)
end
function love.touchreleased(id, x, y, dx, dy, pressure)
if editorMode then return end
if Importer then return end
Game:touchreleased(id, x, y)
end
function love.wheelmoved(x, y)
if editorMode then
if EditorApp.wheelmoved then return EditorApp.wheelmoved(x, y) end
return
end
if Importer then return end
Game:wheelmoved(x, y)
end
function love.mousepressed(x, y, button)
if Importer then return Importer:mousepressed(x, y, button) end
if editorMode and EditorApp.mousepressed then
return EditorApp.mousepressed(x, y, button)
end
end
function love.mousereleased(x, y, button)
if Importer then return end
if editorMode and EditorApp.mousereleased then
return EditorApp.mousereleased(x, y, button)
end
end
function love.textinput(text)
if Importer then return end
if editorMode and EditorApp.textinput then
return EditorApp.textinput(text)
end
end
function love.quit()
if editorMode and EditorApp.quit then
return EditorApp.quit() -- return true to abort quit
end
end
function love.filedropped(file)
if editorMode and EditorApp and EditorApp.filedropped then
return EditorApp.filedropped(file)
end
if Importer then Importer:filedropped(file) end
end
+108
View File
@@ -0,0 +1,108 @@
# Windows double-click bootstrap for the LÖVE2D Pokémon Red port.
# Launched by Play-Windows.bat. Prompts to install any missing tools
# (Python 3 and LÖVE via winget), runs first-time setup, then
# starts the game. Later runs launch the game straight away.
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
function Say($msg) { Write-Host "==> $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host " !! $msg" -ForegroundColor Yellow }
function Err($msg) { Write-Host "error: $msg" -ForegroundColor Red }
function Pause-Exit([int]$code = 0) {
Write-Host ''
Read-Host 'Press Enter to close this window' | Out-Null
exit $code
}
function Ask($question) { # yes by default
$a = Read-Host "$question [Y/n]"
return ($a -notmatch '^(n|no)$')
}
# winget installs update the registry PATH but not this process's copy;
# re-read it so freshly installed tools are usable without a new window.
function Refresh-Path {
$machine = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$user = [Environment]::GetEnvironmentVariable('Path', 'User')
$env:Path = "$machine;$user"
}
function Find-Python {
if (Get-Command py -ErrorAction SilentlyContinue) {
try { if ((& py -3 --version 2>$null) -match '^Python 3') { return $true } } catch {}
}
if (Get-Command python -ErrorAction SilentlyContinue) {
try { if ((& python --version 2>$null) -match '^Python 3') { return $true } } catch {}
}
return $false
}
function Find-Love {
foreach ($name in 'lovec', 'love') {
if (Get-Command $name -ErrorAction SilentlyContinue) { return $true }
}
foreach ($d in @("$env:ProgramFiles\LOVE", "${env:ProgramFiles(x86)}\LOVE", "$env:LOCALAPPDATA\Programs\LOVE")) {
if ($d -and (Test-Path (Join-Path $d 'love.exe'))) { return $true }
}
return $false
}
Write-Host ''
Write-Host ' Pokemon Red - LOVE2D port' -ForegroundColor Cyan
Write-Host ''
# ---------------------------------------------------------------- fast path
if ((Test-Path (Join-Path $Root 'data\generated\maps.lua')) -and (Find-Love)) {
Say 'already set up - launching the game'
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root 'scripts\run.ps1')
if ($LASTEXITCODE -ne 0) { Err 'the game failed to start'; Pause-Exit 1 }
exit 0
}
Say 'first-time setup'
# ------------------------------------------------------------------- winget
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Err 'winget (Windows package manager) is not available.'
Warn 'Install "App Installer" from the Microsoft Store, then run this again.'
Start-Process 'ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1' -ErrorAction SilentlyContinue
Pause-Exit 1
}
# ------------------------------------------------------------------- python
if (-not (Find-Python)) {
Warn 'Python 3 is missing (needed to build the game data)'
if (Ask 'Install Python 3 now via winget?') {
winget install --exact --id Python.Python.3.12 --accept-source-agreements --accept-package-agreements
Refresh-Path
if (-not (Find-Python)) {
Err 'Python still not found after install - close this window and double-click again'
Pause-Exit 1
}
} else { Err 'cannot continue without Python 3'; Pause-Exit 1 }
}
# --------------------------------------------------------------------- LOVE
if (-not (Find-Love)) {
Warn 'LOVE (the game engine) is missing'
if (Ask 'Install LOVE now via winget?') {
winget install --exact --id Love2d.Love2d --accept-source-agreements --accept-package-agreements
Refresh-Path
if (-not (Find-Love)) {
Err 'LOVE still not found after install - close this window and double-click again'
Pause-Exit 1
}
} else { Err 'cannot continue without LOVE'; Pause-Exit 1 }
}
# -------------------------------------------------------------------- build
Write-Host ''
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root 'scripts\setup.ps1')
if ($LASTEXITCODE -ne 0) { Err 'setup failed - see the messages above'; Pause-Exit 1 }
Say 'setup done - launching the game'
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root 'scripts\run.ps1')
if ($LASTEXITCODE -ne 0) { Err 'the game failed to start'; Pause-Exit 1 }
exit 0
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
# Packages the LÖVE2D Pokémon Red port into distributable macOS and
# Windows builds. Runs entirely on macOS (no cross-compiling needed,
# the Windows build reuses LÖVE's prebuilt win64 binaries).
#
# Usage: scripts/build.sh [mac|win|android|ios|all] [--version X.Y.Z] [--identity "Developer ID Application: ..."]
# [--notary-profile NAME] [--no-notarize]
# [--release] # android/ios: release config instead of debug
#
# Output: dist/mac/PokemonRed-macos.zip
# dist/win/PokemonRed-win64.zip
# dist/android/{debug,release}/*.apk (full gradle output stays under
# mobile/android/app/build/outputs/apk/embedNoRecord/)
# dist/ios/<Config>-<sdk>/PokemonRed.app (full xcodebuild output stays
# under mobile/ios/build/Build/Products/)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HERE="$ROOT/.bazinga"
CACHE="$HERE/cache"
WORK="$HERE/work"
DIST="$ROOT/dist"
ENTITLEMENTS="$ROOT/scripts/macos-entitlements.plist"
APP_NAME="PokemonRed"
BUNDLE_ID="com.theboisclub.pokemonred"
LOVE_VERSION="11.5"
VERSION="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo dev)"
IDENTITY=""
TARGET="all"
NOTARY_PROFILE="notary-profile"
NOTARIZE=true
ANDROID_RELEASE=false
IOS_RELEASE=false
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
mac|win|android|ios|all) TARGET="$1" ;;
--version) VERSION="$2"; shift ;;
--identity) IDENTITY="$2"; shift ;;
--notary-profile) NOTARY_PROFILE="$2"; shift ;;
--no-notarize) NOTARIZE=false ;;
--release) ANDROID_RELEASE=true; IOS_RELEASE=true ;;
*) fail "unknown argument: $1" ;;
esac
shift
done
mkdir -p "$CACHE" "$WORK" "$DIST/mac" "$DIST/win"
# --------------------------------------------------------------- game.love
say "packing game.love"
LOVE_FILE="$WORK/game.love"
rm -f "$LOVE_FILE"
(cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \
main.lua conf.lua src data assets tools/rom_manifest.json \
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
if unzip -Z1 "$LOVE_FILE" \
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
fail "game.love unexpectedly contains generated ROM data"
fi
say "game.love: $(du -h "$LOVE_FILE" | cut -f1)"
# --------------------------------------------------------------- macOS
build_mac() {
say "building macOS app"
local love_app="${LOVE_APP:-/Applications/love.app}"
[ -d "$love_app" ] || fail "LÖVE.app not found at $love_app (install it or set LOVE_APP=/path/to/love.app)"
local out_app="$WORK/$APP_NAME.app"
rm -rf "$out_app"
cp -R "$love_app" "$out_app"
# drop any bundled placeholder .love and fuse ours in
find "$out_app/Contents/Resources" -maxdepth 1 -name '*.love' -delete
cp "$LOVE_FILE" "$out_app/Contents/Resources/game.love"
local plist="$out_app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$plist" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleName string $APP_NAME" "$plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$plist" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $APP_NAME" "$plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$plist" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $BUNDLE_ID" "$plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$plist" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" "$plist" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $VERSION" "$plist"
if [ -f "$ROOT/assets/icon.icns" ]; then
cp "$ROOT/assets/icon.icns" "$out_app/Contents/Resources/GameIcon.icns"
fi
local id="$IDENTITY"
if [ -z "$id" ]; then
id="$(security find-identity -v -p codesigning 2>/dev/null | grep 'Developer ID Application' | head -1 | sed -E 's/^[^"]*"(.*)"$/\1/' || true)"
fi
if [ -n "$id" ]; then
say "codesigning with: $id"
codesign --deep --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" --sign "$id" "$out_app"
codesign --verify --deep --strict --verbose=2 "$out_app"
else
warn "no 'Developer ID Application' identity found, shipping unsigned."
warn "install your cert in Keychain Access, then re-run (or pass --identity \"Developer ID Application: Name (TEAMID)\")."
warn "unsigned builds will be Gatekeeper-blocked on other Macs; notarize with 'xcrun notarytool submit' once signed."
NOTARIZE=false
fi
if [ "$NOTARIZE" = true ]; then
if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1; then
warn "keychain profile '$NOTARY_PROFILE' not found/working, skipping notarization."
warn "set it up with: xcrun notarytool store-credentials \"$NOTARY_PROFILE\" --apple-id ... --team-id ... --password ..."
else
local notarize_zip="$WORK/$APP_NAME-notarize.zip"
rm -f "$notarize_zip"
(cd "$WORK" && ditto -c -k --keepParent "$APP_NAME.app" "$notarize_zip")
say "submitting to Apple notary service (this can take a few minutes)"
xcrun notarytool submit "$notarize_zip" --keychain-profile "$NOTARY_PROFILE" --wait
say "stapling notarization ticket"
xcrun stapler staple "$out_app"
rm -f "$notarize_zip"
fi
fi
local zip_out="$DIST/mac/$APP_NAME-macos.zip"
rm -f "$zip_out"
(cd "$WORK" && ditto -c -k --sequesterRsrc --keepParent "$APP_NAME.app" "$zip_out")
say "macOS build: $zip_out"
}
# --------------------------------------------------------------- Windows
build_win() {
say "building Windows (win64) app"
local zip_name="love-$LOVE_VERSION-win64.zip"
local love_zip="$CACHE/$zip_name"
if [ ! -f "$love_zip" ]; then
say "downloading LÖVE $LOVE_VERSION win64 binaries"
curl -fL --progress-bar \
"https://github.com/love2d/love/releases/download/$LOVE_VERSION/$zip_name" \
-o "$love_zip" || fail "download failed, check LOVE_VERSION or your network"
fi
local extract_dir="$WORK/love-win64"
rm -rf "$extract_dir"
mkdir -p "$extract_dir"
unzip -q "$love_zip" -d "$extract_dir"
local love_dir
love_dir="$(find "$extract_dir" -maxdepth 1 -mindepth 1 -type d | head -1)"
local out_dir="$WORK/$APP_NAME-win64"
rm -rf "$out_dir"
mkdir -p "$out_dir"
cp "$love_dir"/*.dll "$out_dir"/
cp "$love_dir"/license.txt "$out_dir"/ 2>/dev/null || true
cat "$love_dir/love.exe" "$LOVE_FILE" > "$out_dir/$APP_NAME.exe"
local zip_out="$DIST/win/$APP_NAME-win64.zip"
rm -f "$zip_out"
(cd "$WORK" && zip -q -9 -r "$zip_out" "$APP_NAME-win64")
say "Windows build: $zip_out"
}
# --------------------------------------------------------------- Android
build_android() {
say "building Android (delegating to scripts/build_android.sh)"
local args=()
if [ "$ANDROID_RELEASE" = true ]; then
args+=(--release)
fi
"$ROOT/scripts/build_android.sh" ${args[@]+"${args[@]}"}
}
# --------------------------------------------------------------- iOS
build_ios() {
say "building iOS (delegating to scripts/build_ios.sh)"
local args=()
if [ "$IOS_RELEASE" = true ]; then
args+=(--release)
fi
"$ROOT/scripts/build_ios.sh" ${args[@]+"${args[@]}"}
}
case "$TARGET" in
mac) build_mac ;;
win) build_win ;;
android) build_android ;;
ios) build_ios ;;
all) build_mac; build_win ;;
esac
case "$TARGET" in
android) say "done. See $DIST/android/" ;;
ios) say "done. See $DIST/ios/" ;;
*) say "done. Artifacts in $DIST" ;;
esac
+235
View File
@@ -0,0 +1,235 @@
#!/usr/bin/env bash
# Packages the LÖVE2D Pokémon Red port into an Android APK via love-android 11.5a.
#
# Usage: scripts/build_android.sh [--release] [--package-only]
#
# (default) assembleEmbedNoRecordDebug (debug keystore)
# --release assembleEmbedNoRecordRelease (requires out-of-band signing)
# --package-only zip game.love + apply branding; skip gradle
#
# Prerequisites:
# - mobile/android vendored love-android tree at tag 11.5a (in-repo; see mobile/ANDROID.md)
# - Android SDK + NDK (SDK API 34, NDK 25.2.9519653)
# - JDK 17
#
# Output (after gradle):
# dist/android/{debug,release}/*.apk (convenience copy)
# mobile/android/app/build/outputs/apk/embedNoRecord/debug/*.apk
# mobile/android/app/build/outputs/apk/embedNoRecord/release/*.apk
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ANDROID_DIR="$ROOT/mobile/android"
EMBED_ASSETS="$ANDROID_DIR/app/src/embed/assets"
LOVE_FILE="$EMBED_ASSETS/game.love"
DIST="$ROOT/dist/android"
APP_NAME="Pokemon Red"
APPLICATION_ID="com.theboisclub.pokemonred"
LOVE_ANDROID_VERSION="11.5a"
NDK_VERSION="25.2.9519653"
RELEASE=false
PACKAGE_ONLY=false
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
--release) RELEASE=true ;;
--package-only) PACKAGE_ONLY=true ;;
-h|--help)
sed -n '2,20p' "$0"
exit 0
;;
*) fail "unknown argument: $1 (try --release or --package-only)" ;;
esac
shift
done
# --------------------------------------------------------------- preconditions
if [ ! -f "$ANDROID_DIR/settings.gradle" ] || [ ! -f "$ANDROID_DIR/gradlew" ]; then
fail "love-android not found at mobile/android/.
The love-android $LOVE_ANDROID_VERSION tree is vendored in this repo, your checkout
looks incomplete. Re-clone or 'git checkout -- mobile/android'. See mobile/ANDROID.md."
fi
if [ ! -d "$ANDROID_DIR/love/src/jni/love/src" ]; then
fail "liblove sources missing under mobile/android/love/src/jni/love/.
They are vendored in this repo, your checkout looks incomplete.
Re-clone or 'git checkout -- mobile/android'. See mobile/ANDROID.md."
fi
# --------------------------------------------------------------- branding
# love-android 11.5+ reads app id / name / orientation from gradle.properties.
# Manifest still gets permission trims. Re-applied every build so refreshing
# the vendored love-android tree does not lose project settings.
apply_android_branding() {
local props="$ANDROID_DIR/gradle.properties"
local manifest="$ANDROID_DIR/app/src/main/AndroidManifest.xml"
[ -f "$props" ] || fail "missing $props"
[ -f "$manifest" ] || fail "missing $manifest"
say "applying Android branding (gradle.properties + permission trim)"
python3 - "$props" "$APPLICATION_ID" "$APP_NAME" <<'PY'
import pathlib, re, sys
path = pathlib.Path(sys.argv[1])
app_id, name = sys.argv[2], sys.argv[3]
text = path.read_text()
def set_prop(text, key, value):
pat = re.compile(rf"(?m)^{re.escape(key)}=.*$")
line = f"{key}={value}"
if pat.search(text):
return pat.sub(line, text)
return text.rstrip() + "\n" + line + "\n"
# Prefer plain app.name; clear byte-array form so it cannot win.
text = re.sub(r"(?m)^app\.name_byte_array=.*\n?", "", text)
text = set_prop(text, "app.name", name)
text = set_prop(text, "app.application_id", app_id)
text = set_prop(text, "app.orientation", "portrait")
path.write_text(text)
PY
python3 - "$manifest" <<'PY'
import pathlib, re, sys
path = pathlib.Path(sys.argv[1])
text = path.read_text()
# Drop network / mic / legacy storage, not needed for offline play.
# Keep VIBRATE (love.system.vibrate) and BLUETOOTH (optional gamepads).
# Orientation / label come from gradle.properties placeholders.
for perm in (
"android.permission.INTERNET",
"android.permission.RECORD_AUDIO",
"android.permission.WRITE_EXTERNAL_STORAGE",
):
text = re.sub(
rf'\s*<uses-permission android:name="{re.escape(perm)}"[^/]*/>\s*',
"\n",
text,
)
text = re.sub(r'\s*android:usesCleartextTraffic="true"', "", text)
path.write_text(text)
PY
}
# --------------------------------------------------------------- game.love
pack_game_love() {
say "packing game.love for love-android embed flavor"
mkdir -p "$EMBED_ASSETS"
rm -f "$LOVE_FILE"
(cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \
main.lua conf.lua src data assets tools/rom_manifest.json \
-x '*.DS_Store' -x '*/.git/*' -x '*/.DS_Store' \
-x 'data/generated/*' -x 'assets/generated/*')
if unzip -Z1 "$LOVE_FILE" \
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
fail "game.love unexpectedly contains generated ROM data"
fi
say "game.love: $(du -h "$LOVE_FILE" | cut -f1) -> $LOVE_FILE"
}
# --------------------------------------------------------------- SDK check
require_android_sdk() {
local sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
if [ -z "$sdk" ]; then
for candidate in \
"$HOME/Library/Android/sdk" \
"$HOME/Android/Sdk" \
/usr/local/lib/android/sdk; do
if [ -d "$candidate" ]; then
sdk="$candidate"
break
fi
done
fi
if [ -z "$sdk" ] || [ ! -d "$sdk" ]; then
fail "Android SDK not found.
Install Android Studio (or command-line tools), then either:
export ANDROID_SDK_ROOT=\$HOME/Library/Android/sdk
or create mobile/android/local.properties with:
sdk.dir=/path/to/Android/sdk
love-android $LOVE_ANDROID_VERSION expects SDK API 34 and NDK $NDK_VERSION
(see mobile/ANDROID.md)."
fi
export ANDROID_SDK_ROOT="$sdk"
export ANDROID_HOME="$sdk"
local props="$ANDROID_DIR/local.properties"
# Always rewrite so a leftover Docker sdk.dir=/opt/android-sdk cannot stick.
printf 'sdk.dir=%s\n' "$sdk" > "$props"
if ! command -v java >/dev/null 2>&1; then
fail "java not found. Install JDK 17 (Android Studio's bundled JDK is fine)."
fi
if [ ! -d "$sdk/ndk/$NDK_VERSION" ]; then
warn "NDK $NDK_VERSION not found under $sdk/ndk/"
warn "Install via SDK Manager (Show Package Details → NDK $NDK_VERSION)."
fi
}
# --------------------------------------------------------------- gradle
run_gradle() {
local task
if $RELEASE; then
task="assembleEmbedNoRecordRelease"
say "building release APK ($task)"
warn "release signing is out-of-band, see mobile/ANDROID.md (Signing)."
warn "without a signingConfig, assembleRelease may produce an unsigned APK or fail."
else
task="assembleEmbedNoRecordDebug"
say "building debug APK ($task), uses the default Android debug keystore"
fi
if ! (
cd "$ANDROID_DIR"
./gradlew --no-daemon "$task"
); then
fail "gradle $task failed.
Packaging already wrote: $LOVE_FILE
Common causes: missing SDK/NDK $NDK_VERSION, or JDK ≠ 17. See mobile/ANDROID.md.
You can still iterate on the .love payload with: scripts/build_android.sh --package-only"
fi
local out_dir
if $RELEASE; then
out_dir="$ANDROID_DIR/app/build/outputs/apk/embedNoRecord/release"
else
out_dir="$ANDROID_DIR/app/build/outputs/apk/embedNoRecord/debug"
fi
if [ -d "$out_dir" ]; then
say "APK output:"
find "$out_dir" -name '*.apk' -exec ls -lh {} \;
local flavor="debug"
$RELEASE && flavor="release"
local dist_dir="$DIST/$flavor"
rm -rf "$dist_dir"
mkdir -p "$dist_dir"
find "$out_dir" -name '*.apk' -exec cp {} "$dist_dir/" \;
say "copied to $dist_dir/"
else
warn "gradle finished but no APK dir at $out_dir, check gradle logs above"
fi
}
# --------------------------------------------------------------- main
apply_android_branding
pack_game_love
if $PACKAGE_ONLY; then
say "package-only: skipping gradle (game.love + branding ready under mobile/android/)"
exit 0
fi
require_android_sdk
run_gradle
say "done"
+386
View File
@@ -0,0 +1,386 @@
#!/usr/bin/env bash
# Packages the LÖVE2D Pokémon Red port into an iOS app via LÖVE 11.5's
# official iOS Xcode project (love-11.5-ios-source.zip).
#
# Usage: scripts/build_ios.sh [--fetch] [--device] [--release] [--package-only]
#
# (default) Simulator Debug (CODE_SIGNING_ALLOWED=NO)
# --device iphoneos SDK (needs signing / DEVELOPMENT_TEAM)
# --release Release configuration
# --fetch Download love-11.5-ios-source.zip into mobile/ios/love-src/
# --package-only Zip game.love + apply plist overlay; skip xcodebuild
#
# Prerequisites:
# - macOS + Xcode (xcodebuild)
# - mobile/ios/love-src/ (see --fetch / mobile/ios/README.md)
# - prebuilt iOS libraries under love-src/platform/xcode/ios/libraries/
#
# Output: dist/ios/<Config>-<sdk>/PokemonRed.app (convenience copy)
# mobile/ios/build/Build/Products/<Config>-<sdk>/PokemonRed.app
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
IOS_DIR="$ROOT/mobile/ios"
LOVE_SRC="$IOS_DIR/love-src"
CACHE="$IOS_DIR/cache"
BUILD_DIR="$IOS_DIR/build"
DIST="$ROOT/dist/ios"
OVERLAY_PLIST="$IOS_DIR/overlays/love-ios.plist"
XCODE_DIR="$LOVE_SRC/platform/xcode"
PROJECT="$XCODE_DIR/love.xcodeproj"
RESOURCES_DIR="$XCODE_DIR/ios/resources"
LOVE_FILE="$RESOURCES_DIR/game.love"
LIBS_DIR="$XCODE_DIR/ios/libraries"
APP_NAME="PokemonRed"
DISPLAY_NAME="Pokemon Red"
BUNDLE_ID="com.theboisclub.pokemonred"
LOVE_VERSION="$(tr -d '[:space:]' < "$IOS_DIR/LOVE_VERSION" 2>/dev/null || echo 11.5)"
IOS_SOURCE_ZIP="love-${LOVE_VERSION}-ios-source.zip"
APPLE_LIBS_ZIP="love-${LOVE_VERSION}-apple-libraries.zip"
IOS_SOURCE_URL="https://github.com/love2d/love/releases/download/${LOVE_VERSION}/${IOS_SOURCE_ZIP}"
APPLE_LIBS_URL="https://github.com/love2d/love/releases/download/${LOVE_VERSION}/${APPLE_LIBS_ZIP}"
FETCH=false
DEVICE=false
RELEASE=false
PACKAGE_ONLY=false
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
--fetch) FETCH=true ;;
--device) DEVICE=true ;;
--release) RELEASE=true ;;
--package-only) PACKAGE_ONLY=true ;;
-h|--help)
sed -n '2,22p' "$0"
exit 0
;;
*) fail "unknown argument: $1 (try --fetch, --device, --release, or --package-only)" ;;
esac
shift
done
# --------------------------------------------------------------- host checks
if [ "$(uname -s)" != "Darwin" ]; then
fail "iOS builds require macOS (Darwin). This host is $(uname -s).
Run scripts/build_ios.sh on a Mac with Xcode installed."
fi
if ! $PACKAGE_ONLY; then
command -v xcodebuild >/dev/null 2>&1 \
|| fail "xcodebuild not found. Install Xcode from the App Store, then run:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"
fi
# --------------------------------------------------------------- fetch love-src
fetch_love_ios() {
mkdir -p "$CACHE"
local zip_path="$CACHE/$IOS_SOURCE_ZIP"
if [ ! -f "$zip_path" ]; then
say "downloading $IOS_SOURCE_ZIP (LÖVE $LOVE_VERSION iOS sources)"
curl -fL --progress-bar "$IOS_SOURCE_URL" -o "$zip_path" \
|| fail "download failed: $IOS_SOURCE_URL"
else
say "using cached $zip_path"
fi
say "extracting into $LOVE_SRC"
rm -rf "$LOVE_SRC"
local tmp
tmp="$(mktemp -d "$CACHE/extract.XXXXXX")"
unzip -q "$zip_path" -d "$tmp"
# Zip root is love-<version>-ios-source/
local extracted
extracted="$(find "$tmp" -maxdepth 1 -mindepth 1 -type d ! -name '__MACOSX' | head -1)"
[ -n "$extracted" ] || fail "unexpected layout inside $IOS_SOURCE_ZIP"
mv "$extracted" "$LOVE_SRC"
rm -rf "$tmp"
say "love-src ready (LÖVE $LOVE_VERSION)"
}
if [ ! -d "$XCODE_DIR/love.xcodeproj" ]; then
if $FETCH; then
fetch_love_ios
else
fail "LÖVE $LOVE_VERSION iOS sources not found at mobile/ios/love-src/.
Fetch them (documented download of love-${LOVE_VERSION}-ios-source.zip):
scripts/build_ios.sh --fetch
Or manually:
mkdir -p mobile/ios/cache
curl -fL -o mobile/ios/cache/$IOS_SOURCE_ZIP \\
$IOS_SOURCE_URL
unzip -q mobile/ios/cache/$IOS_SOURCE_ZIP -d mobile/ios/cache
mv mobile/ios/cache/love-${LOVE_VERSION}-ios-source mobile/ios/love-src
See mobile/ios/README.md."
fi
elif $FETCH; then
say "love-src already present; skipping download (delete mobile/ios/love-src to refresh)"
fi
[ -d "$XCODE_DIR/love.xcodeproj" ] \
|| fail "missing $PROJECT after fetch"
# --------------------------------------------------------------- apple libraries
require_ios_libraries() {
if [ -d "$LIBS_DIR/SDL2.xcframework" ]; then
return 0
fi
fail "prebuilt iOS libraries missing at:
$LIBS_DIR
love-ios expects SDL2.xcframework (and friends) there.
The official love-${LOVE_VERSION}-ios-source.zip normally includes them.
If they are absent, install love-${LOVE_VERSION}-apple-libraries.zip:
mkdir -p mobile/ios/cache
curl -fL -o mobile/ios/cache/$APPLE_LIBS_ZIP \\
$APPLE_LIBS_URL
unzip -q mobile/ios/cache/$APPLE_LIBS_ZIP -d mobile/ios/cache
rm -rf mobile/ios/love-src/platform/xcode/ios/libraries
cp -R mobile/ios/cache/love-apple-dependencies/iOS/libraries \\
mobile/ios/love-src/platform/xcode/ios/libraries
See mobile/ios/README.md (Apple libraries dependency)."
}
require_ios_libraries
# --------------------------------------------------------------- branding / plist
apply_ios_branding() {
[ -f "$OVERLAY_PLIST" ] || fail "missing overlay plist: $OVERLAY_PLIST"
local dest="$XCODE_DIR/ios/love-ios.plist"
say "applying iOS branding (portrait-only Info.plist, display name)"
cp "$OVERLAY_PLIST" "$dest"
}
# --------------------------------------------------------------- game.love
pack_game_love() {
say "packing game.love for love-ios resources"
mkdir -p "$RESOURCES_DIR"
rm -f "$LOVE_FILE"
# Same payload as scripts/build.sh / build_android.sh: game sources only.
(cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \
main.lua conf.lua src data assets tools/rom_manifest.json \
-x '*.DS_Store' -x '*/.git/*' -x '*/.DS_Store' \
-x 'data/generated/*' -x 'assets/generated/*')
if unzip -Z1 "$LOVE_FILE" \
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
fail "game.love unexpectedly contains generated ROM data"
fi
say "game.love: $(du -h "$LOVE_FILE" | cut -f1) -> $LOVE_FILE"
}
# Ensure game.love is in the love-ios Copy Bundle Resources phase (idempotent).
ensure_game_love_in_xcode() {
local pbx="$XCODE_DIR/love.xcodeproj/project.pbxproj"
[ -f "$pbx" ] || fail "missing $pbx"
if grep -q 'ios/resources/game.love' "$pbx"; then
return 0
fi
say "wiring game.love into love-ios Copy Bundle Resources"
python3 - "$pbx" <<'PY'
import pathlib, sys
path = pathlib.Path(sys.argv[1])
text = path.read_text()
if "ios/resources/game.love" in text:
raise SystemExit(0)
file_ref = "A1B2C3D41E5F678901234567"
build_file = "A1B2C3D41E5F678901234568"
file_ref_entry = (
f"\t\t{file_ref} /* game.love */ = {{isa = PBXFileReference; "
f"lastKnownFileType = file; name = game.love; "
f'path = ios/resources/game.love; sourceTree = "<group>"; }};\n'
)
build_file_entry = (
f"\t\t{build_file} /* game.love in Resources */ = {{isa = PBXBuildFile; "
f"fileRef = {file_ref} /* game.love */; }};\n"
)
# PBXBuildFile section
marker = "/* Begin PBXBuildFile section */\n"
if marker not in text:
raise SystemExit("PBXBuildFile section not found")
text = text.replace(marker, marker + build_file_entry, 1)
# PBXFileReference section
marker = "/* Begin PBXFileReference section */\n"
if marker not in text:
raise SystemExit("PBXFileReference section not found")
text = text.replace(marker, marker + file_ref_entry, 1)
# Add to love-ios Resources build phase (FA0B7F041A95AAF3000E1D17)
old = (
"\t\tFA0B7F041A95AAF3000E1D17 /* Resources */ = {\n"
"\t\t\tisa = PBXResourcesBuildPhase;\n"
"\t\t\tbuildActionMask = 2147483647;\n"
"\t\t\tfiles = (\n"
"\t\t\t\tFA5D249C1A96CF4300C6FC8F /* Images.xcassets in Resources */,\n"
"\t\t\t\tFA7C636A1A9C49570000FD29 /* Launch Screen.xib in Resources */,\n"
"\t\t\t);\n"
)
new = (
"\t\tFA0B7F041A95AAF3000E1D17 /* Resources */ = {\n"
"\t\t\tisa = PBXResourcesBuildPhase;\n"
"\t\t\tbuildActionMask = 2147483647;\n"
"\t\t\tfiles = (\n"
"\t\t\t\tFA5D249C1A96CF4300C6FC8F /* Images.xcassets in Resources */,\n"
"\t\t\t\tFA7C636A1A9C49570000FD29 /* Launch Screen.xib in Resources */,\n"
f"\t\t\t\t{build_file} /* game.love in Resources */,\n"
"\t\t\t);\n"
)
if old not in text:
# Fallback: insert before the closing of that files = ( list if markers differ slightly
needle = "\t\tFA0B7F041A95AAF3000E1D17 /* Resources */ = {"
if needle not in text:
raise SystemExit("love-ios Resources build phase not found")
# Insert build file line after "files = (" within that block
idx = text.index(needle)
files_idx = text.index("files = (", idx)
insert_at = text.index("\n", files_idx) + 1
text = (
text[:insert_at]
+ f"\t\t\t\t{build_file} /* game.love in Resources */,\n"
+ text[insert_at:]
)
else:
text = text.replace(old, new, 1)
# Add file ref to the ios group if present
ios_group = "FA5D24961A96CE0A00C6FC8F /* ios */ = {"
if ios_group in text and file_ref not in text[text.index(ios_group):text.index(ios_group)+400]:
# Prefer adding under Resources group, skip if structure unknown; path is absolute enough via sourceTree
pass
path.write_text(text)
print("patched project.pbxproj")
PY
}
# --------------------------------------------------------------- xcodebuild
run_xcodebuild() {
local config sdk destination
if $RELEASE; then
config="Release"
else
config="Debug"
fi
if $DEVICE; then
sdk="iphoneos"
destination="generic/platform=iOS"
else
sdk="iphonesimulator"
destination="generic/platform=iOS Simulator"
fi
mkdir -p "$BUILD_DIR"
# Prefer -target + SYMROOT over -derivedDataPath: modern Xcode requires
# -scheme whenever -derivedDataPath is set, and love-ios ships no shared schemes.
local args=(
-project "$PROJECT"
-target love-ios
-configuration "$config"
-sdk "$sdk"
-destination "$destination"
SYMROOT="$BUILD_DIR/Build/Products"
OBJROOT="$BUILD_DIR/Build/Intermediates"
PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID"
PRODUCT_NAME="$APP_NAME"
MARKETING_VERSION="$LOVE_VERSION"
ONLY_ACTIVE_ARCH=NO
)
if ! $DEVICE; then
# Simulator: no signing required
args+=(CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=)
else
warn "device build: configure signing in Xcode or set DEVELOPMENT_TEAM / CODE_SIGN_IDENTITY"
if [ -n "${DEVELOPMENT_TEAM:-}" ]; then
args+=(DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM")
fi
if [ -n "${CODE_SIGN_IDENTITY:-}" ]; then
args+=(CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY")
fi
fi
if ! xcodebuild -showsdks 2>/dev/null | grep -q "$sdk"; then
fail "Xcode SDK '$sdk' is not installed (xcodebuild -showsdks).
Open Xcode → Settings → Platforms (or Components) and install iOS.
Simulator builds need the iOS Simulator platform; device builds need iOS."
fi
say "xcodebuild love-ios ($config / $sdk)"
set +e
(
cd "$XCODE_DIR"
xcodebuild "${args[@]}"
)
local xc_status=$?
set -e
if [ "$xc_status" -ne 0 ]; then
fail "xcodebuild failed (exit $xc_status).
Common causes:
- iOS platform/SDK not installed in Xcode (Settings → Platforms)
- device build without DEVELOPMENT_TEAM / provisioning (see mobile/ios/README.md)
- Xcode too new for LÖVE $LOVE_VERSION sources (try an older Xcode)
Packaging still succeeded: $LOVE_FILE"
fi
local products="$BUILD_DIR/Build/Products/${config}-${sdk}"
local app="$products/$APP_NAME.app"
if [ ! -d "$app" ]; then
# PRODUCT_NAME override can still leave love.app on older projects
if [ -d "$products/love.app" ]; then
app="$products/love.app"
warn "built app is love.app (PRODUCT_NAME override not applied); fusing game.love anyway"
else
warn "xcodebuild finished but no .app under $products"
find "$BUILD_DIR/Build/Products" -name '*.app' 2>/dev/null | head -20 || true
return 0
fi
fi
# Fuse even if the pbxproj wire-up failed, LÖVE runs any bundled *.love.
if [ ! -f "$app/game.love" ]; then
say "fusing game.love into $(basename "$app")"
cp "$LOVE_FILE" "$app/game.love"
fi
local dist_dir="$DIST/${config}-${sdk}"
rm -rf "$dist_dir"
mkdir -p "$dist_dir"
cp -R "$app" "$dist_dir/"
say "copied to $dist_dir/$(basename "$app")"
say "iOS app: $app"
say "bundle id: $BUNDLE_ID display: $DISPLAY_NAME"
if $DEVICE; then
warn "signing/provisioning is manual, see mobile/ios/README.md"
else
say "simulator tip: xcrun simctl install booted \"$app\""
fi
}
# --------------------------------------------------------------- main
apply_ios_branding
pack_game_love
ensure_game_love_in_xcode
if $PACKAGE_ONLY; then
say "package-only: skipping xcodebuild (game.love + plist ready under mobile/ios/love-src/)"
exit 0
fi
run_xcodebuild
say "done"
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+15
View File
@@ -0,0 +1,15 @@
# One-shot: full setup, then launch the game (Windows).
#
# Usage: powershell -ExecutionPolicy Bypass -File scripts\play.ps1 [-Rom red.gb]
param(
[string]$Rom = $env:ROM_PATH
)
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'setup.ps1') -Rom $Rom
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& (Join-Path $PSScriptRoot 'run.ps1')
exit $LASTEXITCODE
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# One-shot: full setup, then launch the game (macOS-friendly).
#
# Usage: scripts/play.sh [--rom /path/to/pokemon-red.gb]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
"$HERE/setup.sh" "$@"
exec "$HERE/run.sh"
+47
View File
@@ -0,0 +1,47 @@
# Run the LÖVE2D Pokémon Red port (Windows).
#
# Assumes scripts\setup.ps1 has been run once (generated data present and
# LÖVE installed). Extra arguments are passed through to LÖVE.
#
# Link play is peer-to-peer over lua-enet (bundled with LÖVE): one player
# uses START > LINK > HOST A GAME, the other joins the shown address.
# UDP port defaults to 7777; override with $env:POKEPORT_LINK_PORT.
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
function Fail($msg) { Write-Host "error: $msg" -ForegroundColor Red; exit 1 }
if (-not (Test-Path (Join-Path $Root 'data\generated\maps.lua'))) {
Fail 'generated data missing, run scripts\setup.ps1 first'
}
# Prefer lovec.exe (console-attached) so print output lands in the terminal;
# love.exe is a GUI-subsystem binary that swallows stdout.
function Find-Love {
foreach ($name in 'lovec', 'love') {
$cmd = Get-Command $name -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
}
$dirs = @(
"$env:ProgramFiles\LOVE",
"${env:ProgramFiles(x86)}\LOVE",
"$env:LOCALAPPDATA\Programs\LOVE"
)
foreach ($d in $dirs) {
foreach ($name in 'lovec.exe', 'love.exe') {
$p = Join-Path $d $name
if ($d -and (Test-Path $p)) { return $p }
}
}
return $null
}
$LoveBin = Find-Love
if (-not $LoveBin) {
Fail 'LÖVE not found, run scripts\setup.ps1 (or install from https://love2d.org)'
}
& $LoveBin $Root @args
exit $LASTEXITCODE
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Run the LÖVE2D Pokémon Red port (macOS-friendly).
#
# Assumes scripts/setup.sh has been run once (generated data present and
# LÖVE installed). Extra arguments are passed through to LÖVE.
#
# Link play is peer-to-peer (lua-enet, bundled with LÖVE): one player
# picks HOST A GAME in START > LINK and reads out the address shown;
# the other picks JOIN A GAME and types it in. UDP port 7777 by
# default (override with POKEPORT_LINK_PORT on both sides).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
[ -f "$ROOT/data/generated/maps.lua" ] \
|| fail "generated data missing, run scripts/setup.sh first"
find_love() {
command -v love >/dev/null 2>&1 && { echo "love"; return; }
for app in "/Applications/love.app" "$HOME/Applications/love.app"; do
if [ -x "$app/Contents/MacOS/love" ]; then
echo "$app/Contents/MacOS/love"
return
fi
done
return 1
}
LOVE_BIN="$(find_love)" \
|| fail "LÖVE not found, run scripts/setup.sh (or install from https://love2d.org)"
exec "$LOVE_BIN" "$ROOT" "$@"
+100
View File
@@ -0,0 +1,100 @@
# Build game data from a user-provided Pokemon Red ROM and install LÖVE.
#
# Usage:
# powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -Rom C:\path\red.gb
#
# With no explicit path, the first *.gb file in the project root is used.
param(
[string]$Rom = $env:ROM_PATH
)
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$Venv = Join-Path $Root '.venv'
function Say($msg) { Write-Host "==> $msg" -ForegroundColor Green }
function Fail($msg) { Write-Host "error: $msg" -ForegroundColor Red; exit 1 }
function Find-Python {
if (Get-Command py -ErrorAction SilentlyContinue) {
try {
if ((& py -3 --version 2>$null) -match '^Python 3') {
return @('py', '-3')
}
} catch {}
}
if (Get-Command python -ErrorAction SilentlyContinue) {
try {
if ((& python --version 2>$null) -match '^Python 3') {
return @('python')
}
} catch {}
}
return $null
}
$Python = @(Find-Python)
if (-not $Python) { Fail 'Python 3 is required to decode the ROM' }
$PyExe = $Python[0]
$PyArgs = @($Python | Select-Object -Skip 1)
if (-not $Rom) {
$candidate = Get-ChildItem -Path $Root -Filter '*.gb' -File |
Select-Object -First 1
if ($candidate) { $Rom = $candidate.FullName }
}
if (-not $Rom -or -not (Test-Path -LiteralPath $Rom -PathType Leaf)) {
Fail "Pokemon Red ROM not found. Put your .gb file in $Root or pass -Rom C:\path\red.gb"
}
$Rom = (Resolve-Path -LiteralPath $Rom).Path
$VenvPython = Join-Path $Venv 'Scripts\python.exe'
if (-not (Test-Path $VenvPython)) {
Say 'creating Python environment'
& $PyExe @PyArgs -m venv $Venv
if ($LASTEXITCODE -ne 0) { Fail 'venv creation failed' }
}
Say 'installing Pillow'
& $VenvPython -m pip install --quiet --upgrade pip
& $VenvPython -m pip install --quiet pillow
if ($LASTEXITCODE -ne 0) { Fail 'Pillow installation failed' }
Say "decoding game data from $(Split-Path -Leaf $Rom)"
Push-Location $Root
try {
& $VenvPython 'tools\build_data.py' --rom $Rom --clean
if ($LASTEXITCODE -ne 0) { Fail 'ROM extraction failed' }
} finally {
Pop-Location
}
function Find-Love {
foreach ($name in 'lovec', 'love') {
$cmd = Get-Command $name -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
}
foreach ($dir in @(
"$env:ProgramFiles\LOVE",
"${env:ProgramFiles(x86)}\LOVE",
"$env:LOCALAPPDATA\Programs\LOVE"
)) {
if ($dir) {
$candidate = Join-Path $dir 'love.exe'
if (Test-Path $candidate) { return $candidate }
}
}
return $null
}
if (Find-Love) {
Say 'LÖVE found'
} elseif (Get-Command winget -ErrorAction SilentlyContinue) {
Say 'installing LÖVE via winget'
winget install --exact --id Love2d.Love2d --accept-source-agreements --accept-package-agreements
} else {
Fail 'LÖVE 11.x is not installed; install it from https://love2d.org'
}
Say 'setup complete. Start the game with: scripts\run.ps1'
exit 0

Some files were not shown because too many files have changed in this diff Show More