mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e6b5ae73d | |||
| 0b96ac0ab6 | |||
| f6a035947f | |||
| 941181d31c | |||
| 9ceb1a8940 | |||
| 37051a26b5 | |||
| b12a0dac91 | |||
| a722d24759 | |||
| 833388c235 | |||
| 6ff7dd0c38 |
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
+13
-13
@@ -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" })
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
+8
-6
@@ -130,18 +130,20 @@ before `Game:load`, so **it never loads a mod's entry chunk**; only
|
|||||||
duplicate) and returns one row per mod:
|
duplicate) and returns one row per mod:
|
||||||
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
||||||
`badge` is the manifest's `category`, falling back to `profile`, then
|
`badge` is the manifest's `category`, falling back to `profile`, then
|
||||||
`"MOD"`, uppercased. `enabled` reads `options.mods[id]` (missing means
|
`"MOD"`, uppercased. `enabledByVersion` contains an answer for each game;
|
||||||
enabled, matching the loader's own default).
|
missing entries default to enabled (except experimental mods), matching the
|
||||||
|
loader. On the first run with per-game controls, legacy shared choices are
|
||||||
|
copied to every installed game's answer.
|
||||||
- `status` is `"ok"`, `"warn"`, or `"conflict"`, computed by the pure
|
- `status` is `"ok"`, `"warn"`, or `"conflict"`, computed by the pure
|
||||||
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
||||||
and the validated manifests: `conflict` when enabling this mod collides
|
and the validated manifests: `conflict` when enabling this mod collides
|
||||||
with another enabled one; `warn` for an out-of-range `game_version` or an
|
with another enabled one; `warn` for an out-of-range `game_version` or an
|
||||||
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
||||||
`love.*` calls, this half is table-driven by the test suite on its own.
|
`love.*` calls, this half is table-driven by the test suite on its own.
|
||||||
- `LauncherMods.setEnabled(id, bool)` persists `options.mods[id]` as a plain
|
- `LauncherMods.setEnabled(id, bool, version)` persists the selected game's
|
||||||
boolean, the exact shape `Loader:_saveState` writes, so the running game
|
answer, so the running game and the in-game `ManagerState` see the change on
|
||||||
and the in-game `ManagerState` see the change on next boot. The mods panel
|
next boot. The MODS panel renders a coloured checkbox for Red, Blue, Yellow,
|
||||||
calls this on every toggle and re-derives the list right away
|
and Gold on every row and re-derives the list right away
|
||||||
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
||||||
shows without waiting for a reload.
|
shows without waiting for a reload.
|
||||||
- `LauncherMods.installZip(path)` mounts the archive with
|
- `LauncherMods.installZip(path)` mounts the archive with
|
||||||
|
|||||||
@@ -96,14 +96,14 @@ The launcher asks the same question of a mod's dependencies: one whose hard
|
|||||||
dependency does not run on the selected game reads `Needs <id> (not for Gold)`,
|
dependency does not run on the selected game reads `Needs <id> (not for Gold)`,
|
||||||
matching the loader's contagious skip.
|
matching the loader's contagious skip.
|
||||||
|
|
||||||
A separate overlay, `options.modsByVersion[version][id]`, is where a per-game
|
A separate overlay, `options.modsByVersion[version][id]`, holds each game's
|
||||||
enable flag will live. It is a preview: `SaveData.PER_VERSION_MODS` is `false`,
|
enable flag. The launcher shows a coloured Red / Blue / Yellow / Gold checkbox
|
||||||
so `SaveData.modScope` answers nil for every caller and the launcher panel, the
|
for every installed mod, and the loader and in-game manager read the same
|
||||||
in-game manager and the loader all read *and* write the one shared
|
game-specific answer on the next boot. On the first launch after this feature,
|
||||||
`options.mods` flag. Nothing consults the overlay for enablement until that
|
the existing shared state is copied to every game, so a mod that was enabled
|
||||||
flips, which is deliberate: the overlay is plantable from an imported
|
remains enabled everywhere; after that, changing one checkbox affects only
|
||||||
`.g1rmodlist`, and a reader scoped differently from the writers would show a
|
that game. New mods still default to enabled on every game (experimental mods
|
||||||
mod set no boot would honour.
|
retain their explicit opt-in default).
|
||||||
|
|
||||||
That is deliberate. Gold reimplements the battle engine, the overworld, the
|
That is deliberate. Gold reimplements the battle engine, the overworld, the
|
||||||
script VM and the save format, so a Gen 1 mod dropped into a Gold boot would
|
script VM and the save format, so a Gen 1 mod dropped into a Gold boot would
|
||||||
|
|||||||
@@ -260,18 +260,13 @@ selected game reads `Needs <id> (not for Gold)` rather than `Ready`.
|
|||||||
|
|
||||||
### One limit worth knowing
|
### One limit worth knowing
|
||||||
|
|
||||||
**Per-game enable flags are still a preview.** The overlay
|
**Enablement is per game.** The overlay
|
||||||
`options.modsByVersion[version][id]` exists and every surface goes through
|
`options.modsByVersion[version][id]` is read and written through
|
||||||
`SaveData.modEnabled` / `SaveData.setModEnabled`, but
|
`SaveData.modEnabled` / `SaveData.setModEnabled` by the launcher, in-game
|
||||||
`SaveData.PER_VERSION_MODS` is `false`
|
manager, and loader. Existing shared settings are copied to every game the
|
||||||
(`src/core/SaveData.lua:489`). While it is false, `SaveData.modScope` answers
|
first time this version sees the installed mods; from then on, each coloured
|
||||||
nil for every caller, so the launcher panel, the in-game manager *and* the
|
game checkbox changes only that game's next boot. Nothing about this affects a
|
||||||
loader all read and write the one shared `options.mods` flag and the overlay is
|
mod author; it affects what a player can express.
|
||||||
not consulted for enablement anywhere. That matters because the overlay is
|
|
||||||
plantable from an imported `.g1rmodlist`: keeping every reader on the same
|
|
||||||
scope as every writer is what stops a stored per-game flag from showing a mod
|
|
||||||
set no boot would honour. Nothing about this affects a mod author; it affects
|
|
||||||
what a player can currently express.
|
|
||||||
|
|
||||||
Targeting is a different question from enablement and *is* enforced per game,
|
Targeting is a different question from enablement and *is* enforced per game,
|
||||||
as above. The two do not share a switch.
|
as above. The two do not share a switch.
|
||||||
|
|||||||
@@ -12,6 +12,13 @@
|
|||||||
"tintColor": "3b5ca8",
|
"tintColor": "3b5ca8",
|
||||||
"category": "games",
|
"category": "games",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.1.81",
|
||||||
|
"date": "2026-08-13",
|
||||||
|
"size": 9886854,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.81/gen1recomp++-0.1.81-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1091 Encounter Rate is tied to Game Speed\n- #1097 Phase through walls exiting a cave\n- #1100 Allow for control edit and vibration toggle on gold\n- #1104 wrong warp\n- #1135 update for iphone - haptic vibrations\n- #1155 Magnitude always rolls a 4\n- #1157 Sonic Boom doesn't deal set damage\n- #1158 Unable to view opponent's next Pokemon\n- #1159 Dig animation is bugged\n- #1160 64x64 battle sprite followup for mods\n- #1167 [Gold] items in ilex forest missing\n- #1208 Gold Save game not showing properly in LAUNCHER after having multiple saves\n\n## Contributors\n\n- @bryanthaboi\n- @castdrian\n- @MaxTomahawk\n- @Yukitty\n- Codex Agent"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.1.80",
|
"version": "0.1.80",
|
||||||
"date": "2026-08-12",
|
"date": "2026-08-12",
|
||||||
|
|||||||
+34
-13
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ local Prize = require("src.battle.gen2.Prize")
|
|||||||
-- * Gen 1's `user` / `target` / `battler` are battler wrappers around a mon
|
-- * Gen 1's `user` / `target` / `battler` are battler wrappers around a mon
|
||||||
-- ({ mon = , name = , isPlayer = }); Gen 2's engine works on the party mon
|
-- ({ mon = , name = , isPlayer = }); Gen 2's engine works on the party mon
|
||||||
-- table directly, so that is what these payloads carry.
|
-- table directly, so that is what these payloads carry.
|
||||||
-- * Gen 1's `rng` is love.math.random (1..n); Gen 2's injected `random` is
|
-- * Gen 1's `rng` is love.math.random (rng(n) → 1..n, rng(lo,hi) → lo..hi).
|
||||||
-- the cart's BattleRandom convention (0..n-1). Both keys are present.
|
-- Gen 2's cart BattleRandom is random(n) → 0..n-1. Both live on the
|
||||||
|
-- battle: `random` / `roller()` are BattleRandom for damage, accuracy,
|
||||||
|
-- Magnitude, etc.; `rng` is the Gen 1 / love-style view of the same stream.
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
-- The two battle lines that carry the cart's own `line` break: a marker-bearing
|
-- The two battle lines that carry the cart's own `line` break: a marker-bearing
|
||||||
-- literal has to stay reachable from a translation mod (#186, #245), which is
|
-- literal has to stay reachable from a translation mod (#186, #245), which is
|
||||||
@@ -88,6 +90,19 @@ local function rand(random, n)
|
|||||||
return math.random(n) - 1
|
return math.random(n) - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Gen 1 / love.math view of BattleRandom: rng(n) → 1..n, rng(lo,hi) → lo..hi.
|
||||||
|
local function loveStyleRng(random)
|
||||||
|
return function(lo, hi)
|
||||||
|
if hi == nil then
|
||||||
|
local n = lo or 1
|
||||||
|
if n < 1 then n = 1 end
|
||||||
|
return (rand(random, n) or 0) + 1
|
||||||
|
end
|
||||||
|
if hi < lo then lo, hi = hi, lo end
|
||||||
|
return lo + (rand(random, hi - lo + 1) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- data/trainers/leaders.asm. The two lists are ONE array in the ROM: only
|
-- data/trainers/leaders.asm. The two lists are ONE array in the ROM: only
|
||||||
-- KantoGymLeaders carries the -1 terminator, and GymLeaders falls through into
|
-- KantoGymLeaders carries the -1 terminator, and GymLeaders falls through into
|
||||||
-- it, so IsGymLeader matches all twenty-two classes while IsKantoGymLeader
|
-- it, so IsGymLeader matches all twenty-two classes while IsKantoGymLeader
|
||||||
@@ -212,12 +227,15 @@ Battle.KANTO_BADGE_ORDER = {
|
|||||||
-- this wild battle is BATTLETYPE_ROAMING; the caller built `wild`
|
-- this wild battle is BATTLETYPE_ROAMING; the caller built `wild`
|
||||||
-- through Roamers.beginBattle and reads Battle.roaming back to
|
-- through Roamers.beginBattle and reads Battle.roaming back to
|
||||||
-- bank the beast's HP afterwards
|
-- bank the beast's HP afterwards
|
||||||
-- random(n) 0..n-1, injected so a test is deterministic
|
-- random(n) 0..n-1, injected so a test is deterministic (BattleRandom)
|
||||||
|
-- rng(lo,hi) / rng(n) Gen 1 / love.math contract; defaults over `random`
|
||||||
function Battle.new(opts)
|
function Battle.new(opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local self = setmetatable({}, Battle)
|
local self = setmetatable({}, Battle)
|
||||||
self.data = opts.data or {}
|
self.data = opts.data or {}
|
||||||
self.random = opts.random or function(n) return rand(nil, n) end
|
self.random = opts.random or function(n) return rand(nil, n) end
|
||||||
|
-- Same stream as `random`, Gen 1 / love.math calling convention.
|
||||||
|
self.rng = opts.rng or loveStyleRng(self.random)
|
||||||
self.party = opts.party or {}
|
self.party = opts.party or {}
|
||||||
self.trainer = opts.trainer
|
self.trainer = opts.trainer
|
||||||
self.save = opts.save
|
self.save = opts.save
|
||||||
@@ -403,11 +421,8 @@ function Battle:endBattle(outcome)
|
|||||||
Runtime.emit("battle.ended", { battle = self, result = outcome })
|
Runtime.emit("battle.ended", { battle = self, result = outcome })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A never-nil 0..n-1 roller for the hook contexts. `random` is optional on the
|
-- Never-nil BattleRandom (0..n-1) for call sites that want the cart byte.
|
||||||
-- constructor (a headless test injects one, the game leaves it to love.math),
|
-- `battle.rng` is the Gen 1 / love.math view of the same stream.
|
||||||
-- and a mod reaching for ctx.rng must not have to know that. Note the
|
|
||||||
-- convention: this is the cart's BattleRandom byte (0..n-1), NOT Gen 1's
|
|
||||||
-- love.math.random (1..n).
|
|
||||||
function Battle:roller()
|
function Battle:roller()
|
||||||
if not self.rollerFn then
|
if not self.rollerFn then
|
||||||
self.rollerFn = function(n) return rand(self.random, n) end
|
self.rollerFn = function(n) return rand(self.random, n) end
|
||||||
|
|||||||
@@ -265,8 +265,18 @@ Effects.MAGNITUDE_POWER = {
|
|||||||
-- is what damagecalc reads as the move's power -- data/moves/moves.asm stores
|
-- is what damagecalc reads as the move's power -- data/moves/moves.asm stores
|
||||||
-- MAGNITUDE at power 1 precisely because this overwrites it. Returns the
|
-- MAGNITUDE at power 1 precisely because this overwrites it. Returns the
|
||||||
-- power and the magnitude number the text prints.
|
-- power and the magnitude number the text prints.
|
||||||
|
--
|
||||||
|
-- `random` is BattleRandom (0..n-1). If none is supplied, roll via love.math
|
||||||
|
-- / math.random — never hard-code 0 (that always yields Magnitude 4).
|
||||||
function Effects.magnitudePower(random)
|
function Effects.magnitudePower(random)
|
||||||
local roll = random and random(256) or 0
|
local roll
|
||||||
|
if type(random) == "function" then
|
||||||
|
roll = random(256) or 0
|
||||||
|
elseif love and love.math and love.math.random then
|
||||||
|
roll = love.math.random(256) - 1
|
||||||
|
else
|
||||||
|
roll = math.random(256) - 1
|
||||||
|
end
|
||||||
for _, row in ipairs(Effects.MAGNITUDE_POWER) do
|
for _, row in ipairs(Effects.MAGNITUDE_POWER) do
|
||||||
if row[1] >= roll then return row[2], row[3] end
|
if row[1] >= roll then return row[2], row[3] end
|
||||||
end
|
end
|
||||||
|
|||||||
+11
-2
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+63
-8
@@ -304,6 +304,10 @@ function SaveData.defaultOptions()
|
|||||||
-- options.lua written before this key keeps its exact meaning. Read and
|
-- options.lua written before this key keeps its exact meaning. Read and
|
||||||
-- written through SaveData.modEnabled / SaveData.setModEnabled.
|
-- written through SaveData.modEnabled / SaveData.setModEnabled.
|
||||||
modsByVersion = {},
|
modsByVersion = {},
|
||||||
|
-- Set after the first per-game enablement migration. Older options files
|
||||||
|
-- have only `mods`, so the migration copies each installed mod's current
|
||||||
|
-- answer to every game before game-specific toggles begin changing it.
|
||||||
|
modsByVersionMigrated = false,
|
||||||
-- Named setups the player can switch between (#593; src/mods/ModProfile.lua
|
-- Named setups the player can switch between (#593; src/mods/ModProfile.lua
|
||||||
-- owns the shape, src/mods/ManagerState.lua the UI): each row is
|
-- owns the shape, src/mods/ManagerState.lua the UI): each row is
|
||||||
-- { name, enabled = {id=bool}, options = {id={k=v}}, slots = {version=slotId} }.
|
-- { name, enabled = {id=bool}, options = {id={k=v}}, slots = {version=slotId} }.
|
||||||
@@ -563,19 +567,68 @@ end
|
|||||||
-- only holds the games the player actually chose for, so a mod set can differ
|
-- only holds the games the player actually chose for, so a mod set can differ
|
||||||
-- between Red and Gold without either one owning the other's flags.
|
-- between Red and Gold without either one owning the other's flags.
|
||||||
|
|
||||||
-- Whether a per-game answer is honoured at boot. The loader reads the enable
|
-- Per-game answers are live. Every reader and writer goes through modScope,
|
||||||
-- flags once, before any entry chunk (src/mods/Loader.lua _loadState), so this
|
-- so a choice made in the launcher is the choice the next boot loads.
|
||||||
-- flips on with that read and not before: until then every writer keeps to the
|
SaveData.PER_VERSION_MODS = true
|
||||||
-- shared flag and no surface promises what the boot does not do.
|
|
||||||
SaveData.PER_VERSION_MODS = false
|
|
||||||
|
|
||||||
-- The version a write should be scoped to: the game asked for once per-game
|
-- The version a write is scoped to: per-game controls name one game; a nil
|
||||||
-- flags are live, nil (the shared flag) while they are only a preview.
|
-- caller still addresses the legacy shared fallback.
|
||||||
function SaveData.modScope(version)
|
function SaveData.modScope(version)
|
||||||
if SaveData.PER_VERSION_MODS then return version end
|
if SaveData.PER_VERSION_MODS then return version end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Promote an installation that predates per-game flags. `mods` may contain
|
||||||
|
-- manifest rows ({ id, experimental }) or bare ids. A mod with no old entry
|
||||||
|
-- had the loader default: on, except for experimental mods. Copy that answer
|
||||||
|
-- to every game once, preserving any per-game overlay somebody imported before
|
||||||
|
-- this feature shipped. New installs need no rows here: an absent answer
|
||||||
|
-- still defaults to enabled for every game.
|
||||||
|
function SaveData.migrateModEnablement(options, mods)
|
||||||
|
if type(options) ~= "table" or options.modsByVersionMigrated then return false end
|
||||||
|
options.mods = type(options.mods) == "table" and options.mods or {}
|
||||||
|
options.modsByVersion = type(options.modsByVersion) == "table"
|
||||||
|
and options.modsByVersion or {}
|
||||||
|
|
||||||
|
local known = {}
|
||||||
|
for id in pairs(options.mods) do
|
||||||
|
if type(id) == "string" and id ~= "" then known[id] = { id = id } end
|
||||||
|
end
|
||||||
|
for version, bucket in pairs(options.modsByVersion) do
|
||||||
|
if GameVersion.VERSIONS[version] and type(bucket) == "table" then
|
||||||
|
for id in pairs(bucket) do
|
||||||
|
if type(id) == "string" and id ~= "" then known[id] = known[id] or { id = id } end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, mod in ipairs(mods or {}) do
|
||||||
|
local id = type(mod) == "table" and mod.id or mod
|
||||||
|
if type(id) == "string" and id ~= "" then
|
||||||
|
known[id] = type(mod) == "table" and mod or (known[id] or { id = id })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Do not create options.lua just to record an empty migration on a fresh
|
||||||
|
-- no-mod boot. Keep it pending until there is a real installed or saved
|
||||||
|
-- mod answer to preserve.
|
||||||
|
if next(known) == nil then return false end
|
||||||
|
|
||||||
|
for id, mod in pairs(known) do
|
||||||
|
local shared = options.mods[id]
|
||||||
|
if type(shared) ~= "boolean" then shared = not (mod.experimental == true) end
|
||||||
|
for _, version in ipairs(GameVersion.ORDER) do
|
||||||
|
local bucket = options.modsByVersion[version]
|
||||||
|
if type(bucket) ~= "table" then
|
||||||
|
bucket = {}
|
||||||
|
options.modsByVersion[version] = bucket
|
||||||
|
end
|
||||||
|
if type(bucket[id]) ~= "boolean" then bucket[id] = shared end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
options.modsByVersionMigrated = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- true/false as chosen for `version`, else the shared flag, else nil -- the
|
-- true/false as chosen for `version`, else the shared flag, else nil -- the
|
||||||
-- caller owns the default (the loader enables, the launcher keeps
|
-- caller owns the default (the loader enables, the launcher keeps
|
||||||
-- experimental mods off until asked).
|
-- experimental mods off until asked).
|
||||||
@@ -1834,6 +1887,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 +1899,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+308
-40
@@ -100,6 +100,7 @@ end
|
|||||||
-- other half of that guarantee.
|
-- other half of that guarantee.
|
||||||
function LauncherView.update(imp, dt)
|
function LauncherView.update(imp, dt)
|
||||||
if not imp._flex then return end
|
if not imp._flex then return end
|
||||||
|
if imp._launchFade then return end
|
||||||
|
|
||||||
local down = false
|
local down = false
|
||||||
if love.mouse and love.mouse.isDown then
|
if love.mouse and love.mouse.isDown then
|
||||||
@@ -312,6 +313,236 @@ local function cartColor(version)
|
|||||||
return CART_COLOR[version] or PAL.green
|
return CART_COLOR[version] or PAL.green
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local CART_DRAG_SLOP = 8
|
||||||
|
local TAU = math.pi * 2
|
||||||
|
|
||||||
|
local function cartridgeState(imp, version)
|
||||||
|
imp._cartridge = imp._cartridge or {}
|
||||||
|
local state = imp._cartridge[version]
|
||||||
|
if not state then
|
||||||
|
state = { spin = 0, lastTime = Kit.time }
|
||||||
|
imp._cartridge[version] = state
|
||||||
|
end
|
||||||
|
return state
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartridgeLabel(imp, version)
|
||||||
|
imp._cartridgeLabels = imp._cartridgeLabels or {}
|
||||||
|
local label = imp._cartridgeLabels[version]
|
||||||
|
if label ~= nil then return label or nil end
|
||||||
|
local ok, image = pcall(love.graphics.newImage,
|
||||||
|
"assets/labels/" .. tostring(version) .. ".png")
|
||||||
|
if not ok then
|
||||||
|
imp._cartridgeLabels[version] = false
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local iw, ih = image:getDimensions()
|
||||||
|
label = { image = image, width = iw, height = ih }
|
||||||
|
imp._cartridgeLabels[version] = label
|
||||||
|
return label
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartProject(cx, cy, yaw, pitch, x, y, z)
|
||||||
|
local cyaw, syaw = math.cos(yaw), math.sin(yaw)
|
||||||
|
local cpitch, spitch = math.cos(pitch), math.sin(pitch)
|
||||||
|
local rx = x * cyaw + z * syaw
|
||||||
|
local rz = -x * syaw + z * cyaw
|
||||||
|
local ry = y * cpitch - rz * spitch
|
||||||
|
rz = y * spitch + rz * cpitch
|
||||||
|
local perspective = 620 / (620 - rz)
|
||||||
|
return cx + rx * perspective, cy + ry * perspective
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartPolygon(points, color, alpha)
|
||||||
|
local flat = {}
|
||||||
|
for i = 1, #points do
|
||||||
|
flat[#flat + 1], flat[#flat + 2] = points[i][1], points[i][2]
|
||||||
|
end
|
||||||
|
Theme.col(color, alpha or 1)
|
||||||
|
love.graphics.polygon("fill", flat)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartQuad(project, x, y, w, h, z)
|
||||||
|
return {
|
||||||
|
{ project(x, y, z) }, { project(x + w, y, z) },
|
||||||
|
{ project(x + w, y + h, z) }, { project(x, y + h, z) },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartPill(project, x, y, w, h, z, color, alpha)
|
||||||
|
local points, radius = {}, h / 2
|
||||||
|
for i = 0, 10 do
|
||||||
|
local a = math.pi + math.pi * i / 10
|
||||||
|
points[#points + 1] = { project(x + radius + math.cos(a) * radius,
|
||||||
|
y + radius + math.sin(a) * radius, z) }
|
||||||
|
end
|
||||||
|
for i = 0, 10 do
|
||||||
|
local a = math.pi * i / 10
|
||||||
|
points[#points + 1] = { project(x + w - radius + math.cos(a) * radius,
|
||||||
|
y + radius + math.sin(a) * radius, z) }
|
||||||
|
end
|
||||||
|
cartPolygon(points, color, alpha)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartLabelMesh(imp, version, label, points)
|
||||||
|
if not love.graphics.newMesh then return nil end
|
||||||
|
imp._cartridgeLabelMeshes = imp._cartridgeLabelMeshes or {}
|
||||||
|
local mesh = imp._cartridgeLabelMeshes[version]
|
||||||
|
if not mesh then
|
||||||
|
mesh = love.graphics.newMesh({
|
||||||
|
{ 0, 0, 0, 0, 255, 255, 255, 255 },
|
||||||
|
{ 0, 0, 1, 0, 255, 255, 255, 255 },
|
||||||
|
{ 0, 0, 1, 1, 255, 255, 255, 255 },
|
||||||
|
{ 0, 0, 0, 1, 255, 255, 255, 255 },
|
||||||
|
}, "fan", "dynamic")
|
||||||
|
mesh:setTexture(label.image)
|
||||||
|
imp._cartridgeLabelMeshes[version] = mesh
|
||||||
|
end
|
||||||
|
mesh:setVertices({
|
||||||
|
{ points[1][1], points[1][2], 0, 0, 255, 255, 255, 255 },
|
||||||
|
{ points[2][1], points[2][2], 1, 0, 255, 255, 255, 255 },
|
||||||
|
{ points[3][1], points[3][2], 1, 1, 255, 255, 255, 255 },
|
||||||
|
{ points[4][1], points[4][2], 0, 1, 255, 255, 255, 255 },
|
||||||
|
})
|
||||||
|
return mesh
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action)
|
||||||
|
local state = cartridgeState(imp, version)
|
||||||
|
local focused = Kit.focusable(key, x, y, w, h)
|
||||||
|
local hot = Kit.hover(x, y, w, h)
|
||||||
|
local active = state.active
|
||||||
|
local cx, cy = x + w / 2, y + h / 2
|
||||||
|
|
||||||
|
if Kit.mouseClicked and Kit.hit(x, y, w, h) and not Kit.blockClicks then
|
||||||
|
if Kit.mouseDown then
|
||||||
|
state.active = true
|
||||||
|
state.startX, state.startY = Kit.mouseX, Kit.mouseY
|
||||||
|
state.lastDragX, state.lastDragY = Kit.mouseX, Kit.mouseY
|
||||||
|
state.dragged = false
|
||||||
|
active = true
|
||||||
|
else
|
||||||
|
queueAction(imp, key, action)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.active then
|
||||||
|
active = true
|
||||||
|
if Kit.mouseDown then
|
||||||
|
local movedX, movedY = Kit.mouseX - state.startX, Kit.mouseY - state.startY
|
||||||
|
if movedX * movedX + movedY * movedY > CART_DRAG_SLOP * CART_DRAG_SLOP then
|
||||||
|
state.dragged = true
|
||||||
|
end
|
||||||
|
if state.dragged then
|
||||||
|
local dragX = Kit.mouseX - (state.lastDragX or Kit.mouseX)
|
||||||
|
local dragY = Kit.mouseY - (state.lastDragY or Kit.mouseY)
|
||||||
|
state.spin = state.spin + dragX * 0.018
|
||||||
|
state.pitchDrag = clamp((state.pitchDrag or 0) + dragY * 0.010,
|
||||||
|
-1.20, 1.20)
|
||||||
|
end
|
||||||
|
state.lastDragX, state.lastDragY = Kit.mouseX, Kit.mouseY
|
||||||
|
else
|
||||||
|
if not state.dragged then queueAction(imp, key, action) end
|
||||||
|
state.active, active = nil, false
|
||||||
|
state.dragged = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local dt = math.min(0.08, math.max(0, Kit.time - (state.lastTime or Kit.time)))
|
||||||
|
state.lastTime = Kit.time
|
||||||
|
if not state.active then
|
||||||
|
local upright = math.floor(state.spin / TAU + 0.5) * TAU
|
||||||
|
state.spin = state.spin + (upright - state.spin) * math.min(1, dt * 4)
|
||||||
|
state.pitchDrag = (state.pitchDrag or 0)
|
||||||
|
* (1 - math.min(1, dt * 4))
|
||||||
|
end
|
||||||
|
local pointerX = clamp((Kit.mouseX - cx) / math.max(1, w / 2), -1, 1)
|
||||||
|
local pointerY = clamp((Kit.mouseY - cy) / math.max(1, h / 2), -1, 1)
|
||||||
|
local hoverTilt = hot and pointerX * 0.12 or math.sin(Kit.time * 0.75) * 0.035
|
||||||
|
local pressX = active and pointerX * w * 0.025 or 0
|
||||||
|
local pressY = active and pointerY * h * 0.018 or 0
|
||||||
|
local yaw = -0.42 + state.spin + hoverTilt
|
||||||
|
local pitch = 0.14 + (state.pitchDrag or 0)
|
||||||
|
+ (hot and pointerY * 0.08 or math.sin(Kit.time * 0.6) * 0.018)
|
||||||
|
local pressedScale = active and 0.965 or 1
|
||||||
|
|
||||||
|
Kit._audit("control", x, y, w, h, key)
|
||||||
|
if focused then
|
||||||
|
Theme.strokeRounded(x - 3, y - 3, w + 6, h + 6, PAL.lineStrong,
|
||||||
|
Theme.A.focus, 2, Theme.cardRadius() + 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local halfW, halfH = w / 2, h / 2
|
||||||
|
local depth = math.max(8, w * 0.14)
|
||||||
|
local project = function(px, py, pz)
|
||||||
|
return cartProject(cx + pressX, cy + pressY, yaw, pitch,
|
||||||
|
px * pressedScale, py * pressedScale, pz * pressedScale)
|
||||||
|
end
|
||||||
|
|
||||||
|
local capH = h * 3 / 65
|
||||||
|
local mainTop = -halfH + capH
|
||||||
|
local capRight = halfW - w * 5 / 57
|
||||||
|
local mainFront = cartQuad(project, -halfW, mainTop, w, h - capH, depth)
|
||||||
|
local mainBack = cartQuad(project, -halfW, mainTop, w, h - capH, -depth)
|
||||||
|
local capFront = cartQuad(project, -halfW, -halfH,
|
||||||
|
capRight + halfW, capH, depth)
|
||||||
|
local capBack = cartQuad(project, -halfW, -halfH,
|
||||||
|
capRight + halfW, capH, -depth)
|
||||||
|
local shell = cartColor(version)
|
||||||
|
local side = { math.floor(shell[1] * 0.54), math.floor(shell[2] * 0.54),
|
||||||
|
math.floor(shell[3] * 0.54) }
|
||||||
|
|
||||||
|
cartPolygon(mainBack, side, 1)
|
||||||
|
cartPolygon(capBack, side, 1)
|
||||||
|
cartPolygon({ mainFront[2], mainFront[3], mainBack[3], mainBack[2] }, side, 1)
|
||||||
|
cartPolygon({ mainFront[3], mainFront[4], mainBack[4], mainBack[3] }, side, 1)
|
||||||
|
cartPolygon({ mainFront[1], mainFront[2], mainBack[2], mainBack[1] }, side, 1)
|
||||||
|
cartPolygon({ capFront[2], capFront[3], capBack[3], capBack[2] }, side, 1)
|
||||||
|
cartPolygon({ capFront[1], capFront[2], capBack[2], capBack[1] }, side, 1)
|
||||||
|
cartPolygon({ capFront[4], capFront[1], capBack[1], capBack[4] }, side, 1)
|
||||||
|
cartPolygon(mainFront, shell, 1)
|
||||||
|
cartPolygon(capFront, shell, 1)
|
||||||
|
|
||||||
|
local faceZ = depth + 0.8
|
||||||
|
for i = 0, 4 do
|
||||||
|
local ry = mainTop + 7 + i * h * 0.025
|
||||||
|
cartPolygon(cartQuad(project, -halfW + 2, ry, w * 0.13, 2, faceZ), side, 0.7)
|
||||||
|
cartPolygon(cartQuad(project, halfW - w * 0.13 - 2, ry, w * 0.13, 2, faceZ), side, 0.7)
|
||||||
|
end
|
||||||
|
local recessX, recessY = -w * 0.32, mainTop + h * 0.023
|
||||||
|
local recessW, recessH = w * 0.64, h * 0.24
|
||||||
|
cartPolygon(cartQuad(project, recessX, recessY, recessW, recessH, faceZ), shell, 0.88)
|
||||||
|
cartPill(project, recessX + w * 0.025, recessY + h * 0.025,
|
||||||
|
recessW - w * 0.05, h * 0.12, faceZ + 0.5, shell, 0.7)
|
||||||
|
cartPill(project, recessX + w * 0.045, recessY + h * 0.043,
|
||||||
|
recessW - w * 0.09, h * 0.083, faceZ + 0.8, side, 0.42)
|
||||||
|
|
||||||
|
local labelX, labelY = -w * 0.33, -h * 0.20
|
||||||
|
local labelW, labelH = w * 0.66, h * 0.55
|
||||||
|
local plate = cartQuad(project, labelX - 2, labelY - 2, labelW + 4, labelH + 4, faceZ + 0.8)
|
||||||
|
cartPolygon(plate, side, 0.95)
|
||||||
|
local labelPoints = cartQuad(project, labelX, labelY, labelW, labelH, faceZ + 1.2)
|
||||||
|
local label = cartridgeLabel(imp, version)
|
||||||
|
local mesh = label and cartLabelMesh(imp, version, label, labelPoints)
|
||||||
|
if mesh then
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.draw(mesh)
|
||||||
|
elseif label then
|
||||||
|
local artScale = math.min(labelW / label.width, labelH / label.height)
|
||||||
|
love.graphics.draw(label.image, labelPoints[1][1], labelPoints[1][2],
|
||||||
|
0, artScale, artScale)
|
||||||
|
end
|
||||||
|
cartPolygon({
|
||||||
|
{ project(-w * 0.07, h * 0.37, faceZ + 1) },
|
||||||
|
{ project(w * 0.07, h * 0.37, faceZ + 1) },
|
||||||
|
{ project(0, h * 0.43, faceZ + 1) },
|
||||||
|
}, side, 0.70)
|
||||||
|
|
||||||
|
if not state.active and (Kit._activateId == key) then
|
||||||
|
queueAction(imp, key, action)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function modStatusColor(status)
|
local function modStatusColor(status)
|
||||||
if status == "ok" then return Strings("Ready"), PAL.green end
|
if status == "ok" then return Strings("Ready"), PAL.green end
|
||||||
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
||||||
@@ -1011,8 +1242,16 @@ local function buildGamePanel(imp, x, y, w, availH, m, version)
|
|||||||
else tagText, tagCol = Strings("ROM REQUIRED"), PAL.yellow end
|
else tagText, tagCol = Strings("ROM REQUIRED"), PAL.yellow end
|
||||||
local tagW = Kit.textWidth("micro", tagText) + math.floor(18 * m.s)
|
local tagW = Kit.textWidth("micro", tagText) + math.floor(18 * m.s)
|
||||||
local tagH = Kit.textHeight("micro") + math.floor(10 * m.s)
|
local tagH = Kit.textHeight("micro") + math.floor(10 * m.s)
|
||||||
Kit.tag(x + Kit.textWidth("title", Kit.ellipsize("title", gameName, w * 0.6))
|
local tagX = x + Kit.textWidth("title", Kit.ellipsize("title", gameName, w * 0.6))
|
||||||
+ math.floor(12 * m.s), y + (titleH - tagH) / 2, tagW, tagH, tagText, tagCol)
|
+ math.floor(12 * m.s)
|
||||||
|
Kit.tag(tagX, y + (titleH - tagH) / 2, tagW, tagH, tagText, tagCol)
|
||||||
|
if ready then
|
||||||
|
local hint = Strings("(PRESS THE CART TO PLAY)")
|
||||||
|
local hintX = tagX + tagW + math.floor(10 * m.s)
|
||||||
|
local hintW = math.max(0, x + w - hintX)
|
||||||
|
Kit.text("micro", Kit.ellipsize("micro", hint, hintW), hintX,
|
||||||
|
y + (titleH - Kit.textHeight("micro")) / 2, PAL.heading)
|
||||||
|
end
|
||||||
local cy = y + titleH + math.floor(12 * m.s)
|
local cy = y + titleH + math.floor(12 * m.s)
|
||||||
local remaining = availH - (titleH + math.floor(12 * m.s))
|
local remaining = availH - (titleH + math.floor(12 * m.s))
|
||||||
|
|
||||||
@@ -1038,25 +1277,20 @@ local function buildGamePanel(imp, x, y, w, availH, m, version)
|
|||||||
local ly = cy
|
local ly = cy
|
||||||
|
|
||||||
if ready then
|
if ready then
|
||||||
-- Play IS the panel: it takes the space the ROM buttons used to hold, at
|
-- The cartridge takes the Play button's former place. Its portrait
|
||||||
-- the top of the column where the eye lands, wearing this game's own
|
-- ratio comes from a real Game Boy cart rather than stretching the old
|
||||||
-- cartridge colour rather than a generic green.
|
-- horizontal control, and its body colour comes from the active game.
|
||||||
-- Play grows into the room the column has: it is the one thing on this
|
local playH = math.floor(clamp(remaining * 0.52, 112 * m.s, 260 * m.s))
|
||||||
-- screen the player came for, and the space freed by moving ROM and
|
local mgW = math.max(Kit.tapMin(), math.floor(34 * m.s))
|
||||||
-- control management out belongs to it rather than to a gap. Clamped at
|
|
||||||
-- both ends so a short window still gets a real button and a tall one
|
|
||||||
-- does not get a billboard.
|
|
||||||
local playH = math.floor(clamp(remaining * 0.30, 64 * m.s, 132 * m.s))
|
|
||||||
local mgW = playH
|
|
||||||
local bgap = math.floor(8 * m.s)
|
local bgap = math.floor(8 * m.s)
|
||||||
btn(imp, lx, ly, lw - mgW - bgap, playH, "play-" .. version,
|
local cartAreaW = lw - mgW - bgap
|
||||||
Strings("Play ") .. gameName, {
|
local cartW = math.min(cartAreaW, math.floor(playH * 0.88))
|
||||||
fill = cartColor(version), ink = PAL.inverse, font = "stat",
|
local cartX = lx + math.floor((cartAreaW - cartW) / 2)
|
||||||
action = function() imp:play(version) end,
|
cartridgeButton(imp, cartX, ly, cartW, playH, "play-" .. version,
|
||||||
})
|
version, gameName, function() imp:play(version, true) end)
|
||||||
imp._gearIcon = imp._gearIcon
|
imp._gearIcon = imp._gearIcon
|
||||||
or love.graphics.newImage("assets/launcher/gear.png")
|
or love.graphics.newImage("assets/launcher/gear.png")
|
||||||
iconButton(imp, "manage-" .. version, lx + lw - mgW, ly, playH,
|
iconButton(imp, "manage-" .. version, lx + lw - mgW, ly, mgW,
|
||||||
imp._gearIcon, function() imp._gameManage = version end)
|
imp._gearIcon, function() imp._gameManage = version end)
|
||||||
ly = ly + playH + gap
|
ly = ly + playH + gap
|
||||||
end
|
end
|
||||||
@@ -1139,6 +1373,28 @@ local function drawCheck(x, y, size, color)
|
|||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- One compact coloured checkbox for each game. The cartridge colour carries
|
||||||
|
-- the game identity even when the row is narrow; the letter keeps an unchecked
|
||||||
|
-- box legible without relying on colour alone.
|
||||||
|
local function modGameCheckbox(x, y, size, checked, game, id)
|
||||||
|
local color = cartColor(game)
|
||||||
|
local focused = Kit.focusable(id, x, y, size, size)
|
||||||
|
local hot = focused or Kit.hover(x, y, size, size)
|
||||||
|
if love.graphics then
|
||||||
|
if checked then
|
||||||
|
Theme.fillRounded(x, y, size, size, color, 1)
|
||||||
|
drawCheck(x, y, size, PAL.inverse)
|
||||||
|
else
|
||||||
|
Theme.fillRounded(x, y, size, size, PAL.bg, 1)
|
||||||
|
Kit.textCenterBold("micro", game:sub(1, 1):upper(), x,
|
||||||
|
y + (size - Kit.textHeight("micro")) / 2, size, color)
|
||||||
|
end
|
||||||
|
Theme.strokeRounded(x, y, size, size, color,
|
||||||
|
hot and Theme.A.focus or Theme.A.hover, 1)
|
||||||
|
end
|
||||||
|
return Kit.press(x, y, size, size) or Kit._activateId == id
|
||||||
|
end
|
||||||
|
|
||||||
local function buildModsPanel(imp, x, y, w, availH, m)
|
local function buildModsPanel(imp, x, y, w, availH, m)
|
||||||
imp:_ensureMods()
|
imp:_ensureMods()
|
||||||
local ModUpdate = require("src.mods.ModUpdate")
|
local ModUpdate = require("src.mods.ModUpdate")
|
||||||
@@ -1225,15 +1481,14 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
mods = sorted
|
mods = sorted
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A mod row is a fixed height: name line, version + status line, one line
|
-- A mod row is a fixed height: its details first, then a dedicated second
|
||||||
-- of description, and an action row. Fixed because a page of uniform rows
|
-- line of per-game checkboxes. Fixed because a page of uniform rows is
|
||||||
-- is what lets perPage come from the viewport.
|
-- what lets perPage come from the viewport.
|
||||||
local chipH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
local togH = math.floor(26 * m.s)
|
||||||
-- Text block on the left, chips right-aligned beside it: one row, not a
|
local gamesLabel = Strings("Enable for:")
|
||||||
-- text block with a button strip stacked under it.
|
|
||||||
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
|
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
|
||||||
+ Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small")
|
+ Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small")
|
||||||
local rowH = math.floor(8 * m.s) + math.max(textH, chipH)
|
local rowH = math.floor(8 * m.s) + textH + math.floor(8 * m.s) + togH
|
||||||
+ math.floor(8 * m.s)
|
+ math.floor(8 * m.s)
|
||||||
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||||
local listH = availH - (cy - y) - pagerH - gap
|
local listH = availH - (cy - y) - pagerH - gap
|
||||||
@@ -1258,28 +1513,36 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
local px, inner = x + pad, w - 2 * pad
|
local px, inner = x + pad, w - 2 * pad
|
||||||
local ly = ry + math.floor(10 * m.s)
|
local ly = ry + math.floor(10 * m.s)
|
||||||
|
|
||||||
local togW = math.floor(56 * m.s)
|
local togGap = math.floor(4 * m.s)
|
||||||
local togH = math.floor(26 * m.s)
|
|
||||||
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
||||||
|
|
||||||
local togKey = "mod-toggle-" .. mod.id
|
-- These answer separate games, not a single shared install flag. The
|
||||||
-- The toggle reports its own new value, but the importer owns the state:
|
-- importer receives the game id so an experimental confirmation also
|
||||||
-- queue the flip and let _toggleMod (which may raise an experimental-mod
|
-- applies only to the checkbox the player pressed.
|
||||||
-- confirm) decide what actually happens.
|
local flipped = false
|
||||||
local _, flipped = Kit.toggle(px + inner - togW,
|
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
|
||||||
ry + (rowH - togH) / 2, togW, togH, mod.enabled, togKey)
|
Kit.text("micro", gamesLabel, px,
|
||||||
if flipped then
|
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
|
||||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id) end)
|
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
|
||||||
|
for _, game in ipairs(GameVersion.ORDER) do
|
||||||
|
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
||||||
|
if modGameCheckbox(tx, gamesY, togH,
|
||||||
|
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
||||||
|
game, togKey) then
|
||||||
|
local version = game
|
||||||
|
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
||||||
|
flipped = true
|
||||||
|
end
|
||||||
|
tx = tx + togH + togGap
|
||||||
end
|
end
|
||||||
-- The toggle sits inside the row's rect, so its press also passes the
|
-- The checkboxes sit inside the row's rect, so their press also passes the
|
||||||
-- row's hit test; `flipped` gates the row action to everywhere else.
|
-- row hit test; `flipped` gates the row action to everywhere else.
|
||||||
if not flipped
|
if not flipped
|
||||||
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
||||||
local id = mod.id
|
local id = mod.id
|
||||||
queueAction(imp, rowKey, function() imp._modActions = id end)
|
queueAction(imp, rowKey, function() imp._modActions = id end)
|
||||||
end
|
end
|
||||||
local chipsW = togW + math.floor(6 * m.s)
|
local textW = inner
|
||||||
local textW = inner - chipsW - math.floor(12 * m.s)
|
|
||||||
|
|
||||||
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
|
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
|
||||||
-- the games the mod is for, beside its category: the same chip the
|
-- the games the mod is for, beside its category: the same chip the
|
||||||
@@ -1741,7 +2004,7 @@ local function buildConfirmModal(imp, m)
|
|||||||
elseif c.kind == "importOversize" then
|
elseif c.kind == "importOversize" then
|
||||||
imp:_importSave(c.version, c.source, true)
|
imp:_importSave(c.version, c.source, true)
|
||||||
else
|
else
|
||||||
imp:_toggleMod(c.id, true)
|
imp:_toggleMod(c.id, true, c.version)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
@@ -2608,6 +2871,11 @@ function LauncherView.draw(imp)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if imp._launchFade then
|
||||||
|
Theme.fill(0, 0, m.W, m.H, PAL.bg,
|
||||||
|
math.min(1, imp._launchFade.elapsed / imp._launchFade.duration))
|
||||||
|
end
|
||||||
|
|
||||||
Kit.endFrame()
|
Kit.endFrame()
|
||||||
drawPadCursor(imp)
|
drawPadCursor(imp)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2048,6 +2048,15 @@ end
|
|||||||
|
|
||||||
function RomImporter:update(dt)
|
function RomImporter:update(dt)
|
||||||
self.pulse = self.pulse + dt
|
self.pulse = self.pulse + dt
|
||||||
|
if self._launchFade then
|
||||||
|
self._launchFade.elapsed = self._launchFade.elapsed + dt
|
||||||
|
if self._launchFade.elapsed >= self._launchFade.duration then
|
||||||
|
local version = self._launchFade.version
|
||||||
|
self._launchFade = nil
|
||||||
|
self:play(version)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
self:_updatePadCursor(dt)
|
self:_updatePadCursor(dt)
|
||||||
self:_stepBaseRomScan()
|
self:_stepBaseRomScan()
|
||||||
-- Pump the FlexLove view (input polling + the queued click actions). The
|
-- Pump the FlexLove view (input polling + the queued click actions). The
|
||||||
@@ -2436,9 +2445,15 @@ function RomImporter:joystickhat(joystick, hat, direction)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Player pressed Play on a game whose ROM is imported: hand off to boot.
|
-- Player pressed Play on a game whose ROM is imported: hand off to boot.
|
||||||
function RomImporter:play(version)
|
function RomImporter:play(version, fade)
|
||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
if not self.ready[version] then return end
|
if not self.ready[version] then return end
|
||||||
|
if fade then
|
||||||
|
if not self._launchFade then
|
||||||
|
self._launchFade = { version = version, elapsed = 0, duration = 0.24 }
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
self._handedOff = true
|
self._handedOff = true
|
||||||
-- #835: remember the game being launched so the next launcher start opens on
|
-- #835: remember the game being launched so the next launcher start opens on
|
||||||
-- its column (_applyLastVersionTab). It rides options.lua rather than a file
|
-- its column (_applyLastVersionTab). It rides options.lua rather than a file
|
||||||
@@ -3064,15 +3079,19 @@ function RomImporter:_modUpdateInfo(id)
|
|||||||
return self.modUpdateInfo and self.modUpdateInfo[id] or nil
|
return self.modUpdateInfo and self.modUpdateInfo[id] or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Flip a mod's enabled flag (persisted via LauncherMods.setEnabled) and relist
|
-- Flip one game's mod flag (persisted via LauncherMods.setEnabled) and relist
|
||||||
-- so the toggle, count, and every status chip reflect the new resolution.
|
-- so that game's checkbox and status chips reflect the new resolution.
|
||||||
-- Enabling an experimental mod arms a confirm first.
|
-- Enabling an experimental mod arms a confirmation for that same game.
|
||||||
function RomImporter:_toggleMod(id, confirmed)
|
function RomImporter:_toggleMod(id, confirmed, version)
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local cur, experimental = false, false
|
local cur, experimental = false, false
|
||||||
for _, m in ipairs(self.mods or {}) do
|
for _, m in ipairs(self.mods or {}) do
|
||||||
if m.id == id then
|
if m.id == id then
|
||||||
cur = m.enabled
|
if version and m.enabledByVersion then
|
||||||
|
cur = m.enabledByVersion[version] == true
|
||||||
|
else
|
||||||
|
cur = m.enabled
|
||||||
|
end
|
||||||
experimental = m.experimental == true
|
experimental = m.experimental == true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -3080,7 +3099,7 @@ function RomImporter:_toggleMod(id, confirmed)
|
|||||||
local want = not cur
|
local want = not cur
|
||||||
if want and experimental and not confirmed then
|
if want and experimental and not confirmed then
|
||||||
self._modConfirm = {
|
self._modConfirm = {
|
||||||
kind = "experimental", id = id,
|
kind = "experimental", id = id, version = version,
|
||||||
title = "Experimental mod",
|
title = "Experimental mod",
|
||||||
yesLabel = "Enable",
|
yesLabel = "Enable",
|
||||||
lines = {
|
lines = {
|
||||||
@@ -3092,7 +3111,7 @@ function RomImporter:_toggleMod(id, confirmed)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self._modConfirm = nil
|
self._modConfirm = nil
|
||||||
LauncherMods.setEnabled(id, want, self.modScope)
|
LauncherMods.setEnabled(id, want, version or self.modScope)
|
||||||
self:_refreshMods()
|
self:_refreshMods()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+44
-11
@@ -36,6 +36,7 @@ local ModTargets = require("src.mods.ModTargets")
|
|||||||
local Semver = require("src.mods.Semver")
|
local Semver = require("src.mods.Semver")
|
||||||
local Version = require("src.core.Version")
|
local Version = require("src.core.Version")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
local CacheFs = require("src.import.CacheFs")
|
local CacheFs = require("src.import.CacheFs")
|
||||||
|
|
||||||
local LauncherMods = {}
|
local LauncherMods = {}
|
||||||
@@ -107,8 +108,9 @@ end
|
|||||||
-- deriveList(manifests, options [, version]) -> the panel row list, pure.
|
-- deriveList(manifests, options [, version]) -> the panel row list, pure.
|
||||||
-- manifests is an array of validated manifests (Manifest.validate output);
|
-- manifests is an array of validated manifests (Manifest.validate output);
|
||||||
-- options is the options table (options.mods, options.modsByVersion and
|
-- options is the options table (options.mods, options.modsByVersion and
|
||||||
-- options.modsGen2 are read). `version` is the game the panel is showing:
|
-- options.modsGen2 are read). `version` is the game the panel is showing;
|
||||||
-- nil keeps the pre-per-game view, where the shared flag is the whole answer.
|
-- each row also carries its answer for every game so the launcher can render
|
||||||
|
-- the coloured game checkboxes together.
|
||||||
-- Rows come back sorted by id so the panel order is stable.
|
-- Rows come back sorted by id so the panel order is stable.
|
||||||
function LauncherMods.deriveList(manifests, options, version)
|
function LauncherMods.deriveList(manifests, options, version)
|
||||||
local ordered = {}
|
local ordered = {}
|
||||||
@@ -126,8 +128,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
-- this game's choice, then the shared flag, then the default: enabled,
|
-- this game's choice, then the shared flag, then the default: enabled,
|
||||||
-- matching the loader -- except experimental mods, which stay off until
|
-- matching the loader -- except experimental mods, which stay off until
|
||||||
-- the player opts in. Scoped through modScope, so this reads exactly what
|
-- the player opts in. Scoped through modScope, so this reads exactly what
|
||||||
-- setEnabled writes and the loader loads: while per-game flags are a
|
-- setEnabled writes and the loader loads for the selected game.
|
||||||
-- preview the shared flag is the whole answer on every surface.
|
|
||||||
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
||||||
if decided == nil then decided = not m.experimental end
|
if decided == nil then decided = not m.experimental end
|
||||||
if decided then enabledSet[m.id] = true end
|
if decided then enabledSet[m.id] = true end
|
||||||
@@ -152,6 +153,14 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
badge = badge,
|
badge = badge,
|
||||||
description = m.description or "",
|
description = m.description or "",
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
enabledByVersion = (function()
|
||||||
|
local answers = {}
|
||||||
|
for _, game in ipairs(GameVersion.ORDER) do
|
||||||
|
local answer = SaveData.modEnabled(options, m.id, game)
|
||||||
|
answers[game] = answer == true or (answer == nil and not m.experimental)
|
||||||
|
end
|
||||||
|
return answers
|
||||||
|
end)(),
|
||||||
status = status,
|
status = status,
|
||||||
statusDetail = detail,
|
statusDetail = detail,
|
||||||
github = m.github,
|
github = m.github,
|
||||||
@@ -281,7 +290,14 @@ end
|
|||||||
function LauncherMods.list(version)
|
function LauncherMods.list(version)
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
return LauncherMods.deriveList(discover(), options, version)
|
local manifests = discover()
|
||||||
|
-- The first build containing game-specific switches turns the old shared
|
||||||
|
-- state into one explicit answer per installed mod and game. Saving here
|
||||||
|
-- means users who only visit the launcher still receive the migration.
|
||||||
|
if SaveData.migrateModEnablement(options, manifests) then
|
||||||
|
SaveData.saveOptions(options)
|
||||||
|
end
|
||||||
|
return LauncherMods.deriveList(manifests, options, version)
|
||||||
end)
|
end)
|
||||||
if not ok then
|
if not ok then
|
||||||
-- a single bad options/mod file must not blank the launcher
|
-- a single bad options/mod file must not blank the launcher
|
||||||
@@ -364,10 +380,8 @@ function LauncherMods.translationStrings()
|
|||||||
return merged
|
return merged
|
||||||
end
|
end
|
||||||
|
|
||||||
-- setEnabled(id, enabled [, version]): persist options.mods[id] in the exact
|
-- setEnabled(id, enabled [, version]): with a game, persist just that game's
|
||||||
-- shape Loader:_saveState writes (a plain boolean), so the running game and
|
-- answer. The loader and the in-game manager use the same scope on next boot.
|
||||||
-- the in-game ManagerState pick it up unchanged. With `version` the choice
|
|
||||||
-- lands in that game's overlay instead and no other game moves.
|
|
||||||
function LauncherMods.setEnabled(id, enabled, version)
|
function LauncherMods.setEnabled(id, enabled, version)
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
||||||
@@ -384,7 +398,15 @@ function LauncherMods.setAllEnabled(ids, enabled, version)
|
|||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
local scope = SaveData.modScope(version)
|
local scope = SaveData.modScope(version)
|
||||||
for _, id in ipairs(ids or {}) do
|
for _, id in ipairs(ids or {}) do
|
||||||
SaveData.setModEnabled(options, id, enabled, scope)
|
if scope then
|
||||||
|
SaveData.setModEnabled(options, id, enabled, scope)
|
||||||
|
elseif SaveData.PER_VERSION_MODS then
|
||||||
|
for _, game in ipairs(GameVersion.ORDER) do
|
||||||
|
SaveData.setModEnabled(options, id, enabled, game)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
SaveData.setModEnabled(options, id, enabled)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
SaveData.saveOptions(options)
|
SaveData.saveOptions(options)
|
||||||
return true
|
return true
|
||||||
@@ -843,7 +865,7 @@ end
|
|||||||
|
|
||||||
-- uninstall(id) -> true | nil, errString
|
-- uninstall(id) -> true | nil, errString
|
||||||
-- Removes mods/<id>/ from wherever it was installed (the portable game folder
|
-- Removes mods/<id>/ from wherever it was installed (the portable game folder
|
||||||
-- or the save directory, CacheFs decides -- #330) and clears options.mods[id]
|
-- or the save directory, CacheFs decides -- #330) and clears every enable flag
|
||||||
-- so the loader and in-game manager no longer see it. Rejects missing ids.
|
-- so the loader and in-game manager no longer see it. Rejects missing ids.
|
||||||
-- Does not touch other mods' enable state.
|
-- Does not touch other mods' enable state.
|
||||||
function LauncherMods.uninstall(id)
|
function LauncherMods.uninstall(id)
|
||||||
@@ -872,8 +894,19 @@ function LauncherMods.uninstall(id)
|
|||||||
-- Drop the enable flag so a reinstall of the same id starts from the
|
-- Drop the enable flag so a reinstall of the same id starts from the
|
||||||
-- loader's default (enabled) rather than a stale false.
|
-- loader's default (enabled) rather than a stale false.
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
|
local changed = false
|
||||||
if options.mods and options.mods[id] ~= nil then
|
if options.mods and options.mods[id] ~= nil then
|
||||||
options.mods[id] = nil
|
options.mods[id] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
for _, version in ipairs(GameVersion.ORDER) do
|
||||||
|
local bucket = options.modsByVersion and options.modsByVersion[version]
|
||||||
|
if type(bucket) == "table" and bucket[id] ~= nil then
|
||||||
|
bucket[id] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if changed then
|
||||||
SaveData.saveOptions(options)
|
SaveData.saveOptions(options)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
|
|||||||
+20
-4
@@ -270,8 +270,7 @@ function Loader:_targetVersion()
|
|||||||
return version
|
return version
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The version an enable flag is read and written under: this game once
|
-- The version an enable flag is read and written under: this running game.
|
||||||
-- per-game flags are live, nil (the shared flag) while they are a preview.
|
|
||||||
-- Reads and writes go through the same answer so the two can never drift.
|
-- Reads and writes go through the same answer so the two can never drift.
|
||||||
function Loader:_enableScope()
|
function Loader:_enableScope()
|
||||||
return SaveData.modScope(self:_targetVersion())
|
return SaveData.modScope(self:_targetVersion())
|
||||||
@@ -1306,13 +1305,30 @@ function Loader:load(data)
|
|||||||
require("src.mods.Builtins").install(self.content, data, self.generation)
|
require("src.mods.Builtins").install(self.content, data, self.generation)
|
||||||
self:_loadState()
|
self:_loadState()
|
||||||
self:_discover()
|
self:_discover()
|
||||||
|
-- Existing installs stored one shared answer. Once their manifests are
|
||||||
|
-- known, split that answer across every game before the next launcher/game
|
||||||
|
-- toggle can change one independently. _loadState already used the same
|
||||||
|
-- fallback, so this write cannot change the current boot's result.
|
||||||
|
do
|
||||||
|
local options = SaveData.loadOptions(self.fs)
|
||||||
|
local installed = {}
|
||||||
|
for id, mod in pairs(self.mods) do
|
||||||
|
installed[#installed + 1] = {
|
||||||
|
id = id,
|
||||||
|
experimental = mod.manifest and mod.manifest.experimental == true,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if SaveData.migrateModEnablement(options, installed) and self.fs.write then
|
||||||
|
SaveData.saveOptions(options, self.fs)
|
||||||
|
end
|
||||||
|
end
|
||||||
-- Experimental mods stay off until the player opts in: a missing
|
-- Experimental mods stay off until the player opts in: a missing
|
||||||
-- options.mods entry normally means enabled, but experimental flips that.
|
-- options.mods entry normally means enabled, but experimental flips that.
|
||||||
do
|
do
|
||||||
local options = SaveData.loadOptions(self.fs)
|
local options = SaveData.loadOptions(self.fs)
|
||||||
local modsOpt = options.mods or {}
|
local scope = self:_enableScope()
|
||||||
for id, mod in pairs(self.mods) do
|
for id, mod in pairs(self.mods) do
|
||||||
if not self.disabled[id] and modsOpt[id] == nil
|
if not self.disabled[id] and SaveData.modEnabled(options, id, scope) == nil
|
||||||
and mod.manifest.experimental then
|
and mod.manifest.experimental then
|
||||||
self.disabled[id] = true
|
self.disabled[id] = true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -735,8 +735,7 @@ function ManagerState:toggleGen2Force(m)
|
|||||||
}, apply)
|
}, apply)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Where the loader persisted an enable flag: this game's slot once it keeps
|
-- Where the loader persists an enable flag: this running game's slot.
|
||||||
-- them per game, the shared flag until then (SaveData.modScope).
|
|
||||||
function ManagerState:enableScope()
|
function ManagerState:enableScope()
|
||||||
return SaveData.modScope((self:targetGame()))
|
return SaveData.modScope((self:targetGame()))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 /
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 Strings("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 Strings("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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -56,10 +56,24 @@ do
|
|||||||
local m = byId(LauncherMods.deriveList(manifests, { mods = { bbb = false } }))
|
local m = byId(LauncherMods.deriveList(manifests, { mods = { bbb = false } }))
|
||||||
check(m.aaa.enabled, "a mod with no options entry defaults to enabled")
|
check(m.aaa.enabled, "a mod with no options entry defaults to enabled")
|
||||||
check(not m.bbb.enabled, "an explicit false disables the mod")
|
check(not m.bbb.enabled, "an explicit false disables the mod")
|
||||||
|
check(m.aaa.enabledByVersion.red and m.aaa.enabledByVersion.gold,
|
||||||
|
"every row exposes its enabled answer for each game")
|
||||||
eq(m.aaa.status, "ok", "a healthy enabled mod is ok")
|
eq(m.aaa.status, "ok", "a healthy enabled mod is ok")
|
||||||
eq(m.aaa.statusDetail, "Ready", "ok detail reads Ready")
|
eq(m.aaa.statusDetail, "Ready", "ok detail reads Ready")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local manifests = {
|
||||||
|
mf({ id = "one", name = "One", version = "1.0.0", entry = "m.lua" }),
|
||||||
|
}
|
||||||
|
local row = byId(LauncherMods.deriveList(manifests, {
|
||||||
|
mods = { one = true }, modsByVersion = { gold = { one = false } },
|
||||||
|
}, "gold")).one
|
||||||
|
check(row.enabledByVersion.red, "the Red checkbox keeps the shared answer")
|
||||||
|
check(not row.enabledByVersion.gold, "the Gold checkbox reads Gold's answer")
|
||||||
|
check(not row.enabled, "the selected game's row state matches its checkbox")
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- experimental defaults to disabled; github surfaces on the row
|
-- ------- experimental defaults to disabled; github surfaces on the row
|
||||||
|
|
||||||
do
|
do
|
||||||
@@ -291,10 +305,9 @@ end
|
|||||||
|
|
||||||
-- ------- enable flags: the panel reads exactly what the switch writes
|
-- ------- enable flags: the panel reads exactly what the switch writes
|
||||||
--
|
--
|
||||||
-- One scope for both halves (SaveData.modScope). While per-game flags are a
|
-- One scope for both halves (SaveData.modScope). Per-game flags are live, so
|
||||||
-- preview the shared flag is the whole answer, so a modsByVersion overlay --
|
-- a modsByVersion answer -- including one restored from a .g1rmodlist -- is
|
||||||
-- which an imported .g1rmodlist can plant, ModProfile.restoreVersions -- can
|
-- both what the launcher shows and what the next boot reads.
|
||||||
-- never leave the switch showing an answer no writer can reach.
|
|
||||||
|
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
|
||||||
@@ -309,9 +322,8 @@ do
|
|||||||
}
|
}
|
||||||
local planted = { mods = { one = true },
|
local planted = { mods = { one = true },
|
||||||
modsByVersion = { gold = { one = false } } }
|
modsByVersion = { gold = { one = false } } }
|
||||||
local expected = SaveData.PER_VERSION_MODS and false or true
|
|
||||||
eq(byId(LauncherMods.deriveList(manifests, planted, "gold")).one.enabled,
|
eq(byId(LauncherMods.deriveList(manifests, planted, "gold")).one.enabled,
|
||||||
expected, "the overlay is read exactly when a write can reach it")
|
false, "the overlay is read exactly when a write can reach it")
|
||||||
|
|
||||||
-- the round trip, the thing the dead switch failed: flip it, re-derive
|
-- the round trip, the thing the dead switch failed: flip it, re-derive
|
||||||
local options = { mods = {} }
|
local options = { mods = {} }
|
||||||
|
|||||||
@@ -162,6 +162,32 @@ do
|
|||||||
"modScope follows the PER_VERSION_MODS switch")
|
"modScope follows the PER_VERSION_MODS switch")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
-- First launch after per-game controls shipped: preserve every old answer
|
||||||
|
-- and make the old implicit defaults explicit for the installed set.
|
||||||
|
local opts = {
|
||||||
|
mods = { old_on = true, old_off = false },
|
||||||
|
modsByVersion = { gold = { old_on = false } },
|
||||||
|
}
|
||||||
|
check(SaveData.migrateModEnablement(opts, {
|
||||||
|
{ id = "old_on" }, { id = "old_off" }, { id = "implicit" },
|
||||||
|
{ id = "lab", experimental = true },
|
||||||
|
}), "legacy mod state is migrated once")
|
||||||
|
check(opts.modsByVersionMigrated, "the migration is marked complete")
|
||||||
|
eq(SaveData.modEnabled(opts, "old_on", "red"), true,
|
||||||
|
"an old enabled mod is enabled for Red")
|
||||||
|
eq(SaveData.modEnabled(opts, "old_on", "gold"), false,
|
||||||
|
"an already-stored preview answer is preserved")
|
||||||
|
eq(SaveData.modEnabled(opts, "old_off", "blue"), false,
|
||||||
|
"an old disabled mod stays disabled for every game")
|
||||||
|
eq(SaveData.modEnabled(opts, "implicit", "yellow"), true,
|
||||||
|
"an old implicit default is enabled for every game")
|
||||||
|
eq(SaveData.modEnabled(opts, "lab", "red"), false,
|
||||||
|
"an experimental mod keeps its existing opt-in default")
|
||||||
|
check(not SaveData.migrateModEnablement(opts, { { id = "newer" } }),
|
||||||
|
"a later mod install does not rerun the legacy migration")
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- a profile carries the per-game half of a setup
|
-- ------- a profile carries the per-game half of a setup
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -58,6 +58,8 @@ local MATCHUPS = {
|
|||||||
{ attacker = "NORMAL", defender = "ROCK", multiplier = 5 },
|
{ attacker = "NORMAL", defender = "ROCK", multiplier = 5 },
|
||||||
{ attacker = "NORMAL", defender = "STEEL", multiplier = 5 },
|
{ attacker = "NORMAL", defender = "STEEL", multiplier = 5 },
|
||||||
{ attacker = "ELECTRIC", defender = "GROUND", multiplier = 0 },
|
{ attacker = "ELECTRIC", defender = "GROUND", multiplier = 0 },
|
||||||
|
-- Sonic Boom is Normal; Gen 2 StaticDamage still respects Ghost immunity.
|
||||||
|
{ attacker = "NORMAL", defender = "GHOST", multiplier = 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
local MOVES = {
|
local MOVES = {
|
||||||
@@ -144,6 +146,15 @@ local POKEMON = {
|
|||||||
levelMoves = { { level = 1, move = "TACKLE" } },
|
levelMoves = { { level = 1, move = "TACKLE" } },
|
||||||
evolutions = {},
|
evolutions = {},
|
||||||
},
|
},
|
||||||
|
GASTLY = {
|
||||||
|
id = "GASTLY", index = 92, name = "GASTLY",
|
||||||
|
baseStats = { hp = 30, attack = 35, defense = 30, speed = 80,
|
||||||
|
specialAttack = 100, specialDefense = 35 },
|
||||||
|
types = { "GHOST", "POISON" }, catchRate = 190, baseExp = 62,
|
||||||
|
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 127,
|
||||||
|
levelMoves = { { level = 1, move = "LICK" } },
|
||||||
|
evolutions = {},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
local DATA = {
|
local DATA = {
|
||||||
@@ -613,6 +624,21 @@ local battle, player, wild = newBattle()
|
|||||||
check("battle picks the first healthy mon", battle.player, player)
|
check("battle picks the first healthy mon", battle.player, player)
|
||||||
check("wild battle flag", battle.wild, true)
|
check("wild battle flag", battle.wild, true)
|
||||||
|
|
||||||
|
-- Gen 2 battles expose BattleRandom (`random` / :roller()) and the Gen 1 /
|
||||||
|
-- love.math `rng` over the same stream.
|
||||||
|
check("battle.rng is present", type(battle.rng), "function")
|
||||||
|
check("battle.rng(lo,hi) respects bounds", battle.rng(10, 10), 10)
|
||||||
|
check("battle.rng(lo,hi) another fixed point", battle.rng(50, 50), 50)
|
||||||
|
do
|
||||||
|
local roll = battle.rng(0, 255)
|
||||||
|
check("battle.rng(0,255) is an integer", roll == math.floor(roll), true)
|
||||||
|
check("battle.rng(0,255) in range", roll >= 0 and roll <= 255, true)
|
||||||
|
end
|
||||||
|
-- zeroRandom always returns 0, so the love adapter maps:
|
||||||
|
-- rng(n) → 0+1 = 1; rng(lo,hi) → lo + 0 = lo
|
||||||
|
check("battle.rng(n) is 1..n over BattleRandom", battle.rng(7), 1)
|
||||||
|
check("battle.rng(0,99) uses both args", battle.rng(0, 99), 0)
|
||||||
|
|
||||||
-- A move spends PP and deals damage.
|
-- A move spends PP and deals damage.
|
||||||
local before = wild.hp
|
local before = wild.hp
|
||||||
battle:takeTurn({ kind = "move", move = "TACKLE" })
|
battle:takeTurn({ kind = "move", move = "TACKLE" })
|
||||||
@@ -910,6 +936,16 @@ local EFFECT_MOVES = {
|
|||||||
SEISMIC_TOSS = { id = "SEISMIC_TOSS", name = "SEISMICTOSS", power = 1,
|
SEISMIC_TOSS = { id = "SEISMIC_TOSS", name = "SEISMICTOSS", power = 1,
|
||||||
type = "FIGHTING", accuracy = 100, pp = 20,
|
type = "FIGHTING", accuracy = 100, pp = 20,
|
||||||
effect = "EFFECT_LEVEL_DAMAGE" },
|
effect = "EFFECT_LEVEL_DAMAGE" },
|
||||||
|
-- EFFECT_STATIC_DAMAGE: Sonic Boom (20) and Dragon Rage (40).
|
||||||
|
SONICBOOM = { id = "SONICBOOM", name = "SONICBOOM", power = 20,
|
||||||
|
type = "NORMAL", accuracy = 90, pp = 20,
|
||||||
|
effect = "EFFECT_STATIC_DAMAGE" },
|
||||||
|
DRAGON_RAGE = { id = "DRAGON_RAGE", name = "DRAGON RAGE", power = 40,
|
||||||
|
type = "DRAGON", accuracy = 100, pp = 10,
|
||||||
|
effect = "EFFECT_STATIC_DAMAGE" },
|
||||||
|
MAGNITUDE = { id = "MAGNITUDE", name = "MAGNITUDE", power = 1,
|
||||||
|
type = "GROUND", accuracy = 100, pp = 30,
|
||||||
|
effect = "EFFECT_MAGNITUDE" },
|
||||||
LOCK_ON = { id = "LOCK_ON", name = "LOCK-ON", power = 0, type = "NORMAL",
|
LOCK_ON = { id = "LOCK_ON", name = "LOCK-ON", power = 0, type = "NORMAL",
|
||||||
accuracy = 100, accuracyRaw = 0xff, pp = 5, effect = "EFFECT_LOCK_ON" },
|
accuracy = 100, accuracyRaw = 0xff, pp = 5, effect = "EFFECT_LOCK_ON" },
|
||||||
-- data/moves/moves.asm:169, :172, :151.
|
-- data/moves/moves.asm:169, :172, :151.
|
||||||
@@ -1078,6 +1114,83 @@ tossBattle:takeTurn({ kind = "move", move = "SEISMIC_TOSS" })
|
|||||||
check("seismic toss deals the level",
|
check("seismic toss deals the level",
|
||||||
tossBefore - tossWild.hp, tossPlayer.level)
|
tossBefore - tossWild.hp, tossPlayer.level)
|
||||||
|
|
||||||
|
-- EFFECT_STATIC_DAMAGE: Sonic Boom (20) and Dragon Rage (40).
|
||||||
|
-- Cart: constantdamage + resettypematchup (effects.asm StaticDamage).
|
||||||
|
do
|
||||||
|
check("static damage uses move power",
|
||||||
|
Effects.fixedDamage("EFFECT_STATIC_DAMAGE", { level = 10 }, { hp = 50 }, nil,
|
||||||
|
40), 40)
|
||||||
|
check("sonic boom fixed damage is 20",
|
||||||
|
Effects.fixedDamage("EFFECT_STATIC_DAMAGE", { level = 10 }, { hp = 50 }, nil,
|
||||||
|
20), 20)
|
||||||
|
|
||||||
|
local boomBattle, _, boomWild = effectBattle({ "SONICBOOM" })
|
||||||
|
boomWild.hp = 100
|
||||||
|
boomWild.maxHp = 100
|
||||||
|
local boomBefore = boomWild.hp
|
||||||
|
boomBattle:takeTurn({ kind = "move", move = "SONICBOOM" })
|
||||||
|
check("sonic boom deals flat 20", boomBefore - boomWild.hp, 20)
|
||||||
|
|
||||||
|
-- Normal vs Ghost is 0x; StaticDamage's resettypematchup misses.
|
||||||
|
local ghostBattle, _, ghostWild = effectBattle({ "SONICBOOM" })
|
||||||
|
ghostWild.species = "GASTLY"
|
||||||
|
local ghostBefore = ghostWild.hp
|
||||||
|
ghostBattle:takeTurn({ kind = "move", move = "SONICBOOM" })
|
||||||
|
check("sonic boom misses Ghost in Gen 2", ghostWild.hp, ghostBefore)
|
||||||
|
|
||||||
|
local rageBattle, _, rageWild = effectBattle({ "DRAGON_RAGE" })
|
||||||
|
rageWild.hp = 100
|
||||||
|
rageWild.maxHp = 100
|
||||||
|
local rageBefore = rageWild.hp
|
||||||
|
rageBattle:takeTurn({ kind = "move", move = "DRAGON_RAGE" })
|
||||||
|
check("dragon rage deals flat 40", rageBefore - rageWild.hp, 40)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Magnitude: BattleRandom walks magnitude_power.asm. Nil random must not
|
||||||
|
-- collapse to roll 0 (always Magnitude 4); Battle.new always supplies a roller.
|
||||||
|
do
|
||||||
|
local p4, n4 = Effects.magnitudePower(function() return 0 end)
|
||||||
|
check("magnitude roll 0 is power 10", p4, 10)
|
||||||
|
check("magnitude roll 0 is number 4", n4, 4)
|
||||||
|
|
||||||
|
local p8, n8 = Effects.magnitudePower(function() return 200 end)
|
||||||
|
check("magnitude roll 200 is power 90", p8, 90)
|
||||||
|
check("magnitude roll 200 is number 8", n8, 8)
|
||||||
|
|
||||||
|
local seen = {}
|
||||||
|
for roll = 0, 255 do
|
||||||
|
local _, number = Effects.magnitudePower(function() return roll end)
|
||||||
|
seen[number] = true
|
||||||
|
end
|
||||||
|
for want = 4, 10 do
|
||||||
|
check(("magnitude table reaches %d"):format(want), seen[want] == true, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Engine path: inject a mid-table roll and confirm the announce + damage.
|
||||||
|
local magBattle, magPlayer, magWild = newBattle({
|
||||||
|
random = function(n)
|
||||||
|
-- Pin the getmagnitude byte: return 200 whenever n == 256.
|
||||||
|
if n == 256 then return 200 end
|
||||||
|
return 0
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
magPlayer.moves = { { id = "MAGNITUDE", pp = 30, maxPp = 30 } }
|
||||||
|
magWild.hp = 200
|
||||||
|
magWild.maxHp = 200
|
||||||
|
local magEvents = magBattle:takeTurn({ kind = "move", move = "MAGNITUDE" })
|
||||||
|
local announced
|
||||||
|
for _, ev in ipairs(magEvents) do
|
||||||
|
if ev.kind == "message" and type(ev.text) == "string"
|
||||||
|
and ev.text:match("^Magnitude %d+") then
|
||||||
|
announced = ev.text
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
check("magnitude announces rolled number", announced, "Magnitude 8!")
|
||||||
|
check("magnitude deals more than power-1 would",
|
||||||
|
magWild.hp < 200, true)
|
||||||
|
end
|
||||||
|
|
||||||
-- --------------------------------------------------------------- held items
|
-- --------------------------------------------------------------- held items
|
||||||
|
|
||||||
local Ai = require("src.battle.gen2.Ai")
|
local Ai = require("src.battle.gen2.Ai")
|
||||||
|
|||||||
@@ -1017,8 +1017,10 @@ press(ms, "select")
|
|||||||
check(avail[1].enabled == false, "SELECT quick-toggles the focused mod")
|
check(avail[1].enabled == false, "SELECT quick-toggles the focused mod")
|
||||||
check(ms:isStaged(avail[1]), "a flip against boot state is staged")
|
check(ms:isStaged(avail[1]), "a flip against boot state is staged")
|
||||||
check(ms:glyphFor(avail[1]) == ".", "staged mods show the staged glyph")
|
check(ms:glyphFor(avail[1]) == ".", "staged mods show the staged glyph")
|
||||||
check(mgame.save.options.mods.badmod == false,
|
local managerScope = ms:enableScope()
|
||||||
"the live options table mirrors the flip")
|
check(managerScope and mgame.save.options.modsByVersion
|
||||||
|
and mgame.save.options.modsByVersion[managerScope].badmod == false,
|
||||||
|
"the live options table mirrors the flip for this game")
|
||||||
check(ms.restartPending, "staged changes arm the apply screen")
|
check(ms.restartPending, "staged changes arm the apply screen")
|
||||||
ms:discardChanges()
|
ms:discardChanges()
|
||||||
check(avail[1].enabled == true and not ms.restartPending,
|
check(avail[1].enabled == true and not ms.restartPending,
|
||||||
|
|||||||
+3
-3
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user