merge main into dev

This commit is contained in:
bryanthaboi
2026-07-30 11:35:57 -04:00
17 changed files with 348 additions and 52 deletions
+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
+32 -15
View File
@@ -188,6 +188,36 @@ 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
-- Yellow only: ROCKET with wTrainerNo >= $2a is Jessie & James, who share
-- the class and the name "ROCKET" but battle behind their own pic
-- (home/trainers2.asm IsFightingJessieJames). picJessieJames exists only
-- in a Yellow cache extracted after #439, so an older cache keeps the
-- grunt pic until it is re-imported.
function BattleState.trainerPicPath(data, trainer, oppClass, partyIndex)
if oppClass == "OPP_ROCKET" and (partyIndex or 1) >= 42
and trainer and trainer.picJessieJames then
return trainer.picJessieJames
end
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
@@ -547,19 +577,6 @@ local function applySpecialMoves(data, oppClass, partyIndex, party)
end
end
-- Yellow only: ROCKET with wTrainerNo >= $2a is Jessie & James, who share
-- the class and the name "ROCKET" but battle behind their own pic
-- (home/trainers2.asm IsFightingJessieJames). picJessieJames exists only
-- in a Yellow cache extracted after #439, so an older cache keeps the
-- grunt pic until it is re-imported.
function BattleState.trainerPicPath(trainer, oppClass, partyIndex)
if oppClass == "OPP_ROCKET" and (partyIndex or 1) >= 42
and trainer.picJessieJames then
return trainer.picJessieJames
end
return trainer.pic
end
function BattleState.newTrainer(game, oppClass, partyIndex)
local self = newBattle(game)
self.kind = "trainer"
@@ -622,8 +639,8 @@ function BattleState.newTrainer(game, oppClass, partyIndex)
-- wEnemyMonSpecies2 before the intro's SET_PAL_BATTLE
-- (engine/battle/core.asm:6682, engine/gfx/palettes.asm SetPal_Battle)
self.trainerPic = getImage(
BattleState.trainerPicPath(self.trainer, oppClass, partyIndex),
namedPalette(game.data, "MEWMON"))
BattleState.trainerPicPath(game.data, self.trainer, oppClass, partyIndex),
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
@@ -604,6 +604,7 @@ function RomImporter.new(onComplete, opts)
_padCursorActive = false,
_padAxis = { leftx = 0, lefty = 0, righty = 0 },
_padDir = {},
_rawHatDirs = {},
_padInited = false,
}, RomImporter)
@@ -1336,6 +1337,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
@@ -562,6 +562,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") })),
@@ -581,6 +586,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
@@ -583,7 +583,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",
}
+2 -2
View File
@@ -52,11 +52,11 @@ return function(game)
CacheFs.exists(trainer.picJessieJames)
or love.filesystem.getInfo(trainer.picJessieJames) ~= nil)
end
local picked = BattleState.trainerPicPath(trainer, "OPP_ROCKET", 42)
local picked = BattleState.trainerPicPath(game.data, trainer, "OPP_ROCKET", 42)
U.log("pic:", tostring(picked))
check("party 42 selects the duo pic", picked == trainer.picJessieJames)
check("a lone grunt party still selects the class pic",
BattleState.trainerPicPath(trainer, "OPP_ROCKET", 3) == trainer.pic)
BattleState.trainerPicPath(game.data, trainer, "OPP_ROCKET", 3) == trainer.pic)
-- ---- reach the ambush ---------------------------------------------------
-- one strong mon so the fight is survivable if the reader plays it out
+6 -6
View File
@@ -18,24 +18,24 @@ local yellow = { name = "ROCKET", pic = CLASS_PIC, picJessieJames = DUO_PIC }
local grunt = { name = "ROCKET", pic = CLASS_PIC }
for _, party in ipairs({ 42, 43, 44, 45 }) do
T.eq(BattleState.trainerPicPath(yellow, "OPP_ROCKET", party), DUO_PIC,
T.eq(BattleState.trainerPicPath(nil, yellow, "OPP_ROCKET", party), DUO_PIC,
"party " .. party .. " fights behind the duo pic")
end
T.eq(yellow.name, "ROCKET", "the duo keeps the class name")
-- $2a is the first duo party; every grunt below it keeps the class pic
T.eq(BattleState.trainerPicPath(yellow, "OPP_ROCKET", 41), CLASS_PIC,
T.eq(BattleState.trainerPicPath(nil, yellow, "OPP_ROCKET", 41), CLASS_PIC,
"party 41 is still a lone grunt")
T.eq(BattleState.trainerPicPath(yellow, "OPP_ROCKET", 1), CLASS_PIC,
T.eq(BattleState.trainerPicPath(nil, yellow, "OPP_ROCKET", 1), CLASS_PIC,
"the first ROCKET party is still a lone grunt")
T.eq(BattleState.trainerPicPath(yellow, "OPP_ROCKET", nil), CLASS_PIC,
T.eq(BattleState.trainerPicPath(nil, yellow, "OPP_ROCKET", nil), CLASS_PIC,
"an unnumbered ROCKET falls back to party 1")
T.eq(BattleState.trainerPicPath(
T.eq(BattleState.trainerPicPath(nil,
{ pic = "assets/generated/battle/trainers/super_nerd.png",
picJessieJames = DUO_PIC }, "OPP_SUPER_NERD", 42),
"assets/generated/battle/trainers/super_nerd.png",
"the class gate holds: only ROCKET swaps")
T.eq(BattleState.trainerPicPath(grunt, "OPP_ROCKET", 42), CLASS_PIC,
T.eq(BattleState.trainerPicPath(nil, grunt, "OPP_ROCKET", 42), CLASS_PIC,
"a cache with no duo pic keeps the grunt instead of a nil pic")
-- The extractor writes jessie_james.png only when the symbol is in the
+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
@@ -385,6 +385,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": {