CLOSES #1206, CLOSES #1189, CLOSES #1175, CLOSES #1129, CLOSES #1119, CLOSES #1089, CLOSES #1073, CLOSES #1069, CLOSES #1065, CLOSES #1010, CLOSES #990, CLOSES #989, CLOSES #988, CLOSES #978, CLOSES #944, CLOSES #936, CLOSES #1221, CLOSES #1220, CLOSES #1193, CLOSES #1101, CLOSES #1066, CLOSES #1056, CLOSES #1041, CLOSES #984, CLOSES #1225, CLOSES #1214, CLOSES #1011, CLOSES #1035, CLOSES #1219, CLOSES #1048, CLOSES #1047, CLOSES #964, CLOSES #949, CLOSES #1149, CLOSES #1007, CLOSES #914, CLOSES #1115, CLOSES #1146, CLOSES #999, CLOSES #1207, CLOSES #1029, CLOSES #1120, CLOSES #987, CLOSES #983

This commit is contained in:
bryanthaboi
2026-08-13 13:07:20 -04:00
parent 833388c235
commit 37051a26b5
43 changed files with 752 additions and 135 deletions
+13 -13
View File
@@ -246,9 +246,17 @@ return {
-- the table sprites; re-entering the lab applies the same HideObject -- the table sprites; re-entering the lab applies the same HideObject
-- the gift script now does (OaksLab.asm OakGivesPokedex). -- the gift script now does (OaksLab.asm OakGivesPokedex).
onEnter = function(game, ow) onEnter = function(game, ow)
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then local flags = game.save.flags or {}
return if flags.EVENT_GOT_STARTER and not flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB then
local rival = ow:npcByIndex(1)
if rival then
rival.cellX = flags.EVENT_CHOSE_CHARMANDER and 7
or flags.EVENT_CHOSE_SQUIRTLE and 8 or 6
rival.cellY = 4
rival.px, rival.py = rival.cellX * 16, rival.cellY * 16
end
end end
if not flags.EVENT_GOT_POKEDEX then return end
local Commands = require("src.script.Commands") local Commands = require("src.script.Commands")
local ctx = { save = game.save, game = game, overworld = ow } local ctx = { save = game.save, game = game, overworld = ow }
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1") Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
@@ -286,6 +294,7 @@ return {
-- fanfare for the taunt/challenge exchange, same as the Yellow port -- fanfare for the taunt/challenge exchange, same as the Yellow port
-- (oaks_lab_yellow.lua); it was silently dropped here (#596). -- (oaks_lab_yellow.lua); it was silently dropped here (#596).
local rows = { local rows = {
{ "face_player_dir", "up" },
{ "stop_music" }, { "stop_music" },
{ "play_music", "Music_MeetRival" }, { "play_music", "Music_MeetRival" },
{ "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1 { "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1
@@ -309,28 +318,19 @@ return {
local base = #rows local base = #rows
local party = flags.EVENT_CHOSE_BULBASAUR and 3 local party = flags.EVENT_CHOSE_BULBASAUR and 3
or flags.EVENT_CHOSE_SQUIRTLE and 2 or 1 or flags.EVENT_CHOSE_SQUIRTLE and 2 or 1
table.insert(rows, { "save_end_battle_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", party }) table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", party })
-- OaksLabRivalEndBattleScript: heal + flag on win or loss; no blackout -- OaksLabRivalEndBattleScript: heal + flag on win or loss; no blackout
table.insert(rows, { "heal_party" }) table.insert(rows, { "heal_party" })
table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" }) table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" })
-- OaksLabRivalEndBattleScript: on WIN, print the "picked the wrong
-- POKéMON!" gloat, then BOTH win and loss print the shared exit line
-- _OaksLabRivalSmellYouLaterText ("OK! I'll make my POKéMON fight to
-- toughen it up!\012<PLAYER>! Gramps! Smell you later!") before Blue
-- marches out. A loss skips only the gloat (that taunt was already
-- shown in-battle via Rival1WinText), never the exit line (#231). The
-- jump_if_false convergence point is the exit line: base+6 indexes the
-- SmellYouLater row below, so WIN falls IPicked -> SmellYouLater and
-- LOSS jumps straight to SmellYouLater (both then walk-out + hide).
table.insert(rows, { "jump_if_false", base + 6 }) table.insert(rows, { "jump_if_false", base + 6 })
table.insert(rows, { "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" }) table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
-- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then -- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then
-- walk out past the player. The fanfare was dropped here (#683) -- the -- walk out past the player. The fanfare was dropped here (#683) -- the
-- parcel scene above already plays Music_MeetRival on both arrival and -- parcel scene above already plays Music_MeetRival on both arrival and
-- departure (lines 144-146), and this exit should match (#596). -- departure (lines 144-146), and this exit should match (#596).
table.insert(rows, { "stop_music" }) table.insert(rows, { "stop_music" })
table.insert(rows, { "play_music", "Music_MeetRival" }) table.insert(rows, { "play_music", "Music_MeetRival", { start = "rival" } })
table.insert(rows, { "move_npc_to", 1, 4, 11 }) table.insert(rows, { "move_npc_to", 1, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" }) table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
table.insert(rows, { "play_music", "Music_OaksLab" }) table.insert(rows, { "play_music", "Music_OaksLab" })
+10 -3
View File
@@ -226,9 +226,15 @@ return {
}, },
onEnter = function(game, ow) onEnter = function(game, ow)
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then local flags = game.save.flags or {}
return if flags.EVENT_GOT_STARTER and not flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB then
local rival = ow:npcByIndex(RIVAL)
if rival then
rival.cellX, rival.cellY = 7, 4
rival.px, rival.py = 7 * 16, 4 * 16
end
end end
if not flags.EVENT_GOT_POKEDEX then return end
local Commands = require("src.script.Commands") local Commands = require("src.script.Commands")
local ctx = { save = game.save, game = game, overworld = ow } local ctx = { save = game.save, game = game, overworld = ow }
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1") Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
@@ -296,13 +302,14 @@ return {
table.insert(rows, { "wait", 20 }) table.insert(rows, { "wait", 20 })
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" }) table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
table.insert(rows, { "stop_music" }) table.insert(rows, { "stop_music" })
table.insert(rows, { "play_music", "Music_MeetRival" }) table.insert(rows, { "play_music", "Music_MeetRival", { start = "rival" } })
table.insert(rows, { "move_npc_to", RIVAL, 4, 11 }) table.insert(rows, { "move_npc_to", RIVAL, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" }) table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
table.insert(rows, { "play_music", "Music_OaksLab" }) table.insert(rows, { "play_music", "Music_OaksLab" })
-- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009) -- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009)
table.insert(rows, { "face_player_dir", "up" }) table.insert(rows, { "face_player_dir", "up" })
table.insert(rows, { "set_field", "pikachuInBall", false }) table.insert(rows, { "set_field", "pikachuInBall", false })
table.insert(rows, { "spawn_pikachu_follower" })
table.insert(rows, { "play_cry", "PIKACHU" }) table.insert(rows, { "play_cry", "PIKACHU" })
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" }) table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" })
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" }) table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" })
+19 -19
View File
@@ -142,20 +142,23 @@ M.VIRIDIAN_CITY = {
M.BLUES_HOUSE = { M.BLUES_HOUSE = {
talk = { talk = {
TEXT_BLUESHOUSE_DAISY_SITTING = { TEXT_BLUESHOUSE_DAISY_SITTING = {
{ "face_player" }, -- 1 { "face_player" },
{ "check_flag", "EVENT_GOT_TOWN_MAP" }, -- 2 { "check_flag", "EVENT_GOT_TOWN_MAP" },
{ "jump_if_true", 10 }, -- 3 { "jump_if_true", "got_map" },
{ "check_flag", "EVENT_GOT_STARTER" }, -- 4 { "check_flag", "EVENT_GOT_POKEDEX" },
{ "jump_if_false", 12 }, -- 5 { "jump_if_false", "too_early" },
{ "show_text", "_BluesHouseDaisyOfferMapText" }, -- 6 { "show_text", "_BluesHouseDaisyOfferMapText" },
-- _GotMapText: "{PLAYER} got a\n{RAM:wStringBuffer}!" -- the -- _GotMapText: "{PLAYER} got a\n{RAM:wStringBuffer}!" -- the
-- buffer supplies "TOWN MAP" (scripts/BluesHouse.asm GotMapText) -- buffer supplies "TOWN MAP" (scripts/BluesHouse.asm GotMapText)
{ "give_item", "TOWN_MAP", 1, "_GotMapText" }, -- 7 { "give_item", "TOWN_MAP", 1, "_GotMapText" },
{ "set_flag", "EVENT_GOT_TOWN_MAP" }, -- 8 { "hide_object", "BLUES_HOUSE", "BLUESHOUSE_TOWN_MAP" },
{ "jump", 13 }, -- 9 { "set_flag", "EVENT_GOT_TOWN_MAP" },
{ "show_text", "_BluesHouseDaisyUseMapText" }, -- 10 { "jump", "end" },
{ "jump", 13 }, -- 11 { "label", "got_map" },
{ "show_text", "_BluesHouseDaisyRivalAtLabText" }, -- 12 { "show_text", "_BluesHouseDaisyUseMapText" },
{ "jump", "end" },
{ "label", "too_early" },
{ "show_text", "_BluesHouseDaisyRivalAtLabText" },
}, },
}, },
} }
@@ -1058,6 +1061,7 @@ local championsRoomRivalScript = {
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down -- OakCongratulatesPlayerScript: rival faces left, Oak faces down
{ "face_object", 1, "left" }, -- 17 { "face_object", 1, "left" }, -- 17
{ "face_object", 2, "down" }, -- 18 { "face_object", 2, "down" }, -- 18
{ "load_player_starter_name" },
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 19 { "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 19
-- OakDisappointedWithRivalScript: Oak turns to the rival (right) -- OakDisappointedWithRivalScript: Oak turns to the rival (right)
{ "face_object", 2, "right" }, -- 20 { "face_object", 2, "right" }, -- 20
@@ -1067,13 +1071,8 @@ local championsRoomRivalScript = {
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 23 { "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 23
{ "move_npc", 2, "up", 2 }, -- 24 OakExitChampionsRoomMovement { "move_npc", 2, "up", 2 }, -- 24 OakExitChampionsRoomMovement
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 25 { "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 25
-- ChampionsRoomPlayerFollowsOakScript / WalkToHallOfFame_RLEMovement. -- scripts/ChampionsRoom.asm WalkToHallOfFame_RLEMovement
-- The player walks out after Oak instead of the screen just fading on the { "move_player", "left", 1 },
-- spot (#704). Route one tile right before walking north so the player
-- reaches the north-wall HALL_OF_FAME warp without sharing the rival's
-- (4,2) cell. The original simulated movement bypasses entity collision,
-- but this scene should not visibly walk through the defeated rival.
{ "move_player", "right", 1 }, -- 26
{ "move_player", "up", 3 }, -- 27 { "move_player", "up", 3 }, -- 27
-- hand the induction off to the HALL_OF_FAME room (consumed by its -- 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) -- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
@@ -1238,6 +1237,7 @@ local function pokemonTower2FRivalScript(playerX)
{ "jump_if_false", "end" }, -- 6 loss: stay { "jump_if_false", "end" }, -- 6 loss: stay
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7 { "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8 { "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
{ "play_music", "Music_MeetRival", { start = "rival" } },
{ "walk_npc", 1, exitDirs }, -- 9 { "walk_npc", 1, exitDirs }, -- 9
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10 { "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10
{ "jump", "end" }, -- 11 { "jump", "end" }, -- 11
+3
View File
@@ -413,6 +413,9 @@ M.ROUTE_8_GATE = saffronGate("TEXT_ROUTE8GATE_GUARD", { { 2, 3 }, { 2, 4 } }, tr
-- ------------------------------------------------------------------- -- -------------------------------------------------------------------
M.POKEMON_FAN_CLUB = { M.POKEMON_FAN_CLUB = {
onEnter = function(game, ow)
require("src.world.PikachuFollower").onFanClubEntered(game, ow)
end,
talk = { talk = {
TEXT_POKEMONFANCLUB_CHAIRMAN = { TEXT_POKEMONFANCLUB_CHAIRMAN = {
{ "face_player" }, -- 1 { "face_player" }, -- 1
+18 -13
View File
@@ -142,7 +142,7 @@ M.MT_MOON_POKECENTER = {
local Commands = require("src.script.Commands") local Commands = require("src.script.Commands")
Commands.give_pokemon({ save = game.save, game = game, overworld = ow }, Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
"MAGIKARP", 5) "MAGIKARP", 5)
push(game, ("%s got a\nMAGIKARP!"):format(game.save.player.name), done) push(game, t._GotMonText or "{PLAYER} got\n{RAM:wNameBuffer}!", done)
end) end)
end, end,
}, },
@@ -512,24 +512,29 @@ M.ROUTE_24 = {
local flags = game.save.flags local flags = game.save.flags
local function battleOrDone() local function battleOrDone()
if ow:trainerDefeated(npc) then if ow:trainerDefeated(npc) then
push(game, "I hate this!\nMy dreams of\nTEAM ROCKET...", done) push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
done)
else else
ow:engageTrainer(npc, done) ow:engageTrainer(npc, done)
end end
end end
if not flags.EVENT_GOT_NUGGET then if not flags.EVENT_GOT_NUGGET then
push(game, "Congratulations!\nYou beat our 5\ncontest trainers!\f" local t = text(game)
.. "You just earned a\nfabulous prize!", function() push(game, t._Route24CooltrainerM1YouBeatOurContestText .. "\f"
.. t._Route24CooltrainerM1YouJustEarnedAPrizeText, function()
require("src.core.Sound").play(game.data, "Get_Item1")
if not require("src.inventory.Bag").add(game.save, "NUGGET", 1,
game.data) then
push(game, t._Route24CooltrainerM1NoRoomText, done)
return
end
flags.EVENT_GOT_NUGGET = true flags.EVENT_GOT_NUGGET = true
require("src.inventory.Bag").add(game.save, "NUGGET", 1) game.stringBuffer = game.data.items.NUGGET.name
push(game, ("%s received\na NUGGET!"):format(game.save.player.name), push(game, t._Route24CooltrainerM1ReceivedNuggetText, function()
function() require("src.core.Sound").play(game.data, "Get_Item1")
ask(game, "By the way, would\nyou like to join\nTEAM ROCKET?", push(game, t._Route24CooltrainerM1JoinTeamRocketText,
function() battleOrDone)
push(game, "Arrgh! You are\nnot convinced?\fThen I'll show\n" end)
.. "you my power!", battleOrDone)
end)
end)
end) end)
return return
end end
+22 -10
View File
@@ -508,12 +508,12 @@ M.PEWTER_CITY = {
-- Rival ambush: show the hidden rival, walk him up to the player, run -- 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 -- the battle rows, march him back and hide him. On a loss the walk is
-- skipped (the blackout rebuilds the map mid-script). -- skipped (the blackout rebuilds the map mid-script).
local function runAmbush(game, ow, rows, playerFacing) local function runAmbush(game, ow, rows, playerFacing, musicOpts)
if ow.runner:isRunning() then return false end if ow.runner:isRunning() then return false end
ow.player.facing = playerFacing ow.player.facing = playerFacing
-- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music -- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music
-- takes over and the map theme returns after the victory jingle -- takes over and the map theme returns after the victory jingle
require("src.core.Music").play(game.data, "Music_MeetRival") require("src.core.Music").play(game.data, "Music_MeetRival", nil, musicOpts)
ow.runner:run(rows) ow.runner:run(rows)
return true return true
end end
@@ -567,10 +567,12 @@ local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag,
{ "face_object", objIndex, rivalFacing }, -- 3 { "face_object", objIndex, rivalFacing }, -- 3
{ "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4 { "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4
{ "rival_battle", oppClass, baseParty }, -- 5 { "rival_battle", oppClass, baseParty }, -- 5
{ "jump_if_false", 11 }, -- 6 { "jump_if_false", 12 }, -- 6
{ "set_flag", beatFlag }, -- 7 { "set_flag", beatFlag }, -- 7
{ "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8 { "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8
{ "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9 { "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9
{ "play_music", "Music_MeetRival", { start = "rival",
tempo = n == 2 and 100 or nil } },
{ "walk_npc", objIndex, route22ExitDirs(n, py) }, -- 10 { "walk_npc", objIndex, route22ExitDirs(n, py) }, -- 10
{ "hide_object", "ROUTE_22", objName }, -- 11 { "hide_object", "ROUTE_22", objName }, -- 11
} }
@@ -594,7 +596,8 @@ M.ROUTE_22 = {
if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then
return runAmbush(game, ow, return runAmbush(game, ow,
route22Scene(2, 2, "ROUTE22_RIVAL2", "OPP_RIVAL2", 10, route22Scene(2, 2, "ROUTE22_RIVAL2", "OPP_RIVAL2", 10,
"EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE", y), playerFacing) "EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE", y), playerFacing,
{ tempo = 100 })
end end
return false return false
end, end,
@@ -618,10 +621,11 @@ local function ceruleanRivalScene(px, py)
{ "face_object", 1, "down" }, -- 3 { "face_object", 1, "down" }, -- 3
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4 { "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5 { "rival_battle", "OPP_RIVAL1", 7 }, -- 5
{ "jump_if_false", 11 }, -- 6 { "jump_if_false", 12 }, -- 6
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7 { "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8 { "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9 { "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9
{ "play_music", "Music_MeetRival", { start = "rival" } },
{ "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 10 { "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 10
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 11 { "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 11
} }
@@ -730,7 +734,7 @@ local JIGGLYPUFF_SILENCE, JIGGLYPUFF_STEP, JIGGLYPUFF_TAIL = 32, 24, 48
-- Built as a TextBox `auto` table: auto.sound fires the frame the last -- Built as a TextBox `auto` table: auto.sound fires the frame the last
-- page has typed out (PrintText returning), and auto.tick then runs once -- page has typed out (PrintText returning), and auto.tick then runs once
-- per frame while the gate it returns still reads as playing. -- per frame while the gate it returns still reads as playing.
local function jigglypuffDance(game, npc) local function jigglypuffDance(game, npc, ow)
local Music = require("src.core.Music") local Music = require("src.core.Music")
-- .findMatchingFacingDirectionLoop: the rotation picks up at the entry -- .findMatchingFacingDirectionLoop: the rotation picks up at the entry
-- matching the sprite's current facing (showMapText has just turned it -- matching the sprite's current facing (showMapText has just turned it
@@ -776,7 +780,13 @@ local function jigglypuffDance(game, npc)
if npc then npc.facing = JIGGLYPUFF_SPIN[step] end if npc then npc.facing = JIGGLYPUFF_SPIN[step] end
return return
end end
if frames >= JIGGLYPUFF_TAIL then phase = "done" end if frames >= JIGGLYPUFF_TAIL then
phase = "done"
if require("src.core.GameVersion").isYellow()
and require("src.world.PikachuFollower").starterInParty(game.save) then
ow.pikachuPewterSleepScene = true
end
end
end, end,
} }
end end
@@ -789,7 +799,7 @@ M.PEWTER_POKECENTER = {
local TextBox = require("src.render.TextBox") local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, game.stack:push(TextBox.new(game,
text(game)._PewterPokecenterJigglypuffText or "JIGGLYPUFF: Puu\npupuu!", text(game)._PewterPokecenterJigglypuffText or "JIGGLYPUFF: Puu\npupuu!",
done, { auto = jigglypuffDance(game, npc) })) done, { auto = jigglypuffDance(game, npc, ow) }))
end, end,
}, },
} }
@@ -864,10 +874,11 @@ M.SILPH_CO_7F = {
{ "face_object", 9, "up" }, -- 4 { "face_object", 9, "up" }, -- 4
{ "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5 { "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5
{ "rival_battle", "OPP_RIVAL2", 7 }, -- 6 { "rival_battle", "OPP_RIVAL2", 7 }, -- 6
{ "jump_if_false", 12 }, -- 7 { "jump_if_false", 13 }, -- 7
{ "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8 { "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8
{ "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9 { "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9
{ "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10 { "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10
{ "play_music", "Music_MeetRival", { start = "rival" } },
{ "move_npc_to", 9, 5, y + 1 }, -- 11 { "move_npc_to", 9, 5, y + 1 }, -- 11
{ "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 12 { "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 12
}, "down") }, "down")
@@ -899,10 +910,11 @@ M.SS_ANNE_2F = {
{ "face_object", 2, onLeft and "down" or "right" }, -- 3 { "face_object", 2, onLeft and "down" or "right" }, -- 3
{ "show_text", "_SSAnne2FRivalText" }, -- 4 { "show_text", "_SSAnne2FRivalText" }, -- 4
{ "rival_battle", "OPP_RIVAL2", 1 }, -- 5 { "rival_battle", "OPP_RIVAL2", 1 }, -- 5
{ "jump_if_false", 11 }, -- 6 { "jump_if_false", 12 }, -- 6
{ "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7 { "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7
{ "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8 { "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8
{ "show_text", "_SSAnne2FRivalCutMasterText" }, -- 9 { "show_text", "_SSAnne2FRivalCutMasterText" }, -- 9
{ "play_music", "Music_MeetRival", { start = "rival" } },
{ "walk_npc", 2, ssAnne2FRivalExitDirs(onLeft) }, -- 10 { "walk_npc", 2, ssAnne2FRivalExitDirs(onLeft) }, -- 10
{ "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 11 { "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 11
}, onLeft and "up" or "left") }, onLeft and "up" or "left")
+34 -13
View File
@@ -613,6 +613,7 @@ local function newBattle(game)
self.phase = "intro" self.phase = "intro"
self.menuIndex = 1 self.menuIndex = 1
self.moveIndex = 1 self.moveIndex = 1
self.playerMoveListIndex = 1
self.frame = 0 self.frame = 0
return self return self
end end
@@ -929,6 +930,11 @@ function BattleState:sayNext(text)
table.insert(self.queue, self.nextInsert, { text = text }) table.insert(self.queue, self.nextInsert, { text = text })
end end
function BattleState:sayNextWaitSfx(text, sfx)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { text = text, waitForLearningSfx = sfx })
end
-- sayNext for a page that ends in `text_end` (see sayAuto) (#765) -- sayNext for a page that ends in `text_end` (see sayAuto) (#765)
function BattleState:sayNextAuto(text, delay) function BattleState:sayNextAuto(text, delay)
self.nextInsert = (self.nextInsert or 0) + 1 self.nextInsert = (self.nextInsert or 0) + 1
@@ -1255,13 +1261,13 @@ function BattleState:updateQueue()
-- no subanimation player: keep the single-sound fallback (with -- no subanimation player: keep the single-sound fallback (with
-- the move's pitch/tempo modifiers; GROWL/ROAR play the -- the move's pitch/tempo modifiers; GROWL/ROAR play the
-- attacker's cry -- GetMoveSound/IsCryMove) -- attacker's cry -- GetMoveSound/IsCryMove)
if item.anim == "GROWL" or item.anim == "ROAR" then if self:animationsOn() and (item.anim == "GROWL" or item.anim == "ROAR") then
local attacker = item.attackerIsPlayer and self.player or self.enemy local attacker = item.attackerIsPlayer and self.player or self.enemy
if attacker then if attacker then
require("src.core.Sound").playMoveCry(self.data, attacker.mon.species, require("src.core.Sound").playMoveCry(self.data, attacker.mon.species,
anim and anim.tempo) anim and anim.tempo)
end end
elseif anim and anim.sound then elseif self:animationsOn() and anim and anim.sound then
local Sound = require("src.core.Sound") local Sound = require("src.core.Sound")
if Sound.playMove then if Sound.playMove then
Sound.playMove(self.data, anim) Sound.playMove(self.data, anim)
@@ -1361,6 +1367,11 @@ function BattleState:updateQueue()
self.current = nil self.current = nil
end end
elseif not (item and item.choice) then elseif not (item and item.choice) then
if item and item.waitForLearningSfx and not item.soundStarted then
item.soundStarted = true
self.waitingSound = item.waitForLearningSfx()
return true
end
-- The page is typed out and waiting on the player: PromptText -- The page is typed out and waiting on the player: PromptText
-- (home/text.asm:209-217) writes '▼' at (18,16) and ManualTextScroll -- (home/text.asm:209-217) writes '▼' at (18,16) and ManualTextScroll
-- blinks it until A/B, so the arrow belongs on a finished page and not -- blinks it until A/B, so the arrow belongs on a finished page and not
@@ -1746,6 +1757,7 @@ end
local function sendOutMonCursors(self) local function sendOutMonCursors(self)
self.menuIndex = 1 self.menuIndex = 1
self.moveIndex = 1 self.moveIndex = 1
self.playerMoveListIndex = 1
end end
-- core.asm:297-300: both sides' FLINCHED bits are cleared as a turn's move -- core.asm:297-300: both sides' FLINCHED bits are cleared as a turn's move
@@ -2044,8 +2056,10 @@ function BattleState:update(dt)
if self.moveSwapIndex then if self.moveSwapIndex then
self:swapMoves(self.moveSwapIndex, self.moveIndex) self:swapMoves(self.moveSwapIndex, self.moveIndex)
self.moveSwapIndex = nil self.moveSwapIndex = nil
self.moveIndex = math.min(self.playerMoveListIndex or 1, #moves)
else else
self.moveSwapIndex = self.moveIndex self.moveSwapIndex = self.moveIndex
self.moveIndex = math.min(self.playerMoveListIndex or 1, #moves)
end end
elseif input:wasPressed("b") then elseif input:wasPressed("b") then
require("src.core.Sound").play(self.data, "Press_AB") require("src.core.Sound").play(self.data, "Press_AB")
@@ -2068,6 +2082,7 @@ function BattleState:update(dt)
self.phase = "messages" self.phase = "messages"
self.afterQueue = "menu" self.afterQueue = "menu"
else else
self.playerMoveListIndex = self.moveIndex
self:resolveTurn(mv) self:resolveTurn(mv)
end end
end end
@@ -2423,11 +2438,9 @@ function BattleState:resolveTurn(playerAction)
end end
local order local order
if pFirst then if pFirst then
order = { { self.player, self.enemy, playerAction }, order = { { true, playerAction }, { false, enemyAction } }
{ self.enemy, self.player, enemyAction } }
else else
order = { { self.enemy, self.player, enemyAction }, order = { { false, enemyAction }, { true, playerAction } }
{ self.player, self.enemy, playerAction } }
end end
self.phase = "messages" self.phase = "messages"
@@ -2435,7 +2448,9 @@ function BattleState:resolveTurn(playerAction)
for _, entry in ipairs(order) do for _, entry in ipairs(order) do
self:act(function() self:act(function()
self:executeAction(entry[1], entry[2], entry[3]) local user = entry[1] and self.player or self.enemy
local target = entry[1] and self.enemy or self.player
self:executeAction(user, target, entry[2])
end) end)
end end
self:act(function() self:endOfTurn() end) self:act(function() self:endOfTurn() end)
@@ -2945,7 +2960,7 @@ function BattleState:applyHitFx(hit)
Sound.play(self.data, hit.sfx) Sound.play(self.data, hit.sfx)
end end
end end
if not t or not self:animationsOn() then return end if not t then return end
if t == 1 then if t == 1 then
-- PredefShakeScreenVertically b=8: the window drops by b for 3 frames -- PredefShakeScreenVertically b=8: the window drops by b for 3 frames
-- then home for 3, b counting down -- then home for 3, b counting down
@@ -3839,9 +3854,12 @@ function BattleState:awardExp()
participants, alive = 1, { self.player.mon } participants, alive = 1, { self.player.mon }
end end
local function applyShare(mon, split, announce) local function applyShare(mon, split, announce)
local playerId = self.game.save.player and self.game.save.player.id
local traded = mon.otId ~= nil and playerId ~= nil
and mon.otId ~= playerId or mon.traded == true and mon.otId == nil
local levels, gained = Experience.apply(self.data, mon, self.enemy.def, local levels, gained = Experience.apply(self.data, mon, self.enemy.def,
self.enemy.mon.level, self.kind == "trainer", self.enemy.mon.level, self.kind == "trainer",
split, mon.traded) split, traded)
-- Track level-ups for EvolveAfterBattle (OverworldState:afterBattle -> -- Track level-ups for EvolveAfterBattle (OverworldState:afterBattle ->
-- Evolution.checkParty). B-cancel leaves the mon at/above threshold; -- Evolution.checkParty). B-cancel leaves the mon at/above threshold;
-- without this gate it re-triggers after every later fight (#213). -- without this gate it re-triggers after every later fight (#213).
@@ -3865,7 +3883,7 @@ function BattleState:awardExp()
local text = Strings.source("%s gained\n%d EXP. Points!") local text = Strings.source("%s gained\n%d EXP. Points!")
if announce == "expAll" then if announce == "expAll" then
text = Strings.source("%s gained\nwith EXP.ALL,\v%d EXP. Points!") text = Strings.source("%s gained\nwith EXP.ALL,\v%d EXP. Points!")
elseif mon.traded then elseif traded then
text = Strings.source("%s gained\na boosted\v%d EXP. Points!") text = Strings.source("%s gained\na boosted\v%d EXP. Points!")
end end
self:sayNext(Strings(text, name, gained)) self:sayNext(Strings(text, name, gained))
@@ -4129,15 +4147,17 @@ function BattleState:learnMove(mon, moveId)
if #mon.moves < 4 then if #mon.moves < 4 then
table.insert(mon.moves, { id = moveId, pp = mdef.pp }) table.insert(mon.moves, { id = moveId, pp = mdef.pp })
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId }) Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
self:sayNext(self:romText("_MimicLearnedMoveText", "%s learned\n%s!", mon.nickname or self.data.pokemon[mon.species].name, self:sayNextWaitSfx(self:romText("_MimicLearnedMoveText", "%s learned\n%s!", mon.nickname or self.data.pokemon[mon.species].name,
mdef.name)) mdef.name), function()
return require("src.core.Sound").play(self.data, "Level_Up")
end)
return return
end end
-- the "trying to learn" preamble lives inside MoveLearnMenu:enter; -- the "trying to learn" preamble lives inside MoveLearnMenu:enter;
-- ordered insert so multi-level gains keep each level's checks -- ordered insert so multi-level gains keep each level's checks
-- between its own stat box and the next "grew to level" text -- between its own stat box and the next "grew to level" text
self:uiNext(function() self:uiNext(function()
return self:buildScreen("MoveLearnMenu", mon, moveId) return self:buildScreen("MoveLearnMenu", mon, moveId, nil, "Level_Up")
end) end)
end end
@@ -5791,6 +5811,7 @@ function BattleState:drawTextArea()
Font.draw(self.data.moves[m.id].name, 16, (7 + i) * 8) Font.draw(self.data.moves[m.id].name, 16, (7 + i) * 8)
end end
Font.drawCode(0xED, 8, (7 + self.mimicIndex) * 8) Font.drawCode(0xED, 8, (7 + self.mimicIndex) * 8)
Font.draw(Strings("WHICH TECHNIQUE?"), 8, 112)
end end
end end
+1 -2
View File
@@ -310,8 +310,7 @@ function EffectRegistry.runDamaging(battle, ctx, record)
if record and record.afterDamage then if record and record.afterDamage then
record.afterDamage(ctx, totalDealt) record.afterDamage(ctx, totalDealt)
elseif moveInst.struggle then elseif moveInst.struggle then
-- struggle recoils even when its effect id resolves to no record local recoil = math.max(1, math.floor(totalDealt / 2))
local recoil = math.max(1, math.floor(dmg / 2))
battle:sayNext(romText(battle.data, "_HitWithRecoilText", "%s's\nhit with recoil!", displayName(user))) battle:sayNext(romText(battle.data, "_HitWithRecoilText", "%s's\nhit with recoil!", displayName(user)))
battle:applyDamage(user, recoil) battle:applyDamage(user, recoil)
end end
+7 -8
View File
@@ -348,8 +348,9 @@ MoveEffects.primary = {
target.disabledTurns = battle.rng(1, 8) target.disabledTurns = battle.rng(1, 8)
local id = target.curMoves[slot].id local id = target.curMoves[slot].id
-- _MoveWasDisabledText: "X's / MOVE was / disabled!" -- _MoveWasDisabledText: "X's / MOVE was / disabled!"
return { romText(battle.data, "_MoveWasDisabledText", "%s's\n%s was\ndisabled!", displayName(target), return { romText(battle.data, "_MoveWasDisabledText", "%s's\n%s was\ndisabled!",
battle.data.moves[id].name) } { TARGET = displayName(target),
["RAM:wNameBuffer"] = battle.data.moves[id].name }) }
end, end,
SPLASH_EFFECT = function(battle) SPLASH_EFFECT = function(battle)
@@ -441,11 +442,10 @@ local function hitsFrom(dist, ctx)
return dist[r + 1] return dist[r + 1]
end end
-- drain_hp.asm halves the RAW wDamage IN PLACE (minimum 1) and heals -- engine/battle/core.asm ApplyDamageToEnemyPokemon
-- that amount, so Counter would see the halved value
local function drainHalf(label, text) local function drainHalf(label, text)
return function(ctx) return function(ctx)
local heal = math.max(1, math.floor(ctx.rawDamage / 2)) local heal = math.max(1, math.floor(ctx.totalDealt / 2))
ctx.battle.lastDamage = heal ctx.battle.lastDamage = heal
local mon = ctx.user.mon local mon = ctx.user.mon
mon.hp = math.min(mon.stats.hp, mon.hp + heal) mon.hp = math.min(mon.stats.hp, mon.hp + heal)
@@ -529,9 +529,8 @@ MoveEffects.full = {
RECOIL_EFFECT = { RECOIL_EFFECT = {
afterDamage = function(ctx) afterDamage = function(ctx)
-- recoil.asm reads the RAW computed wDamage (not the HP actually -- engine/battle/move_effects/recoil.asm
-- removed): overkill and substitute hits recoil at full strength local recoil = math.max(1, math.floor(ctx.totalDealt
local recoil = math.max(1, math.floor(ctx.rawDamage
/ (ctx.moveInst.struggle and 2 or 4))) / (ctx.moveInst.struggle and 2 or 4)))
ctx.say(romText(ctx.battle.data, "_HitWithRecoilText", "%s's\nhit with recoil!", displayName(ctx.user))) ctx.say(romText(ctx.battle.data, "_HitWithRecoilText", "%s's\nhit with recoil!", displayName(ctx.user)))
ctx.battle:applyDamage(ctx.user, recoil) ctx.battle:applyDamage(ctx.user, recoil)
+3 -1
View File
@@ -243,7 +243,9 @@ function Status.beforeMove(battler, rng, battle, selectedMoveId)
local shown = moves and moves[selectedMoveId] and moves[selectedMoveId].name local shown = moves and moves[selectedMoveId] and moves[selectedMoveId].name
or tostring(selectedMoveId) or tostring(selectedMoveId)
table.insert(msgs, romText(battle and battle.data, "_MoveIsDisabledText", table.insert(msgs, romText(battle and battle.data, "_MoveIsDisabledText",
"%s's\n%s is\ndisabled!", name(battler), shown)) "%s's\n%s is\ndisabled!", {
USER = name(battler), ["RAM:wNameBuffer"] = shown,
}))
return false, msgs return false, msgs
end end
end end
+11 -2
View File
@@ -1230,8 +1230,17 @@ function Engine.new(data, header, options)
engine.tempo = header.tempo engine.tempo = header.tempo
engine.tempoLocked = true engine.tempoLocked = true
end end
for _, spec in ipairs(chip and chip.channels local channels = chip and chip.channels or headerChannels(banks, header)
or headerChannels(banks, header)) do if header.startChannels then
local byNumber = {}
for _, start in ipairs(header.startChannels) do
byNumber[start.number] = start.address
end
for _, spec in ipairs(channels) do
spec.address = byNumber[spec.number] or spec.address
end
end
for _, spec in ipairs(channels) do
local frameTicks = options.frameTicks local frameTicks = options.frameTicks
local hardware = (spec.number - 1) % 4 + 1 local hardware = (spec.number - 1) % 4 + 1
if hardware == 4 then if hardware == 4 then
+18 -2
View File
@@ -689,10 +689,26 @@ function Game2:usePartyItem(itemId)
self:say(result.text) self:say(result.text)
return return
end end
self:consumeItem(itemId) if action == "stone" then
if action == "candy" then local party = (self.save and self.save.party) or {}
local index
for i, member in ipairs(party) do
if member == mon then index = i break end
end
Screens.push(self, "Gen2EvolutionAnim", {
mon = mon, entry = result.evolution, index = index,
party = party, save = self.save,
force = true,
onDone = function(evolution)
if evolution and evolution.evolved then self:consumeItem(itemId) end
self.stack:pop()
end,
})
elseif action == "candy" then
self:consumeItem(itemId)
self:say(result.text, function() self:afterRareCandy(mon, result) end) self:say(result.text, function() self:afterRareCandy(mon, result) end)
else else
self:consumeItem(itemId)
self:say(result.text) self:say(result.text)
end end
end end
+17 -2
View File
@@ -64,6 +64,7 @@ state = {
fanfareResume = false, -- start/resume state.source when the fanfare ends fanfareResume = false, -- start/resume state.source when the fanfare ends
fade = nil, -- active volume-ramp fade-out (see Music.fadeOut) fade = nil, -- active volume-ramp fade-out (see Music.fadeOut)
tempo = nil, -- alternate-tempo override in force for `current` tempo = nil, -- alternate-tempo override in force for `current`
start = nil,
failed = {}, -- labels whose def could not be started; logged once failed = {}, -- labels whose def could not be started; logged once
} }
@@ -215,9 +216,11 @@ function Music.play(data, song, loop, ctx)
song = selectSong(song, ctx) song = selectSong(song, ctx)
local tempo = ctx and ctx.tempo or nil local tempo = ctx and ctx.tempo or nil
local start = ctx and ctx.start or nil
-- a hook may silence the cue outright, or swap in a label the dedupe -- a hook may silence the cue outright, or swap in a label the dedupe
-- below has to compare against -- below has to compare against
if not song or (song == state.current and tempo == state.tempo) then return end if not song or (song == state.current and tempo == state.tempo
and start == state.start) then return end
local def = songDef(data, song) local def = songDef(data, song)
if not def or state.failed[song] then return end if not def or state.failed[song] then return end
@@ -240,6 +243,17 @@ function Music.play(data, song, loop, ctx)
slowed.tempo = tempo slowed.tempo = tempo
def = slowed def = slowed
end end
if start == "rival" and song == "Music_MeetRival"
and def.bank == 2 and def.address == 17050 then
local started = {}
for key, value in pairs(def) do started[key] = value end
started.startChannels = {
{ number = 1, address = 0x71a2 },
{ number = 2, address = 0x721d },
{ number = 3, address = 0x72b5 },
}
def = started
end
local wantLoop = loop ~= false local wantLoop = loop ~= false
local src, loopSrc, isChip, err = startSong(data, def, wantLoop) local src, loopSrc, isChip, err = startSong(data, def, wantLoop)
if not src then if not src then
@@ -276,6 +290,7 @@ function Music.play(data, song, loop, ctx)
state.source, state.loopSource, state.chip = src, loopSrc, isChip state.source, state.loopSource, state.chip = src, loopSrc, isChip
state.current = song state.current = song
state.tempo = tempo state.tempo = tempo
state.start = start
if Runtime.wants("music.started") then if Runtime.wants("music.started") then
Runtime.emit("music.started", { Runtime.emit("music.started", {
song = song, previous = previous, chip = isChip, song = song, previous = previous, chip = isChip,
@@ -290,7 +305,7 @@ function Music.stop()
stopSource(state.loopSource) stopSource(state.loopSource)
require("src.core.ChipAudio").stopMusic() require("src.core.ChipAudio").stopMusic()
state.current, state.source, state.loopSource, state.fade = nil, nil, nil, nil state.current, state.source, state.loopSource, state.fade = nil, nil, nil, nil
state.tempo = nil state.tempo, state.start = nil, nil
state.chip = false state.chip = false
state.pendingRestore = nil state.pendingRestore = nil
if previous and Runtime.wants("music.stopped") then if previous and Runtime.wants("music.stopped") then
+8
View File
@@ -33,6 +33,14 @@ return function(data, label, fallback, ...)
local args = { ... } local args = { ... }
if #args == 0 then return text end if #args == 0 then return text end
if #args == 1 and type(args[1]) == "table" then
local values = args[1]
return (text:gsub("%b{}", function(token)
local value = values[token] or values[token:sub(2, -2)]
return value == nil and token or tostring(value)
end))
end
local slots, named = 0, 0 local slots, named = 0, 0
for token in text:gmatch("%b{}") do for token in text:gmatch("%b{}") do
slots = slots + 1 slots = slots + 1
+3 -1
View File
@@ -1834,6 +1834,8 @@ function SaveData.newGame(boot)
boot = type(boot) == "table" and boot or {} boot = type(boot) == "table" and boot or {}
local map = boot.startMap or "REDS_HOUSE_2F" local map = boot.startMap or "REDS_HOUSE_2F"
local x, y = boot.startX or 3, boot.startY or 6 local x, y = boot.startX or 3, boot.startY or 6
local facing = boot.startFacing or "down"
if map == "REDS_HOUSE_2F" and boot.version ~= "yellow" then facing = "up" end
local heal = SaveData.defaultHeal(boot) local heal = SaveData.defaultHeal(boot)
local save = { local save = {
meta = { format = Version.saveFormat, mods = {} }, meta = { format = Version.saveFormat, mods = {} },
@@ -1844,7 +1846,7 @@ function SaveData.newGame(boot)
map = map, map = map,
x = x, x = x,
y = y, y = y,
facing = boot.startFacing or "down", facing = facing,
name = boot.playerName or "RED", name = boot.playerName or "RED",
rival = boot.rivalName or "BLUE", rival = boot.rivalName or "BLUE",
-- 16-bit trainer ID rolled at new game (wPlayerID, filled from -- 16-bit trainer ID rolled at new game (wPlayerID, filled from
+14
View File
@@ -450,6 +450,20 @@ record("RARE_CANDY", "candy", function(ctx)
return rareCandy(ctx.mon, ctx.data) return rareCandy(ctx.mon, ctx.data)
end) end)
for _, itemId in ipairs({ "SUN_STONE", "MOON_STONE", "FIRE_STONE",
"THUNDERSTONE", "WATER_STONE", "LEAF_STONE" }) do
record(itemId, "stone", function(ctx)
if ctx.mon.item == "EVERSTONE" then
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
end
local Evolution = require("src.core.gen2.Evolution")
local entry = Evolution.checkMon(ctx.data, ctx.mon,
{ force = true, item = ctx.item })
if not entry then return { used = false, text = ItemEffects.TEXT_NO_EFFECT } end
return { used = true, evolution = entry }
end)
end
for itemId in pairs(ItemEffects.REVIVE) do for itemId in pairs(ItemEffects.REVIVE) do
record(itemId, "revive", function(ctx) return revive(ctx.item, ctx.mon) end) record(itemId, "revive", function(ctx) return revive(ctx.item, ctx.mon) end)
end end
+11
View File
@@ -210,6 +210,17 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
-- (ItemUsePokeFlute, engine/items/item_effects.asm); never consumed. -- (ItemUsePokeFlute, engine/items/item_effects.asm); never consumed.
if itemId == "POKE_FLUTE" then if itemId == "POKE_FLUTE" then
if not battle then if not battle then
if ow and ow.map and ow.map.id == "PEWTER_POKECENTER"
and ow.pikachuPewterSleepScene then
local Follower = require("src.world.PikachuFollower")
local pika = Follower.current(ow)
local player = ow.player
if pika and player
and math.abs(pika.cellX - player.cellX) + math.abs(pika.cellY - player.cellY) == 1 then
return "flute_wake_pikachu", { romText(data, "_PlayedFluteHadEffectText",
"{PLAYER} played the\nPOKé FLUTE.") }
end
end
-- standing next to a not-yet-beaten Snorlax: this is the ONLY way -- standing next to a not-yet-beaten Snorlax: this is the ONLY way
-- Snorlax wakes -- using the flute from the item-use menu, never -- Snorlax wakes -- using the flute from the item-use menu, never
-- just talking to it with the flute in the bag (see -- just talking to it with the flute in the bag (see
+1
View File
@@ -140,6 +140,7 @@ function Evolution.learnEvolutionMoves(game, mon, onDone)
if #mon.moves < 4 then if #mon.moves < 4 then
table.insert(mon.moves, { id = moveId, pp = mdef.pp }) table.insert(mon.moves, { id = moveId, pp = mdef.pp })
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId }) Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
require("src.core.Sound").play(game.data, "Get_Item1")
game.stack:push(TextBox.new(game, game.stack:push(TextBox.new(game,
romText(game.data, "_LearnedMove1Text", romText(game.data, "_LearnedMove1Text",
"%s learned\n%s!", name, mdef.name), nextStep)) "%s learned\n%s!", name, mdef.name), nextStep))
+6 -1
View File
@@ -62,6 +62,8 @@ local function getObpImage(path, colors, group)
return obpCache[key] return obpCache[key]
end end
SpriteRenderer.obpImage = getObpImage
-- hot reload drops the sheets; live instances hold their own image, so -- hot reload drops the sheets; live instances hold their own image, so
-- the world rebuilds them (MapLoader.invalidateAll) rather than this -- the world rebuilds them (MapLoader.invalidateAll) rather than this
function SpriteRenderer.invalidate() function SpriteRenderer.invalidate()
@@ -281,7 +283,7 @@ end
-- swapped and OAM_XFLIP on each (data/sprites/facings.asm:192-197). Optional -- swapped and OAM_XFLIP on each (data/sprites/facings.asm:192-197). Optional
-- and trailing, so every existing call site is unchanged. -- and trailing, so every existing call site is unchanged.
function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip, function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip,
topHalf, forceFlip) topHalf, forceFlip, frameOverride)
local x, y = self:getScreenOrigin(px, py, camX, camY) local x, y = self:getScreenOrigin(px, py, camX, camY)
local image = self.image local image = self.image
local redraw = false local redraw = false
@@ -329,6 +331,9 @@ function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip,
-- still 3-frame sprites turn to face (the nurse at her machine, -- still 3-frame sprites turn to face (the nurse at her machine,
-- facePlayer on STAY NPCs) but never show walk frames. -- facePlayer on STAY NPCs) but never show walk frames.
local frame, flip = pose(self, facing, walkPhase, stepFlip) local frame, flip = pose(self, facing, walkPhase, stepFlip)
if frameOverride and self.frames[frameOverride] then
frame, flip = frameOverride, false
end
if forceFlip then flip = true end if forceFlip then flip = true end
local quad = self.frames[frame] local quad = self.frames[frame]
local drawHeight = self.frameHeight local drawHeight = self.frameHeight
+1
View File
@@ -130,6 +130,7 @@ TextBox.TOKENS = {
STRBUF = function(game) return game.stringBuffer end, STRBUF = function(game) return game.stringBuffer end,
RAM = function(game, arg) RAM = function(game, arg)
if arg == "wStringBuffer" then return game.stringBuffer end if arg == "wStringBuffer" then return game.stringBuffer end
if arg == "wNameBuffer" then return game.stringBuffer end
if arg == "wBoxNumString" then return game.boxNumString end if arg == "wBoxNumString" then return game.boxNumString end
-- SendNewMonToBox / _SentToBoxText reads the deposited nick here -- SendNewMonToBox / _SentToBoxText reads the deposited nick here
if arg == "wBoxMonNicks" then return game.boxMonNicks end if arg == "wBoxMonNicks" then return game.boxMonNicks end
+18
View File
@@ -100,6 +100,7 @@ O.tradeFlags = O.townVisited + 44 -- 2B (flag_array NUM_
-- Sits 2 bytes (wPlayerCoins) past O.coins per the walk above; absolute -- Sits 2 bytes (wPlayerCoins) past O.coins per the walk above; absolute
-- 0x2852 (#763, #857). -- 0x2852 (#763, #857).
O.toggleObjectFlags = O.coins + 2 -- 32B O.toggleObjectFlags = O.coins + 2 -- 32B
O.hiddenItemFlags = O.townVisited - 27
-- Play time (wPlayTimeHours/Maxed/Minutes/Seconds/Frames) lives INSIDE the -- Play time (wPlayTimeHours/Maxed/Minutes/Seconds/Frames) lives INSIDE the
-- sMainData window (wMainDataStart..wMainDataEnd is copied verbatim into -- sMainData window (wMainDataStart..wMainDataEnd is copied verbatim into
-- SRAM), 1866 bytes past wMainDataStart -- reached from the checksum-verified -- SRAM), 1866 bytes past wMainDataStart -- reached from the checksum-verified
@@ -770,6 +771,15 @@ function GenSave.decode(bytes, data, opts)
end end
end end
if data.hiddenItems then
save.hiddenTaken = {}
for i, row in ipairs(data.hiddenItems) do
if bitGet(bytes, O.hiddenItemFlags, i - 1) then
save.hiddenTaken[row[1] .. "_" .. row[2] .. "_" .. row[3]] = true
end
end
end
-- FLY destinations. wTownVisitedFlag's bit index IS the town's map index: -- FLY destinations. wTownVisitedFlag's bit index IS the town's map index:
-- engine/items/town_map.asm BuildFlyLocationsList loads the 16-bit value -- engine/items/town_map.asm BuildFlyLocationsList loads the 16-bit value
-- into de and rotates it right one bit per iteration with b counting up -- into de and rotates it right one bit per iteration with b counting up
@@ -953,6 +963,14 @@ function GenSave.encode(save, data, template)
end end
end end
if data.hiddenItems then
local taken = save.hiddenTaken or {}
for i, row in ipairs(data.hiddenItems) do
local key = row[1] .. "_" .. row[2] .. "_" .. row[3]
bitSet(buf, O.hiddenItemFlags, i - 1, taken[key] and true or false)
end
end
-- FLY destinations back into wTownVisitedFlag (see the decode note), so a -- FLY destinations back into wTownVisitedFlag (see the decode note), so a
-- save exported from this port is flyable on hardware (#263). A save -- save exported from this port is flyable on hardware (#263). A save
-- table with no `visited` key at all says nothing about the set, so leave -- table with no `visited` key at all says nothing about the set, so leave
+7
View File
@@ -49,6 +49,7 @@ local DATA_MODULES = {
charmap = { "src.save_convert.data.charmap", "src/save_convert/data/charmap.lua" }, charmap = { "src.save_convert.data.charmap", "src/save_convert/data/charmap.lua" },
eventFlags = { "src.save_convert.data.event_flags", "src/save_convert/data/event_flags.lua" }, eventFlags = { "src.save_convert.data.event_flags", "src/save_convert/data/event_flags.lua" },
toggleObjects = { "src.save_convert.data.toggle_objects", "src/save_convert/data/toggle_objects.lua" }, toggleObjects = { "src.save_convert.data.toggle_objects", "src/save_convert/data/toggle_objects.lua" },
hiddenItems = { "src.save_convert.data.hidden_items", "src/save_convert/data/hidden_items.lua" },
} }
local OPTIONAL_MODULES = { tilesets = true, audio = true } local OPTIONAL_MODULES = { tilesets = true, audio = true }
@@ -64,6 +65,10 @@ local YELLOW_EVENT_FLAGS = {
"src.save_convert.data.event_flags_yellow", "src.save_convert.data.event_flags_yellow",
"src/save_convert/data/event_flags_yellow.lua", "src/save_convert/data/event_flags_yellow.lua",
} }
local YELLOW_HIDDEN_ITEMS = {
"src.save_convert.data.hidden_items_yellow",
"src/save_convert/data/hidden_items_yellow.lua",
}
local function loadTable(requirePath, filePath) local function loadTable(requirePath, filePath)
local ok, mod = pcall(require, requirePath) local ok, mod = pcall(require, requirePath)
@@ -133,6 +138,8 @@ local function ensureData(gameVersion)
if name ~= "charmap" then if name ~= "charmap" then
if name == "eventFlags" and gameVersion == "yellow" then if name == "eventFlags" and gameVersion == "yellow" then
spec = YELLOW_EVENT_FLAGS -- Yellow's bit numbering differs (#838) spec = YELLOW_EVENT_FLAGS -- Yellow's bit numbering differs (#838)
elseif name == "hiddenItems" and gameVersion == "yellow" then
spec = YELLOW_HIDDEN_ITEMS
end end
local mod = loadCacheTable(gameVersion, spec[2]) local mod = loadCacheTable(gameVersion, spec[2])
if not mod then if not mod then
+30
View File
@@ -0,0 +1,30 @@
return {
{ "VIRIDIAN_FOREST", 1, 18 }, { "VIRIDIAN_FOREST", 16, 42 },
{ "MT_MOON_B2F", 18, 12 }, { "ROUTE_25", 38, 3 },
{ "ROUTE_9", 14, 7 }, { "SS_ANNE_KITCHEN", 13, 9 },
{ "SS_ANNE_B1F_ROOMS", 3, 1 }, { "ROUTE_10", 9, 17 },
{ "ROUTE_10", 16, 53 }, { "ROCKET_HIDEOUT_B1F", 21, 15 },
{ "ROCKET_HIDEOUT_B3F", 27, 17 }, { "ROCKET_HIDEOUT_B4F", 25, 1 },
{ "POKEMON_TOWER_5F", 4, 12 }, { "ROUTE_13", 1, 14 },
{ "ROUTE_13", 16, 13 }, { "POKEMON_MANSION_B1F", 1, 9 },
{ "SAFARI_ZONE_GATE", 10, 1 }, { "SAFARI_ZONE_WEST", 6, 5 },
{ "SILPH_CO_5F", 12, 3 }, { "SILPH_CO_9F", 2, 15 },
{ "COPYCATS_HOUSE_2F", 1, 1 }, { "CERULEAN_CAVE_1F", 14, 11 },
{ "CERULEAN_CAVE_B1F", 27, 3 }, { "POWER_PLANT", 17, 16 },
{ "POWER_PLANT", 12, 1 }, { "SEAFOAM_ISLANDS_B2F", 15, 15 },
{ "SEAFOAM_ISLANDS_B4F", 25, 17 }, { "POKEMON_MANSION_1F", 8, 16 },
{ "POKEMON_MANSION_3F", 1, 9 }, { "ROUTE_23", 9, 44 },
{ "ROUTE_23", 19, 70 }, { "ROUTE_23", 8, 90 },
{ "VICTORY_ROAD_2F", 5, 2 }, { "VICTORY_ROAD_2F", 26, 7 },
{ "UNUSED_MAP_6F", 14, 11 }, { "VIRIDIAN_CITY", 14, 4 },
{ "ROUTE_11", 48, 5 }, { "ROUTE_12", 2, 63 },
{ "ROUTE_17", 15, 14 }, { "ROUTE_17", 8, 45 },
{ "ROUTE_17", 17, 72 }, { "ROUTE_17", 4, 91 },
{ "ROUTE_17", 8, 121 }, { "UNDERGROUND_PATH_NORTH_SOUTH", 3, 4 },
{ "UNDERGROUND_PATH_NORTH_SOUTH", 4, 34 },
{ "UNDERGROUND_PATH_WEST_EAST", 12, 2 },
{ "UNDERGROUND_PATH_WEST_EAST", 21, 5 }, { "CELADON_CITY", 48, 15 },
{ "ROUTE_25", 10, 1 }, { "MT_MOON_B2F", 33, 9 },
{ "SEAFOAM_ISLANDS_B3F", 9, 16 }, { "VERMILION_CITY", 14, 11 },
{ "CERULEAN_CITY", 15, 8 }, { "ROUTE_4", 40, 3 },
}
@@ -0,0 +1,31 @@
return {
{ "SILPH_CO_5F", 12, 3 }, { "SILPH_CO_9F", 2, 15 },
{ "POKEMON_MANSION_3F", 1, 9 }, { "POKEMON_MANSION_B1F", 1, 9 },
{ "SAFARI_ZONE_WEST", 6, 5 }, { "CERULEAN_CAVE_2F", 16, 13 },
{ "CERULEAN_CAVE_B1F", 8, 14 }, { "UNUSED_MAP_6F", 14, 11 },
{ "SEAFOAM_ISLANDS_B2F", 15, 15 }, { "SEAFOAM_ISLANDS_B3F", 9, 16 },
{ "SEAFOAM_ISLANDS_B4F", 25, 17 }, { "VIRIDIAN_FOREST", 1, 18 },
{ "VIRIDIAN_FOREST", 16, 42 }, { "MT_MOON_B2F", 18, 12 },
{ "MT_MOON_B2F", 33, 9 }, { "SS_ANNE_B1F_ROOMS", 3, 1 },
{ "SS_ANNE_KITCHEN", 13, 9 }, { "UNDERGROUND_PATH_NORTH_SOUTH", 3, 4 },
{ "UNDERGROUND_PATH_NORTH_SOUTH", 4, 34 },
{ "UNDERGROUND_PATH_WEST_EAST", 12, 2 },
{ "UNDERGROUND_PATH_WEST_EAST", 21, 5 }, { "ROCKET_HIDEOUT_B1F", 21, 15 },
{ "ROCKET_HIDEOUT_B3F", 27, 17 }, { "ROCKET_HIDEOUT_B4F", 25, 1 },
{ "ROUTE_10", 9, 17 }, { "ROUTE_10", 16, 53 },
{ "POWER_PLANT", 17, 16 }, { "POWER_PLANT", 12, 1 },
{ "ROUTE_11", 48, 5 }, { "ROUTE_12", 2, 63 },
{ "ROUTE_13", 1, 14 }, { "ROUTE_13", 16, 13 },
{ "ROUTE_17", 15, 14 }, { "ROUTE_17", 8, 45 },
{ "ROUTE_17", 17, 72 }, { "ROUTE_17", 4, 91 },
{ "ROUTE_17", 8, 121 }, { "ROUTE_23", 9, 44 },
{ "ROUTE_23", 19, 70 }, { "ROUTE_23", 8, 90 },
{ "VICTORY_ROAD_2F", 5, 2 }, { "VICTORY_ROAD_2F", 26, 7 },
{ "ROUTE_25", 38, 3 }, { "ROUTE_25", 10, 1 },
{ "ROUTE_4", 40, 3 }, { "ROUTE_9", 14, 7 },
{ "COPYCATS_HOUSE_2F", 1, 1 }, { "VIRIDIAN_CITY", 14, 4 },
{ "CERULEAN_CITY", 15, 8 }, { "CERULEAN_CAVE_1F", 18, 7 },
{ "POKEMON_TOWER_5F", 4, 12 }, { "VERMILION_CITY", 14, 11 },
{ "CELADON_CITY", 48, 15 }, { "SAFARI_ZONE_GATE", 10, 1 },
{ "POKEMON_MANSION_1F", 8, 16 },
}
+17 -3
View File
@@ -526,6 +526,22 @@ function Commands.set_field(ctx, key, value)
ctx.save[key] = value ctx.save[key] = value
end end
function Commands.load_player_starter_name(ctx)
local flags = ctx.save.flags or {}
local species = flags.EVENT_CHOSE_PIKACHU and "PIKACHU"
or flags.EVENT_CHOSE_CHARMANDER and "CHARMANDER"
or flags.EVENT_CHOSE_SQUIRTLE and "SQUIRTLE"
or flags.EVENT_CHOSE_BULBASAUR and "BULBASAUR"
or (ctx.save.party and ctx.save.party[1] and ctx.save.party[1].species)
local def = species and ctx.game.data.pokemon[species]
ctx.game.stringBuffer = def and def.name or species or ""
end
function Commands.spawn_pikachu_follower(ctx)
require("src.world.PikachuFollower").onMapEntered(
ctx.game, ctx.overworld, nil, false)
end
local function toggleObject(ctx, mapId, objName, visible) local function toggleObject(ctx, mapId, objName, visible)
local save = ctx.save local save = ctx.save
save.objectToggles = save.objectToggles or {} save.objectToggles = save.objectToggles or {}
@@ -1131,9 +1147,7 @@ end
-- opts.tempo is the Music_*AlternateTempo override (audio/alternate_tempo.asm -- opts.tempo is the Music_*AlternateTempo override (audio/alternate_tempo.asm
-- re-points channel 1 at a stub that only changes the song's `tempo`) (#847). -- re-points channel 1 at a stub that only changes the song's `tempo`) (#847).
function Commands.play_music(ctx, songId, opts) function Commands.play_music(ctx, songId, opts)
local tempo = opts and opts.tempo require("src.core.Music").play(ctx.game.data, songId, nil, opts)
require("src.core.Music").play(ctx.game.data, songId, nil,
tempo and { tempo = tempo } or nil)
if opts and opts.keep and ctx.overworld then if opts and opts.keep and ctx.overworld then
ctx.overworld.keepMusicOnce = true ctx.overworld.keepMusicOnce = true
end end
+19 -7
View File
@@ -60,6 +60,14 @@ local function useOn(game, battle, id, target, list, moveIndex, picker)
return return
end end
if result == "flute_wake_pikachu" then
require("src.core.Sound").play(game.data, "Pokeflute")
showMessages(game, payload, function()
game.overworld.pikachuPewterSleepScene = nil
end)
return
end
-- field POKé FLUTE next to a not-yet-beaten Snorlax: "had effect" text, -- field POKé FLUTE next to a not-yet-beaten Snorlax: "had effect" text,
-- then the woke-up/battle sequence (data/scripts/story.lua snorlaxWake) -- then the woke-up/battle sequence (data/scripts/story.lua snorlaxWake)
if result == "flute_wake" then if result == "flute_wake" then
@@ -142,12 +150,9 @@ local function useOn(game, battle, id, target, list, moveIndex, picker)
list:close() list:close()
local ow = game.overworld local ow = game.overworld
local p = ow and ow.player local p = ow and ow.player
if ow and p then if ow and p and ow:facingIsShoreOrWater() then
local fx, fy = p:facingCell() ow:goFishing(id)
if ow.map:inBounds(fx, fy) and ow.map:isWaterCell(fx, fy) then return
ow:goFishing(id)
return
end
end end
showMessages(game, { Strings("No good! It's not\neven near water.") }) showMessages(game, { Strings("No good! It's not\neven near water.") })
return return
@@ -176,19 +181,25 @@ local function useOn(game, battle, id, target, list, moveIndex, picker)
end end
if #target.moves < 4 then if #target.moves < 4 then
table.insert(target.moves, { id = moveId, pp = mdef.pp }) table.insert(target.moves, { id = moveId, pp = mdef.pp })
require("src.core.Sound").play(game.data, "Get_Item1")
showMessages(game, { Strings("%s learned\n%s!", target.nickname or showMessages(game, { Strings("%s learned\n%s!", target.nickname or
game.data.pokemon[target.species].name, mdef.name) }) game.data.pokemon[target.species].name, mdef.name) })
if result == "learn" then consume(game, id) end if result == "learn" then consume(game, id) end
list.items = buildItems(game)
list.index = math.min(list.index, math.max(1, #list.items))
taught() taught()
else else
require("src.ui.Screens").push(game, "MoveLearnMenu", target, moveId, require("src.ui.Screens").push(game, "MoveLearnMenu", target, moveId,
function(learned) function(learned)
if learned and result == "learn" then consume(game, id) end if learned and result == "learn" then consume(game, id) end
if learned then
list.items = buildItems(game)
list.index = math.min(list.index, math.max(1, #list.items))
end
if learned then taught() end if learned then taught() end
end) end)
end end
end end
list:close()
teach() teach()
return return
end end
@@ -322,6 +333,7 @@ local function useOn(game, battle, id, target, list, moveIndex, picker)
local mdef = game.data.moves[moveId] local mdef = game.data.moves[moveId]
if #target.moves < 4 then if #target.moves < 4 then
table.insert(target.moves, { id = moveId, pp = mdef.pp }) table.insert(target.moves, { id = moveId, pp = mdef.pp })
require("src.core.Sound").play(game.data, "Get_Item1")
local name = target.nickname or def.name local name = target.nickname or def.name
showMessages(game, { Strings("%s learned\n%s!", name, mdef.name) }, showMessages(game, { Strings("%s learned\n%s!", name, mdef.name) },
nextStep) nextStep)
+17
View File
@@ -119,6 +119,13 @@ local function deposit(game)
onChoose = function(item, list) onChoose = function(item, list)
local mon = game.save.party[item.value] local mon = game.save.party[item.value]
if not mon then return end if not mon then return end
local Follower = require("src.world.PikachuFollower")
if Follower.isFollowingDisabled(game.overworld)
and Follower.isStarterPikachu(game.save, mon) then
game.stack:push(TextBox.new(game, t._SleepingPikachuText2
or Strings("There isn't any\nresponse...")))
return
end
monSubmenu(game, "DEPOSIT", mon, function() monSubmenu(game, "DEPOSIT", mon, function()
if #game.save.party <= 1 then if #game.save.party <= 1 then
list.footer = Strings("You need at least\none POKéMON!") list.footer = Strings("You need at least\none POKéMON!")
@@ -167,6 +174,16 @@ local function release(game)
local mon = box[list.index] local mon = box[list.index]
if not mon then return end if not mon then return end
local name = monName(game, mon) local name = monName(game, mon)
if require("src.core.GameVersion").isYellow()
and mon.species == "PIKACHU"
and mon.otId == game.save.player.id
and mon.ot == game.save.player.name then
require("src.core.Sound").playCry(game.data, mon.species)
game.stack:push(TextBox.new(game,
(t._PikachuUnhappyText or Strings("%s looks\nunhappy about it!", name))
:gsub("{RAM:wNameBuffer}", name)))
return
end
game.stack:push(TextBox.new(game, game.stack:push(TextBox.new(game,
Strings("Once released,\n%s is\ngone forever. OK?", name), nil, { Strings("Once released,\n%s is\ngone forever. OK?", name), nil, {
defaultNo = true, noSound = true, defaultNo = true, noSound = true,
+7 -2
View File
@@ -17,12 +17,13 @@ local HM_MOVES = {
CUT = true, FLY = true, SURF = true, STRENGTH = true, FLASH = true, CUT = true, FLY = true, SURF = true, STRENGTH = true, FLASH = true,
} }
function MoveLearnMenu.new(game, mon, newMoveId, onDone) function MoveLearnMenu.new(game, mon, newMoveId, onDone, learnedSound)
local self = setmetatable({}, MoveLearnMenu) local self = setmetatable({}, MoveLearnMenu)
self.game = game self.game = game
self.mon = mon self.mon = mon
self.newMoveId = newMoveId self.newMoveId = newMoveId
self.onDone = onDone self.onDone = onDone
self.learnedSound = learnedSound or "Get_Item1"
self.index = 1 self.index = 1
-- forget-list UI only after TryingToLearn YES (learn_move.asm .loop) -- forget-list UI only after TryingToLearn YES (learn_move.asm .loop)
self.selecting = false self.selecting = false
@@ -118,6 +119,7 @@ function MoveLearnMenu:finish(learned)
self.selecting = false self.selecting = false
game.stack:pop() game.stack:pop()
local msg local msg
local opts
if learned then if learned then
-- pokered pages this as four texts in a row; _ForgotAndText carries -- pokered pages this as four texts in a row; _ForgotAndText carries
-- the "And..." tail -- the "And..." tail
@@ -127,13 +129,16 @@ function MoveLearnMenu:finish(learned)
"\f%s forgot\n%s!\fAnd...", name, self.forgot) "\f%s forgot\n%s!\fAnd...", name, self.forgot)
.. "\f" .. romText(game.data, "_LearnedMove1Text", .. "\f" .. romText(game.data, "_LearnedMove1Text",
"%s learned\n%s!", name, mdef.name) "%s learned\n%s!", name, mdef.name)
opts = { auto = { sound = function()
return require("src.core.Sound").play(game.data, self.learnedSound)
end, wait = true } }
else else
msg = romText(game.data, "_DidNotLearnText", msg = romText(game.data, "_DidNotLearnText",
"%s\ndid not learn\v%s!", name, mdef.name) "%s\ndid not learn\v%s!", name, mdef.name)
end end
game.stack:push(TextBox.new(game, msg, function() game.stack:push(TextBox.new(game, msg, function()
if self.onDone then self.onDone(learned) end if self.onDone then self.onDone(learned) end
end)) end, opts))
end end
function MoveLearnMenu:draw() function MoveLearnMenu:draw()
+23
View File
@@ -72,6 +72,21 @@ end
local function sameItems(_, items) return items end local function sameItems(_, items) return items end
local function followerUnavailable(game, mon)
local ow = game.overworld
local Follower = require("src.world.PikachuFollower")
return Follower.isFollowingDisabled(ow)
and Follower.isStarterPikachu(game.save, mon)
end
local function refuseUnavailable(self)
self.swapFrom = nil
local TextBox = require("src.render.TextBox")
local t = self.game.data and self.game.data.text or {}
self.game.stack:push(TextBox.new(self.game,
t._SleepingPikachuText1 or Strings("There isn't any\nresponse...")))
end
-- where DIG escapes work: escape_rope_tilesets.asm (Agatha's room is -- where DIG escapes work: escape_rope_tilesets.asm (Agatha's room is
-- excluded by map id in ItemUseEscapeRope) -- excluded by map id in ItemUseEscapeRope)
local DIG_TILESETS = { FOREST = true, CEMETERY = true, CAVERN = true, local DIG_TILESETS = { FOREST = true, CEMETERY = true, CAVERN = true,
@@ -367,6 +382,10 @@ function PartyMenu:update(dt)
self.submenu = nil self.submenu = nil
elseif input:wasPressed("a") then elseif input:wasPressed("a") then
local mon = party[self.index] local mon = party[self.index]
if followerUnavailable(self.game, mon) then
refuseUnavailable(self)
return
end
local entry = self.subItems[self.subIndex] local entry = self.subItems[self.subIndex]
local action = entry.action local action = entry.action
if not action and entry.onSelect then if not action and entry.onSelect then
@@ -577,6 +596,10 @@ function PartyMenu:update(dt)
if self.onCancel then self.onCancel() end if self.onCancel then self.onCancel() end
elseif input:wasPressed("a") and #party > 0 then elseif input:wasPressed("a") and #party > 0 then
local mon = party[self.index] local mon = party[self.index]
if followerUnavailable(self.game, mon) then
refuseUnavailable(self)
return
end
if self.softboiledFrom then if self.softboiledFrom then
local user = party[self.softboiledFrom] local user = party[self.softboiledFrom]
local heal = math.floor(user.stats.hp / 5) local heal = math.floor(user.stats.hp / 5)
+21
View File
@@ -104,6 +104,27 @@ local function sell(game)
dialogue = true, dialogue = true,
money = function() return game.save.money end, money = function() return game.save.money end,
footer = greet, footer = greet,
onSelectKey = function(item, l)
if not item then return end
if not l.swapIndex then
l.swapIndex = l.index
return
end
local order = Bag.order(game.save)
order[l.swapIndex], order[l.index] = order[l.index], order[l.swapIndex]
l.swapIndex = nil
require("src.core.Sound").play(game.data, "Swap")
local rebuilt = {}
for _, id in ipairs(order) do
local def = game.data.items[id]
rebuilt[#rebuilt + 1] = {
value = id,
label = def and def.name or id,
right = "x" .. game.save.inventory[id],
}
end
l.items = rebuilt
end,
onChoose = function(item) onChoose = function(item)
local def = game.data.items[item.value] local def = game.data.items[item.value]
-- only key items and HMs are unsellable (pokemart.asm IsKeyItem / -- only key items and HMs are unsellable (pokemart.asm IsKeyItem /
+2 -1
View File
@@ -34,7 +34,8 @@ function StartMenu.new(game)
-- POKéMON is always listed (draw_start_menu.asm prints it even with -- POKéMON is always listed (draw_start_menu.asm prints it even with
-- an empty party; selecting it then just no-ops) -- an empty party; selecting it then just no-ops)
table.insert(items, { label = Strings("POKéMON"), onSelect = function() table.insert(items, { label = Strings("POKéMON"),
keepOpen = #game.save.party == 0, onSelect = function()
if #game.save.party == 0 then return end if #game.save.party == 0 then return end
Screens.push(game, "PartyMenu", { onCancel = reopen }) Screens.push(game, "PartyMenu", { onCancel = reopen })
end }) end })
+15 -8
View File
@@ -200,8 +200,9 @@ function TitleState.new(game, opts)
self.versionFull = imagePath(title.versionRibbon) ~= nil self.versionFull = imagePath(title.versionRibbon) ~= nil
self.version = tryImage(imagePath(title.versionRibbon or title.version) self.version = tryImage(imagePath(title.versionRibbon or title.version)
or "assets/generated/title/red_version.png") or "assets/generated/title/red_version.png")
self.player = tryImage(imagePath(title.player) self.playerPath = imagePath(title.player)
or "assets/generated/title/player.png") or "assets/generated/title/player.png"
self.player = tryImage(self.playerPath)
-- ..(engine/movie/title2.asm ln 85) -- ..(engine/movie/title2.asm ln 85)
if self.player then if self.player then
local pw, ph = self.player:getDimensions() local pw, ph = self.player:getDimensions()
@@ -597,6 +598,12 @@ end
-- ..(engine/movie/title.asm ln 28) -- ..(engine/movie/title.asm ln 28)
function TitleState:draw() function TitleState:draw()
local PaletteFX = require("src.render.PaletteFX")
local playerImage = self.player
if playerImage and PaletteFX.usesSpriteObp() then
playerImage = require("src.render.SpriteRenderer").obpImage(
self.playerPath, PaletteFX.ogObj())
end
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144) love.graphics.rectangle("fill", 0, 0, 160, 144)
local scrollY = -(self.scy or 0) local scrollY = -(self.scy or 0)
@@ -660,8 +667,8 @@ function TitleState:draw()
-- layout has no cycling mon and no Red art (title_yellow.asm). -- layout has no cycling mon and no Red art (title_yellow.asm).
if spriteTrueColor then if spriteTrueColor then
local cover local cover
if self.player then if playerImage then
local pw, ph = self.player:getDimensions() local pw, ph = playerImage:getDimensions()
cover = { 82, 80, pw, ph } cover = { 82, 80, pw, ph }
end end
markVisibleTrueColor(x, y, w, h, cover) markVisibleTrueColor(x, y, w, h, cover)
@@ -670,11 +677,11 @@ function TitleState:draw()
-- Red is OAM in the original: he draws over the mon's box edge -- Red is OAM in the original: he draws over the mon's box edge
if self.playerQuads then if self.playerQuads then
for _, part in ipairs(self.playerQuads) do for _, part in ipairs(self.playerQuads) do
love.graphics.draw(self.player, part[1], 82 + part[2], 80 + part[3]) love.graphics.draw(playerImage, part[1], 82 + part[2], 80 + part[3])
end end
love.graphics.draw(self.player, self.ballQuad, 82, self.ballY) love.graphics.draw(playerImage, self.ballQuad, 82, self.ballY)
elseif self.player then elseif playerImage then
love.graphics.draw(self.player, 82, 80) love.graphics.draw(playerImage, 82, 80)
end end
end end
self:drawCopyright(136 + (preRibbon and 0 or scrollY)) self:drawCopyright(136 + (preRibbon and 0 or scrollY))
+11 -3
View File
@@ -272,9 +272,6 @@ function EvolutionAnim:commit()
evolved.level, evolved) evolved.level, evolved)
end end
-- One LearnMove call per move the new species picks up at this level. There
-- is no forget screen in the Gold port yet, so a full moveset reports the way
-- src/battle/gen2/Battle.lua's own level-up path does instead of prompting.
function EvolutionAnim:nextLearn() function EvolutionAnim:nextLearn()
self.learnIndex = (self.learnIndex or 0) + 1 self.learnIndex = (self.learnIndex or 0) + 1
local moveId = self.pending and self.pending[self.learnIndex] local moveId = self.pending and self.pending[self.learnIndex]
@@ -289,6 +286,17 @@ function EvolutionAnim:nextLearn()
self.learned[#self.learned + 1] = moveId self.learned[#self.learned + 1] = moveId
self.lines = { self.nick .. " learned", moveName .. "!" } self.lines = { self.nick .. " learned", moveName .. "!" }
elseif reason == "full" then elseif reason == "full" then
if self.game and self.game.learnMoveOn then
self.phase = "waitingLearn"
return self.game:learnMoveOn(self.evolved, moveId, function(learned)
if learned then
self.learned[#self.learned + 1] = moveId
else
self.full[#self.full + 1] = moveId
end
self:nextLearn()
end)
end
self.full[#self.full + 1] = moveId self.full[#self.full + 1] = moveId
self.lines = { self.nick .. " wants to", "learn " .. moveName .. "!" } self.lines = { self.nick .. " wants to", "learn " .. moveName .. "!" }
else else
+2 -1
View File
@@ -128,7 +128,8 @@ end
function NPC:draw(camX, camY) function NPC:draw(camX, camY)
local sprite, px, py, facing, phase, flip = self:pose() local sprite, px, py, facing, phase, flip = self:pose()
sprite:draw(px, py, camX, camY, facing, phase, flip) sprite:draw(px, py, camX, camY, facing, phase, flip, nil, nil,
self.frameOverride)
end end
return NPC return NPC
+22 -2
View File
@@ -1087,6 +1087,7 @@ function OverworldState:update(dt)
-- the player lands on desk Oak. -- the player lands on desk Oak.
local scripted = self.runner:isRunning() or #self.scriptMoves > 0 local scripted = self.runner:isRunning() or #self.scriptMoves > 0
or self.engaging or self.emote or self.teleportOut or self.engaging or self.emote or self.teleportOut
or self.flyAnim or self.flyArrive
if not scripted and not self.transitioning then if not scripted and not self.transitioning then
self:checkTrainerSight() self:checkTrainerSight()
-- CheckFightingMapTrainers (home/trainers.asm) zeroes hJoyHeld and -- CheckFightingMapTrainers (home/trainers.asm) zeroes hJoyHeld and
@@ -1095,6 +1096,7 @@ function OverworldState:update(dt)
-- the player can never start another step after being spotted. -- the player can never start another step after being spotted.
scripted = self.runner:isRunning() or #self.scriptMoves > 0 scripted = self.runner:isRunning() or #self.scriptMoves > 0
or self.engaging or self.emote or self.teleportOut or self.engaging or self.emote or self.teleportOut
or self.flyAnim or self.flyArrive
end end
if not scripted and not self.transitioning then if not scripted and not self.transitioning then
self:handleInput() self:handleInput()
@@ -1664,6 +1666,10 @@ end
-- Goldeen/Poliwag L10; Super Rod uses the map's extracted fishing group -- Goldeen/Poliwag L10; Super Rod uses the map's extracted fishing group
-- (no group means "Not even a nibble!"). -- (no group means "Not even a nibble!").
function OverworldState:goFishing(rod) function OverworldState:goFishing(rod)
if GameVersion.isYellow() then
Game.save.pikachuEmotionModifier = 2
Game.save.pikachuMood = 0x81
end
local pool, always = fishingPool(Game.data, rod, self.map.id) local pool, always = fishingPool(Game.data, rod, self.map.id)
local enc local enc
if Runtime.wantsHook("encounter.fishing") then if Runtime.wantsHook("encounter.fishing") then
@@ -2905,6 +2911,11 @@ end
-- POKéMON" and "fighting fit". -- POKéMON" and "fighting fit".
function OverworldState:nurseHeal(onDone, npc) function OverworldState:nurseHeal(onDone, npc)
local t = Game.data.text local t = Game.data.text
if self.map.id == "PEWTER_POKECENTER" and self.pikachuPewterSleepScene then
Game.stack:push(TextBox.new(Game,
t._LooksContentText or "PIKACHU looks\ncontent.", onDone))
return
end
local bye = t._PokemonCenterFarewellText or romText(Game.data, "_PokemonCenterFarewellText", "We hope to see\nyou again!") local bye = t._PokemonCenterFarewellText or romText(Game.data, "_PokemonCenterFarewellText", "We hope to see\nyou again!")
local hello = t._PokemonCenterWelcomeText local hello = t._PokemonCenterWelcomeText
or Strings("Welcome to our\nPOKéMON CENTER!") or Strings("Welcome to our\nPOKéMON CENTER!")
@@ -2978,9 +2989,13 @@ function OverworldState:finishNurseHeal(bye, onDone, npc)
end)) end))
end end
if not npc then farewell() return end if not npc then farewell() return end
npc.facing = "up" npc.frameOverride = 3
-- bubble = false is the silent world hold, this port's DelayFrames -- bubble = false is the silent world hold, this port's DelayFrames
self.emote = { npc = npc, frames = 20, bubble = false, onDone = farewell } self.emote = { npc = npc, frames = 20, bubble = false, onDone = function()
npc.frameOverride = nil
npc:facePlayer(self.player)
farewell()
end }
end)) end))
end end
@@ -2992,6 +3007,11 @@ end
-- for the original serial handshake; declining prints "Please come again!" -- for the original serial handshake; declining prints "Please come again!"
function OverworldState:cableClubReceptionist(onDone) function OverworldState:cableClubReceptionist(onDone)
local t = Game.data.text local t = Game.data.text
if self.map.id == "PEWTER_POKECENTER" and self.pikachuPewterSleepScene then
Game.stack:push(TextBox.new(Game,
t._LooksContentText or "PIKACHU looks\ncontent.", onDone))
return
end
local welcome = t._CableClubNPCWelcomeText or romText(Game.data, "_CableClubNPCWelcomeText", "Welcome to the\nCable Club!") local welcome = t._CableClubNPCWelcomeText or romText(Game.data, "_CableClubNPCWelcomeText", "Welcome to the\nCable Club!")
if not Game.save.flags.EVENT_GOT_POKEDEX then if not Game.save.flags.EVENT_GOT_POKEDEX then
-- CableClubNPC .didNotConnect path before the pokedex -- CableClubNPC .didNotConnect path before the pokedex
+43 -1
View File
@@ -65,6 +65,17 @@ function PikachuFollower.starterInParty(save, needHealthy)
return nil return nil
end end
function PikachuFollower.isStarterPikachu(save, mon)
if not (mon and mon.species == "PIKACHU") then return false end
local player = save.player or {}
return mon.otId == player.id and mon.ot == player.name
end
function PikachuFollower.isFollowingDisabled(ow)
return ow and (ow.pikachuBillsScene or ow.pikachuFanClubScene
or ow.pikachuPewterSleepScene) and true or false
end
-- ModifyPikachuHappiness. mon is the party mon the event applied to for -- ModifyPikachuHappiness. mon is the party mon the event applied to for
-- the per-mon reasons (IsThisPartyMonStarterPikachu); GYMLEADER and -- the per-mon reasons (IsThisPartyMonStarterPikachu); GYMLEADER and
-- WALKING instead require any healthy starter in the party -- WALKING instead require any healthy starter in the party
@@ -199,6 +210,7 @@ function PikachuFollower.onMapEntered(game, ow, opts, viaMapLoad)
-- Bill's House owns a short scripted scene that deliberately keeps -- Bill's House owns a short scripted scene that deliberately keeps
-- Pikachu off the normal trailing loop. A new map instance ends it. -- Pikachu off the normal trailing loop. A new map instance ends it.
ow.pikachuBillsScene = nil ow.pikachuBillsScene = nil
ow.pikachuFanClubScene = nil
remove(ow) remove(ow)
if not shouldSpawn(game, ow) then return end if not shouldSpawn(game, ow) then return end
-- opts.keepPikachu is the follower a connection crossing kept alive: -- opts.keepPikachu is the follower a connection crossing kept alive:
@@ -410,7 +422,8 @@ end
-- (pikachu_follow.asm keeps it one walk step behind) -- (pikachu_follow.asm keeps it one walk step behind)
function PikachuFollower.update(game, ow) function PikachuFollower.update(game, ow)
if ow.pikaHop then return end -- the counter hop owns the follower (#417) if ow.pikaHop then return end -- the counter hop owns the follower (#417)
if ow.pikachuBillsScene then return end if ow.pikachuBillsScene or ow.pikachuFanClubScene
or ow.pikachuPewterSleepScene then return end
local npc = findFollower(ow) local npc = findFollower(ow)
if not npc then if not npc then
if shouldSpawn(game, ow) then PikachuFollower.onMapEntered(game, ow) end if shouldSpawn(game, ow) then PikachuFollower.onMapEntered(game, ow) end
@@ -713,6 +726,13 @@ function PikachuFollower.talk(game, ow, npc, done)
ow.player.facing = OPPOSITE[npc.facing] or ow.player.facing ow.player.facing = OPPOSITE[npc.facing] or ow.player.facing
local save = game.save local save = game.save
local emotion = selectEmotion(game, ow, save) local emotion = selectEmotion(game, ow, save)
if ow.pikachuPewterSleepScene then
local finish = done
done = function()
ow.pikachuPewterSleepScene = nil
if finish then finish() end
end
end
local e = EMOTIONS[emotion] or EMOTIONS[1] local e = EMOTIONS[emotion] or EMOTIONS[1]
if e.turnAway then if e.turnAway then
npc.facing = ow.player.facing -- pikaemotion_9: back to the player npc.facing = ow.player.facing -- pikaemotion_9: back to the player
@@ -798,6 +818,28 @@ local function movePikachu(ow, npc, steps, onDone)
nextStep(1) nextStep(1)
end end
function PikachuFollower.onFanClubEntered(game, ow)
if not (GameVersion.isYellow() and ow.map
and ow.map.id == "POKEMON_FAN_CLUB") then return end
local starter = PikachuFollower.starterInParty(game.save)
local npc = findFollower(ow)
if not npc or (starter and starter.status) then return end
ow.pikachuFanClubScene = true
ow.pikachuMapScriptActive = true
ow.player.facing = "down"
for _, other in ipairs(ow.npcs or {}) do
if other.def and other.def.name == "POKEMONFANCLUB_SEEL" then
other.movementStatus = 2
other.facing = "down"
break
end
end
billsHouseEmotion(game, ow, npc, "EXCLAMATION_BUBBLE")
movePikachu(ow, npc, { { "up", 1 }, { "right", 3 }, { "up", 1 } }, function()
npc.facing = "up"
end)
end
function PikachuFollower.onBillsHouseEnter(game, ow) function PikachuFollower.onBillsHouseEnter(game, ow)
if not (GameVersion.isYellow() and ow.map and ow.map.id == "BILLS_HOUSE") then if not (GameVersion.isYellow() and ow.map and ow.map.id == "BILLS_HOUSE") then
return return
+82
View File
@@ -0,0 +1,82 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local ItemEffects = require("src.core.gen2.ItemEffects")
local Game2 = require("src.core.Game2")
local Screens = require("src.ui.Screens")
local EvolutionAnim = require("src.ui.gen2.EvolutionAnim")
local data = {
pokemon = {
SUNKERN = {
evolutions = {
{ method = "EVOLVE_ITEM", item = "SUN_STONE", into = "SUNFLORA" },
},
},
},
}
local mon = { species = "SUNKERN", item = nil }
T.eq(ItemEffects.partyAction("SUN_STONE"), "stone",
"SUN STONE opens the party target flow")
local result = ItemEffects.useOnMon("SUN_STONE", mon, data)
T.check(result.used, "SUN STONE succeeds on SUNKERN")
T.eq(result.evolution and result.evolution.into, "SUNFLORA",
"SUN STONE selects SUNFLORA")
mon.item = "EVERSTONE"
result = ItemEffects.useOnMon("SUN_STONE", mon, data)
T.check(not result.used, "held EVERSTONE refuses the evolution")
local function startStone()
local partyOptions, evolutionOptions
local game = setmetatable({
data = {
pokemon = data.pokemon,
screens = {
Gen2PartyMenu = function(_, options)
partyOptions = options
return {}
end,
Gen2EvolutionAnim = function(_, options)
evolutionOptions = options
return {}
end,
},
},
save = {
party = { { species = "SUNKERN", item = nil } },
inventory = { SUN_STONE = 1 },
},
stack = { pop = function() end, push = function() end },
}, Game2)
Screens.invalidate()
game:usePartyItem("SUN_STONE")
partyOptions.onChoose(nil, game.save.party[1])
return game, evolutionOptions
end
local game, evolution = startStone()
T.check(evolution.force, "SUN STONE evolution sets wForceEvolution")
T.eq(game.save.inventory.SUN_STONE, 1,
"SUN STONE remains until evolution succeeds")
local animation = EvolutionAnim.new({ data = { pokemon = data.pokemon } }, {
mon = game.save.party[1], entry = evolution and evolution.entry,
force = evolution and evolution.force,
})
T.check(not animation:cancelPressed({ wasPressed = function(_, key)
return key == "b"
end }), "B cannot cancel a forced stone evolution")
evolution.onDone({ canceled = true })
T.eq(game.save.inventory.SUN_STONE, 1,
"a canceled evolution does not consume SUN STONE")
game, evolution = startStone()
evolution.onDone({ evolved = { species = "SUNFLORA" } })
T.eq(game.save.inventory.SUN_STONE, nil,
"a completed evolution consumes SUN STONE")
Screens.invalidate()
T.finish("gen2 sun stone bug 1219")
+5 -4
View File
@@ -65,7 +65,7 @@ T.check(pushed[1].text:find(BYE, 1, true) == nil,
pushed[1].onDone() pushed[1].onDone()
T.eq(#pushed, 1, "the farewell waits for the bow") T.eq(#pushed, 1, "the farewell waits for the bow")
T.eq(nurse.facing, "up", "image index $14: the nurse bows") T.eq(nurse.frameOverride, 3, "image index $1: the nurse bows")
T.check(fakeSelf.emote ~= nil, "the bow is a world hold, not a text pause") T.check(fakeSelf.emote ~= nil, "the bow is a world hold, not a text pause")
local hold = fakeSelf.emote or {} local hold = fakeSelf.emote or {}
T.eq(hold.npc, nurse, "the hold is anchored on the nurse") T.eq(hold.npc, nurse, "the hold is anchored on the nurse")
@@ -79,11 +79,12 @@ if hold.onDone then hold.onDone() end
T.eq(#pushed, 2, "the farewell follows the bow") T.eq(#pushed, 2, "the farewell follows the bow")
local farewell = pushed[2] or {} local farewell = pushed[2] or {}
T.eq(farewell.text, BYE, "second box is the farewell text") T.eq(farewell.text, BYE, "second box is the farewell text")
T.eq(nurse.facing, "up", "she is still bowed while the farewell prints") T.eq(nurse.frameOverride, nil, "the bow ends before the farewell prints")
T.eq(nurse.facing, "down", "the nurse faces the player for the farewell")
if farewell.onDone then farewell.onDone() end if farewell.onDone then farewell.onDone() end
T.eq(nurse.facing, "down", "the trailing UpdateSprites faces her back") T.eq(nurse.facing, "down", "the trailing UpdateSprites keeps her facing")
T.eq(faced, 1, "she is turned back exactly once") T.eq(faced, 2, "she is turned back before and after the farewell")
T.eq(finished, 1, "control returns to the player once, after the farewell") T.eq(finished, 1, "control returns to the player once, after the farewell")
-- === no nurse sprite (the Yellow/rest-stop callers): no bow, same text -- === no nurse sprite (the Yellow/rest-stop callers): no bow, same text
+55
View File
@@ -0,0 +1,55 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local GenSave = require("src.save_convert.GenSave")
local romText = require("src.core.RomText")
local localized = {
text = {
_MoveIsDisabledText = "The move {RAM:wNameBuffer} from {USER} is disabled!",
},
}
T.eq(romText(localized, "_MoveIsDisabledText", "%s's %s is disabled!", {
USER = "PIKACHU", ["RAM:wNameBuffer"] = "THUNDER",
}), "The move THUNDER from PIKACHU is disabled!",
"named ROM tokens survive translation reordering")
GenSave.setCharmap(loadfile("src/save_convert/data/charmap.lua")())
local data = {
pokemon = {}, moves = {}, items = {}, maps = {},
hiddenItems = loadfile("src/save_convert/data/hidden_items.lua")(),
}
local save = {
player = { name = "RED", id = 1, map = "PALLET_TOWN", x = 0, y = 0 },
rival = { name = "BLUE" }, party = {}, boxes = {}, inventory = {},
pcItems = {}, flags = {}, pokedex = { seen = {}, owned = {} },
hiddenTaken = {
VIRIDIAN_FOREST_1_18 = true,
VIRIDIAN_CITY_14_4 = true,
},
}
for i = 1, 12 do save.boxes[i] = {} end
local bytes = GenSave.encode(save, data, nil)
local decoded = GenSave.decode(bytes, data)
T.check(decoded.hiddenTaken.VIRIDIAN_FOREST_1_18,
"Viridian Forest hidden potion imports from wObtainedHiddenItemsFlags")
T.check(decoded.hiddenTaken.VIRIDIAN_CITY_14_4,
"Viridian City hidden potion imports from wObtainedHiddenItemsFlags")
T.check(not decoded.hiddenTaken.ROUTE_9_14_7,
"an uncollected hidden item remains available")
love = love or require("tests.love_stub")
local start = require("src.ui.StartMenu").new({
data = {}, save = {
flags = {}, party = {}, inventory = {}, options = {},
player = { name = "RED" }, pokedex = { owned = {} },
},
})
local pokemonRow
for _, row in ipairs(start.items) do
if row.label == "POKéMON" then pokemonRow = row break end
end
T.check(pokemonRow and pokemonRow.keepOpen,
"the empty-party POKéMON row leaves the start menu open")
T.finish("open menu bugs 949 and 1149")
+3 -3
View File
@@ -60,14 +60,14 @@ for _, r in ipairs(rows) do
end end
check(not hasRecord, "CHAMPIONS_ROOM rival script no longer calls record_hall_of_fame") check(not hasRecord, "CHAMPIONS_ROOM rival script no longer calls record_hall_of_fame")
-- The post-battle walk takes the right-hand detour before heading north, so -- The post-battle walk takes the left-hand detour before heading north, so
-- the player does not visibly pass through the rival at (4,2). -- the player does not visibly pass through the rival at (4,2).
local route = {} local route = {}
for _, r in ipairs(rows) do for _, r in ipairs(rows) do
if r[1] == "move_player" then route[#route + 1] = r end if r[1] == "move_player" then route[#route + 1] = r end
end end
eq(route[#route - 1] and route[#route - 1][2], "right", eq(route[#route - 1] and route[#route - 1][2], "left",
"walk-out route first moves right around the rival") "walk-out route first moves left around the rival")
eq(route[#route] and route[#route][2], "up", eq(route[#route] and route[#route][2], "up",
"walk-out route then heads north to Hall of Fame") "walk-out route then heads north to Hall of Fame")
+3 -3
View File
@@ -203,13 +203,13 @@ do
eq(tb.waitFrames, 60, "for the 60 frames AnimationBlinkEnemyMon takes") eq(tb.waitFrames, 60, "for the 60 frames AnimationBlinkEnemyMon takes")
end end
-- the OPTIONS animation toggle still gates the whole thing; the sound does not -- engine/battle/animations.asm PlayApplyingAttackAnimation
do do
local tb = freshBattle() local tb = freshBattle()
Game.save.options.animations = false Game.save.options.animations = false
tb:applyHitFx({ animType = 5, sfx = "Damage" }) tb:applyHitFx({ animType = 5, sfx = "Damage" })
eq(tb.fx.shakeProg, nil, "animations off arms no shake") check(tb.fx.shakeProg ~= nil, "animations off keeps the hit shake")
eq(tb.fx.blink, nil, "and no blink") eq(tb.fx.blink, nil, "type 5 remains a shake, not a blink")
Game.save.options.animations = true Game.save.options.animations = true
end end
+93
View File
@@ -0,0 +1,93 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local S = require("tests.harness").suite("parity Yellow disabled Pikachu")
local check, eq = S.check, S.eq
local Data = require("src.core.Data")
Data:load()
local GameVersion = require("src.core.GameVersion")
local Follower = require("src.world.PikachuFollower")
local ItemEffects = require("src.inventory.ItemEffects")
local PartyMenu = require("src.ui.PartyMenu")
local BoxMenu = require("src.ui.BoxMenu")
local TextBox = require("src.render.TextBox")
local oldVersion = GameVersion.get()
GameVersion.set("yellow")
local save = {
player = { id = 7, name = "RED" },
party = {
{ species = "PIKACHU", otId = 7, ot = "RED", hp = 20 },
{ species = "PIDGEY", otId = 7, ot = "RED", hp = 20 },
},
}
check(Follower.isStarterPikachu(save, save.party[1]), "the player Pikachu is identified")
check(not Follower.isStarterPikachu(save, save.party[2]), "other party members are not starter Pikachu")
local pika = { pikachuFollower = true, cellX = 3, cellY = 4, facing = "down" }
local seel = { def = { name = "POKEMONFANCLUB_SEEL" }, cellX = 1, cellY = 4 }
local moves = {}
local ow = {
map = { id = "POKEMON_FAN_CLUB" },
player = { cellX = 3, cellY = 5, facing = "up" },
npcs = { pika, seel },
scriptMove = function(_, npc, dir, tiles, done)
moves[#moves + 1] = { dir, tiles }
npc.facing = dir
if done then done() end
end,
}
Follower.onFanClubEntered({ save = save, data = Data }, ow)
check(ow.pikachuFanClubScene, "Fan Club disables normal Pikachu following")
check(ow.pikachuMapScriptActive, "Fan Club sets the map-script flag")
eq(ow.player.facing, "down", "Fan Club resets the player direction")
eq(moves[1] and moves[1][1], "up", "Fan Club starts with slide-up displacement")
eq(moves[1] and moves[1][2], 1, "Fan Club slide-up spans one tile")
eq(moves[2] and moves[2][1], "right", "Fan Club then walks right")
eq(moves[2] and moves[2][2], 3, "Fan Club walks right three tiles")
eq(moves[3] and moves[3][1], "up", "Fan Club ends walking up")
eq(moves[3] and moves[3][2], 1, "Fan Club final up spans one tile")
eq(seel.movementStatus, 2, "Fan Club puts Seel into movement delay")
eq(seel.facing, "down", "Fan Club turns Seel down")
check(Follower.isFollowingDisabled(ow), "disabled Fan Club follower blocks starter selection")
local sleepOw = {
map = { id = "PEWTER_POKECENTER" },
player = { cellX = 3, cellY = 5 },
npcs = { pika },
pikachuPewterSleepScene = true,
}
local result = ItemEffects.use(Data, save, "POKE_FLUTE", nil, nil, nil, sleepOw)
eq(result, "flute_wake_pikachu", "Poké Flute is allowed next to sleeping Pikachu")
check(Follower.isFollowingDisabled(sleepOw), "sleeping Pikachu disables normal follower actions")
local pushed = {}
local partyGame = {
save = save,
overworld = sleepOw,
stack = { push = function(_, state) pushed[#pushed + 1] = state end },
input = { wasPressed = function(_, key) return key == "a" end },
}
PartyMenu.new(partyGame):update()
check(getmetatable(pushed[#pushed]) == TextBox,
"sleeping Pikachu cannot be selected from the party menu")
local boxGame = {
save = save,
overworld = sleepOw,
data = { pokemon = { PIKACHU = { name = "PIKACHU" }, PIDGEY = { name = "PIDGEY" } }, text = {} },
stack = { push = function(_, state) pushed[#pushed + 1] = state end },
}
local pc = BoxMenu.new(boxGame)
pc.items[2].onSelect()
local depositList = pushed[#pushed]
depositList.onChoose(depositList.items[1], depositList)
check(getmetatable(pushed[#pushed]) == TextBox,
"sleeping Pikachu cannot be deposited into Bill's PC")
eq(#save.party, 2, "PC refusal leaves the party unchanged")
GameVersion.set(oldVersion)
S.finish()
+6 -7
View File
@@ -891,8 +891,7 @@ do
check(tb:lockedAction(tb.enemy) == nil, "victim is free after the release") check(tb:lockedAction(tb.enemy) == nil, "victim is free after the release")
end end
-- #2: recoil and drain use the RAW computed damage, not the HP-capped -- engine/battle/core.asm ApplyDamageToEnemyPokemon
-- amount dealt
do do
Game.save.party = { Pokemon.new(Data, "BULBASAUR", 20) } Game.save.party = { Pokemon.new(Data, "BULBASAUR", 20) }
local rb = BattleState.newWild(Game, "RATTATA", 3) local rb = BattleState.newWild(Game, "RATTATA", 3)
@@ -903,8 +902,8 @@ do
rb.rng = mkseq({ 0, 255, 255 }) rb.rng = mkseq({ 0, 255, 255 })
local hpBefore = rb.player.mon.hp local hpBefore = rb.player.mon.hp
rb:performMove(rb.player, rb.enemy, { id = "TAKE_DOWN", pp = 10 }) rb:performMove(rb.player, rb.enemy, { id = "TAKE_DOWN", pp = 10 })
eq(hpBefore - rb.player.mon.hp, math.floor(raw / 4), eq(hpBefore - rb.player.mon.hp, 1,
"recoil is raw damage / 4 even when only 1 HP was dealt") "recoil is capped damage / 4 with a minimum of 1")
local db = BattleState.newWild(Game, "RATTATA", 3) local db = BattleState.newWild(Game, "RATTATA", 3)
db.enemy.mon.hp = 1 db.enemy.mon.hp = 1
@@ -914,9 +913,9 @@ do
check(rawD >= 4, "raw MEGA DRAIN damage is meaningful (" .. rawD .. ")") check(rawD >= 4, "raw MEGA DRAIN damage is meaningful (" .. rawD .. ")")
db.rng = mkseq({ 0, 255, 255 }) db.rng = mkseq({ 0, 255, 255 })
db:performMove(db.player, db.enemy, { id = "MEGA_DRAIN", pp = 10 }) db:performMove(db.player, db.enemy, { id = "MEGA_DRAIN", pp = 10 })
eq(db.player.mon.hp - 1, math.floor(rawD / 2), eq(db.player.mon.hp - 1, 1,
"drain heals raw damage / 2 even when only 1 HP was dealt") "drain heals capped damage / 2 with a minimum of 1")
eq(db.lastDamage, math.floor(rawD / 2), eq(db.lastDamage, 1,
"drain halves wDamage in place (Counter would see the half)") "drain halves wDamage in place (Counter would see the half)")
end end