Compare commits

..

15 Commits

Author SHA1 Message Date
bryanthaboi eea0dec4a6 Merge pull request #459 from johnjohto/fix-generic-joystick-input
Handle generic joystick input
2026-07-30 11:27:41 -04:00
bryanthaboi 25d241f12c Merge pull request #456 from johnjohto/fix-yellow-npc-trades
Fix Yellow NPC trades
2026-07-30 11:26:56 -04:00
bryanthaboi b9a89c1fd2 Merge pull request #434 from johnjohto/fuchsia-swamp-trainer-support-upstream
Support custom trainer palettes and base portraits
2026-07-30 11:23:56 -04:00
johnjohto 8a649e8d95 Handle generic joystick input
Unmapped handheld controllers emit joystick events, leaving the launcher
cursor and in-game input inactive.
2026-07-30 10:38:24 -04:00
johnjohto 7b208043b9 Fix Yellow NPC trades 2026-07-30 10:17:30 -04:00
johnjohto 26dfad0682 Merge remote-tracking branch 'upstream/main' 2026-07-30 10:13:03 -04:00
johnjohto 17eee1ca0e Test custom trainer portrait support 2026-07-30 00:00:36 -04:00
johnjohto 7aa06942be Support custom trainer palettes and base portraits 2026-07-29 23:59:59 -04:00
johnjohto d3010dc060 Merge pull request #1 from johnjohto/fuchsia-swamp-trainer-support
Support custom trainer palettes and base portraits
2026-07-29 23:52:41 -04:00
johnjohto 53e1a535dd Support custom trainer palettes and base portraits 2026-07-29 23:51:10 -04:00
bryanthaboi 5c0a7d1456 Merge pull request #422 from bryanthaboi/dev
big android energy
2026-07-29 16:07:31 -04:00
bryanthaboi e10f6e2032 Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-07-29 16:01:06 -04:00
bryanthaboi c61643edaa big android energy 2026-07-29 16:01:04 -04:00
bryanthaboi 0b7b0b48d1 Add Kotaku badge to README 2026-07-29 15:33:43 -04:00
bryanthaboi 2ff459e2c4 Update README to include Poke Yellow 2026-07-29 15:17:24 -04:00
18 changed files with 406 additions and 33 deletions
+4 -1
View File
@@ -1,6 +1,6 @@
# Gen1Recomp
A native LÖVE2D recreation of Poke Red and Blue. The engine and map
A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
behavior are hand-written Lua; game data and graphics are decoded from a ROM
supplied by the player.
@@ -9,6 +9,9 @@ supplied by the player.
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
<p align="center"> <a href="https://www.polygon.com/pokemon-red-blue-3d-voxel-mod-battle-pixels-gameplay-footage-remake/"> <img src="https://img.shields.io/badge/AS%20SEEN%20ON-POLYGON-ea2e49?style=for-the-badge" alt="As seen on Polygon"> </a> </p>
<p align="center"> <a href="https://kotaku.com/pokemon-red-blue-recompilation-project-voxel-3d-mod-2000720281"> <img src="https://img.shields.io/badge/AS%20SEEN%20ON-KOTAKU-ea2e49?style=for-the-badge" alt="As seen on KOTAKU"> </a> </p>
### Watch the latest update video
+24
View File
@@ -331,6 +331,30 @@ function love.gamepadaxis(joystick, axis, value)
Game:gamepadaxis(joystick, axis, value)
end
function love.joystickpressed(joystick, button)
if editorMode then return end
if Importer then return Importer:joystickpressed(joystick, button) end
Game:joystickpressed(joystick, button)
end
function love.joystickreleased(joystick, button)
if editorMode then return end
if Importer then return Importer:joystickreleased(joystick, button) end
Game:joystickreleased(joystick, button)
end
function love.joystickaxis(joystick, axis, value)
if editorMode then return end
if Importer then return Importer:joystickaxis(joystick, axis, value) end
Game:joystickaxis(joystick, axis, value)
end
function love.joystickhat(joystick, hat, direction)
if editorMode then return end
if Importer then return Importer:joystickhat(joystick, hat, direction) end
Game:joystickhat(joystick, hat, direction)
end
function love.joystickremoved(joystick)
if editorMode then return end
if Importer then return end
+4 -1
View File
@@ -70,7 +70,10 @@ The APK lands under `app/build/outputs/apk/embedNoRecord/debug/`.
### Payload path
`app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`,
`data/`, `assets/`, and `tools/rom_manifest.json`. Generated game data,
`data/`, `assets/`, and the Red, Blue, and Yellow ROM manifests. The Android
packer verifies the Yellow manifest before it packages; if a partial source
export omitted it, it restores the file from this checkout's Git data and then
falls back to the project's GitHub copy. Generated game data,
scripts, tests, and mobile build sources are excluded.
## Branding (applied by the build script)
+67 -1
View File
@@ -26,6 +26,11 @@ APP_NAME="gen1recomp"
APPLICATION_ID="com.theboisclub.pokemonred"
LOVE_ANDROID_VERSION="11.5a"
NDK_VERSION="25.2.9519653"
YELLOW_MANIFEST_RELATIVE="tools/rom_manifest_yellow.json"
# A fresh source checkout normally supplies this through Git. This URL is
# deliberately only a last resort for incomplete source exports: the manifest
# contains extraction metadata, never a ROM or extracted game data.
YELLOW_MANIFEST_URL="${YELLOW_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_yellow.json}"
VERSION=""
PACKAGE_ONLY=false
@@ -72,6 +77,59 @@ if [ ! -d "$ANDROID_DIR/love/src/jni/love/src" ]; then
Re-clone or 'git checkout -- mobile/android'. See mobile/ANDROID.md."
fi
# ------------------------------------------------------- Yellow import metadata
# Android packages game.love itself rather than reusing scripts/build.sh's
# archive. Keep a partial source export from silently shipping an APK that can
# list Yellow but cannot import it. Prefer the exact manifest from this
# checkout's Git object database; only then fall back to the public repository.
yellow_manifest_is_valid() {
local path="$1"
python3 - "$path" <<'PY'
import json, pathlib, sys
try:
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text())
except (OSError, ValueError):
raise SystemExit(1)
raise SystemExit(0 if manifest.get("romSha1") ==
"cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1" else 1)
PY
}
ensure_yellow_manifest() {
local manifest="$ROOT/$YELLOW_MANIFEST_RELATIVE"
local staged
staged="$(mktemp)"
if yellow_manifest_is_valid "$manifest"; then
rm -f "$staged"
return
fi
warn "Yellow import manifest is missing or invalid; recovering it before packaging"
if git -C "$ROOT" show "HEAD:$YELLOW_MANIFEST_RELATIVE" > "$staged" 2>/dev/null \
&& yellow_manifest_is_valid "$staged"; then
mkdir -p "$(dirname "$manifest")"
mv "$staged" "$manifest"
say "restored Yellow import manifest from this checkout's Git data"
return
fi
if command -v curl >/dev/null 2>&1 \
&& curl --fail --location --retry 2 --connect-timeout 15 \
--output "$staged" "$YELLOW_MANIFEST_URL" \
&& yellow_manifest_is_valid "$staged"; then
mkdir -p "$(dirname "$manifest")"
mv "$staged" "$manifest"
say "downloaded Yellow import manifest from the project repository"
return
fi
rm -f "$staged"
fail "Yellow import manifest is unavailable. Git recovery failed and could not download $YELLOW_MANIFEST_URL"
}
# --------------------------------------------------------------- branding
# love-android 11.5+ reads app id / name / orientation from gradle.properties.
# Manifest still gets permission trims. Re-applied every build so refreshing
@@ -135,6 +193,7 @@ PY
# --------------------------------------------------------------- game.love
pack_game_love() {
say "packing game.love for love-android embed flavor"
ensure_yellow_manifest
mkdir -p "$EMBED_ASSETS"
rm -f "$LOVE_FILE"
# tools/save-editor ships with the app: the launcher's Edit button on a save
@@ -142,14 +201,21 @@ pack_game_love() {
(cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \
main.lua conf.lua src data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.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
unzip -Z1 "$LOVE_FILE" | grep -qx 'tools/save-editor/App.lua' \
# Do not pipe unzip straight into grep here: on a large archive grep can
# finish early and make unzip report SIGPIPE under `set -o pipefail`.
local archive_entries
archive_entries="$(unzip -Z1 "$LOVE_FILE")"
grep -qx 'tools/save-editor/App.lua' <<< "$archive_entries" \
|| fail "game.love is missing the save editor (Edit on a save row would crash)"
grep -qx "$YELLOW_MANIFEST_RELATIVE" <<< "$archive_entries" \
|| fail "game.love is missing the Yellow ROM import manifest"
say "game.love: $(du -h "$LOVE_FILE" | cut -f1) -> $LOVE_FILE"
# This script packs its own game.love (it does not reuse build.sh's), so it
+23 -1
View File
@@ -188,6 +188,27 @@ local function namedPalette(data, name)
return { name = key, colors = colors }
end
-- Custom trainer portraits can opt into the same Advanced OBJ palette source
-- as their overworld walker. Vanilla trainers preserve the hardware-faithful
-- MEWMON fallback used during the battle introduction.
function BattleState.trainerPalette(data, trainer)
local source = trainer and trainer.paletteSource
if source then
local PaletteFX = require("src.render.PaletteFX")
local colors, group = PaletteFX.spriteObp({ paletteSource = source }, trainer.id)
if colors then
return { name = "trainer:" .. source .. ":" .. tostring(group), colors = colors }
end
end
return namedPalette(data, "MEWMON")
end
function BattleState.trainerPicPath(data, trainer)
if trainer and trainer.pic then return trainer.pic end
local base = trainer and trainer.basePic and data.trainers[trainer.basePic]
return base and base.pic or nil
end
-- The battle-BGP fade variant of a pic (AnimationFlashScreen and the
-- SetAnimationBGPalette effects remap the four BG shades; on the SGB
-- the colorizer then colors the REMAPPED shade, so a faded pic shows
@@ -608,7 +629,8 @@ function BattleState.newTrainer(game, oppClass, partyIndex)
-- MonsterPalettes[0] = PAL_MEWMON -- InitBattleCommon zeroes
-- wEnemyMonSpecies2 before the intro's SET_PAL_BATTLE
-- (engine/battle/core.asm:6682, engine/gfx/palettes.asm SetPal_Battle)
self.trainerPic = getImage(self.trainer.pic, namedPalette(game.data, "MEWMON"))
self.trainerPic = getImage(BattleState.trainerPicPath(game.data, self.trainer),
BattleState.trainerPalette(game.data, self.trainer))
self.introText = Strings("%s wants\nto fight!", self.trainer.name)
return self
end
+23
View File
@@ -38,6 +38,22 @@ local CONSTANT_DEFAULTS = {
},
}
-- data/events/trades.asm in Pokemon Yellow has its own TradeMons table.
-- Old Yellow imports were built from the Red manifest, so correct that
-- table after loading as well as in the fixed import manifest below.
local YELLOW_TRADES = {
{ give = "LICKITUNG", get = "DUGTRIO", dialogset = 1, nickname = "GURIO" },
{ give = "CLEFAIRY", get = "MR_MIME", dialogset = 1, nickname = "MILES" },
{ give = "BUTTERFREE", get = "BEEDRILL", dialogset = 3, nickname = "STINGER" },
{ give = "KANGASKHAN", get = "MUK", dialogset = 1, nickname = "STICKY" },
{ give = "MEW", get = "MEW", dialogset = 3, nickname = "BART" },
{ give = "TANGELA", get = "PARASECT", dialogset = 1, nickname = "SPIKE" },
{ give = "PIDGEOT", get = "PIDGEOT", dialogset = 2, nickname = "MARTY" },
{ give = "GOLDUCK", get = "RHYDON", dialogset = 2, nickname = "BUFFY" },
{ give = "GROWLITHE", get = "DEWGONG", dialogset = 3, nickname = "CEZANNE" },
{ give = "CUBONE", get = "MACHOKE", dialogset = 3, nickname = "RICKY" },
}
-- field.boot is the total-conversion override point for the new game; the
-- values match what SaveData.newGame and the Oak speech used to inline.
local BOOT_DEFAULTS = {
@@ -58,6 +74,12 @@ local function copy(value)
return out
end
function Data:applyVersionedFieldData()
if require("src.core.GameVersion").isYellow() then
self.field.trades = copy(YELLOW_TRADES)
end
end
-- Fills only what the cache is missing, so an importer that learns to
-- stamp one of these keys silently takes over from the engine.
function Data:seedDefaults()
@@ -77,6 +99,7 @@ function Data:seedDefaults()
if constants.dexDigits == nil then
constants.dexDigits = math.max(3, #tostring(constants.dexSize))
end
self:applyVersionedFieldData()
local boot = self.field.boot
if boot == nil then
boot = {}
+19
View File
@@ -491,6 +491,25 @@ function Game:gamepadaxis(joystick, axis, value)
Input:gamepadaxis(joystick, axis, value)
end
function Game:joystickpressed(joystick, button)
TouchControls:noteGamepad()
Input:joystickpressed(joystick, button)
end
function Game:joystickreleased(joystick, button)
Input:joystickreleased(joystick, button)
end
function Game:joystickaxis(joystick, axis, value)
if math.abs(value) > 0.5 then TouchControls:noteGamepad() end
Input:joystickaxis(joystick, axis, value)
end
function Game:joystickhat(joystick, hat, direction)
if direction ~= "c" then TouchControls:noteGamepad() end
Input:joystickhat(joystick, hat, direction)
end
-- Window focus/visibility flips: a release due while unfocused/hidden can
-- be swallowed by the OS. Reset on both edges -- gaining focus with a
-- physically held key won't re-fire keypressed, so trusting leftover
+45
View File
@@ -39,6 +39,20 @@ local DEFAULT_GAMEPAD_BINDINGS = {
local STICK_ON = 0.5
local STICK_OFF = 0.3
-- Generic SDL joysticks expose the left stick as the first two numbered
-- axes and the D-pad as a hat. This is common on Linux handhelds whose
-- controller has no game-controller database entry.
local RAW_BUTTON_BINDINGS = {
[1] = "a", [2] = "b",
[7] = "select", [8] = "start", [9] = "select", [10] = "start",
}
local HAT_DIRECTIONS = {
u = { "up" }, d = { "down" }, l = { "left" }, r = { "right" },
lu = { "left", "up" }, ru = { "right", "up" },
ld = { "left", "down" }, rd = { "right", "down" },
}
function Input:init()
self:applyBindings(nil)
self:reset()
@@ -79,6 +93,7 @@ function Input:reset()
self.sources = {}
self.stickAxis = { x = 0, y = 0 }
self.stickDir = nil
self.hatDirs = {}
end
-- Multiple physical sources (W + Up, d-pad + stick, etc.) can claim the
@@ -179,6 +194,16 @@ function Input:gamepadreleased(joystick, button)
end
end
function Input:joystickpressed(joystick, button)
local btn = RAW_BUTTON_BINDINGS[button]
if btn then press(self, btn, "joy:" .. button) end
end
function Input:joystickreleased(joystick, button)
local btn = RAW_BUTTON_BINDINGS[button]
if btn then release(self, btn, "joy:" .. button) end
end
-- left stick treated as a continuous held direction, same 4-way rule as
-- the touch swipe d-pad: whichever axis has the larger magnitude wins.
function Input:gamepadaxis(joystick, axis, value)
@@ -214,6 +239,26 @@ function Input:gamepadaxis(joystick, axis, value)
end
end
function Input:joystickaxis(joystick, axis, value)
if axis == 1 then
self:gamepadaxis(joystick, "leftx", value)
elseif axis == 2 then
self:gamepadaxis(joystick, "lefty", value)
end
end
function Input:joystickhat(joystick, hat, direction)
local source = "hat:" .. hat
for _, btn in ipairs(self.hatDirs[hat] or {}) do
release(self, btn, source)
end
local dirs = HAT_DIRECTIONS[direction] or {}
for _, btn in ipairs(dirs) do
press(self, btn, source)
end
self.hatDirs[hat] = dirs
end
function Input:isDown(btn)
return self.state[btn] or false
end
+31
View File
@@ -540,6 +540,7 @@ function RomImporter.new(onComplete, opts)
_padCursorActive = false,
_padAxis = { leftx = 0, lefty = 0, righty = 0 },
_padDir = {},
_rawHatDirs = {},
_padInited = false,
}, RomImporter)
@@ -1184,6 +1185,36 @@ function RomImporter:gamepadaxis(_, axis, value)
end
end
function RomImporter:joystickpressed(joystick, button)
if button == 1 then self:gamepadpressed(joystick, "a") end
end
function RomImporter:joystickreleased(joystick, button)
if button == 1 then self:gamepadreleased(joystick, "a") end
end
function RomImporter:joystickaxis(joystick, axis, value)
if axis == 1 then
self:gamepadaxis(joystick, "leftx", value)
elseif axis == 2 then
self:gamepadaxis(joystick, "lefty", value)
end
end
function RomImporter:joystickhat(_, hat, direction)
for _, dir in ipairs(self._rawHatDirs[hat] or {}) do
self._padDir[dir] = nil
end
local dirs = ({
u = { "dpup" }, d = { "dpdown" }, l = { "dpleft" }, r = { "dpright" },
lu = { "dpleft", "dpup" }, ru = { "dpright", "dpup" },
ld = { "dpleft", "dpdown" }, rd = { "dpright", "dpdown" },
})[direction] or {}
for _, dir in ipairs(dirs) do self._padDir[dir] = true end
self._rawHatDirs[hat] = dirs
if #dirs > 0 then self:_activatePadCursor() end
end
-- Player pressed Play on a game whose ROM is imported: hand off to boot.
function RomImporter:play(version)
if self.workState == "working" then return end
+9
View File
@@ -561,6 +561,11 @@ R.trainers = {
index = f.opt(f.int(0, 255)),
-- unused vanilla classes ship without a pic, so it cannot be required
pic = f.opt(f.path),
-- Optional Advanced-mode OBJ palette source for a custom trainer portrait.
-- It follows the same ROM crosswalk form as sprites.paletteSource.
paletteSource = f.opt(f.str),
-- Reuse a base trainer class's portrait without redistributing its asset.
basePic = f.opt(f.id("trainers")),
baseMoney = f.opt(f.int(0)),
parties = f.list(f.list(f.rec{ level = f.int(1),
species = f.id("pokemon") })),
@@ -580,6 +585,10 @@ R.sprites = {
frames = f.int(1),
walker = f.opt(f.bool),
trueColor = f.opt(f.bool),
-- Mod art can opt into an existing ROM sprite's Advanced-mode OBJ
-- palette assignment without claiming that the image itself came from
-- the ROM (which is what `source` documents on imported records).
paletteSource = f.opt(f.str),
},
example = 'mod.content.sprites:register("SPRITE_HERO", { image = "...", frames = 6 })',
}
+1 -1
View File
@@ -537,7 +537,7 @@ end
function PaletteFX.spriteObp(spriteDef, seed)
local pack = PaletteFX.gbcPack()
local w = pack and pack.world
local src = spriteDef and spriteDef.source
local src = spriteDef and (spriteDef.paletteSource or spriteDef.source)
if not (w and src) then return nil end
local idx = tonumber(src:match("%[(%d+)%]"))
-- RedBikeSprite loads outside SpriteSheetPointerTable
+3 -1
View File
@@ -50,7 +50,9 @@ local CALLBACK_NAMES = {
"keypressed", "keyreleased", "textinput",
"mousepressed", "mousereleased", "mousemoved", "wheelmoved",
"touchpressed", "touchmoved", "touchreleased",
"gamepadpressed", "gamepadreleased", "gamepadaxis", "joystickremoved",
"gamepadpressed", "gamepadreleased", "gamepadaxis",
"joystickpressed", "joystickreleased", "joystickaxis", "joystickhat",
"joystickremoved",
"focus", "visible", "resize", "filedropped", "directorydropped",
"errorhandler", "threaderror", "lowmemory",
}
+45
View File
@@ -46,6 +46,51 @@ Input:step()
check(Input:wasPressed("left"), "stick flick edges wasPressed")
check(not Input:isDown("left"), "stick flick does not stick isDown")
-- Linux handhelds without an SDL game-controller mapping send raw joystick
-- axes and D-pad hats instead of gamepad events.
Input:reset()
Input:joystickaxis(nil, 1, -0.9)
Input:step()
check(Input:isDown("left"), "raw joystick left axis holds left")
Input:joystickaxis(nil, 1, 0)
check(not Input:isDown("left"), "raw joystick axis release clears left")
Input:reset()
Input:joystickhat(nil, 1, "u")
Input:step()
check(Input:isDown("up"), "raw joystick hat holds up")
Input:joystickhat(nil, 1, "c")
check(not Input:isDown("up"), "raw joystick hat release clears up")
Input:reset()
Input:joystickpressed(nil, 1)
Input:step()
check(Input:isDown("a"), "raw joystick primary button presses A")
-- The launcher has a separate virtual cursor, so prove generic joystick
-- events reach its left-stick and D-pad state too.
local RomImporter = require("src.import.RomImporter")
local importer = setmetatable({
_padCursor = { x = 0, y = 0 }, _padCursorActive = false,
_padAxis = { leftx = 0, lefty = 0, righty = 0 },
_padDir = {}, _rawHatDirs = {}, _padInited = true,
}, RomImporter)
local clicked = false
function importer:mousepressed(_, _, button)
clicked = button == 1
end
importer:joystickaxis(nil, 1, -0.8)
check(importer._padAxis.leftx == -0.8,
"raw joystick left axis reaches the launcher cursor")
importer:joystickhat(nil, 1, "r")
check(importer._padDir.dpright,
"raw joystick hat reaches the launcher cursor")
importer:joystickhat(nil, 1, "c")
check(not importer._padDir.dpright,
"raw joystick hat release clears the launcher cursor")
importer:joystickpressed(nil, 1)
check(clicked, "raw joystick primary button clicks the launcher cursor")
-- Drivers that only inject pressQueue still get a one-step hold.
Input:reset()
table.insert(Input.pressQueue, "down")
+20
View File
@@ -11,6 +11,7 @@ local Font = require("src.render.Font")
Font.load(Data)
local BattleState = require("src.battle.BattleState")
local PaletteFX = require("src.render.PaletteFX")
local Catching = require("src.battle.Catching")
local Damage = require("src.battle.Damage")
local Events = require("src.mods.Events")
@@ -327,6 +328,25 @@ end
-- ------- ai_classes: brains and layer records
-- ------- custom trainer portraits: palette sources and base portraits
do
local oldMode = PaletteFX.mode
PaletteFX.mode = "redpp"
local custom = { id = "TEST_TRAINER",
paletteSource = "ROM:SpriteSheetPointerTable[21]" }
local pal = BattleState.trainerPalette(Data, custom)
local expected = PaletteFX.spriteObp({ paletteSource = custom.paletteSource }, custom.id)
PaletteFX.mode = oldMode
check(pal and expected and pal.colors[3][1] == expected[3][1]
and pal.colors[3][2] == expected[3][2]
and pal.colors[3][3] == expected[3][3],
"a custom trainer portrait resolves its Advanced OBJ palette source")
check(BattleState.trainerPicPath(Data, { basePic = "OPP_ENGINEER" })
== Data.trainers.OPP_ENGINEER.pic,
"a custom trainer can reuse a base trainer portrait by id")
end
do
Data.ai_classes = { OPP_YOUNGSTER = { brain = function(battle)
return { id = "SPLASH", pp = 1, brained = true }
+59
View File
@@ -0,0 +1,59 @@
-- Pokemon Yellow has a different TradeMons table from Red and Blue.
-- The import manifest must agree, and Data:seedDefaults repairs Yellow
-- caches made before that manifest carried the Yellow table (#453).
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local Data = require("src.core.Data")
if not (Data.field and Data.field.trades) then Data:load() end
local GameVersion = require("src.core.GameVersion")
local S = require("tests.harness").suite("parity Yellow trades")
local check, eq = S.check, S.eq
local oldVersion = GameVersion.get()
local oldTrades = {}
for i, trade in ipairs(Data.field.trades) do
oldTrades[i] = {}
for key, value in pairs(trade) do oldTrades[i][key] = value end
end
GameVersion.set("yellow")
Data:applyVersionedFieldData()
local expected = {
{ "LICKITUNG", "DUGTRIO", "GURIO" },
{ "CLEFAIRY", "MR_MIME", "MILES" },
{ "BUTTERFREE", "BEEDRILL", "STINGER" },
{ "KANGASKHAN", "MUK", "STICKY" },
{ "MEW", "MEW", "BART" },
{ "TANGELA", "PARASECT", "SPIKE" },
{ "PIDGEOT", "PIDGEOT", "MARTY" },
{ "GOLDUCK", "RHYDON", "BUFFY" },
{ "GROWLITHE", "DEWGONG", "CEZANNE" },
{ "CUBONE", "MACHOKE", "RICKY" },
}
local manifestFile = assert(io.open("tools/rom_manifest_yellow.json", "r"))
local manifest = manifestFile:read("*a")
manifestFile:close()
eq(#Data.field.trades, #expected, "Yellow has ten in-game trade rows")
for i, want in ipairs(expected) do
local got = Data.field.trades[i]
check(got.give == want[1] and got.get == want[2] and got.nickname == want[3],
("Yellow trade %d is %s for %s (%s)"):format(i, want[1], want[2], want[3]))
local row = ('"get": "%s",%%s+"give": "%s",%%s+"nickname": "%s"')
:format(want[2], want[1], want[3])
check(manifest:find(row) ~= nil,
("Yellow manifest carries trade %d"):format(i))
end
local ricky = Data.field.trades[10]
check(ricky.give == "CUBONE" and ricky.get == "MACHOKE",
"the Underground Path trade is Cubone for Machoke")
Data.field.trades = oldTrades
GameVersion.set(oldVersion)
return S:finish()
+1
View File
@@ -3277,6 +3277,7 @@ runSuites(orderedGlob("tests/parity_*.lua", {
"tests/parity_flavor.lua", "tests/parity_trainer_sight.lua",
"tests/parity_static.lua", "tests/parity_trashcans.lua",
"tests/parity_hof.lua", "tests/parity_trade_gift.lua",
"tests/parity_yellow_trades.lua",
"tests/parity_intro.lua", "tests/parity_tilt.lua",
"tests/parity_gbcfx.lua",
}))
+1
View File
@@ -382,6 +382,7 @@ def derive(red, pokeyellow, symbols_path):
except SystemExit as exc:
print(f"warning: parse_credits failed ({exc}); keeping Red credits")
# TODO: hand-author a Yellow credits banner if pret layout drifts.
yellow["field"]["trades"] = field.parse_trades(pokeyellow)
finally:
util.ASM_DEFINES = saved
+27 -27
View File
@@ -10552,63 +10552,63 @@
"trades": [
{
"dialogset": 1,
"get": "NIDORINA",
"give": "NIDORINO",
"nickname": "TERRY"
"get": "DUGTRIO",
"give": "LICKITUNG",
"nickname": "GURIO"
},
{
"dialogset": 1,
"get": "MR_MIME",
"give": "ABRA",
"nickname": "MARCEL"
"give": "CLEFAIRY",
"nickname": "MILES"
},
{
"dialogset": 3,
"get": "BEEDRILL",
"give": "BUTTERFREE",
"nickname": "CHIKUCHIKU"
"nickname": "STINGER"
},
{
"dialogset": 1,
"get": "SEEL",
"give": "PONYTA",
"nickname": "SAILOR"
"get": "MUK",
"give": "KANGASKHAN",
"nickname": "STICKY"
},
{
"dialogset": 3,
"get": "FARFETCHD",
"give": "SPEAROW",
"nickname": "DUX"
"get": "MEW",
"give": "MEW",
"nickname": "BART"
},
{
"dialogset": 1,
"get": "LICKITUNG",
"give": "SLOWBRO",
"nickname": "MARC"
"get": "PARASECT",
"give": "TANGELA",
"nickname": "SPIKE"
},
{
"dialogset": 2,
"get": "JYNX",
"give": "POLIWHIRL",
"nickname": "LOLA"
"get": "PIDGEOT",
"give": "PIDGEOT",
"nickname": "MARTY"
},
{
"dialogset": 2,
"get": "ELECTRODE",
"give": "RAICHU",
"nickname": "DORIS"
"get": "RHYDON",
"give": "GOLDUCK",
"nickname": "BUFFY"
},
{
"dialogset": 3,
"get": "TANGELA",
"give": "VENONAT",
"nickname": "CRINKLES"
"get": "DEWGONG",
"give": "GROWLITHE",
"nickname": "CEZANNE"
},
{
"dialogset": 3,
"get": "NIDORAN_F",
"give": "NIDORAN_M",
"nickname": "SPOT"
"get": "MACHOKE",
"give": "CUBONE",
"nickname": "RICKY"
}
],
"warpCarpets": {