mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
initial commit
This commit is contained in:
@@ -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 |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
@@ -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
|
||||
@@ -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 },
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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!" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
distance−1 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
|
||||
distance−1 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.
|
||||
@@ -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 0–1 "Red", skip, tiles 5–9
|
||||
"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 19–20 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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+108
@@ -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
|
||||
Executable
+202
@@ -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
|
||||
Executable
+235
@@ -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"
|
||||
Executable
+386
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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
|
||||
Executable
+11
@@ -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"
|
||||
@@ -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
@@ -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" "$@"
|
||||
@@ -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
Reference in New Issue
Block a user