mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
This commit is contained in:
@@ -92,7 +92,7 @@ return {
|
|||||||
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
||||||
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
||||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
||||||
{ "play_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
||||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
||||||
{ "jump", 13 }, -- 11
|
{ "jump", 13 }, -- 11
|
||||||
{ "show_text", "_BoxIsFullText" }, -- 12
|
{ "show_text", "_BoxIsFullText" }, -- 12
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
local function coinGiver(opts)
|
local function coinGiver(opts)
|
||||||
return function(game, ow, npc, done)
|
return function(game, ow, npc, done)
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Sound = require("src.core.Sound")
|
|
||||||
local t = game.data.text
|
local t = game.data.text
|
||||||
local function push(label, fallback, onDone)
|
local function push(label, fallback, onDone, popts)
|
||||||
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done))
|
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done,
|
||||||
|
popts))
|
||||||
end
|
end
|
||||||
if game.save.flags[opts.event] then
|
if game.save.flags[opts.event] then
|
||||||
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
|
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
|
||||||
@@ -30,9 +30,10 @@ local function coinGiver(opts)
|
|||||||
end
|
end
|
||||||
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
|
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
|
||||||
game.save.flags[opts.event] = true
|
game.save.flags[opts.event] = true
|
||||||
Sound.play(game.data, "Get_Item1")
|
-- the ReceivedNCoinsText strings carry sound_get_item_1
|
||||||
push(opts.receivedLabel,
|
push(opts.receivedLabel,
|
||||||
("{PLAYER} received\n%d coins!"):format(opts.amount))
|
("{PLAYER} received\n%d coins!"):format(opts.amount), nil,
|
||||||
|
TextBox.soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ return {
|
|||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Commands = require("src.script.Commands")
|
local Commands = require("src.script.Commands")
|
||||||
local t = game.data.text
|
local t = game.data.text
|
||||||
local function say(label, cb)
|
local function say(label, cb, sopts)
|
||||||
game.stack:push(TextBox.new(game, t[label] or label, cb))
|
game.stack:push(TextBox.new(game, t[label] or label, cb, sopts))
|
||||||
end
|
end
|
||||||
|
|
||||||
if game.save.flags.EVENT_GOT_OLD_AMBER then
|
if game.save.flags.EVENT_GOT_OLD_AMBER then
|
||||||
@@ -39,8 +39,9 @@ return {
|
|||||||
game.save.flags.EVENT_GOT_OLD_AMBER = true
|
game.save.flags.EVENT_GOT_OLD_AMBER = true
|
||||||
Commands.hide_object({ save = game.save, overworld = ow, game = game },
|
Commands.hide_object({ save = game.save, overworld = ow, game = game },
|
||||||
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
|
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
-- .ReceivedOldAmberText carries sound_get_item_1
|
||||||
say("_Museum1FScientist2ReceivedOldAmberText", done)
|
say("_Museum1FScientist2ReceivedOldAmberText", done,
|
||||||
|
TextBox.soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
|||||||
-- inside give_pokemon). Show the received text first so the
|
-- inside give_pokemon). Show the received text first so the
|
||||||
-- nickname prompt follows "you got X", matching Gen1.
|
-- nickname prompt follows "you got X", matching Gen1.
|
||||||
-- The received text carries sound_get_key_item (OaksLab.asm
|
-- The received text carries sound_get_key_item (OaksLab.asm
|
||||||
-- OaksLabReceivedMonText); the jingle plays as the box opens
|
-- OaksLabReceivedMonText), so the jingle fires once the box has
|
||||||
-- (same beat as the Yellow port's starter, #668).
|
-- typed out and holds it (same beat as the Yellow port's starter, #668).
|
||||||
{ "play_sound", "Get_Key_Item" }, -- 8
|
{ "text_sound", "Get_Key_Item" }, -- 8
|
||||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 9
|
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 9
|
||||||
{ "give_pokemon", species, 5 }, -- 10
|
{ "give_pokemon", species, 5 }, -- 10
|
||||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 11
|
{ "set_flag", "EVENT_GOT_STARTER" }, -- 11
|
||||||
@@ -54,7 +54,7 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
|||||||
{ "face_object", 1, "up" }, -- 15
|
{ "face_object", 1, "up" }, -- 15
|
||||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 16
|
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 16
|
||||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 17
|
{ "hide_object", "OAKS_LAB", rivalBall }, -- 17
|
||||||
{ "play_sound", "Get_Key_Item" }, -- 18 (sound_get_key_item)
|
{ "text_sound", "Get_Key_Item" }, -- 18 (sound_get_key_item)
|
||||||
{ "show_text", "_OaksLabRivalReceivedMonText",
|
{ "show_text", "_OaksLabRivalReceivedMonText",
|
||||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||||
@@ -106,8 +106,8 @@ return {
|
|||||||
{ "check_item", "OAKS_PARCEL" },
|
{ "check_item", "OAKS_PARCEL" },
|
||||||
{ "jump_if_false", "raise_young" },
|
{ "jump_if_false", "raise_young" },
|
||||||
-- OaksLabOak1Text.got_parcel → RivalArrives + OakGivesPokedex
|
-- OaksLabOak1Text.got_parcel → RivalArrives + OakGivesPokedex
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||||
{ "take_item", "OAKS_PARCEL", 1 },
|
{ "take_item", "OAKS_PARCEL", 1 },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
@@ -128,8 +128,8 @@ return {
|
|||||||
{ "face_object", 1, "up" },
|
{ "face_object", 1, "up" },
|
||||||
{ "face_object", 5, "down" },
|
{ "face_object", 5, "down" },
|
||||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||||
{ "face_object", 1, "up" },
|
{ "face_object", 1, "up" },
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ return {
|
|||||||
{ "jump_if_false", "raise_young" },
|
{ "jump_if_false", "raise_young" },
|
||||||
-- .DeliverParcelText: parcel handover, then the Pokédex scene
|
-- .DeliverParcelText: parcel handover, then the Pokédex scene
|
||||||
-- (OaksLabRivalArrivesAtOaksRequestScript -> OakGivesPokedexScript)
|
-- (OaksLabRivalArrivesAtOaksRequestScript -> OakGivesPokedexScript)
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||||
{ "take_item", "OAKS_PARCEL", 1 },
|
{ "take_item", "OAKS_PARCEL", 1 },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
@@ -62,8 +62,8 @@ return {
|
|||||||
{ "face_object", RIVAL, "up" },
|
{ "face_object", RIVAL, "up" },
|
||||||
{ "face_object", OAK1, "down" },
|
{ "face_object", OAK1, "down" },
|
||||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||||
{ "face_object", RIVAL, "up" },
|
{ "face_object", RIVAL, "up" },
|
||||||
@@ -113,8 +113,8 @@ return {
|
|||||||
{ "jump_if_true", "come_see" },
|
{ "jump_if_true", "come_see" },
|
||||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||||
{ "give_item", "POKE_BALL", 5, false },
|
{ "give_item", "POKE_BALL", 5, false },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
||||||
{ "jump", "end" },
|
{ "jump", "end" },
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ return {
|
|||||||
-- rival starter baseline (RIVAL_STARTER_JOLTEON) at snatch time
|
-- rival starter baseline (RIVAL_STARTER_JOLTEON) at snatch time
|
||||||
rows[#rows + 1] = { "set_field", "rivalStarter", 1 }
|
rows[#rows + 1] = { "set_field", "rivalStarter", 1 }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText1" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText1" }
|
||||||
rows[#rows + 1] = { "play_sound", "Get_Key_Item" }
|
rows[#rows + 1] = { "text_sound", "Get_Key_Item" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText2" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText2" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText3" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText3" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText4" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText4" }
|
||||||
@@ -196,7 +196,7 @@ return {
|
|||||||
rows[#rows + 1] = { "face_object", OAK1, "down" }
|
rows[#rows + 1] = { "face_object", OAK1, "down" }
|
||||||
-- OaksLabPlayerReceivedMonText clears wMonDataLocation, so AskName runs (#1013)
|
-- OaksLabPlayerReceivedMonText clears wMonDataLocation, so AskName runs (#1013)
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
|
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
|
||||||
rows[#rows + 1] = { "play_sound", "Get_Key_Item" }
|
rows[#rows + 1] = { "text_sound", "Get_Key_Item" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
|
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
|
||||||
rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5 }
|
rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5 }
|
||||||
-- DisablePikachuOverworldSpriteDrawing keeps it in the ball (#1009)
|
-- DisablePikachuOverworldSpriteDrawing keeps it in the ball (#1009)
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ M.BILLS_HOUSE = {
|
|||||||
|
|
||||||
M.ROUTE_25 = {
|
M.ROUTE_25 = {
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
|
game.save.pikachuMapScriptActive = nil
|
||||||
local flags = game.save.flags
|
local flags = game.save.flags
|
||||||
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then return end
|
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then return end
|
||||||
local Commands = require("src.script.Commands")
|
local Commands = require("src.script.Commands")
|
||||||
@@ -329,6 +330,7 @@ M.VERMILION_CITY = {
|
|||||||
-- only read while EVENT_1ST_LOCK_OPENED is unset (the gym is only
|
-- only read while EVENT_1ST_LOCK_OPENED is unset (the gym is only
|
||||||
-- reachable through this map, so a fresh visit always re-rolls).
|
-- reachable through this map, so a fresh visit always re-rolls).
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
|
game.save.pikachuMapScriptActive = nil
|
||||||
local puz = game.save.trashPuzzle or {}
|
local puz = game.save.trashPuzzle or {}
|
||||||
game.save.trashPuzzle = puz
|
game.save.trashPuzzle = puz
|
||||||
puz.first = love.math.random(0, 7) * 2
|
puz.first = love.math.random(0, 7) * 2
|
||||||
|
|||||||
@@ -648,8 +648,10 @@ local function mtMoonFossil(itemId, otherName, gotFlag)
|
|||||||
end
|
end
|
||||||
local idef = game.data.items[itemId]
|
local idef = game.data.items[itemId]
|
||||||
game.stringBuffer = idef and idef.name or itemId
|
game.stringBuffer = idef and idef.name or itemId
|
||||||
require("src.core.Sound").play(game.data, "Get_Key_Item")
|
|
||||||
local dirs = mtMoonNerdWalk(ow.player.cellX, ow.player.cellY, itemId)
|
local dirs = mtMoonNerdWalk(ow.player.cellX, ow.player.cellY, itemId)
|
||||||
|
-- MtMoonB2FReceivedFossilText: text_far, sound_get_key_item,
|
||||||
|
-- text_waitbutton -- the jingle plays after the box has typed and
|
||||||
|
-- the button wait comes after it
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
t._MtMoonB2FReceivedFossilText
|
t._MtMoonB2FReceivedFossilText
|
||||||
or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"),
|
or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"),
|
||||||
@@ -662,11 +664,11 @@ local function mtMoonFossil(itemId, otherName, gotFlag)
|
|||||||
ow.runner:run({
|
ow.runner:run({
|
||||||
{ "walk_npc", 1, dirs },
|
{ "walk_npc", 1, dirs },
|
||||||
{ "text_opts", { auto = true } },
|
{ "text_opts", { auto = true } },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" },
|
{ "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "hide_object", "MT_MOON_B2F", otherName },
|
{ "hide_object", "MT_MOON_B2F", otherName },
|
||||||
}, { onDone = done })
|
}, { onDone = done })
|
||||||
end))
|
end, TextBox.soundOpts(game, "Get_Key_Item")))
|
||||||
end }))
|
end }))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+7
-11
@@ -8,9 +8,9 @@ local M = {}
|
|||||||
|
|
||||||
local function text(game) return game.data.text end
|
local function text(game) return game.data.text end
|
||||||
|
|
||||||
local function push(game, s, done)
|
local function push(game, s, done, opts)
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The question stays on screen under the YES/NO menu. The dojo prize
|
-- The question stays on screen under the YES/NO menu. The dojo prize
|
||||||
@@ -263,7 +263,7 @@ M.SILPH_CO_7F = {
|
|||||||
{ "jump_if_false", "box_full" },
|
{ "jump_if_false", "box_full" },
|
||||||
-- flag ahead of the jingle, like the Celadon EEVEE (#426)
|
-- flag ahead of the jingle, like the Celadon EEVEE (#426)
|
||||||
{ "set_flag", "EVENT_GOT_LAPRAS" },
|
{ "set_flag", "EVENT_GOT_LAPRAS" },
|
||||||
{ "play_sound", "Get_Item1" },
|
{ "text_sound", "Get_Item1" },
|
||||||
{ "show_text", "_GotMonText", { RAM = "LAPRAS" } },
|
{ "show_text", "_GotMonText", { RAM = "LAPRAS" } },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
|
||||||
{ "jump", "end" },
|
{ "jump", "end" },
|
||||||
@@ -307,12 +307,11 @@ M.COPYCATS_HOUSE_2F = {
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.stringBuffer = game.data.items.TM_MIMIC.name
|
game.stringBuffer = game.data.items.TM_MIMIC.name
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
|
||||||
Bag.remove(game.save, "POKE_DOLL", 1)
|
Bag.remove(game.save, "POKE_DOLL", 1)
|
||||||
game.save.flags.EVENT_GOT_TM31 = true
|
game.save.flags.EVENT_GOT_TM31 = true
|
||||||
push(game, t._CopycatsHouse2FCopycatReceivedTM31Text, function()
|
push(game, t._CopycatsHouse2FCopycatReceivedTM31Text, function()
|
||||||
push(game, t._CopycatsHouse2FCopycatTM31Explanation1Text, done)
|
push(game, t._CopycatsHouse2FCopycatTM31Explanation1Text, done)
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
@@ -478,7 +477,6 @@ M.CELADON_MART_ROOF = {
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.save.flags[g.flag] = true
|
game.save.flags[g.flag] = true
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
|
||||||
local subs = { player = game.save.player.name,
|
local subs = { player = game.save.player.name,
|
||||||
ram = game.data.items[g.tm].name }
|
ram = game.data.items[g.tm].name }
|
||||||
local explain = fill(t[g.explain] or "", subs)
|
local explain = fill(t[g.explain] or "", subs)
|
||||||
@@ -490,7 +488,7 @@ M.CELADON_MART_ROOF = {
|
|||||||
else
|
else
|
||||||
done()
|
done()
|
||||||
end
|
end
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
onCancel = done,
|
onCancel = done,
|
||||||
@@ -522,7 +520,6 @@ M.ROUTE_24 = {
|
|||||||
local t = text(game)
|
local t = text(game)
|
||||||
push(game, t._Route24CooltrainerM1YouBeatOurContestText .. "\f"
|
push(game, t._Route24CooltrainerM1YouBeatOurContestText .. "\f"
|
||||||
.. t._Route24CooltrainerM1YouJustEarnedAPrizeText, function()
|
.. t._Route24CooltrainerM1YouJustEarnedAPrizeText, function()
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
|
||||||
if not require("src.inventory.Bag").add(game.save, "NUGGET", 1,
|
if not require("src.inventory.Bag").add(game.save, "NUGGET", 1,
|
||||||
game.data) then
|
game.data) then
|
||||||
push(game, t._Route24CooltrainerM1NoRoomText, done)
|
push(game, t._Route24CooltrainerM1NoRoomText, done)
|
||||||
@@ -531,11 +528,10 @@ M.ROUTE_24 = {
|
|||||||
flags.EVENT_GOT_NUGGET = true
|
flags.EVENT_GOT_NUGGET = true
|
||||||
game.stringBuffer = game.data.items.NUGGET.name
|
game.stringBuffer = game.data.items.NUGGET.name
|
||||||
push(game, t._Route24CooltrainerM1ReceivedNuggetText, function()
|
push(game, t._Route24CooltrainerM1ReceivedNuggetText, function()
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
|
||||||
push(game, t._Route24CooltrainerM1JoinTeamRocketText,
|
push(game, t._Route24CooltrainerM1JoinTeamRocketText,
|
||||||
battleOrDone)
|
battleOrDone)
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
battleOrDone()
|
battleOrDone()
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ local M = {}
|
|||||||
|
|
||||||
local function text(game) return game.data.text end
|
local function text(game) return game.data.text end
|
||||||
|
|
||||||
local function push(game, s, done)
|
local function push(game, s, done, opts)
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- fill the extracted text placeholders ({RAM:...}, {PLAYER})
|
-- fill the extracted text placeholders ({RAM:...}, {PLAYER})
|
||||||
@@ -25,8 +25,8 @@ local function gift(opts)
|
|||||||
local t = text(game)
|
local t = text(game)
|
||||||
local itemName = game.data.items[opts.item].name
|
local itemName = game.data.items[opts.item].name
|
||||||
local subs = { ram = itemName, player = game.save.player.name }
|
local subs = { ram = itemName, player = game.save.player.name }
|
||||||
local function say(label, fallback, cb)
|
local function say(label, fallback, cb, sopts)
|
||||||
push(game, fill(t[label] or fallback, subs), cb)
|
push(game, fill(t[label] or fallback, subs), cb, sopts)
|
||||||
end
|
end
|
||||||
if game.save.flags[opts.flag] then
|
if game.save.flags[opts.flag] then
|
||||||
say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done)
|
say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done)
|
||||||
@@ -39,15 +39,16 @@ local function gift(opts)
|
|||||||
end
|
end
|
||||||
game.save.flags[opts.flag] = true
|
game.save.flags[opts.flag] = true
|
||||||
local idef = game.data.items[opts.item]
|
local idef = game.data.items[opts.item]
|
||||||
require("src.core.Sound").play(game.data,
|
-- the received texts carry sound_get_item_1 / sound_get_key_item, so
|
||||||
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1")
|
-- the jingle only fires once that box has typed out
|
||||||
say(opts.received, "{PLAYER} received\n{RAM:}!", function()
|
say(opts.received, "{PLAYER} received\n{RAM:}!", function()
|
||||||
if opts.explain then
|
if opts.explain then
|
||||||
say(opts.explain, "", done)
|
say(opts.explain, "", done)
|
||||||
else
|
else
|
||||||
done()
|
done()
|
||||||
end
|
end
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game,
|
||||||
|
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1"))
|
||||||
end
|
end
|
||||||
if opts.pre then say(opts.pre, opts.preFallback or "", give) else give() end
|
if opts.pre then say(opts.pre, opts.preFallback or "", give) else give() end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,14 @@
|
|||||||
-- is full; `gotFlag` (pokered's EVENT_GOT_TM*) is set only on a
|
-- is full; `gotFlag` (pokered's EVENT_GOT_TM*) is set only on a
|
||||||
-- successful give, which is what makes the leader's talk script retry
|
-- successful give, which is what makes the leader's talk script retry
|
||||||
-- later (gyms.lua).
|
-- later (gyms.lua).
|
||||||
|
--
|
||||||
|
-- `badgeSound` / `tmSound` are the text sound command each gym's reward
|
||||||
|
-- text carries right after its FIRST label -- home/text.asm TextCommand_SOUND
|
||||||
|
-- plays it once that page has typed out and then blocks on
|
||||||
|
-- WaitForSoundToFinish, so the jingle sits between the pages rather than
|
||||||
|
-- under them. macros/scripts/text.asm defines sound_level_up as
|
||||||
|
-- sound_get_item_1, so Pewter's and Viridian's badge lines are Get_Item1
|
||||||
|
-- too. Vermilion, Celadon and Fuchsia carry no sound on the badge text.
|
||||||
|
|
||||||
local function range(prefix, first, last)
|
local function range(prefix, first, last)
|
||||||
local t = {}
|
local t = {}
|
||||||
@@ -50,6 +58,8 @@ return {
|
|||||||
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
|
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
|
||||||
{ "ROUTE_22", "ROUTE22_RIVAL1" },
|
{ "ROUTE_22", "ROUTE22_RIVAL1" },
|
||||||
},
|
},
|
||||||
|
badgeSound = "Get_Item1", -- sound_level_up
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_PewterGymBrockReceivedBoulderBadgeText",
|
"_PewterGymBrockReceivedBoulderBadgeText",
|
||||||
"_PewterGymBrockBoulderBadgeInfoText",
|
"_PewterGymBrockBoulderBadgeInfoText",
|
||||||
@@ -64,6 +74,8 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM11",
|
gotFlag = "EVENT_GOT_TM11",
|
||||||
noRoom = "_CeruleanGymMistyTM11NoRoomText",
|
noRoom = "_CeruleanGymMistyTM11NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
|
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
|
||||||
|
badgeSound = "Get_Key_Item",
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_CeruleanGymMistyReceivedCascadeBadgeText",
|
"_CeruleanGymMistyReceivedCascadeBadgeText",
|
||||||
},
|
},
|
||||||
@@ -76,6 +88,7 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM24",
|
gotFlag = "EVENT_GOT_TM24",
|
||||||
noRoom = "_VermilionGymLTSurgeTM24NoRoomText",
|
noRoom = "_VermilionGymLTSurgeTM24NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
|
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
|
||||||
|
tmSound = "Get_Key_Item",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_VermilionGymLTSurgeReceivedThunderBadgeText",
|
"_VermilionGymLTSurgeReceivedThunderBadgeText",
|
||||||
},
|
},
|
||||||
@@ -89,6 +102,7 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM21",
|
gotFlag = "EVENT_GOT_TM21",
|
||||||
noRoom = "_CeladonGymTM21NoRoomText",
|
noRoom = "_CeladonGymTM21NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
|
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_CeladonGymErikaReceivedRainbowBadgeText",
|
"_CeladonGymErikaReceivedRainbowBadgeText",
|
||||||
},
|
},
|
||||||
@@ -102,6 +116,7 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM06",
|
gotFlag = "EVENT_GOT_TM06",
|
||||||
noRoom = "_FuchsiaGymKogaTM06NoRoomText",
|
noRoom = "_FuchsiaGymKogaTM06NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
|
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
|
||||||
|
tmSound = "Get_Key_Item",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_FuchsiaGymKogaReceivedSoulBadgeText",
|
"_FuchsiaGymKogaReceivedSoulBadgeText",
|
||||||
},
|
},
|
||||||
@@ -115,6 +130,8 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM46",
|
gotFlag = "EVENT_GOT_TM46",
|
||||||
noRoom = "_SaffronGymSabrinaTM46NoRoomText",
|
noRoom = "_SaffronGymSabrinaTM46NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
|
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
|
||||||
|
badgeSound = "Get_Key_Item",
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_SaffronGymSabrinaReceivedMarshBadgeText",
|
"_SaffronGymSabrinaReceivedMarshBadgeText",
|
||||||
},
|
},
|
||||||
@@ -128,6 +145,8 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM38",
|
gotFlag = "EVENT_GOT_TM38",
|
||||||
noRoom = "_CinnabarGymBlaineTM38NoRoomText",
|
noRoom = "_CinnabarGymBlaineTM38NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
|
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
|
||||||
|
badgeSound = "Get_Key_Item",
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
|
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
|
||||||
},
|
},
|
||||||
@@ -141,6 +160,8 @@ return {
|
|||||||
gotFlag = "EVENT_GOT_TM27",
|
gotFlag = "EVENT_GOT_TM27",
|
||||||
noRoom = "_ViridianGymGiovanniTM27NoRoomText",
|
noRoom = "_ViridianGymGiovanniTM27NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
|
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
|
||||||
|
badgeSound = "Get_Item1", -- sound_level_up
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_ViridianGymGiovanniReceivedEarthBadgeText",
|
"_ViridianGymGiovanniReceivedEarthBadgeText",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
|||||||
|
|
||||||
## Pokémon Gold (Gen 2)
|
## Pokémon Gold (Gen 2)
|
||||||
|
|
||||||
A fourth game the launcher can import and play, built from pret/pokegold the same way Red/Blue/Yellow are built from pokered. Port extras beyond the cartridge:
|
A fourth game the launcher can import and play, built from pret/pokegold the same way Red/Blue/Yellow are built from pokered (and pokeyellow). Port extras beyond the cartridge:
|
||||||
|
|
||||||
* **COLOR, zoom, tilt, GBC FX, and quick save/load**
|
* **COLOR, zoom, tilt, GBC FX, and quick save/load**
|
||||||
* **UI that stays fixed while the overworld zooms**
|
* **UI that stays fixed while the overworld zooms**
|
||||||
|
|||||||
+119
-69
@@ -501,6 +501,9 @@ local function makeBattler(data, mon, isPlayer, save)
|
|||||||
badgeBoosts = badgeBoosts,
|
badgeBoosts = badgeBoosts,
|
||||||
statuses = data.statuses,
|
statuses = data.statuses,
|
||||||
shownHP = mon.hp, -- the HP the bar displays (UpdateHPBar drain)
|
shownHP = mon.hp, -- the HP the bar displays (UpdateHPBar drain)
|
||||||
|
-- the bar's own length in GetHPBarLength pixels; it trails shownHP
|
||||||
|
-- because UpdateHPBar_AnimateHPBar slides it one pixel at a time
|
||||||
|
shownPx = Timing.hpBarPixels(mon.hp, math.max(1, mon.stats.hp)),
|
||||||
-- HUD status label (DrawHUDsAndHPBars); mon.status can land mid-move
|
-- HUD status label (DrawHUDsAndHPBars); mon.status can land mid-move
|
||||||
-- while the tilemap still shows the prior condition until the next
|
-- while the tilemap still shows the prior condition until the next
|
||||||
-- post-action HUD refresh (core.asm after Execute*Move)
|
-- post-action HUD refresh (core.asm after Execute*Move)
|
||||||
@@ -563,10 +566,6 @@ local function markOwned(game, species)
|
|||||||
local dex = game.save.pokedex
|
local dex = game.save.pokedex
|
||||||
if dex then
|
if dex then
|
||||||
dex.seen[species] = true
|
dex.seen[species] = true
|
||||||
if not dex.owned[species] then
|
|
||||||
-- new dex page registered (SFX_DEX_PAGE_ADDED)
|
|
||||||
require("src.core.Sound").play(game.data, "Dex_Page_Added")
|
|
||||||
end
|
|
||||||
dex.owned[species] = true
|
dex.owned[species] = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -935,6 +934,17 @@ function BattleState:sayNextWaitSfx(text, sfx)
|
|||||||
table.insert(self.queue, self.nextInsert, { text = text, waitForLearningSfx = sfx })
|
table.insert(self.queue, self.nextInsert, { text = text, waitForLearningSfx = sfx })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- WaitForSoundToFinish for a sound an act() has already started: PlayCry
|
||||||
|
-- ends in `jp WaitForSoundToFinish` (home/pokemon.asm), so every cry in
|
||||||
|
-- Gen 1 holds whatever the ROM does next. `src` is the audio source, or a
|
||||||
|
-- getter the row calls at execution time when the source is only known then.
|
||||||
|
function BattleState:waitSfxNext(src)
|
||||||
|
self.nextInsert = (self.nextInsert or 0) + 1
|
||||||
|
table.insert(self.queue, self.nextInsert,
|
||||||
|
{ waitSound = type(src) == "function" and src
|
||||||
|
or function() return src end })
|
||||||
|
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
|
||||||
@@ -1022,22 +1032,39 @@ function BattleState:stepHPDrain()
|
|||||||
and b.shownHP >= b.drainFloor then
|
and b.shownHP >= b.drainFloor then
|
||||||
goal = b.drainFloor
|
goal = b.drainFloor
|
||||||
end
|
end
|
||||||
|
local maxHP = math.max(1, b.mon.stats.hp)
|
||||||
|
local playerSide = (b == self.player)
|
||||||
|
local targetPx = Timing.hpBarPixels(b.shownHP, maxHP)
|
||||||
|
if not b.shownPx then b.shownPx = targetPx end
|
||||||
if (b.drainHold or 0) > 0 then
|
if (b.drainHold or 0) > 0 then
|
||||||
b.drainHold = b.drainHold - 1
|
b.drainHold = b.drainHold - 1
|
||||||
busy = true
|
busy = true
|
||||||
|
elseif b.shownPx ~= targetPx then
|
||||||
|
-- .barAnimationLoop redraws the bar one pixel at a time, `ld c, 2 /
|
||||||
|
-- call DelayFrames` apiece (:141-148), so a single HP point that
|
||||||
|
-- spans several pixels still slides instead of jumping
|
||||||
|
b.shownPx = b.shownPx + ((b.shownPx > targetPx) and -1 or 1)
|
||||||
|
b.drainHold = Timing.HP_BAR_PIXEL_STEP - 1
|
||||||
|
busy = true
|
||||||
elseif b.shownHP ~= goal then
|
elseif b.shownHP ~= goal then
|
||||||
local maxHP = math.max(1, b.mon.stats.hp)
|
local spent = 0
|
||||||
local playerSide = (b == self.player)
|
|
||||||
local cost = 0
|
|
||||||
-- consume whole HP steps until this frame's budget is spent; on the
|
-- consume whole HP steps until this frame's budget is spent; on the
|
||||||
-- enemy HUD several free steps can land in the same frame
|
-- enemy HUD several free steps can land in the same frame
|
||||||
while b.shownHP ~= goal and cost < 1 do
|
repeat
|
||||||
local nextHP = b.shownHP + ((b.shownHP > goal) and -1 or 1)
|
b.shownHP = b.shownHP + ((b.shownHP > goal) and -1 or 1)
|
||||||
cost = cost + Timing.hpDrainStepFrames(b.shownHP, nextHP,
|
spent = spent + (playerSide and Timing.HP_BAR_HP_STEP or 0)
|
||||||
maxHP, playerSide)
|
targetPx = Timing.hpBarPixels(b.shownHP, maxHP)
|
||||||
b.shownHP = nextHP
|
until b.shownHP == goal or targetPx ~= b.shownPx or spent >= 1
|
||||||
|
if spent > 0 then
|
||||||
|
b.drainHold = spent - 1
|
||||||
|
elseif targetPx ~= b.shownPx then
|
||||||
|
-- the enemy HUD printed no number, so this frame is already the
|
||||||
|
-- first of the pixel step the crossing just asked for
|
||||||
|
b.shownPx = b.shownPx + ((b.shownPx > targetPx) and -1 or 1)
|
||||||
|
b.drainHold = Timing.HP_BAR_PIXEL_STEP - 1
|
||||||
|
else
|
||||||
|
b.drainHold = 0
|
||||||
end
|
end
|
||||||
b.drainHold = math.max(0, cost - 1)
|
|
||||||
b.draining = true
|
b.draining = true
|
||||||
busy = true
|
busy = true
|
||||||
elseif b.draining then
|
elseif b.draining then
|
||||||
@@ -1431,7 +1458,7 @@ end
|
|||||||
function BattleState:playEntranceCry(battler)
|
function BattleState:playEntranceCry(battler)
|
||||||
local mon = battler and battler.mon
|
local mon = battler and battler.mon
|
||||||
if not mon then return end
|
if not mon then return end
|
||||||
require("src.core.Sound").playCry(self.data, mon.species,
|
return require("src.core.Sound").playCry(self.data, mon.species,
|
||||||
mon.status == "SLP" and 37 or 11)
|
mon.status == "SLP" and 37 or 11)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1555,7 +1582,23 @@ function BattleState:enter()
|
|||||||
-- default stays opaque for every other battle and for older saves.
|
-- default stays opaque for every other battle and for older saves.
|
||||||
self.isOpaque = self:bgMode() ~= "world"
|
self.isOpaque = self:bgMode() ~= "world"
|
||||||
self.introSlide = Timing.BATTLE_SLIDE_IN_FRAMES
|
self.introSlide = Timing.BATTLE_SLIDE_IN_FRAMES
|
||||||
self.showEnemyTrainer = self.kind == "trainer" and self.trainerPic ~= nil
|
-- GetTrainerInformation .linkBattle (home/trainers2.asm:26-31): the link
|
||||||
|
-- foe's pic is RedPicFront whatever either save looks like, and
|
||||||
|
-- InitBattleCommon loads and tilemaps it at hlcoord 12,0 for every
|
||||||
|
-- wIsInBattle == 2 battle (core.asm:6681-6688), the link one included.
|
||||||
|
-- wEnemyMonSpecies2 is zeroed under it, so the pic palette is PAL_MEWMON
|
||||||
|
-- exactly as it is for a trainer.
|
||||||
|
if self.kind == "link" and not self.trainerPic then
|
||||||
|
local frontPath, frontTrueColor =
|
||||||
|
require("src.pokemon.Sprites").playerPath(self.data, "front",
|
||||||
|
{ kind = "battle", battle = self })
|
||||||
|
if frontPath and require("src.render.Assets").exists(frontPath) then
|
||||||
|
self.trainerPic = getImage(frontPath, namedPalette(self.data, "MEWMON"),
|
||||||
|
frontTrueColor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.showEnemyTrainer = (self.kind == "trainer" or self.kind == "link")
|
||||||
|
and self.trainerPic ~= nil
|
||||||
-- DrawAllPokeballs (common_text.asm:27) puts the party ball rows AND the
|
-- DrawAllPokeballs (common_text.asm:27) puts the party ball rows AND the
|
||||||
-- HUD corner/underline tiles under them (PlacePlayerHUDTiles /
|
-- HUD corner/underline tiles under them (PlacePlayerHUDTiles /
|
||||||
-- PlaceEnemyHUDTiles, draw_hud_pokeball_gfx.asm:119-165) on screen with
|
-- PlaceEnemyHUDTiles, draw_hud_pokeball_gfx.asm:119-165) on screen with
|
||||||
@@ -1587,13 +1630,13 @@ function BattleState:enter()
|
|||||||
-- a different point in each battle kind, so queue it per branch
|
-- a different point in each battle kind, so queue it per branch
|
||||||
local function queueEnemyCry()
|
local function queueEnemyCry()
|
||||||
self:act(function()
|
self:act(function()
|
||||||
self:playEntranceCry(self.enemy)
|
self:waitSfxNext(self:playEntranceCry(self.enemy))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
-- PrintBeginningBattleText (engine/battle/common_text.asm:10-19): a wild
|
-- PrintBeginningBattleText (engine/battle/common_text.asm:10-19): a wild
|
||||||
-- battle calls PlayCry BEFORE PrintText WildMonAppearedText, so the cry
|
-- battle calls PlayCry BEFORE PrintText WildMonAppearedText, and PlayCry
|
||||||
-- sounds with the "Wild X appeared!" box instead of waiting on the A
|
-- ends in WaitForSoundToFinish, so the cry runs to its end and only then
|
||||||
-- press that clears its `prompt` (#303). The Silph-Scope-less tower
|
-- does the "Wild X appeared!" box open (#303). The Silph-Scope-less tower
|
||||||
-- ghost gets no cry at all (common_text.asm:43-48), and neither does the
|
-- ghost gets no cry at all (common_text.asm:43-48), and neither does the
|
||||||
-- unveiled MAROWAK: .isMarowak never reaches PlayCry (#492).
|
-- unveiled MAROWAK: .isMarowak never reaches PlayCry (#492).
|
||||||
if self.kind ~= "trainer" and self.kind ~= "link"
|
if self.kind ~= "trainer" and self.kind ~= "link"
|
||||||
@@ -1611,7 +1654,10 @@ function BattleState:enter()
|
|||||||
-- The sfx is extracted as "Trainer_Appeared" (tools/rom_manifest.json
|
-- The sfx is extracted as "Trainer_Appeared" (tools/rom_manifest.json
|
||||||
-- sfxHeaders, bank 8 / $42bb -- the same header pokered names
|
-- sfxHeaders, bank 8 / $42bb -- the same header pokered names
|
||||||
-- SFX_Silph_Scope); nothing had ever played it.
|
-- SFX_Silph_Scope); nothing had ever played it.
|
||||||
if self.kind == "trainer" then
|
--
|
||||||
|
-- A link battle is wIsInBattle == 2, so PrintBeginningBattleText takes the
|
||||||
|
-- same .trainerBattle arm and owes the sfx too (common_text.asm:20-23).
|
||||||
|
if self.kind == "trainer" or self.kind == "link" then
|
||||||
self:act(function()
|
self:act(function()
|
||||||
self.introSfx = require("src.core.Sound").play(self.data,
|
self.introSfx = require("src.core.Sound").play(self.data,
|
||||||
"Trainer_Appeared")
|
"Trainer_Appeared")
|
||||||
@@ -1629,13 +1675,17 @@ function BattleState:enter()
|
|||||||
-- battle -- not on a switch, and not when the beaten trainer's pic
|
-- battle -- not on a switch, and not when the beaten trainer's pic
|
||||||
-- scrolls back in (#317, #282)
|
-- scrolls back in (#317, #282)
|
||||||
self:act(function() self.introBalls = nil end)
|
self:act(function() self.introBalls = nil end)
|
||||||
if self.kind == "trainer" then
|
if self.kind == "trainer" or self.kind == "link" then
|
||||||
-- EnemySendOutFirstMon (core.asm:1308-1310): SlideTrainerPicOffScreen
|
local foeName = self.trainer and self.trainer.name
|
||||||
-- walks the foe's pic off the RIGHT edge (hlcoord 18,0, a = 8 tiles,
|
or self.opponentName or Strings("FOE")
|
||||||
-- one tile every 2 frames) BEFORE TrainerSentOutText -- the pic does
|
if self.showEnemyTrainer then
|
||||||
-- not blink out under the text (#317)
|
-- EnemySendOutFirstMon (core.asm:1308-1310): SlideTrainerPicOffScreen
|
||||||
self:act(function() self:slidePic("foe", 0, 64, 4) end)
|
-- walks the foe's pic off the RIGHT edge (hlcoord 18,0, a = 8 tiles,
|
||||||
table.insert(self.queue, { wait = 16 })
|
-- one tile every 2 frames) BEFORE TrainerSentOutText -- the pic does
|
||||||
|
-- not blink out under the text (#317)
|
||||||
|
self:act(function() self:slidePic("foe", 0, 64, 4) end)
|
||||||
|
table.insert(self.queue, { wait = 16 })
|
||||||
|
end
|
||||||
self:act(function()
|
self:act(function()
|
||||||
self.showEnemyTrainer = false
|
self.showEnemyTrainer = false
|
||||||
-- the slot is EMPTY from here until AnimateSendingOutMon runs below:
|
-- the slot is EMPTY from here until AnimateSendingOutMon runs below:
|
||||||
@@ -1648,7 +1698,7 @@ function BattleState:enter()
|
|||||||
self.enemySendingOut = true
|
self.enemySendingOut = true
|
||||||
self:slidePic("foe")
|
self:slidePic("foe")
|
||||||
end)
|
end)
|
||||||
self:say(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
|
self:say(Strings("%s sent\nout %s!", foeName, self.enemy.name))
|
||||||
self:act(function()
|
self:act(function()
|
||||||
-- EnemySendOutFirstMon (core.asm:1421-1434): after the text the
|
-- EnemySendOutFirstMon (core.asm:1421-1434): after the text the
|
||||||
-- pic grows out of the ball (AnimateSendingOutMon), then the cry
|
-- pic grows out of the ball (AnimateSendingOutMon), then the cry
|
||||||
@@ -1656,18 +1706,6 @@ function BattleState:enter()
|
|||||||
self:startGrowIn(self.enemy)
|
self:startGrowIn(self.enemy)
|
||||||
end)
|
end)
|
||||||
queueEnemyCry()
|
queueEnemyCry()
|
||||||
elseif self.kind == "link" then
|
|
||||||
-- Colosseum has no foe trainer pic, but the enemy mon still grows
|
|
||||||
-- out of the ball after "X sent out Y!" (not the wild "already there"
|
|
||||||
-- intro that LinkBattle previously inherited from newWild).
|
|
||||||
self.enemySendingOut = true
|
|
||||||
self:say(Strings("%s sent\nout %s!", self.opponentName or Strings("FOE"),
|
|
||||||
self.enemy.name))
|
|
||||||
self:act(function()
|
|
||||||
self.enemySendingOut = false
|
|
||||||
self:startGrowIn(self.enemy)
|
|
||||||
end)
|
|
||||||
queueEnemyCry()
|
|
||||||
end
|
end
|
||||||
-- StartBattle .foundFirstAliveEnemyMon (core.asm:152-156): the `call nz`
|
-- StartBattle .foundFirstAliveEnemyMon (core.asm:152-156): the `call nz`
|
||||||
-- gates only EnemySendOutFirstMon -- the `ld c, 40 / call DelayFrames`
|
-- gates only EnemySendOutFirstMon -- the `ld c, 40 / call DelayFrames`
|
||||||
@@ -1698,7 +1736,7 @@ function BattleState:enter()
|
|||||||
-- SendOutMon (core.asm:1757-1762): after the poof the mon grows
|
-- SendOutMon (core.asm:1757-1762): after the poof the mon grows
|
||||||
-- out of the ball (AnimateSendingOutMon at hlcoord 4,11)
|
-- out of the ball (AnimateSendingOutMon at hlcoord 4,11)
|
||||||
self:startGrowIn(self.player)
|
self:startGrowIn(self.player)
|
||||||
self:playEntranceCry(self.player)
|
self:waitSfxNext(self:playEntranceCry(self.player))
|
||||||
end)
|
end)
|
||||||
self:markParticipant()
|
self:markParticipant()
|
||||||
end
|
end
|
||||||
@@ -1876,7 +1914,10 @@ function BattleState:update(dt)
|
|||||||
if self.phase == "menu" then
|
if self.phase == "menu" then
|
||||||
for _, b in ipairs({ self.player, self.enemy }) do
|
for _, b in ipairs({ self.player, self.enemy }) do
|
||||||
if b then
|
if b then
|
||||||
if b.shownHP then b.shownHP = b.mon.hp end
|
if b.shownHP then
|
||||||
|
b.shownHP = b.mon.hp
|
||||||
|
b.shownPx = Timing.hpBarPixels(b.mon.hp, math.max(1, b.mon.stats.hp))
|
||||||
|
end
|
||||||
b.drainFloor = nil
|
b.drainFloor = nil
|
||||||
b.shownStatus = b.mon.status
|
b.shownStatus = b.mon.status
|
||||||
end
|
end
|
||||||
@@ -2294,10 +2335,10 @@ function BattleState:oldManThrow()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:ballChain("TOSS_ANIM", true, 3, "POKE_BALL")
|
self:ballChain("TOSS_ANIM", true, 3, "POKE_BALL")
|
||||||
self:actNext(function()
|
-- ItemUseBallText05: text_far, sound_caught_mon, text_promptbutton --
|
||||||
require("src.core.Sound").play(self.data, "Caught_Mon")
|
-- the fanfare follows the caught text and holds the prompt
|
||||||
end)
|
self:sayNextWaitSfx(Strings("All right!\n%s was\ncaught!", self.enemy.name),
|
||||||
self:sayNext(Strings("All right!\n%s was\ncaught!", self.enemy.name))
|
function() return require("src.core.Sound").play(self.data, "Caught_Mon") end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2481,7 +2522,7 @@ function BattleState:resolveSwitch(newMon)
|
|||||||
self.sendingOut = false
|
self.sendingOut = false
|
||||||
-- SendOutMon (core.asm:1757-1762): poof, then the grow-in
|
-- SendOutMon (core.asm:1757-1762): poof, then the grow-in
|
||||||
self:startGrowIn(self.player)
|
self:startGrowIn(self.player)
|
||||||
self:playEntranceCry(self.player)
|
self:waitSfxNext(self:playEntranceCry(self.player))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
self:act(function()
|
self:act(function()
|
||||||
@@ -3793,7 +3834,7 @@ function BattleState:onFaint(battler)
|
|||||||
if battler.isPlayer then
|
if battler.isPlayer then
|
||||||
-- RemoveFaintedPlayerMon (core.asm:1040-1042): the player mon's
|
-- RemoveFaintedPlayerMon (core.asm:1040-1042): the player mon's
|
||||||
-- faint plays its ordinary species cry -- no Faint_Fall
|
-- faint plays its ordinary species cry -- no Faint_Fall
|
||||||
Sound.playCry(self.data, battler.mon.species)
|
self.faintCry = Sound.playCry(self.data, battler.mon.species)
|
||||||
elseif self.kind ~= "wild" then
|
elseif self.kind ~= "wild" then
|
||||||
-- FaintEnemyPokemon (core.asm:732-771): the enemy faint plays no
|
-- FaintEnemyPokemon (core.asm:732-771): the enemy faint plays no
|
||||||
-- species cry; trainer battles get SFX_FAINT_FALL, then SFX_FAINT_THUD
|
-- species cry; trainer battles get SFX_FAINT_FALL, then SFX_FAINT_THUD
|
||||||
@@ -3808,6 +3849,11 @@ function BattleState:onFaint(battler)
|
|||||||
end)
|
end)
|
||||||
self.nextInsert = (self.nextInsert or 0) + 1
|
self.nextInsert = (self.nextInsert or 0) + 1
|
||||||
table.insert(self.queue, self.nextInsert, { wait = Timing.FAINT_SLIDE })
|
table.insert(self.queue, self.nextInsert, { wait = Timing.FAINT_SLIDE })
|
||||||
|
if battler.isPlayer then
|
||||||
|
-- RemoveFaintedPlayerMon ends `call PlayCry / jp PrintText`, and PlayCry
|
||||||
|
-- is `jp WaitForSoundToFinish`, so "X fainted!" waits out the cry
|
||||||
|
self:waitSfxNext(function() return self.faintCry end)
|
||||||
|
end
|
||||||
if not battler.isPlayer and self.kind ~= "wild" then
|
if not battler.isPlayer and self.kind ~= "wild" then
|
||||||
-- FaintEnemyPokemon's SFX_FAINT_THUD lands as the slide does (after
|
-- FaintEnemyPokemon's SFX_FAINT_THUD lands as the slide does (after
|
||||||
-- Faint_Fall, before EnemyMonFaintedText)
|
-- Faint_Fall, before EnemyMonFaintedText)
|
||||||
@@ -3895,9 +3941,11 @@ function BattleState:awardExp()
|
|||||||
-- experience.asm:248 fires per grew-level text
|
-- experience.asm:248 fires per grew-level text
|
||||||
require("src.world.PikachuFollower")
|
require("src.world.PikachuFollower")
|
||||||
.modifyHappiness(game.save, "LEVELUP", mon)
|
.modifyHappiness(game.save, "LEVELUP", mon)
|
||||||
self:sayNext(Strings("%s grew\nto level %d!", name, lv))
|
-- GrewLevelText: text_far, sound_level_up, text_end (experience.asm:
|
||||||
|
-- 369-372); PrintStatsBox only runs once PrintText has returned
|
||||||
|
self:sayNextWaitSfx(Strings("%s grew\nto level %d!", name, lv),
|
||||||
|
function() return require("src.core.Sound").play(game.data, "Level_Up") end)
|
||||||
self:uiNext(function()
|
self:uiNext(function()
|
||||||
require("src.core.Sound").play(game.data, "Level_Up")
|
|
||||||
return StatBox.new(game, mon)
|
return StatBox.new(game, mon)
|
||||||
end)
|
end)
|
||||||
-- After PrintStatsBox, experience.asm reloads the active battler's
|
-- After PrintStatsBox, experience.asm reloads the active battler's
|
||||||
@@ -4042,7 +4090,7 @@ function BattleState:enemyMonFainted()
|
|||||||
self.enemySendingOut = false
|
self.enemySendingOut = false
|
||||||
self:startGrowIn(self.enemy)
|
self:startGrowIn(self.enemy)
|
||||||
self:actNext(function()
|
self:actNext(function()
|
||||||
self:playEntranceCry(self.enemy)
|
self:waitSfxNext(self:playEntranceCry(self.enemy))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -4081,7 +4129,7 @@ function BattleState:enemyMonFainted()
|
|||||||
self:actNext(function()
|
self:actNext(function()
|
||||||
self.sendingOut = false
|
self.sendingOut = false
|
||||||
self:startGrowIn(self.player)
|
self:startGrowIn(self.player)
|
||||||
self:playEntranceCry(self.player)
|
self:waitSfxNext(self:playEntranceCry(self.player))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
return
|
return
|
||||||
@@ -4277,7 +4325,7 @@ function BattleState:openReplacementMenu()
|
|||||||
self.sendingOut = false
|
self.sendingOut = false
|
||||||
-- SendOutMon (core.asm:1757-1762): poof, then the grow-in
|
-- SendOutMon (core.asm:1757-1762): poof, then the grow-in
|
||||||
self:startGrowIn(self.player)
|
self:startGrowIn(self.player)
|
||||||
self:playEntranceCry(self.player)
|
self:waitSfxNext(self:playEntranceCry(self.player))
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
@@ -4321,11 +4369,10 @@ function BattleState:safariAction(choice)
|
|||||||
-- above DoBallTossSpecialEffects's <= ULTRA_BALL check)
|
-- above DoBallTossSpecialEffects's <= ULTRA_BALL check)
|
||||||
self:ballChain(self:tossAnimFor("SAFARI_BALL"), caught, shakes, "SAFARI_BALL")
|
self:ballChain(self:tossAnimFor("SAFARI_BALL"), caught, shakes, "SAFARI_BALL")
|
||||||
if caught then
|
if caught then
|
||||||
-- ItemUseBallText05's sound_caught_mon: fanfare with the text
|
-- ItemUseBallText05: text_far, sound_caught_mon, text_promptbutton --
|
||||||
self:actNext(function()
|
-- the fanfare follows the caught text and holds the prompt
|
||||||
require("src.core.Sound").play(self.data, "Caught_Mon")
|
self:sayNextWaitSfx(Strings("All right!\n%s was\ncaught!", self.enemy.name),
|
||||||
end)
|
function() return require("src.core.Sound").play(self.data, "Caught_Mon") end)
|
||||||
self:sayNext(Strings("All right!\n%s was\ncaught!", self.enemy.name))
|
|
||||||
-- same ItemUseBall .captured flow as a regular ball
|
-- same ItemUseBall .captured flow as a regular ball
|
||||||
self:act(function() self:storeCaughtMon() end)
|
self:act(function() self:storeCaughtMon() end)
|
||||||
else
|
else
|
||||||
@@ -4553,8 +4600,12 @@ function BattleState:storeCaughtMon()
|
|||||||
markOwned(game, species)
|
markOwned(game, species)
|
||||||
stampOT(game.save, self.enemy.mon)
|
stampOT(game.save, self.enemy.mon)
|
||||||
if isNew then
|
if isNew then
|
||||||
-- _ItemUseBallText06 + ShowPokedexData
|
-- _ItemUseBallText06 + ShowPokedexData: text_far, sound_dex_page_added,
|
||||||
self:sayNext(Strings("New POKéDEX data\nwill be added for\n%s!", self.enemy.name))
|
-- text_promptbutton (item_effects.asm:624-629), so the fanfare follows
|
||||||
|
-- the box rather than firing when the dex bit is set
|
||||||
|
self:sayNextWaitSfx(
|
||||||
|
Strings("New POKéDEX data\nwill be added for\n%s!", self.enemy.name),
|
||||||
|
function() return require("src.core.Sound").play(self.data, "Dex_Page_Added") end)
|
||||||
self:uiNext(function()
|
self:uiNext(function()
|
||||||
return self:buildScreen("DexEntryMenu", species)
|
return self:buildScreen("DexEntryMenu", species)
|
||||||
end)
|
end)
|
||||||
@@ -4704,12 +4755,10 @@ function BattleState:throwBall(ball)
|
|||||||
self:ballChain(self:tossAnimFor(ball), caught, shakes, ball)
|
self:ballChain(self:tossAnimFor(ball), caught, shakes, ball)
|
||||||
if caught then
|
if caught then
|
||||||
-- ItemUseBallText05 carries sound_caught_mon (item_effects.asm:
|
-- ItemUseBallText05 carries sound_caught_mon (item_effects.asm:
|
||||||
-- 608-614): the fanfare sounds with the caught message, before
|
-- 608-614): text_far, sound_caught_mon, text_promptbutton -- the
|
||||||
-- the prompt, not after the text is dismissed
|
-- fanfare follows the caught message and holds the prompt
|
||||||
self:actNext(function()
|
self:sayNextWaitSfx(Strings("All right!\n%s was\ncaught!", self.enemy.name),
|
||||||
require("src.core.Sound").play(self.data, "Caught_Mon")
|
function() return require("src.core.Sound").play(self.data, "Caught_Mon") end)
|
||||||
end)
|
|
||||||
self:sayNext(Strings("All right!\n%s was\ncaught!", self.enemy.name))
|
|
||||||
self:act(function() self:storeCaughtMon() end)
|
self:act(function() self:storeCaughtMon() end)
|
||||||
else
|
else
|
||||||
self:sayNext(self:ballMissMessage(shakes))
|
self:sayNext(self:ballMissMessage(shakes))
|
||||||
@@ -5196,7 +5245,8 @@ function BattleState:sgbBattlePals()
|
|||||||
local function bar(b)
|
local function bar(b)
|
||||||
if not b then return pals.GREENBAR end
|
if not b then return pals.GREENBAR end
|
||||||
local hp = b.shownHP or b.mon.hp
|
local hp = b.shownHP or b.mon.hp
|
||||||
return pals[PaletteFX.barPalName(hp, b.mon.stats.hp)] or pals.GREENBAR
|
return pals[PaletteFX.barPalName(hp, b.mon.stats.hp, b.shownPx)]
|
||||||
|
or pals.GREENBAR
|
||||||
end
|
end
|
||||||
local function mon(b, placeholder)
|
local function mon(b, placeholder)
|
||||||
if placeholder or not b then return pals.MEWMON or pals.GREENBAR end
|
if placeholder or not b then return pals.MEWMON or pals.GREENBAR end
|
||||||
@@ -5594,7 +5644,7 @@ function BattleState:drawHUDs(slide)
|
|||||||
hudTile(0x73, 8, 16)
|
hudTile(0x73, 8, 16)
|
||||||
drawHPBar(barData, 2, 2,
|
drawHPBar(barData, 2, 2,
|
||||||
{ hp = shownHP(self.enemy), stats = self.enemy.mon.stats },
|
{ hp = shownHP(self.enemy), stats = self.enemy.mon.stats },
|
||||||
nil, grayFill)
|
nil, grayFill, nil, self.enemy.shownPx)
|
||||||
hudTile(0x74, 8, 24)
|
hudTile(0x74, 8, 24)
|
||||||
for i = 2, 9 do hudTile(0x76, i * 8, 24) end
|
for i = 2, 9 do hudTile(0x76, i * 8, 24) end
|
||||||
hudTile(0x78, 80, 24)
|
hudTile(0x78, 80, 24)
|
||||||
@@ -5675,7 +5725,7 @@ function BattleState:drawHUDs(slide)
|
|||||||
end
|
end
|
||||||
drawHPBar(barData, 10, 9,
|
drawHPBar(barData, 10, 9,
|
||||||
{ hp = shownHP(self.player), stats = self.player.mon.stats },
|
{ hp = shownHP(self.player), stats = self.player.mon.stats },
|
||||||
1, grayFill) -- wHPBarType 1: the $6D cap
|
1, grayFill, nil, self.player.shownPx) -- wHPBarType 1: the $6D cap
|
||||||
Font.draw(("%3d/%3d"):format(shownHP(self.player), self.player.mon.stats.hp), 88, 80)
|
Font.draw(("%3d/%3d"):format(shownHP(self.player), self.player.mon.stats.hp), 88, 80)
|
||||||
hudTile(0x73, 144, 80)
|
hudTile(0x73, 144, 80)
|
||||||
hudTile(0x77, 144, 88)
|
hudTile(0x77, 144, 88)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ local function drawStatusPanel(battle, battler, x, y, player)
|
|||||||
HudTiles.drawHPBar(battle.data, tx + 1, ty + 2, {
|
HudTiles.drawHPBar(battle.data, tx + 1, ty + 2, {
|
||||||
hp = shownHP(battler),
|
hp = shownHP(battler),
|
||||||
stats = battler.mon.stats,
|
stats = battler.mon.stats,
|
||||||
}, nil, monoMode(), tw - 5)
|
}, nil, monoMode(), tw - 5, battler.shownPx)
|
||||||
|
|
||||||
if player then
|
if player then
|
||||||
Font.draw(("%3d/%3d"):format(shownHP(battler), battler.mon.stats.hp),
|
Font.draw(("%3d/%3d"):format(shownHP(battler), battler.mon.stats.hp),
|
||||||
|
|||||||
@@ -334,6 +334,12 @@ function BattleCheckpoint.restore(game, checkpoint, copy)
|
|||||||
battle.player.mon.hp, battle.player.mon.status
|
battle.player.mon.hp, battle.player.mon.status
|
||||||
battle.enemy.shownHP, battle.enemy.shownStatus =
|
battle.enemy.shownHP, battle.enemy.shownStatus =
|
||||||
battle.enemy.mon.hp, battle.enemy.mon.status
|
battle.enemy.mon.hp, battle.enemy.mon.status
|
||||||
|
-- the bar's own pixel length is derived HUD state, not captured: it
|
||||||
|
-- settles with shownHP above (UpdateHPBar_AnimateHPBar's end position)
|
||||||
|
local Timing = require("src.core.Timing")
|
||||||
|
for _, b in ipairs({ battle.player, battle.enemy }) do
|
||||||
|
b.shownPx = Timing.hpBarPixels(b.mon.hp, math.max(1, b.mon.stats.hp))
|
||||||
|
end
|
||||||
|
|
||||||
local ow = game.overworld
|
local ow = game.overworld
|
||||||
if not ow or type(ow.restoreBattleContinuation) ~= "function"
|
if not ow or type(ow.restoreBattleContinuation) ~= "function"
|
||||||
|
|||||||
+53
-133
@@ -289,38 +289,6 @@ local function textField(imp, x, y, w, h, key, rawText, placeholder, focused, ac
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A square icon control: the header's gear and quit, and the game panel's
|
|
||||||
-- manage button. Inverts to a solid white fill when hot, the same signal
|
|
||||||
-- every other control here uses, and rounds to the shared control radius.
|
|
||||||
-- `image` draws a texture; `drawFn(x, y, size, hot)` draws a hand-rolled
|
|
||||||
-- glyph (the quit X, which ships no asset).
|
|
||||||
local function iconButton(imp, key, x, y, size, image, action, drawFn)
|
|
||||||
Kit._audit("control", x, y, size, size, key)
|
|
||||||
local focused = Kit.focusable(key, x, y, size, size)
|
|
||||||
local hot = focused or Kit.hover(x, y, size, size)
|
|
||||||
Theme.fillRounded(x, y, size, size, hot and PAL.ink or PAL.surface, 1)
|
|
||||||
Theme.strokeRounded(x, y, size, size, PAL.line,
|
|
||||||
hot and Theme.A.focus or Theme.A.hairline, 1)
|
|
||||||
if image then
|
|
||||||
local iw, ih = image:getDimensions()
|
|
||||||
local pad = math.floor(size * 0.24)
|
|
||||||
local s = math.min((size - 2 * pad) / iw, (size - 2 * pad) / ih)
|
|
||||||
if hot then love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
else love.graphics.setColor(1, 1, 1, 0.85) end
|
|
||||||
love.graphics.draw(image, Theme.snap(x + (size - iw * s) / 2),
|
|
||||||
Theme.snap(y + (size - ih * s) / 2), 0, s, s)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
elseif drawFn then
|
|
||||||
drawFn(x, y, size, hot)
|
|
||||||
end
|
|
||||||
if action and (Kit.press(x, y, size, size) or Kit._activateId == key) then
|
|
||||||
queueAction(imp, key, action)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The cartridge colour for a game, matching its tab in the header. Play
|
|
||||||
-- wears it, so "which game is this button going to boot" is answered before
|
|
||||||
-- the label is read. Unknown versions fall back to the commit green.
|
|
||||||
local CART_COLOR = {
|
local CART_COLOR = {
|
||||||
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
|
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
|
||||||
gold = PAL.railAmber,
|
gold = PAL.railAmber,
|
||||||
@@ -687,7 +655,7 @@ local function buildModScopeRow(imp, x, y, w, m)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar
|
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar
|
||||||
local profiles, activeProf = LauncherMods.getProfiles()
|
local _, activeProf = LauncherMods.getProfiles()
|
||||||
local isCompact = (w < math.floor(500 * m.s))
|
local isCompact = (w < math.floor(500 * m.s))
|
||||||
local nameText = tostring(activeProf or "Default")
|
local nameText = tostring(activeProf or "Default")
|
||||||
local profLabel = isCompact and nameText or Strings("Profile: %s", nameText)
|
local profLabel = isCompact and nameText or Strings("Profile: %s", nameText)
|
||||||
@@ -696,38 +664,30 @@ local function buildModScopeRow(imp, x, y, w, m)
|
|||||||
local gearX = x + w - gearW
|
local gearX = x + w - gearW
|
||||||
local profX = gearX - profW - math.floor(4 * m.s)
|
local profX = gearX - profW - math.floor(4 * m.s)
|
||||||
|
|
||||||
-- Tapping main profile button cycles to the next profile (styles match iconButton)
|
btn(imp, profX, y, profW, h, "mod-scope-profile", profLabel, {
|
||||||
Kit._audit("control", profX, y, profW, h, "mod-scope-profile")
|
face = "invert", font = "micro",
|
||||||
local focused = Kit.focusable("mod-scope-profile", profX, y, profW, h)
|
action = function()
|
||||||
local hot = focused or Kit.hover(profX, y, profW, h)
|
local list, cur = LauncherMods.getProfiles()
|
||||||
Theme.fillRounded(profX, y, profW, h, hot and PAL.ink or PAL.surface, 1)
|
local nextIdx = 1
|
||||||
Theme.strokeRounded(profX, y, profW, h, PAL.line,
|
for i, p in ipairs(list) do
|
||||||
hot and Theme.A.focus or Theme.A.hairline, 1)
|
if p.name == cur then
|
||||||
Kit.textCenterBold("micro", profLabel, profX,
|
nextIdx = (i % #list) + 1
|
||||||
y + (h - Kit.textHeight("micro")) / 2, profW,
|
break
|
||||||
hot and PAL.inverse or PAL.heading)
|
end
|
||||||
if Kit.press(profX, y, profW, h) or Kit._activateId == "mod-scope-profile" then
|
|
||||||
local nextIdx = 1
|
|
||||||
for i, p in ipairs(profiles) do
|
|
||||||
if p.name == activeProf then
|
|
||||||
nextIdx = (i % #profiles) + 1
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
local nextProf = list[nextIdx] and list[nextIdx].name
|
||||||
local nextProf = profiles[nextIdx] and profiles[nextIdx].name
|
if nextProf then
|
||||||
if nextProf then
|
|
||||||
queueAction(imp, "mod-scope-profile", function()
|
|
||||||
LauncherMods.applyProfile(nextProf)
|
LauncherMods.applyProfile(nextProf)
|
||||||
if imp._refreshMods then imp:_refreshMods() end
|
if imp._refreshMods then imp:_refreshMods() end
|
||||||
end)
|
end
|
||||||
end
|
end,
|
||||||
end
|
})
|
||||||
|
|
||||||
-- Tapping gear button opens the Profile Manager modal
|
|
||||||
imp._gearIcon = imp._gearIcon or (love and love.graphics and love.graphics.newImage and love.graphics.newImage("assets/launcher/gear.png"))
|
imp._gearIcon = imp._gearIcon or (love and love.graphics and love.graphics.newImage and love.graphics.newImage("assets/launcher/gear.png"))
|
||||||
iconButton(imp, "mod-profile-gear", gearX, y, gearW, imp._gearIcon, function()
|
btn(imp, gearX, y, gearW, gearW, "mod-profile-gear", "", {
|
||||||
imp._profilesPopup = true
|
face = "invert", image = imp._gearIcon,
|
||||||
end)
|
action = function() imp._profilesPopup = true end,
|
||||||
|
})
|
||||||
|
|
||||||
if #options >= 2 then
|
if #options >= 2 then
|
||||||
for _, opt in ipairs(options) do
|
for _, opt in ipairs(options) do
|
||||||
@@ -836,8 +796,7 @@ local function buildHeader(imp, m)
|
|||||||
local padX = math.floor(12 * m.s)
|
local padX = math.floor(12 * m.s)
|
||||||
local chipW = math.max(tw + 2 * padX, gear)
|
local chipW = math.max(tw + 2 * padX, gear)
|
||||||
local lx = m.x + m.pad
|
local lx = m.x + m.pad
|
||||||
Theme.fill(lx, by, chipW, gear, PAL.bg, 1)
|
Kit.card(lx, by, chipW, gear, "badge")
|
||||||
Theme.stroke(lx, by, chipW, gear, PAL.yellow, Theme.A.hover, 1)
|
|
||||||
local th = Kit.textHeight("small")
|
local th = Kit.textHeight("small")
|
||||||
Kit.text("small", label, lx + math.floor((chipW - tw) / 2),
|
Kit.text("small", label, lx + math.floor((chipW - tw) / 2),
|
||||||
by + math.floor((gear - th) / 2), PAL.yellow)
|
by + math.floor((gear - th) / 2), PAL.yellow)
|
||||||
@@ -856,17 +815,20 @@ local function buildHeader(imp, m)
|
|||||||
imp._gearIcon = imp._gearIcon
|
imp._gearIcon = imp._gearIcon
|
||||||
or love.graphics.newImage("assets/launcher/gear.png")
|
or love.graphics.newImage("assets/launcher/gear.png")
|
||||||
rx = rx - gear
|
rx = rx - gear
|
||||||
iconButton(imp, "gear", rx, by, gear, imp._gearIcon,
|
btn(imp, rx, by, gear, gear, "gear", "", {
|
||||||
function() imp:_openSettings() end)
|
face = "invert", image = imp._gearIcon,
|
||||||
|
action = function() imp:_openSettings() end,
|
||||||
|
})
|
||||||
|
|
||||||
-- Quit, top-right corner.
|
btn(imp, quitX, by, gear, gear, "quit", "", {
|
||||||
iconButton(imp, "quit", quitX, by, gear, nil,
|
face = "invert",
|
||||||
function() imp:_quitApp() end,
|
action = function() imp:_quitApp() end,
|
||||||
function(x, y, size, hot)
|
drawFn = function(x, y, w, h, hot)
|
||||||
local pad = math.floor(size * 0.32)
|
local pad = math.floor(w * 0.32)
|
||||||
drawCross(x + pad, y + pad, size - 2 * pad,
|
drawCross(x + pad, y + pad, w - 2 * pad,
|
||||||
hot and { 0, 0, 0, 1 } or { 1, 1, 1, 0.85 })
|
hot and { 0, 0, 0, 1 } or { 1, 1, 1, 0.85 })
|
||||||
end)
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
-- The self-update control lives in the FOOTER next to the BCG mark (small,
|
-- The self-update control lives in the FOOTER next to the BCG mark (small,
|
||||||
-- out of the wordmark's way -- it used to overlap the logo on a phone). It
|
-- out of the wordmark's way -- it used to overlap the logo on a phone). It
|
||||||
@@ -885,22 +847,16 @@ local function buildHeader(imp, m)
|
|||||||
-- bright cart gold; Gold (Gen 2) uses the deeper amber so the two do not
|
-- bright cart gold; Gold (Gen 2) uses the deeper amber so the two do not
|
||||||
-- collide.
|
-- collide.
|
||||||
local tabs = {
|
local tabs = {
|
||||||
{ id = "red", letter = "R", label = Strings("RED"), color = PAL.railRed },
|
{ id = "red", letter = "R", color = PAL.railRed },
|
||||||
{ id = "blue", letter = "B", label = Strings("BLUE"), color = PAL.railBlue },
|
{ id = "blue", letter = "B", color = PAL.railBlue },
|
||||||
{ id = "yellow", letter = "Y", label = Strings("YELLOW"), color = PAL.railGold },
|
{ id = "yellow", letter = "Y", color = PAL.railGold },
|
||||||
{ id = "gold", letter = "G", label = Strings("GOLD"), color = PAL.railAmber },
|
{ id = "gold", letter = "G", color = PAL.railAmber },
|
||||||
{ id = "mods", icon = imp._modsIcon, label = Strings("MODS") },
|
{ id = "mods", icon = imp._modsIcon },
|
||||||
{ id = "find", icon = imp._findIcon, label = Strings("FIND MODS") },
|
{ id = "find", icon = imp._findIcon },
|
||||||
}
|
}
|
||||||
local tabH = m.chip
|
local tabH = m.chip
|
||||||
local tx = m.x + m.pad
|
local tx = m.x + m.pad
|
||||||
local ty = y + math.floor(6 * m.s)
|
local ty = y + math.floor(6 * m.s)
|
||||||
-- Wrap the strip instead of running off the edge.
|
|
||||||
--
|
|
||||||
-- Six tabs used to escape a phone width when an active icon tab spelled its
|
|
||||||
-- name out (FIND MODS at 412x915). Game tabs (R/B/Y/G) stay glyph-only even
|
|
||||||
-- when active; only MODS / FIND MODS expand. Still wrap when the next tab
|
|
||||||
-- would not fit so the divider below moves with the row count.
|
|
||||||
local tabLeft = tx
|
local tabLeft = tx
|
||||||
local tabRight = m.x + m.w - m.pad
|
local tabRight = m.x + m.w - m.pad
|
||||||
local tabGap = math.floor(6 * m.s)
|
local tabGap = math.floor(6 * m.s)
|
||||||
@@ -908,51 +864,16 @@ local function buildHeader(imp, m)
|
|||||||
for _, t in ipairs(tabs) do
|
for _, t in ipairs(tabs) do
|
||||||
local active = imp.tab == t.id
|
local active = imp.tab == t.id
|
||||||
local key = "tab-" .. t.id
|
local key = "tab-" .. t.id
|
||||||
-- Cartridge tabs stay square (letter only). Icon tabs still expand to
|
local w = tabH
|
||||||
-- show MODS / FIND MODS when selected.
|
|
||||||
local expand = active and t.icon ~= nil
|
|
||||||
local labelW = expand and Kit.textWidth("tab", t.label) or 0
|
|
||||||
local w = expand and (tabH + math.floor(8 * m.s) + labelW + math.floor(12 * m.s))
|
|
||||||
or tabH
|
|
||||||
-- Never wrap the first tab of a row: if one tab alone is wider than the
|
|
||||||
-- panel there is nowhere better to put it, and wrapping would loop.
|
|
||||||
if tx > tabLeft and tx + w > tabRight then
|
if tx > tabLeft and tx + w > tabRight then
|
||||||
tx = tabLeft
|
tx = tabLeft
|
||||||
ty = ty + tabH + tabRowGap
|
ty = ty + tabH + tabRowGap
|
||||||
end
|
end
|
||||||
Kit._audit("control", tx, ty, w, tabH, key)
|
btn(imp, tx, ty, w, tabH, key, "", {
|
||||||
local focused = Kit.focusable(key, tx, ty, w, tabH)
|
face = "tab", font = "tab", color = t.color, active = active,
|
||||||
local hot = focused or Kit.hover(tx, ty, w, tabH)
|
image = t.icon, letter = t.letter,
|
||||||
local invert = active or hot
|
action = function() imp:_switchTab(t.id) end,
|
||||||
local tint = t.color or PAL.ink
|
})
|
||||||
Theme.fillRounded(tx, ty, w, tabH, invert and tint or PAL.surface, 1)
|
|
||||||
if not invert then
|
|
||||||
Theme.strokeRounded(tx, ty, w, tabH, tint,
|
|
||||||
t.color and Theme.A.hover or Theme.A.hairline, 1)
|
|
||||||
end
|
|
||||||
-- Ink on a filled tab must contrast with THAT fill: black on the light
|
|
||||||
-- red/blue/gold cartridge colours, which are all high-luminance.
|
|
||||||
local ink = invert and PAL.inverse or (t.color or PAL.text)
|
|
||||||
if t.icon then
|
|
||||||
local iw, ih = t.icon:getDimensions()
|
|
||||||
local pad = math.floor(tabH * 0.24)
|
|
||||||
local s = math.min((tabH - 2 * pad) / iw, (tabH - 2 * pad) / ih)
|
|
||||||
if invert then love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
else love.graphics.setColor(1, 1, 1, 0.9) end
|
|
||||||
love.graphics.draw(t.icon, Theme.snap(tx + (tabH - iw * s) / 2),
|
|
||||||
Theme.snap(ty + (tabH - ih * s) / 2), 0, s, s)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
else
|
|
||||||
Kit.textCenter("tab", t.letter, tx,
|
|
||||||
ty + (tabH - Kit.textHeight("tab")) / 2, tabH, ink)
|
|
||||||
end
|
|
||||||
if expand then
|
|
||||||
Kit.text("tab", t.label, tx + tabH + math.floor(4 * m.s),
|
|
||||||
ty + (tabH - Kit.textHeight("tab")) / 2, ink)
|
|
||||||
end
|
|
||||||
if Kit.press(tx, ty, w, tabH) or Kit._activateId == key then
|
|
||||||
queueAction(imp, key, function() imp:_switchTab(t.id) end)
|
|
||||||
end
|
|
||||||
tx = tx + w + tabGap
|
tx = tx + w + tabGap
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1454,8 +1375,10 @@ local function buildGamePanel(imp, x, y, w, availH, m, version)
|
|||||||
version, gameName, function() imp:play(version, true) end)
|
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, mgW,
|
btn(imp, lx + lw - mgW, ly, mgW, mgW, "manage-" .. version, "", {
|
||||||
imp._gearIcon, function() imp._gameManage = version end)
|
face = "invert", image = imp._gearIcon,
|
||||||
|
action = function() imp._gameManage = version end,
|
||||||
|
})
|
||||||
ly = ly + playH + gap
|
ly = ly + playH + gap
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1743,8 +1666,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
local focused = Kit.focusable(rowKey, x, ry, w, rowH)
|
local focused = Kit.focusable(rowKey, x, ry, w, rowH)
|
||||||
local hot = focused or Kit.hover(x, ry, w, rowH)
|
local hot = focused or Kit.hover(x, ry, w, rowH)
|
||||||
if isFullyDisabled then
|
if isFullyDisabled then
|
||||||
Theme.fillRounded(x, ry, w, rowH, PAL.bg, 0.8, Theme.cardRadius())
|
Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted")
|
||||||
Theme.strokeRounded(x, ry, w, rowH, PAL.muted, hot and Theme.A.hover or 0.25, 1, Theme.cardRadius())
|
|
||||||
else
|
else
|
||||||
Kit.card(x, ry, w, rowH, hot)
|
Kit.card(x, ry, w, rowH, hot)
|
||||||
end
|
end
|
||||||
@@ -3020,8 +2942,7 @@ local function buildSettingsModal(imp, m)
|
|||||||
else
|
else
|
||||||
local row = item.row
|
local row = item.row
|
||||||
local key = "set-" .. i
|
local key = "set-" .. i
|
||||||
Theme.strokeRounded(px + pad, ry, pw - 2 * pad, rowH, PAL.line,
|
Kit.card(px + pad, ry, pw - 2 * pad, rowH, "hairline")
|
||||||
Theme.A.hairline, 1)
|
|
||||||
local ix = px + pad + math.floor(12 * m.s)
|
local ix = px + pad + math.floor(12 * m.s)
|
||||||
-- Where the label prints, and where the control band starts. Stacked:
|
-- Where the label prints, and where the control band starts. Stacked:
|
||||||
-- label on its own full-width line, controls on the line below it.
|
-- label on its own full-width line, controls on the line below it.
|
||||||
@@ -3136,8 +3057,7 @@ local function buildDepResolverModal(imp, m)
|
|||||||
cy = cy + Kit.textHeight("small") + math.floor(10 * m.s)
|
cy = cy + Kit.textHeight("small") + math.floor(10 * m.s)
|
||||||
|
|
||||||
-- Security Disclaimer Banner Callout Card
|
-- Security Disclaimer Banner Callout Card
|
||||||
Theme.fillRounded(px + pad, cy, pw - 2 * pad, warnH, PAL.rowBg, 1, Theme.radius())
|
Kit.card(px + pad, cy, pw - 2 * pad, warnH, "warn")
|
||||||
Theme.strokeRounded(px + pad, cy, pw - 2 * pad, warnH, PAL.yellow, Theme.A.hover, 1, Theme.radius())
|
|
||||||
local warnMsg = Strings("Caution: Only pull dependencies from sources you trust.\nVerify source repositories before fetching.")
|
local warnMsg = Strings("Caution: Only pull dependencies from sources you trust.\nVerify source repositories before fetching.")
|
||||||
Kit.text("micro", warnMsg, px + pad + math.floor(12 * m.s), cy + math.floor(5 * m.s), PAL.yellow)
|
Kit.text("micro", warnMsg, px + pad + math.floor(12 * m.s), cy + math.floor(5 * m.s), PAL.yellow)
|
||||||
cy = cy + warnH + math.floor(12 * m.s)
|
cy = cy + warnH + math.floor(12 * m.s)
|
||||||
@@ -3173,7 +3093,7 @@ local function buildDepResolverModal(imp, m)
|
|||||||
if ry + rowH >= cy and ry <= cy + listH then
|
if ry + rowH >= cy and ry <= cy + listH then
|
||||||
-- Item Card Fill & Stroke (matching launcher card interiors & radius)
|
-- Item Card Fill & Stroke (matching launcher card interiors & radius)
|
||||||
local hot = Kit.hover(px + pad, ry, pw - 2 * pad, rowH)
|
local hot = Kit.hover(px + pad, ry, pw - 2 * pad, rowH)
|
||||||
Theme.row(px + pad, ry, pw - 2 * pad, rowH, hot and "hover" or "normal")
|
Kit.card(px + pad, ry, pw - 2 * pad, rowH, hot and "rowHover" or "row")
|
||||||
|
|
||||||
local ix = px + pad + math.floor(12 * m.s)
|
local ix = px + pad + math.floor(12 * m.s)
|
||||||
local innerW = pw - 2 * pad - math.floor(24 * m.s)
|
local innerW = pw - 2 * pad - math.floor(24 * m.s)
|
||||||
|
|||||||
+120
-11
@@ -9,15 +9,16 @@
|
|||||||
-- cable pull.
|
-- cable pull.
|
||||||
--
|
--
|
||||||
-- Cable rules: no experience, no money, no items; either side may RUN
|
-- Cable rules: no experience, no money, no items; either side may RUN
|
||||||
-- (a draw); a fainted mon is auto-replaced by the next healthy party
|
-- (a draw); a fainted mon is replaced from the party menu and the chosen
|
||||||
-- member (the original prompts; documented divergence). Badge stat
|
-- slot rides the wire, the way ChooseNextMon hands it to
|
||||||
-- boosts don't apply on either side (divergence: Gen 1 famously kept
|
-- LinkBattleExchangeData. Badge stat boosts don't apply on either side
|
||||||
-- them in link battles).
|
-- (divergence: Gen 1 famously kept them in link battles).
|
||||||
|
|
||||||
local Fingerprint = require("src.link.Fingerprint")
|
local Fingerprint = require("src.link.Fingerprint")
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
local Handshake = require("src.link.Handshake")
|
local Handshake = require("src.link.Handshake")
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
|
local Party = require("src.pokemon.Party")
|
||||||
local Protocol = require("src.link.Protocol")
|
local Protocol = require("src.link.Protocol")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local TurnOrder = require("src.battle.TurnOrder")
|
local TurnOrder = require("src.battle.TurnOrder")
|
||||||
@@ -257,7 +258,9 @@ function LinkBattle.new(game, net, opts)
|
|||||||
self.enemyParty = theirParty
|
self.enemyParty = theirParty
|
||||||
self.playerParty = myParty -- intro ball row uses the clamped copies
|
self.playerParty = myParty -- intro ball row uses the clamped copies
|
||||||
self.opponentName = theirName
|
self.opponentName = theirName
|
||||||
self.introText = Strings("%s wants\nto battle!", theirName)
|
-- _TrainerWantsToFightText (data/text/text_2.asm:1257): wIsInBattle == 2
|
||||||
|
-- takes PrintBeginningBattleText's .trainerBattle arm, link included
|
||||||
|
self.introText = Strings("%s wants\nto fight!", theirName)
|
||||||
self.remoteHashes = {}
|
self.remoteHashes = {}
|
||||||
self.localHashes = {}
|
self.localHashes = {}
|
||||||
self.remoteParts = {}
|
self.remoteParts = {}
|
||||||
@@ -327,6 +330,68 @@ function LinkBattle.new(game, net, opts)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ChooseNextMon (engine/battle/core.asm:1086-1103): the replacement after
|
||||||
|
-- a faint is a free party-menu pick in a link battle too -- DisplayPartyMenu
|
||||||
|
-- runs first, a fainted pick or a cancel goes back to it
|
||||||
|
-- (.goBackToPartyMenu), and only the chosen slot rides
|
||||||
|
-- LinkBattleExchangeData.
|
||||||
|
local function chooseReplacement(s)
|
||||||
|
s.linkReplacement = nil
|
||||||
|
s:uiNext(function()
|
||||||
|
return s:buildScreen("PartyMenu", {
|
||||||
|
battle = s,
|
||||||
|
party = myParty,
|
||||||
|
forceSwitch = true,
|
||||||
|
onSwitch = function(mon)
|
||||||
|
if mon.hp > 0 then s.linkReplacement = mon end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
s:actNext(function()
|
||||||
|
local mon = s.linkReplacement
|
||||||
|
s.linkReplacement = nil
|
||||||
|
if s.result then return end
|
||||||
|
if not mon then
|
||||||
|
chooseReplacement(s)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for i, m in ipairs(myParty) do
|
||||||
|
if m == mon then send({ type = "replace", index = i }) break end
|
||||||
|
end
|
||||||
|
sendOutPlayer(s, mon)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ReplaceFaintedEnemyMon (core.asm:892-905) reads the peer's slot back out
|
||||||
|
-- of the exchange, and EnemySendOutFirstMon (core.asm:1315-1320) decodes it
|
||||||
|
-- as wSerialExchangeNybbleReceiveData - 4. HandlePlayerMonFainted
|
||||||
|
-- (core.asm:989-996) always runs ChooseNextMon BEFORE that read, so on a
|
||||||
|
-- double faint both machines commit their own slot before they block on the
|
||||||
|
-- peer's; our queue can reach the enemy's handler first, so the wait
|
||||||
|
-- pre-empts itself with the local pick rather than deadlocking on a message
|
||||||
|
-- neither side is going to send.
|
||||||
|
local function awaitReplacement(s)
|
||||||
|
s:actNext(function()
|
||||||
|
if s.result then return end
|
||||||
|
if s.player.mon.hp <= 0 and not s.linkChoiceQueued
|
||||||
|
and Party.firstHealthy(myParty) then
|
||||||
|
s.linkChoiceQueued = true
|
||||||
|
chooseReplacement(s)
|
||||||
|
awaitReplacement(s)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local idx = s.remoteReplace
|
||||||
|
if not idx then
|
||||||
|
awaitReplacement(s)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
s.remoteReplace = nil
|
||||||
|
local mon = theirParty[idx]
|
||||||
|
if not mon or mon.hp <= 0 then mon = Party.firstHealthy(theirParty) end
|
||||||
|
sendOutEnemy(s, mon)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- decode a remote action message against the enemy battler
|
-- decode a remote action message against the enemy battler
|
||||||
local function decodeTheirAction(s, msg)
|
local function decodeTheirAction(s, msg)
|
||||||
return decodeWireAction(s, msg, s.enemy)
|
return decodeWireAction(s, msg, s.enemy)
|
||||||
@@ -540,12 +605,17 @@ function LinkBattle.new(game, net, opts)
|
|||||||
submit(s, { type = "action", kind = "run" }, nil)
|
submit(s, { type = "action", kind = "run" }, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- fainted mons auto-replace with the next healthy teammate, in party
|
-- linkChoiceQueued: awaitReplacement already pre-empted itself with this
|
||||||
-- order, identically on both machines
|
-- side's ChooseNextMon and put the slot on the wire, so the handler the
|
||||||
|
-- faint queued has nothing left to do but drop the flag
|
||||||
self.playerMonFainted = function(s)
|
self.playerMonFainted = function(s)
|
||||||
|
if s.linkChoiceQueued then
|
||||||
|
s.linkChoiceQueued = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
for _, mon in ipairs(myParty) do
|
for _, mon in ipairs(myParty) do
|
||||||
if mon.hp > 0 then
|
if mon.hp > 0 then
|
||||||
s:act(function() sendOutPlayer(s, mon) end)
|
if not s.result then chooseReplacement(s) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -558,7 +628,7 @@ function LinkBattle.new(game, net, opts)
|
|||||||
self.enemyMonFainted = function(s)
|
self.enemyMonFainted = function(s)
|
||||||
for _, mon in ipairs(theirParty) do
|
for _, mon in ipairs(theirParty) do
|
||||||
if mon.hp > 0 then
|
if mon.hp > 0 then
|
||||||
s:act(function() sendOutEnemy(s, mon) end)
|
if not s.result then awaitReplacement(s) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -579,6 +649,9 @@ function LinkBattle.new(game, net, opts)
|
|||||||
s.remoteHashes[msg.turn or 0] = msg.value
|
s.remoteHashes[msg.turn or 0] = msg.value
|
||||||
s.remoteParts[msg.turn or 0] = msg.parts
|
s.remoteParts[msg.turn or 0] = msg.parts
|
||||||
checkHashes(s)
|
checkHashes(s)
|
||||||
|
elseif msg.type == "replace" then
|
||||||
|
local idx = math.floor(tonumber(msg.index) or 1)
|
||||||
|
s.remoteReplace = math.max(1, math.min(#theirParty, idx))
|
||||||
elseif msg.type == "bye" then
|
elseif msg.type == "bye" then
|
||||||
-- only a draw if our own simulation hasn't already decided
|
-- only a draw if our own simulation hasn't already decided
|
||||||
-- (the winner's bye can arrive while we're still animating)
|
-- (the winner's bye can arrive while we're still animating)
|
||||||
@@ -770,6 +843,34 @@ function LinkBattle.newSpectator(game, net, opts)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function awaitHostReplacement(s)
|
||||||
|
s:actNext(function()
|
||||||
|
if s.result then return end
|
||||||
|
local idx = table.remove(s.hostReplace, 1)
|
||||||
|
if not idx then
|
||||||
|
awaitHostReplacement(s)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local mon = hostParty[idx]
|
||||||
|
if not mon or mon.hp <= 0 then mon = Party.firstHealthy(hostParty) end
|
||||||
|
sendOutHost(s, mon)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function awaitGuestReplacement(s)
|
||||||
|
s:actNext(function()
|
||||||
|
if s.result then return end
|
||||||
|
local idx = table.remove(s.guestReplace, 1)
|
||||||
|
if not idx then
|
||||||
|
awaitGuestReplacement(s)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local mon = guestParty[idx]
|
||||||
|
if not mon or mon.hp <= 0 then mon = Party.firstHealthy(guestParty) end
|
||||||
|
sendOutGuest(s, mon)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local function resolveSpecTurn(s, hostMsg, guestMsg)
|
local function resolveSpecTurn(s, hostMsg, guestMsg)
|
||||||
if hostMsg.kind == "run" or guestMsg.kind == "run" then
|
if hostMsg.kind == "run" or guestMsg.kind == "run" then
|
||||||
endSpectate(s, "The match ended.")
|
endSpectate(s, "The match ended.")
|
||||||
@@ -840,7 +941,7 @@ function LinkBattle.newSpectator(game, net, opts)
|
|||||||
self.playerMonFainted = function(s)
|
self.playerMonFainted = function(s)
|
||||||
for _, mon in ipairs(hostParty) do
|
for _, mon in ipairs(hostParty) do
|
||||||
if mon.hp > 0 then
|
if mon.hp > 0 then
|
||||||
s:act(function() sendOutHost(s, mon) end)
|
if not s.result then awaitHostReplacement(s) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -852,7 +953,7 @@ function LinkBattle.newSpectator(game, net, opts)
|
|||||||
self.enemyMonFainted = function(s)
|
self.enemyMonFainted = function(s)
|
||||||
for _, mon in ipairs(guestParty) do
|
for _, mon in ipairs(guestParty) do
|
||||||
if mon.hp > 0 then
|
if mon.hp > 0 then
|
||||||
s:act(function() sendOutGuest(s, mon) end)
|
if not s.result then awaitGuestReplacement(s) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -862,6 +963,7 @@ function LinkBattle.newSpectator(game, net, opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
self.hostMsg, self.guestMsg = nil, nil
|
self.hostMsg, self.guestMsg = nil, nil
|
||||||
|
self.hostReplace, self.guestReplace = {}, {}
|
||||||
local baseUpdate = self.update
|
local baseUpdate = self.update
|
||||||
self.update = function(s, dt)
|
self.update = function(s, dt)
|
||||||
net:update()
|
net:update()
|
||||||
@@ -875,6 +977,13 @@ function LinkBattle.newSpectator(game, net, opts)
|
|||||||
s.hostMsg, s.guestMsg = nil, nil
|
s.hostMsg, s.guestMsg = nil, nil
|
||||||
resolveSpecTurn(s, h, g)
|
resolveSpecTurn(s, h, g)
|
||||||
end
|
end
|
||||||
|
elseif inner.type == "replace" then
|
||||||
|
local idx = math.floor(tonumber(inner.index) or 1)
|
||||||
|
if msg.side == "host" then
|
||||||
|
table.insert(s.hostReplace, math.max(1, math.min(#hostParty, idx)))
|
||||||
|
else
|
||||||
|
table.insert(s.guestReplace, math.max(1, math.min(#guestParty, idx)))
|
||||||
|
end
|
||||||
elseif inner.type == "bye" or inner.type == "forfeit" then
|
elseif inner.type == "bye" or inner.type == "forfeit" then
|
||||||
if not s.result then endSpectate(s, "The match ended.") end
|
if not s.result then endSpectate(s, "The match ended.") end
|
||||||
end
|
end
|
||||||
|
|||||||
+87
-17
@@ -19,6 +19,7 @@ LinkState.__index = LinkState
|
|||||||
LinkState.isOpaque = true
|
LinkState.isOpaque = true
|
||||||
|
|
||||||
local CURSOR = 0xED
|
local CURSOR = 0xED
|
||||||
|
local CURSOR_HOLLOW = 0xEC
|
||||||
local ANY = "ANY" -- sentinel: a leading nil array entry breaks ipairs under
|
local ANY = "ANY" -- sentinel: a leading nil array entry breaks ipairs under
|
||||||
-- LuaJIT even though # still reports the full size, so
|
-- LuaJIT even though # still reports the full size, so
|
||||||
-- the level picker cycles this string instead of nil,
|
-- the level picker cycles this string instead of nil,
|
||||||
@@ -114,6 +115,7 @@ end
|
|||||||
function LinkState:exitWith(message, reason)
|
function LinkState:exitWith(message, reason)
|
||||||
DiscordPresence.setJoinCode(nil)
|
DiscordPresence.setJoinCode(nil)
|
||||||
self.game.linkSession = nil -- back to the player's own GAME SPEED
|
self.game.linkSession = nil -- back to the player's own GAME SPEED
|
||||||
|
if self.game.linkNet == self.net then self.game.linkNet = nil end
|
||||||
Runtime.emit("link.ended", { reason = reason or (message and "error" or "bye") })
|
Runtime.emit("link.ended", { reason = reason or (message and "error" or "bye") })
|
||||||
if self.net then self.net:close() end
|
if self.net then self.net:close() end
|
||||||
self.game.stack:pop()
|
self.game.stack:pop()
|
||||||
@@ -519,6 +521,9 @@ function LinkState:startMode(mode, isHost)
|
|||||||
})
|
})
|
||||||
self.net:send(self.trade:opening())
|
self.net:send(self.trade:opening())
|
||||||
self.index = 1
|
self.index = 1
|
||||||
|
self.theirIndex = 1
|
||||||
|
self.side = "mine"
|
||||||
|
self.pickChoice = nil
|
||||||
else
|
else
|
||||||
self.stage = "battleWait"
|
self.stage = "battleWait"
|
||||||
-- the host deals the shared RNG seed for the lockstep simulation
|
-- the host deals the shared RNG seed for the lockstep simulation
|
||||||
@@ -536,7 +541,14 @@ end
|
|||||||
-- trade flow
|
-- trade flow
|
||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function LinkState:openStats(mon)
|
||||||
|
if not mon then return end
|
||||||
|
self.game.linkNet = self.net
|
||||||
|
Screens.push(self.game, "SummaryMenu", mon)
|
||||||
|
end
|
||||||
|
|
||||||
function LinkState:updateTrade(input)
|
function LinkState:updateTrade(input)
|
||||||
|
if self.game.linkNet == self.net then self.game.linkNet = nil end
|
||||||
for _, msg in ipairs(self.net:poll()) do
|
for _, msg in ipairs(self.net:poll()) do
|
||||||
local reply = self.trade:handle(msg)
|
local reply = self.trade:handle(msg)
|
||||||
if reply then self.net:send(reply) end
|
if reply then self.net:send(reply) end
|
||||||
@@ -591,10 +603,51 @@ function LinkState:updateTrade(input)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if t.stage == "picking" and input:wasPressed("up") then
|
-- pokered engine/link/cable_club.asm TradeCenter_SelectMon: A on one of
|
||||||
self.index = math.max(1, self.index - 1)
|
-- your own mons opens the "STATS TRADE" row (.displayStatsTradeMenu)
|
||||||
|
-- and only TRADE commits the pick, while the enemy list carries its own
|
||||||
|
-- cursor whose A shows that mon's status pages (.displayEnemyMonStats).
|
||||||
|
-- The cart's enemy path sets hl but never wMonDataLocation, the way the
|
||||||
|
-- battle menu's STATS (engine/battle/core.asm) does, so LoadMonData_ reads
|
||||||
|
-- the player's party and it draws YOUR mon at that slot -- an omission,
|
||||||
|
-- not behaviour, so we show the peer's mon.
|
||||||
|
if t.stage == "picking" and self.pickChoice then
|
||||||
|
if input:wasPressed("left") then
|
||||||
|
self.pickChoice = 1
|
||||||
|
elseif input:wasPressed("right") then
|
||||||
|
self.pickChoice = 2
|
||||||
|
elseif input:wasPressed("b") then
|
||||||
|
self.pickChoice = nil -- .cancelPlayerMonChoice: back to the list, not
|
||||||
|
-- out of the trade
|
||||||
|
elseif input:wasPressed("a") then
|
||||||
|
if self.pickChoice == 1 then
|
||||||
|
self.pickChoice = nil
|
||||||
|
self:openStats(self.game.save.party[self.index])
|
||||||
|
elseif t:canPick(self.index) then
|
||||||
|
self.pickChoice = nil
|
||||||
|
self.side = "mine"
|
||||||
|
self.net:send(t:pick(self.index))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif t.stage == "picking" and input:wasPressed("up") then
|
||||||
|
if self.side == "theirs" then
|
||||||
|
self.theirIndex = math.max(1, self.theirIndex - 1)
|
||||||
|
else
|
||||||
|
self.index = math.max(1, self.index - 1)
|
||||||
|
end
|
||||||
elseif t.stage == "picking" and input:wasPressed("down") then
|
elseif t.stage == "picking" and input:wasPressed("down") then
|
||||||
self.index = math.min(#self.game.save.party, self.index + 1)
|
if self.side == "theirs" then
|
||||||
|
self.theirIndex = math.min(#(t.theirParty or {}), self.theirIndex + 1)
|
||||||
|
else
|
||||||
|
self.index = math.min(#self.game.save.party, self.index + 1)
|
||||||
|
end
|
||||||
|
elseif t.stage == "picking" and input:wasPressed("right") then
|
||||||
|
if t.theirParty and #t.theirParty > 0 then
|
||||||
|
self.side = "theirs"
|
||||||
|
self.theirIndex = math.min(self.theirIndex, #t.theirParty)
|
||||||
|
end
|
||||||
|
elseif t.stage == "picking" and input:wasPressed("left") then
|
||||||
|
self.side = "mine"
|
||||||
elseif self.confirmed == nil and input:wasPressed("b") then
|
elseif self.confirmed == nil and input:wasPressed("b") then
|
||||||
-- once confirm=true has been sent to the peer, backing out here
|
-- once confirm=true has been sent to the peer, backing out here
|
||||||
-- would desync the two sides (the peer may already be committing
|
-- would desync the two sides (the peer may already be committing
|
||||||
@@ -603,8 +656,10 @@ function LinkState:updateTrade(input)
|
|||||||
self.net:send({ type = "bye" })
|
self.net:send({ type = "bye" })
|
||||||
self:exitWith(Strings("The trade was\ncancelled."))
|
self:exitWith(Strings("The trade was\ncancelled."))
|
||||||
elseif t.stage == "picking" and input:wasPressed("a") then
|
elseif t.stage == "picking" and input:wasPressed("a") then
|
||||||
if t:canPick(self.index) then
|
if self.side == "theirs" then
|
||||||
self.net:send(t:pick(self.index))
|
self:openStats((t.theirParty or {})[self.theirIndex])
|
||||||
|
else
|
||||||
|
self.pickChoice = 1
|
||||||
end
|
end
|
||||||
elseif t.stage == "confirming" and self.confirmed == nil then
|
elseif t.stage == "confirming" and self.confirmed == nil then
|
||||||
if input:wasPressed("a") then
|
if input:wasPressed("a") then
|
||||||
@@ -738,25 +793,40 @@ function LinkState:draw()
|
|||||||
local label = (mon.nickname or def.name):sub(1, 8)
|
local label = (mon.nickname or def.name):sub(1, 8)
|
||||||
if not t:canPick(i) then label = label .. "X" end
|
if not t:canPick(i) then label = label .. "X" end
|
||||||
Font.draw(label, 16, 20 + i * 12)
|
Font.draw(label, 16, 20 + i * 12)
|
||||||
if i == self.index then Font.drawCode(CURSOR, 8, 20 + i * 12) end
|
if i == self.index and self.side ~= "theirs" then
|
||||||
|
Font.drawCode(CURSOR, 8, 20 + i * 12)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Font.draw(Strings("THEIRS"), 84, 20)
|
Font.draw(Strings("THEIRS"), 84, 20)
|
||||||
for i, mon in ipairs(t.theirParty or {}) do
|
for i, mon in ipairs(t.theirParty or {}) do
|
||||||
local def = self.game.data.pokemon[mon.species]
|
local def = self.game.data.pokemon[mon.species]
|
||||||
Font.draw((mon.nickname or def.name):sub(1, 8), 92, 20 + i * 12)
|
Font.draw((mon.nickname or def.name):sub(1, 8), 92, 20 + i * 12)
|
||||||
if t.theirPick == i then Font.drawCode(CURSOR, 84, 20 + i * 12) end
|
if self.side == "theirs" and i == self.theirIndex then
|
||||||
|
Font.drawCode(CURSOR, 84, 20 + i * 12)
|
||||||
|
elseif t.theirPick == i then
|
||||||
|
Font.drawCode(CURSOR_HOLLOW, 84, 20 + i * 12)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
local hint
|
if self.pickChoice then
|
||||||
if t.stage == "waitRecords" then hint = "Comparing games..."
|
Font.draw(Strings("STATS"), 16, 128)
|
||||||
elseif t.stage == "waitParty" then hint = "Exchanging data..."
|
Font.draw(Strings("TRADE"), 96, 128)
|
||||||
elseif t.stage == "picking" then
|
Font.drawCode(CURSOR, self.pickChoice == 1 and 8 or 88, 128)
|
||||||
hint = t:canPick(self.index) and "Pick one to trade"
|
else
|
||||||
or Strings("X: not on theirs")
|
local hint
|
||||||
elseif t.stage == "waitPick" then hint = "Waiting for them..."
|
if t.stage == "waitRecords" then hint = "Comparing games..."
|
||||||
elseif t.stage == "confirming" then
|
elseif t.stage == "waitParty" then hint = "Exchanging data..."
|
||||||
hint = self.confirmed and "Waiting..." or Strings("A: trade B: cancel")
|
elseif t.stage == "picking" then
|
||||||
|
if self.side == "theirs" then hint = Strings("A: stats")
|
||||||
|
else
|
||||||
|
hint = t:canPick(self.index) and "Pick one to trade"
|
||||||
|
or Strings("X: not on theirs")
|
||||||
|
end
|
||||||
|
elseif t.stage == "waitPick" then hint = "Waiting for them..."
|
||||||
|
elseif t.stage == "confirming" then
|
||||||
|
hint = self.confirmed and "Waiting..." or Strings("A: trade B: cancel")
|
||||||
|
end
|
||||||
|
Font.draw(hint or "", 8, 132)
|
||||||
end
|
end
|
||||||
Font.draw(hint or "", 8, 132)
|
|
||||||
|
|
||||||
elseif self.stage == "battleWait" or self.stage == "battleRunning" then
|
elseif self.stage == "battleWait" or self.stage == "battleRunning" then
|
||||||
drawTitle("LINK BATTLE")
|
drawTitle("LINK BATTLE")
|
||||||
|
|||||||
@@ -140,10 +140,12 @@ 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")
|
-- LearnedMove1Text: text_far, sound_get_item_1, text_promptbutton
|
||||||
|
-- (learn_move.asm), so the jingle rides the box
|
||||||
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,
|
||||||
|
TextBox.soundOpts(game, "Get_Item1")))
|
||||||
else
|
else
|
||||||
-- LearnMoveFromLevelUp with a full moveset: the forget UI
|
-- LearnMoveFromLevelUp with a full moveset: the forget UI
|
||||||
Screens.push(game, "MoveLearnMenu", mon, moveId, nextStep)
|
Screens.push(game, "MoveLearnMenu", mon, moveId, nextStep)
|
||||||
|
|||||||
@@ -141,13 +141,20 @@ end
|
|||||||
-- Tinting first would double-apply the color: GREENBAR's fill {0,189,0} has
|
-- Tinting first would double-apply the color: GREENBAR's fill {0,189,0} has
|
||||||
-- red channel 0, so the tint zeroes the whole bar's red and the zone's
|
-- red channel 0, so the tint zeroes the whole bar's red and the zone's
|
||||||
-- red-channel-keyed shade shader then maps every pixel to color 3 = black.
|
-- red-channel-keyed shade shader then maps every pixel to color 3 = black.
|
||||||
function HudTiles.drawHPBar(data, tx, ty, mon, barType, grayFill, segments)
|
--
|
||||||
|
-- pixels: an explicit 0..48 bar length on GetHPBarLength's scale, for a
|
||||||
|
-- caller that is animating the bar between two HP values
|
||||||
|
-- (UpdateHPBar_AnimateHPBar); it scales with `segments` like the color
|
||||||
|
-- thresholds do. Without it the length comes from mon.hp as before.
|
||||||
|
function HudTiles.drawHPBar(data, tx, ty, mon, barType, grayFill, segments, pixels)
|
||||||
local x, y = tx * 8, ty * 8
|
local x, y = tx * 8, ty * 8
|
||||||
segments = math.max(1, math.floor(segments or 6))
|
segments = math.max(1, math.floor(segments or 6))
|
||||||
HudTiles.tile(0x71, x, y)
|
HudTiles.tile(0x71, x, y)
|
||||||
HudTiles.tile(0x62, x + 8, y)
|
HudTiles.tile(0x62, x + 8, y)
|
||||||
local px = 0
|
local px = 0
|
||||||
if mon.stats.hp > 0 and mon.hp > 0 then
|
if pixels then
|
||||||
|
px = math.max(0, math.floor(pixels * segments / 6))
|
||||||
|
elseif mon.stats.hp > 0 and mon.hp > 0 then
|
||||||
px = math.max(1, math.floor(mon.hp * segments * 8 / mon.stats.hp))
|
px = math.max(1, math.floor(mon.hp * segments * 8 / mon.stats.hp))
|
||||||
end
|
end
|
||||||
local tint
|
local tint
|
||||||
|
|||||||
@@ -691,10 +691,12 @@ function PaletteFX.spriteObp(spriteDef, seed)
|
|||||||
return PaletteFX.darkObp(w.spritePalettes[group], group)
|
return PaletteFX.darkObp(w.spritePalettes[group], group)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- GetHealthBarColor (home/palettes.asm) on the standard 48px bar
|
-- GetHealthBarColor (home/palettes.asm) on the standard 48px bar. It reads
|
||||||
function PaletteFX.barPalName(hp, maxHp)
|
-- the bar's own length, so a caller mid-drain passes the animated `pixels`
|
||||||
local px = maxHp > 0 and math.floor(hp * 48 / maxHp) or 0
|
-- rather than let it be re-derived from hp.
|
||||||
if hp > 0 and px < 1 then px = 1 end
|
function PaletteFX.barPalName(hp, maxHp, pixels)
|
||||||
|
local px = pixels or (maxHp > 0 and math.floor(hp * 48 / maxHp) or 0)
|
||||||
|
if not pixels and hp > 0 and px < 1 then px = 1 end
|
||||||
return px >= 27 and "GREENBAR" or px >= 10 and "YELLOWBAR" or "REDBAR"
|
return px >= 27 and "GREENBAR" or px >= 10 and "YELLOWBAR" or "REDBAR"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,26 @@ function TextBox.new(game, text, onDone, opts)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- soundOpts: a jingle carried at the end of the string as a trailing text
|
||||||
|
-- command (sound_get_item_1 and friends -> home/text.asm TextCommand_SOUND),
|
||||||
|
-- which runs PlaySound then WaitForSoundToFinish once the last page has
|
||||||
|
-- typed, so the box holds until the fanfare is over. Merges into a caller's
|
||||||
|
-- opts table; auto.wait keeps the trailing button press the plain A/B path
|
||||||
|
-- gives every other box.
|
||||||
|
function TextBox.soundOpts(game, sound, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local auto = opts.auto
|
||||||
|
-- auto = true is the no-button-wait arming (text_opts); keep that choice
|
||||||
|
if auto == true then auto = { wait = false } end
|
||||||
|
auto = auto or {}
|
||||||
|
if auto.wait == nil then auto.wait = true end
|
||||||
|
auto.delay = auto.delay or 0
|
||||||
|
auto.sound = type(sound) == "function" and sound
|
||||||
|
or function() return require("src.core.Sound").play(game.data, sound) end
|
||||||
|
opts.auto = auto
|
||||||
|
return opts
|
||||||
|
end
|
||||||
|
|
||||||
-- The runtime tokens substitute() knows, as handlers the tokens registry
|
-- The runtime tokens substitute() knows, as handlers the tokens registry
|
||||||
-- serves. Each is fn(game, arg) -> replacement, or nil to drop the token.
|
-- serves. Each is fn(game, arg) -> replacement, or nil to drop the token.
|
||||||
-- RAM keeps pokered's stale-buffer semantics: give_item copies the item
|
-- RAM keeps pokered's stale-buffer semantics: give_item copies the item
|
||||||
|
|||||||
+15
-2
@@ -592,6 +592,17 @@ function Commands.play_sound(ctx, soundId)
|
|||||||
require("src.core.Sound").play(ctx.game.data, soundId)
|
require("src.core.Sound").play(ctx.game.data, soundId)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- text_sound <soundId>: the jingle the ROM parks at the END of a string as
|
||||||
|
-- a trailing text command (sound_get_item_1, sound_get_key_item ->
|
||||||
|
-- home/text.asm TextCommand_SOUND). It arms the NEXT show_text the same
|
||||||
|
-- way play_cry arms ctx.pendingCry, so the fanfare fires once the last page
|
||||||
|
-- has typed and the box holds on WaitForSoundToFinish before the button
|
||||||
|
-- wait. play_sound stays the bare PlaySound used for the non-blocking
|
||||||
|
-- beats (Bill's teleporter, the S.S. Anne horn).
|
||||||
|
function Commands.text_sound(ctx, soundId)
|
||||||
|
ctx.textOpts = TextBox.soundOpts(ctx.game, soundId, ctx.textOpts)
|
||||||
|
end
|
||||||
|
|
||||||
-- play_once <songId>: one-shot jingle (Music_PkmnHealed, etc.); blocks
|
-- play_once <songId>: one-shot jingle (Music_PkmnHealed, etc.); blocks
|
||||||
-- until it finishes so heal-rest scripts (Mom, captain text_asm) match
|
-- until it finishes so heal-rest scripts (Mom, captain text_asm) match
|
||||||
-- Gen1's wait-on-channel loop. The map theme resumes when it ends
|
-- Gen1's wait-on-channel loop. The map theme resumes when it ends
|
||||||
@@ -1050,8 +1061,10 @@ function Commands.trade(ctx, tradeIndex, doneFlag)
|
|||||||
onDone = function() runner:resume() end,
|
onDone = function() runner:resume() end,
|
||||||
})
|
})
|
||||||
runner:yield()
|
runner:yield()
|
||||||
-- TradedForText (sound_get_key_item) then the dialogset's thanks
|
-- TradedForText carries sound_get_key_item after the text, so the jingle
|
||||||
require("src.core.Sound").play(data, "Get_Key_Item")
|
-- rides the box and blocks it (home/text.asm TextCommand_SOUND), then the
|
||||||
|
-- dialogset's thanks
|
||||||
|
Commands.text_sound(ctx, "Get_Key_Item")
|
||||||
say(texts.tradedFor or "_TradedForText")
|
say(texts.tradedFor or "_TradedForText")
|
||||||
say(texts.thanks or "_Thanks" .. dialogset .. "Text")
|
say(texts.thanks or "_Thanks" .. dialogset .. "Text")
|
||||||
end
|
end
|
||||||
|
|||||||
+7
-6
@@ -33,12 +33,12 @@ local function save_name(game)
|
|||||||
return game.save.player.name
|
return game.save.player.name
|
||||||
end
|
end
|
||||||
|
|
||||||
local function showMessages(game, msgs, onDone)
|
local function showMessages(game, msgs, onDone, opts)
|
||||||
if not msgs or #msgs == 0 then
|
if not msgs or #msgs == 0 then
|
||||||
if onDone then onDone() end
|
if onDone then onDone() end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.stack:push(TextBox.new(game, table.concat(msgs, "\f"), onDone))
|
game.stack:push(TextBox.new(game, table.concat(msgs, "\f"), onDone, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- run the use-flow for an item on a chosen target. `picker` is the party
|
-- run the use-flow for an item on a chosen target. `picker` is the party
|
||||||
@@ -181,9 +181,11 @@ 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")
|
-- LearnedMove1Text: text_far, sound_get_item_1, text_promptbutton
|
||||||
|
-- (learn_move.asm), so the jingle rides the box
|
||||||
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) }, nil,
|
||||||
|
TextBox.soundOpts(game, "Get_Item1"))
|
||||||
if result == "learn" then consume(game, id) end
|
if result == "learn" then consume(game, id) end
|
||||||
list.items = buildItems(game)
|
list.items = buildItems(game)
|
||||||
list.index = math.min(list.index, math.max(1, #list.items))
|
list.index = math.min(list.index, math.max(1, #list.items))
|
||||||
@@ -333,10 +335,9 @@ 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, TextBox.soundOpts(game, "Get_Item1"))
|
||||||
else
|
else
|
||||||
require("src.ui.Screens").push(game, "MoveLearnMenu",
|
require("src.ui.Screens").push(game, "MoveLearnMenu",
|
||||||
target, moveId, nextStep)
|
target, moveId, nextStep)
|
||||||
|
|||||||
@@ -46,9 +46,27 @@ function EvolutionState:sgbPalettes(game)
|
|||||||
return P.wholeNamed(game.data, "MEWMON")
|
return P.wholeNamed(game.data, "MEWMON")
|
||||||
end
|
end
|
||||||
|
|
||||||
local FLASH_FRAMES = 220
|
|
||||||
-- evolution.asm EvolveMon delays 80 frames before .animLoop, polling nothing (#968, #1031)
|
-- evolution.asm EvolveMon delays 80 frames before .animLoop, polling nothing (#968, #1031)
|
||||||
local CANCEL_GRACE_FRAMES = 80
|
local CANCEL_GRACE_FRAMES = 80
|
||||||
|
-- .animLoop starts at `lb bc, $1, $10` and runs 8 iterations: iteration k holds
|
||||||
|
-- the old pic for c = 18-2k frames inside Evolution_CheckForCancel (72 in all),
|
||||||
|
-- then Evolution_BackAndForthAnim swaps b = k times, each swap a pair of
|
||||||
|
-- Evolution_ChangeMonPic that end in Delay3 (6 frames a swap, 216 in all).
|
||||||
|
local ANIM_LOOP_FRAMES = 288
|
||||||
|
local FLASH_FRAMES = CANCEL_GRACE_FRAMES + ANIM_LOOP_FRAMES
|
||||||
|
|
||||||
|
-- which pic is on screen t frames into .animLoop
|
||||||
|
local function evoShowsNew(t)
|
||||||
|
for b = 1, 8 do
|
||||||
|
local hold = 18 - 2 * b
|
||||||
|
if t < hold then return false end
|
||||||
|
t = t - hold
|
||||||
|
local swap = b * 6
|
||||||
|
if t < swap then return t % 6 < 3 end
|
||||||
|
t = t - swap
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function frontSprite(game, species, mon)
|
local function frontSprite(game, species, mon)
|
||||||
local path, trueColor = require("src.pokemon.Sprites").path(
|
local path, trueColor = require("src.pokemon.Sprites").path(
|
||||||
@@ -140,14 +158,10 @@ function EvolutionState:draw()
|
|||||||
else
|
else
|
||||||
sprite, spriteTrueColor = self.newSprite, self.newSpriteTrueColor
|
sprite, spriteTrueColor = self.newSprite, self.newSpriteTrueColor
|
||||||
end
|
end
|
||||||
|
elseif evoShowsNew(self.t - CANCEL_GRACE_FRAMES) then
|
||||||
|
sprite, spriteTrueColor = self.newSprite, self.newSpriteTrueColor
|
||||||
else
|
else
|
||||||
local period = math.max(4, 28 - math.floor(self.t / 40) * 6)
|
sprite, spriteTrueColor = self.oldSprite, self.oldSpriteTrueColor
|
||||||
local showNew = math.floor(self.t / period) % 2 == 1
|
|
||||||
if showNew then
|
|
||||||
sprite, spriteTrueColor = self.newSprite, self.newSpriteTrueColor
|
|
||||||
else
|
|
||||||
sprite, spriteTrueColor = self.oldSprite, self.oldSpriteTrueColor
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
if sprite then
|
if sprite then
|
||||||
local x = math.floor((160 - sprite:getWidth()) / 2)
|
local x = math.floor((160 - sprite:getWidth()) / 2)
|
||||||
|
|||||||
+16
-3
@@ -6,6 +6,7 @@ local GameVersion = require("src.core.GameVersion")
|
|||||||
local Music = require("src.core.Music")
|
local Music = require("src.core.Music")
|
||||||
local Sound = require("src.core.Sound")
|
local Sound = require("src.core.Sound")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
|
local Timing = require("src.core.Timing")
|
||||||
|
|
||||||
local IntroMovie = {}
|
local IntroMovie = {}
|
||||||
IntroMovie.__index = IntroMovie
|
IntroMovie.__index = IntroMovie
|
||||||
@@ -232,6 +233,10 @@ function IntroMovie:fightStep()
|
|||||||
self:finish()
|
self:finish()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
-- an op that consumed frames ends ON its last frame; only the instant
|
||||||
|
-- ops (sfx / pose / frame, which are plain writes between DelayFrames
|
||||||
|
-- calls in PlayIntroScene) chain into the next op the same frame
|
||||||
|
local timed = false
|
||||||
if op.sfx then
|
if op.sfx then
|
||||||
Sound.play(self.game.data, op.sfx)
|
Sound.play(self.game.data, op.sfx)
|
||||||
elseif op.pose then
|
elseif op.pose then
|
||||||
@@ -250,6 +255,7 @@ function IntroMovie:fightStep()
|
|||||||
end
|
end
|
||||||
self.opTimer = self.opTimer + 1
|
self.opTimer = self.opTimer + 1
|
||||||
if self.opTimer < (op.px or math.abs(op.dx)) then return end
|
if self.opTimer < (op.px or math.abs(op.dx)) then return end
|
||||||
|
timed = true
|
||||||
elseif op.anim then
|
elseif op.anim then
|
||||||
-- one {dy,dx} delta per 5 frames (AnimateIntroNidorino: DelayFrames 5)
|
-- one {dy,dx} delta per 5 frames (AnimateIntroNidorino: DelayFrames 5)
|
||||||
if self.opTimer % 5 == 0 then
|
if self.opTimer % 5 == 0 then
|
||||||
@@ -259,9 +265,11 @@ function IntroMovie:fightStep()
|
|||||||
end
|
end
|
||||||
self.opTimer = self.opTimer + 1
|
self.opTimer = self.opTimer + 1
|
||||||
if self.opTimer < #ANIM[op.anim] * 5 then return end
|
if self.opTimer < #ANIM[op.anim] * 5 then return end
|
||||||
|
timed = true
|
||||||
elseif op.wait then
|
elseif op.wait then
|
||||||
self.opTimer = self.opTimer + 1
|
self.opTimer = self.opTimer + 1
|
||||||
if self.opTimer < op.wait then return end
|
if self.opTimer < op.wait then return end
|
||||||
|
timed = true
|
||||||
elseif op.fade then
|
elseif op.fade then
|
||||||
self.opTimer = self.opTimer + 1
|
self.opTimer = self.opTimer + 1
|
||||||
self.fade = self.opTimer / op.fade
|
self.fade = self.opTimer / op.fade
|
||||||
@@ -270,6 +278,7 @@ function IntroMovie:fightStep()
|
|||||||
end
|
end
|
||||||
self.opIndex = self.opIndex + 1
|
self.opIndex = self.opIndex + 1
|
||||||
self.opTimer = 0
|
self.opTimer = 0
|
||||||
|
if timed then return end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -284,8 +293,9 @@ function IntroMovie:update(dt)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
if input:wasPressed("a") or input:wasPressed("b")
|
if input:wasPressed("a") or input:wasPressed("start") then
|
||||||
or input:wasPressed("start") then
|
-- CheckForUserInterruption (home/overworld.asm:2395) returns carry only
|
||||||
|
-- on a fresh START or A -- B alone never skips the intro.
|
||||||
-- PlayIntro still GBFadeOutToWhite's after an interrupted scene; the
|
-- PlayIntro still GBFadeOutToWhite's after an interrupted scene; the
|
||||||
-- white hold stands in for that beat before the title is built.
|
-- white hold stands in for that beat before the title is built.
|
||||||
self:exitToTitle()
|
self:exitToTitle()
|
||||||
@@ -300,7 +310,10 @@ function IntroMovie:update(dt)
|
|||||||
end
|
end
|
||||||
if self.timer >= SPLASH_FRAMES then self:startPhase(3) end
|
if self.timer >= SPLASH_FRAMES then self:startPhase(3) end
|
||||||
else
|
else
|
||||||
self:fightStep()
|
-- PlayShootingStar ends `jp Delay3` once Music_IntroBattle is playing
|
||||||
|
-- (intro.asm:337), so PlayIntroScene's first op is not on the music's
|
||||||
|
-- own frame
|
||||||
|
if self.timer > Timing.DELAY3 then self:fightStep() end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+88
-35
@@ -87,6 +87,15 @@ local function refuseUnavailable(self)
|
|||||||
t._SleepingPikachuText1 or Strings("There isn't any\nresponse...")))
|
t._SleepingPikachuText1 or Strings("There isn't any\nresponse...")))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- .newBadgeRequired (start_sub_menus.asm): every badge-gated arm of
|
||||||
|
-- .outOfBattleMovePointers prints this and jumps back to the open submenu
|
||||||
|
local function refuseBadge(self)
|
||||||
|
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._NewBadgeRequiredText or Strings("No! A new BADGE\nis required.")))
|
||||||
|
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,
|
||||||
@@ -411,6 +420,23 @@ function PartyMenu:update(dt)
|
|||||||
-- flyTo (OverworldController) validates the fly-warp + runs the
|
-- flyTo (OverworldController) validates the fly-warp + runs the
|
||||||
-- departure/warp, so we just hand it the chosen mapId (#195).
|
-- departure/warp, so we just hand it the chosen mapId (#195).
|
||||||
local ow = self.game.overworld
|
local ow = self.game.overworld
|
||||||
|
-- .fly checks THUNDERBADGE first, then CheckIfInOutsideMap
|
||||||
|
-- (OVERWORLD + PLATEAU -- Route 23 / Indigo Plateau outdoor -- not
|
||||||
|
-- OVERWORLD alone, #83); both refusals loop back to the submenu
|
||||||
|
if ow and not ow:partyKnows("FLY") then
|
||||||
|
refuseBadge(self)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if ow and not Map.isOutside(ow.map.def,
|
||||||
|
FieldDefaults.field(self.game.data, "outsideTilesets")) then
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
local def = self.game.data.pokemon[mon.species]
|
||||||
|
local txt = (self.game.data.text._CannotFlyHereText
|
||||||
|
or Strings("{RAM:wNameBuffer} can't\nFLY here."))
|
||||||
|
:gsub("{RAM:wNameBuffer}", mon.nickname or def.name)
|
||||||
|
self.game.stack:push(TextBox.new(self.game, txt))
|
||||||
|
return -- .loop: submenu stays open behind the message
|
||||||
|
end
|
||||||
self.game.stack:pop() -- close the party menu
|
self.game.stack:pop() -- close the party menu
|
||||||
Screens.push(self.game, "TownMap", { fly = true, onFly = function(mapId)
|
Screens.push(self.game, "TownMap", { fly = true, onFly = function(mapId)
|
||||||
if ow then ow:flyTo(mapId) end
|
if ow then ow:flyTo(mapId) end
|
||||||
@@ -423,9 +449,17 @@ function PartyMenu:update(dt)
|
|||||||
-- over the menu, and the cave is lit when the blink hands the
|
-- over the menu, and the cave is lit when the blink hands the
|
||||||
-- screen back, never under the text (#385).
|
-- screen back, never under the text (#385).
|
||||||
local ow = self.game.overworld
|
local ow = self.game.overworld
|
||||||
|
if ow and not ow:partyKnows("FLASH") then
|
||||||
|
refuseBadge(self)
|
||||||
|
return
|
||||||
|
end
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Transition = require("src.render.Transition")
|
local Transition = require("src.render.Transition")
|
||||||
self.game.save.flashLit = true
|
-- .flash prints on any map, but the light it records is this map's:
|
||||||
|
-- home/overworld.asm re-arms wMapPalOffset on the next dark map, so
|
||||||
|
-- a FLASH used in daylight must not carry into Rock Tunnel
|
||||||
|
local wasDark = ow and ow.dark
|
||||||
|
if wasDark then self.game.save.flashLit = true end
|
||||||
self.game.stack:push(TextBox.new(self.game,
|
self.game.stack:push(TextBox.new(self.game,
|
||||||
self.game.data.text._FlashLightsAreaText
|
self.game.data.text._FlashLightsAreaText
|
||||||
or Strings("A blinding FLASH\nlights the area!"), function()
|
or Strings("A blinding FLASH\nlights the area!"), function()
|
||||||
@@ -440,13 +474,13 @@ function PartyMenu:update(dt)
|
|||||||
-- seconds of per-pixel atlas baking on a phone -- on screen as a
|
-- seconds of per-pixel atlas baking on a phone -- on screen as a
|
||||||
-- solid white frame with nothing under it, which reads as a
|
-- solid white frame with nothing under it, which reads as a
|
||||||
-- lockup (#610).
|
-- lockup (#610).
|
||||||
ow:setDark(false)
|
if wasDark then ow:setDark(false) end
|
||||||
self.game.stack:push(Transition.whiteFlash(self.game))
|
self.game.stack:push(Transition.whiteFlash(self.game))
|
||||||
end))
|
end))
|
||||||
return
|
return
|
||||||
elseif action == "surf" then
|
elseif action == "surf" then
|
||||||
-- start_sub_menus.asm .surf: SOULBADGE-gated (checked at list time
|
-- start_sub_menus.asm .surf: SOULBADGE-gated (useSurfFieldMove),
|
||||||
-- above), then IsSurfingAllowed (the Cycling Road / Seafoam B4F
|
-- then IsSurfingAllowed (the Cycling Road / Seafoam B4F
|
||||||
-- current refusals, both of which loop back to the submenu), then
|
-- current refusals, both of which loop back to the submenu), then
|
||||||
-- ItemUseSurfboard: while surfing it tries to dismount instead;
|
-- ItemUseSurfboard: while surfing it tries to dismount instead;
|
||||||
-- otherwise it mounts only if the FACING tile is water, else
|
-- otherwise it mounts only if the FACING tile is water, else
|
||||||
@@ -503,8 +537,8 @@ function PartyMenu:update(dt)
|
|||||||
return -- .loop: submenu stays open behind the message
|
return -- .loop: submenu stays open behind the message
|
||||||
elseif action == "cut" then
|
elseif action == "cut" then
|
||||||
-- start_sub_menus.asm .cut -> predef UsedCut (engine/overworld/cut.asm):
|
-- start_sub_menus.asm .cut -> predef UsedCut (engine/overworld/cut.asm):
|
||||||
-- CASCADEBADGE-gated (list time); _NothingToCutText loops back to the
|
-- CASCADEBADGE-gated (useCutFieldMove); _NothingToCutText loops back
|
||||||
-- submenu when the FACING tile isn't a cuttable tree.
|
-- to the submenu when the FACING tile isn't a cuttable tree.
|
||||||
local ow = self.game.overworld
|
local ow = self.game.overworld
|
||||||
local reason = ow:useCutFieldMove()
|
local reason = ow:useCutFieldMove()
|
||||||
if reason == "ok" then
|
if reason == "ok" then
|
||||||
@@ -522,7 +556,7 @@ function PartyMenu:update(dt)
|
|||||||
self.game.stack:push(TextBox.new(self.game, txt))
|
self.game.stack:push(TextBox.new(self.game, txt))
|
||||||
return -- .loop: submenu stays open behind the message
|
return -- .loop: submenu stays open behind the message
|
||||||
elseif action == "strength" then
|
elseif action == "strength" then
|
||||||
-- start_sub_menus.asm .strength: RAINBOWBADGE-gated (list time);
|
-- start_sub_menus.asm .strength: RAINBOWBADGE-gated;
|
||||||
-- predef PrintStrengthText (field_move_messages.asm) sets
|
-- predef PrintStrengthText (field_move_messages.asm) sets
|
||||||
-- BIT_STRENGTH_ACTIVE of wStatusFlags1 -- the sole gate
|
-- BIT_STRENGTH_ACTIVE of wStatusFlags1 -- the sole gate
|
||||||
-- push_boulder.asm reads -- then prints _UsedStrengthText (no
|
-- push_boulder.asm reads -- then prints _UsedStrengthText (no
|
||||||
@@ -532,6 +566,10 @@ function PartyMenu:update(dt)
|
|||||||
-- .strength, GBPalWhiteOutWithDelay3 blinks the screen white
|
-- .strength, GBPalWhiteOutWithDelay3 blinks the screen white
|
||||||
-- before CloseTextDisplay returns to the map.
|
-- before CloseTextDisplay returns to the map.
|
||||||
local ow = self.game.overworld
|
local ow = self.game.overworld
|
||||||
|
if ow and not ow:partyKnows("STRENGTH") then
|
||||||
|
refuseBadge(self)
|
||||||
|
return
|
||||||
|
end
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Transition = require("src.render.Transition")
|
local Transition = require("src.render.Transition")
|
||||||
local def = self.game.data.pokemon[mon.species]
|
local def = self.game.data.pokemon[mon.species]
|
||||||
@@ -565,6 +603,33 @@ function PartyMenu:update(dt)
|
|||||||
-- centralizes the spin -> fade -> warp so BagMenu's ESCAPE ROPE shares
|
-- centralizes the spin -> fade -> warp so BagMenu's ESCAPE ROPE shares
|
||||||
-- the exact departure; the fade + warp fire when the spin ends.
|
-- the exact departure; the fade + warp fire when the spin ends.
|
||||||
local ow = self.game.overworld
|
local ow = self.game.overworld
|
||||||
|
if entry.move == "TELEPORT" then
|
||||||
|
-- .teleport: TELEPORT works only OUTDOORS (CheckIfInOutsideMap --
|
||||||
|
-- OVERWORLD + PLATEAU, #83); dark maps don't block it
|
||||||
|
if ow and not Map.isOutside(ow.map.def,
|
||||||
|
FieldDefaults.field(self.game.data, "outsideTilesets")) then
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
local def = self.game.data.pokemon[mon.species]
|
||||||
|
local txt = (self.game.data.text._CannotUseTeleportNowText
|
||||||
|
or Strings("{RAM:wNameBuffer} can't\nuse TELEPORT now."))
|
||||||
|
:gsub("{RAM:wNameBuffer}", mon.nickname or def.name)
|
||||||
|
self.game.stack:push(TextBox.new(self.game, txt))
|
||||||
|
return -- .loop: submenu stays open behind the message
|
||||||
|
end
|
||||||
|
elseif ow and not (DIG_TILESETS[ow.map.def.tileset]
|
||||||
|
and ow.map.id ~= "AGATHAS_ROOM") then
|
||||||
|
-- .dig runs ItemUseEscapeRope (it sets wCurItem = ESCAPE_ROPE):
|
||||||
|
-- usable in the dungeon tilesets of escape_rope_tilesets.asm minus
|
||||||
|
-- Agatha's room, even in the dark (Rock Tunnel); anywhere else
|
||||||
|
-- .notUsable -> ItemUseNotTime, the same line BagMenu prints for a
|
||||||
|
-- bagged ESCAPE ROPE
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
self.game.stack:push(TextBox.new(self.game,
|
||||||
|
self.game.data.text._ItemUseNotTimeText
|
||||||
|
or Strings("OAK: %s!\nThis isn't the\ntime to use that!",
|
||||||
|
self.game.save.player.name)))
|
||||||
|
return -- .loop: submenu stays open behind the message
|
||||||
|
end
|
||||||
self.game.stack:pop()
|
self.game.stack:pop()
|
||||||
if ow then ow:beginTeleportOut() end
|
if ow then ow:beginTeleportOut() end
|
||||||
return
|
return
|
||||||
@@ -658,42 +723,30 @@ function PartyMenu:update(dt)
|
|||||||
-- Battle still excludes this list via `not self.battle`. Softboiled
|
-- Battle still excludes this list via `not self.battle`. Softboiled
|
||||||
-- can appear for a fainted user; its heal transfer then no-ops.
|
-- can appear for a fainted user; its heal transfer then no-ops.
|
||||||
if not self.battle and ow then
|
if not self.battle and ow then
|
||||||
-- FLY/TELEPORT: CheckIfInOutsideMap (OVERWORLD + PLATEAU --
|
-- GetMonFieldMoves (engine/menus/text_box.asm) matches the mon's
|
||||||
-- Route 23 / Indigo Plateau outdoor), not OVERWORLD alone (#83)
|
-- four moves against FieldMoveDisplayData and nothing else -- no
|
||||||
local outside = Map.isOutside(ow.map.def,
|
-- badge, no map, no tileset test. Every one of those lives in
|
||||||
FieldDefaults.field(self.game.data, "outsideTilesets"))
|
-- .outOfBattleMovePointers, i.e. on selection, where the refusal
|
||||||
|
-- prints and .loop returns to this still-open submenu (#1022).
|
||||||
for _, mv in ipairs(mon.moves) do
|
for _, mv in ipairs(mon.moves) do
|
||||||
if mv.id == "FLY" and outside
|
if mv.id == "FLY" then
|
||||||
and self.game.save.inventory.THUNDERBADGE then
|
|
||||||
table.insert(items, { label = Strings("FLY"), action = "fly" })
|
table.insert(items, { label = Strings("FLY"), action = "fly" })
|
||||||
elseif mv.id == "FLASH" and ow.dark
|
elseif mv.id == "FLASH" then
|
||||||
and self.game.save.inventory.BOULDERBADGE then
|
|
||||||
table.insert(items, { label = Strings("FLASH"), action = "flash" })
|
table.insert(items, { label = Strings("FLASH"), action = "flash" })
|
||||||
elseif mv.id == "CUT" and self.game.save.inventory.CASCADEBADGE then
|
elseif mv.id == "CUT" then
|
||||||
-- CUT/SURF/STRENGTH are party-menu field moves too
|
|
||||||
-- (start_sub_menus.asm .outOfBattleMovePointers); listed here
|
|
||||||
-- with the same list-time badge filter this file already uses
|
|
||||||
-- for FLY/FLASH. The facing-tile/activation check happens on
|
|
||||||
-- selection (useCutFieldMove/useSurfFieldMove).
|
|
||||||
table.insert(items, { label = Strings("CUT"), action = "cut" })
|
table.insert(items, { label = Strings("CUT"), action = "cut" })
|
||||||
elseif mv.id == "SURF" and self.game.save.inventory.SOULBADGE then
|
elseif mv.id == "SURF" then
|
||||||
table.insert(items, { label = Strings("SURF"), action = "surf" })
|
table.insert(items, { label = Strings("SURF"), action = "surf" })
|
||||||
elseif mv.id == "STRENGTH" and self.game.save.inventory.RAINBOWBADGE then
|
elseif mv.id == "STRENGTH" then
|
||||||
table.insert(items, { label = Strings("STRENGTH"), action = "strength" })
|
table.insert(items, { label = Strings("STRENGTH"), action = "strength" })
|
||||||
elseif mv.id == "SOFTBOILED" then
|
elseif mv.id == "SOFTBOILED" then
|
||||||
table.insert(items, { label = Strings("SOFTBOILED"), action = "softboiled" })
|
table.insert(items, { label = Strings("SOFTBOILED"), action = "softboiled" })
|
||||||
elseif mv.id == "TELEPORT" and outside then
|
elseif mv.id == "TELEPORT" then
|
||||||
-- TELEPORT works only OUTDOORS (start_sub_menus.asm
|
table.insert(items, { label = Strings("TELEPORT"),
|
||||||
-- .teleport -> CheckIfInOutsideMap); dark maps don't
|
action = "escape", move = "TELEPORT" })
|
||||||
-- block it
|
elseif mv.id == "DIG" then
|
||||||
table.insert(items, { label = Strings("TELEPORT"), action = "escape" })
|
table.insert(items, { label = Strings("DIG"),
|
||||||
elseif mv.id == "DIG" and DIG_TILESETS[ow.map.def.tileset]
|
action = "escape", move = "DIG" })
|
||||||
and ow.map.id ~= "AGATHAS_ROOM" then
|
|
||||||
-- DIG runs ItemUseEscapeRope (.dig sets wCurItem =
|
|
||||||
-- ESCAPE_ROPE): usable in the dungeon tilesets of
|
|
||||||
-- escape_rope_tilesets.asm minus Agatha's room, even in
|
|
||||||
-- the dark (Rock Tunnel)
|
|
||||||
table.insert(items, { label = Strings("DIG"), action = "escape" })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+128
-78
@@ -368,6 +368,7 @@ Kit._navPrevN = 0
|
|||||||
Kit._navSeen = {}
|
Kit._navSeen = {}
|
||||||
Kit._navQueue = nil
|
Kit._navQueue = nil
|
||||||
Kit._activateId = nil
|
Kit._activateId = nil
|
||||||
|
Kit._ringShown = false
|
||||||
|
|
||||||
-- Register a focusable. Returns true when it currently holds the ring.
|
-- Register a focusable. Returns true when it currently holds the ring.
|
||||||
-- Shielded widgets do not register: while a modal owns the frame the ring
|
-- Shielded widgets do not register: while a modal owns the frame the ring
|
||||||
@@ -383,11 +384,12 @@ function Kit.focusable(id, x, y, w, h)
|
|||||||
-- First focusable ever drawn adopts the ring, so keyboard users start
|
-- First focusable ever drawn adopts the ring, so keyboard users start
|
||||||
-- somewhere rather than nowhere.
|
-- somewhere rather than nowhere.
|
||||||
if Kit.focusId == nil then Kit.focusId = id end
|
if Kit.focusId == nil then Kit.focusId = id end
|
||||||
return Kit.focusId == id
|
return Kit._ringShown and Kit.focusId == id
|
||||||
end
|
end
|
||||||
|
|
||||||
function Kit.navigate(dir)
|
function Kit.navigate(dir)
|
||||||
Kit._navQueue = dir
|
Kit._navQueue = dir
|
||||||
|
Kit._ringShown = true
|
||||||
end
|
end
|
||||||
|
|
||||||
function Kit.activateFocused()
|
function Kit.activateFocused()
|
||||||
@@ -396,6 +398,7 @@ end
|
|||||||
|
|
||||||
function Kit.setFocus(id)
|
function Kit.setFocus(id)
|
||||||
Kit.focusId = id
|
Kit.focusId = id
|
||||||
|
Kit._ringShown = id ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pick the nearest focusable in `dir` from the current one. Candidates must
|
-- Pick the nearest focusable in `dir` from the current one. Candidates must
|
||||||
@@ -530,13 +533,10 @@ Kit._audit = audit
|
|||||||
function Kit.tapMin() return math.floor(30 * Kit.scale) end
|
function Kit.tapMin() return math.floor(30 * Kit.scale) end
|
||||||
|
|
||||||
-- ---------------------------------------------------------------- surfaces
|
-- ---------------------------------------------------------------- surfaces
|
||||||
function Kit.card(x, y, w, h, emphasis)
|
function Kit.card(x, y, w, h, variant)
|
||||||
Theme.card(x, y, w, h, emphasis)
|
Theme.card(x, y, w, h, variant)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A list row. `id` opts it into the focus ring; pass nil for decorative
|
|
||||||
-- rows. Returns (clicked, inkColor) -- a selected row fills white, so the
|
|
||||||
-- caller must print with the returned ink or it will draw white on white.
|
|
||||||
function Kit.row(x, y, w, h, selected, id)
|
function Kit.row(x, y, w, h, selected, id)
|
||||||
audit("row", x, y, w, h, id or "row")
|
audit("row", x, y, w, h, id or "row")
|
||||||
local focused = id and Kit.focusable(id, x, y, w, h) or false
|
local focused = id and Kit.focusable(id, x, y, w, h) or false
|
||||||
@@ -560,7 +560,7 @@ end
|
|||||||
-- hairline says the same thing for one rect.)
|
-- hairline says the same thing for one rect.)
|
||||||
function Kit.emptyBox(x, y, w, h, message)
|
function Kit.emptyBox(x, y, w, h, message)
|
||||||
if not G then return end
|
if not G then return end
|
||||||
Theme.strokeRounded(x, y, w, h, PAL.line, 0.22, 1, Theme.radius())
|
Theme.card(x, y, w, h, "empty")
|
||||||
Kit.textCenter("button", Kit.ellipsize("button", message, w - 24 * Kit.scale),
|
Kit.textCenter("button", Kit.ellipsize("button", message, w - 24 * Kit.scale),
|
||||||
x, y + (h - Kit.textHeight("button")) / 2, w, PAL.muted)
|
x, y + (h - Kit.textHeight("button")) / 2, w, PAL.muted)
|
||||||
end
|
end
|
||||||
@@ -598,61 +598,130 @@ local KINDS = {
|
|||||||
disabled = { fill = PAL.steel, ink = PAL.inverse, flat = true },
|
disabled = { fill = PAL.steel, ink = PAL.inverse, flat = true },
|
||||||
}
|
}
|
||||||
Kit.KINDS = KINDS
|
Kit.KINDS = KINDS
|
||||||
|
local NO_OPTS = {}
|
||||||
|
|
||||||
-- opts: { kind, font, enabled, align, id, glow, fill, ink }
|
|
||||||
-- id -- opts into the focus ring (give every real control one)
|
|
||||||
-- glow -- a pulsing outline for "something is waiting for you" (the
|
|
||||||
-- update button). No blend-mode change: the alpha of the
|
|
||||||
-- existing outline is animated instead.
|
|
||||||
-- fill/ink -- override the kind's colours. The ONE caller is the
|
|
||||||
-- launcher's Play button, which wears its cartridge colour
|
|
||||||
-- (red/blue/gold) rather than a semantic one: on that screen
|
|
||||||
-- "which game am I launching" outranks "what kind of verb is
|
|
||||||
-- this", and the colour is already the tab's identity.
|
|
||||||
-- Returns true when activated, by click OR by the focus ring's Enter/A.
|
|
||||||
function Kit.button(x, y, w, h, label, opts)
|
function Kit.button(x, y, w, h, label, opts)
|
||||||
opts = opts or {}
|
opts = opts or NO_OPTS
|
||||||
local enabled = opts.enabled ~= false
|
local enabled = opts.enabled ~= false
|
||||||
-- Disabled buttons audit too: they stay visible, so they still must not
|
|
||||||
-- paint over a neighbour.
|
|
||||||
audit("control", x, y, w, h, label)
|
audit("control", x, y, w, h, label)
|
||||||
local focused = enabled and opts.id
|
local focused = enabled and opts.id
|
||||||
and Kit.focusable(opts.id, x, y, w, h) or false
|
and Kit.focusable(opts.id, x, y, w, h) or false
|
||||||
local kind = KINDS[enabled and (opts.kind or "ghost") or "disabled"]
|
|
||||||
if enabled and opts.fill then
|
|
||||||
kind = { fill = opts.fill, ink = opts.ink or PAL.inverse }
|
|
||||||
end
|
|
||||||
local hot = enabled and Kit.hover(x, y, w, h)
|
local hot = enabled and Kit.hover(x, y, w, h)
|
||||||
|
local face = opts.face or "fill"
|
||||||
if G then
|
local B = Theme.BUTTON
|
||||||
-- The fill IS the control: a rounded, embossed, colour-coded key. A
|
local radius = opts.radius or B.radius
|
||||||
-- disabled button keeps its shape in a dead grey rather than
|
local active = opts.active or opts.on
|
||||||
-- disappearing, so a layout never reflows on state.
|
local invert = false
|
||||||
Theme.fillRounded(x, y, w, h, kind.fill, enabled and 1 or 0.45)
|
local fill, ink, stroke, strokeA, doEmboss, doRing, glowA
|
||||||
Theme.emboss(x, y, w, h, enabled and (hot and 1.3 or 1) or 0.4)
|
if face == "invert" then
|
||||||
if hot or focused then
|
invert = hot
|
||||||
-- White ring outside the fill: legible on green, blue, yellow, red and
|
fill = invert and (opts.hotFill or PAL.ink) or (opts.fill or PAL.surface)
|
||||||
-- white alike, which one darker/lighter shade per colour would not be.
|
ink = invert and (opts.hotInk or PAL.inverse) or (opts.ink or PAL.heading)
|
||||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4, PAL.lineStrong,
|
stroke = opts.stroke or PAL.line
|
||||||
Theme.A.focus, 2, Theme.radius() + 2)
|
strokeA = invert and Theme.A.focus or Theme.A.hairline
|
||||||
elseif opts.glow and enabled then
|
doRing = focused and not hot
|
||||||
-- "Something is waiting for you" (the update button): a pulsing ring.
|
elseif face == "tab" then
|
||||||
-- Pure alpha on one existing stroke -- no extra draw calls, no blend
|
invert = active or focused or hot
|
||||||
-- mode change.
|
local tint = opts.color or opts.fill or PAL.ink
|
||||||
local a = 0.25 + 0.75 * (0.5 + 0.5 * math.sin(Kit.time * 3))
|
fill = invert and tint or PAL.surface
|
||||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4, PAL.lineStrong, a, 2,
|
ink = invert and PAL.inverse or (opts.color or PAL.text)
|
||||||
Theme.radius() + 2)
|
if not invert then
|
||||||
|
stroke = tint
|
||||||
|
strokeA = opts.color and Theme.A.hover or Theme.A.hairline
|
||||||
end
|
end
|
||||||
local fname = opts.font or "button"
|
elseif face == "chip" then
|
||||||
local ink = enabled and kind.ink or PAL.inverse
|
local c = opts.color or PAL.line
|
||||||
local ty = y + (h - Kit.textHeight(fname)) / 2
|
invert = active and true or false
|
||||||
local shown = Kit.ellipsize(fname, label, w - 16 * Kit.scale)
|
if active then
|
||||||
-- Button labels are bold: they are the shortest, most-scanned text on
|
fill = c
|
||||||
-- screen and sit on a saturated fill.
|
ink = PAL.inverse
|
||||||
if opts.align == "left" then
|
doEmboss = true
|
||||||
Kit.textBold(fname, shown, x + 10 * Kit.scale, ty, ink)
|
|
||||||
else
|
else
|
||||||
Kit.textCenterBold(fname, shown, x, ty, w, ink)
|
fill = PAL.bg
|
||||||
|
ink = c
|
||||||
|
stroke = c
|
||||||
|
strokeA = (focused or hot) and Theme.A.focus or Theme.A.hover
|
||||||
|
end
|
||||||
|
doRing = focused or hot
|
||||||
|
else
|
||||||
|
local kind = KINDS[enabled and (opts.kind or "ghost") or "disabled"]
|
||||||
|
fill = (enabled and opts.fill) or kind.fill
|
||||||
|
ink = (enabled and opts.ink) or kind.ink
|
||||||
|
doEmboss = true
|
||||||
|
doRing = hot or focused
|
||||||
|
if opts.glow and enabled and not doRing then
|
||||||
|
glowA = B.glowBase + B.glowAmp * (0.5 + 0.5 * math.sin(Kit.time * B.glowHz))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if opts.emboss ~= nil then doEmboss = opts.emboss end
|
||||||
|
if opts.ring ~= nil then doRing = opts.ring end
|
||||||
|
if G then
|
||||||
|
Theme.fillRounded(x, y, w, h, fill, enabled and 1 or B.disabledA, radius)
|
||||||
|
if doEmboss then
|
||||||
|
local es = enabled and ((hot or focused) and B.embossHot or B.embossRest)
|
||||||
|
or B.embossDisabled
|
||||||
|
Theme.emboss(x, y, w, h, es)
|
||||||
|
end
|
||||||
|
if strokeA then
|
||||||
|
Theme.strokeRounded(x, y, w, h, stroke, strokeA, 1, radius)
|
||||||
|
end
|
||||||
|
if doRing then
|
||||||
|
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
||||||
|
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
||||||
|
Theme.A.focus, B.ringWidth, radius + B.ringPad)
|
||||||
|
elseif glowA then
|
||||||
|
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
||||||
|
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
||||||
|
glowA, B.ringWidth, radius + B.ringPad)
|
||||||
|
end
|
||||||
|
local fname = opts.font or ((face == "chip") and "micro" or "button")
|
||||||
|
local ty = y + (h - Kit.textHeight(fname)) / 2
|
||||||
|
local image = opts.image
|
||||||
|
local drawFn = opts.drawFn
|
||||||
|
local letter = opts.letter
|
||||||
|
local hasLabel = label and label ~= ""
|
||||||
|
local bold = opts.bold
|
||||||
|
if bold == nil then bold = face ~= "tab" end
|
||||||
|
if image then
|
||||||
|
local box = h
|
||||||
|
local boxX, boxY = x, y
|
||||||
|
if not hasLabel then
|
||||||
|
box = math.min(w, h)
|
||||||
|
boxX = x + (w - box) / 2
|
||||||
|
boxY = y + (h - box) / 2
|
||||||
|
end
|
||||||
|
local iw, ih = image:getDimensions()
|
||||||
|
local pad = math.floor(box * (opts.iconPad or B.iconPad))
|
||||||
|
local s = math.min((box - 2 * pad) / iw, (box - 2 * pad) / ih)
|
||||||
|
if invert then Theme.col(PAL.inverse, 1)
|
||||||
|
else Theme.col(PAL.ink, B.iconRestA) end
|
||||||
|
G.draw(image, Theme.snap(boxX + (box - iw * s) / 2),
|
||||||
|
Theme.snap(boxY + (box - ih * s) / 2), 0, s, s)
|
||||||
|
if hasLabel then
|
||||||
|
local lx = x + h + B.letterGap * Kit.scale
|
||||||
|
if bold then Kit.textBold(fname, label, lx, ty, ink)
|
||||||
|
else Kit.text(fname, label, lx, ty, ink) end
|
||||||
|
end
|
||||||
|
elseif drawFn then
|
||||||
|
drawFn(x, y, w, h, invert or hot or focused)
|
||||||
|
elseif letter then
|
||||||
|
Kit.textCenter(fname, letter, x, ty, h, ink)
|
||||||
|
if hasLabel then
|
||||||
|
local lx = x + h + B.letterGap * Kit.scale
|
||||||
|
if bold then Kit.textBold(fname, label, lx, ty, ink)
|
||||||
|
else Kit.text(fname, label, lx, ty, ink) end
|
||||||
|
end
|
||||||
|
elseif hasLabel then
|
||||||
|
local shown = Kit.ellipsize(fname, label, w - B.labelInset * Kit.scale)
|
||||||
|
if opts.align == "left" then
|
||||||
|
local lx = x + B.labelPad * Kit.scale
|
||||||
|
if bold then Kit.textBold(fname, shown, lx, ty, ink)
|
||||||
|
else Kit.text(fname, shown, lx, ty, ink) end
|
||||||
|
elseif bold then
|
||||||
|
Kit.textCenterBold(fname, shown, x, ty, w, ink)
|
||||||
|
else
|
||||||
|
Kit.textCenter(fname, shown, x, ty, w, ink)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not enabled then return false end
|
if not enabled then return false end
|
||||||
@@ -661,7 +730,6 @@ function Kit.button(x, y, w, h, label, opts)
|
|||||||
and Kit._activateId == opts.id)
|
and Kit._activateId == opts.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A small square control: +/- steppers, arrow cyclers, the row X.
|
|
||||||
function Kit.stepper(x, y, w, h, glyph, opts)
|
function Kit.stepper(x, y, w, h, glyph, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
opts.kind = opts.kind or "ghost"
|
opts.kind = opts.kind or "ghost"
|
||||||
@@ -669,31 +737,13 @@ function Kit.stepper(x, y, w, h, glyph, opts)
|
|||||||
return Kit.button(x, y, w, h, glyph, opts)
|
return Kit.button(x, y, w, h, glyph, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A pill toggle (badges, dex SEEN/OWN, sub-tabs). `on` inverts it.
|
local CHIP_OPTS = { face = "chip", font = "micro" }
|
||||||
|
|
||||||
function Kit.chip(x, y, w, h, label, on, color, id)
|
function Kit.chip(x, y, w, h, label, on, color, id)
|
||||||
audit("control", x, y, w, h, label)
|
CHIP_OPTS.active = on
|
||||||
local focused = id and Kit.focusable(id, x, y, w, h) or false
|
CHIP_OPTS.color = color
|
||||||
local c = color or PAL.line
|
CHIP_OPTS.id = id
|
||||||
if G then
|
return Kit.button(x, y, w, h, label, CHIP_OPTS)
|
||||||
local hot = focused or Kit.hover(x, y, w, h)
|
|
||||||
if on then
|
|
||||||
Theme.fillRounded(x, y, w, h, c, 1)
|
|
||||||
Theme.emboss(x, y, w, h, 1)
|
|
||||||
Kit.textCenterBold("micro", label, x,
|
|
||||||
y + (h - Kit.textHeight("micro")) / 2, w, PAL.inverse)
|
|
||||||
else
|
|
||||||
Theme.fillRounded(x, y, w, h, PAL.bg, 1)
|
|
||||||
Theme.strokeRounded(x, y, w, h, c,
|
|
||||||
hot and Theme.A.focus or Theme.A.hover, 1)
|
|
||||||
Kit.textCenterBold("micro", label, x,
|
|
||||||
y + (h - Kit.textHeight("micro")) / 2, w, c)
|
|
||||||
end
|
|
||||||
if hot then
|
|
||||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4, PAL.lineStrong,
|
|
||||||
Theme.A.focus, 2, Theme.radius() + 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return Kit.press(x, y, w, h) or (id ~= nil and Kit._activateId == id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A status label with no interaction: outlined text, the "INSTALLED"/"UPDATE"
|
-- A status label with no interaction: outlined text, the "INSTALLED"/"UPDATE"
|
||||||
|
|||||||
+95
-20
@@ -35,8 +35,8 @@ local PAL = {
|
|||||||
-- it in the same hue, so a card reads as a raised object rather than as an
|
-- it in the same hue, so a card reads as a raised object rather than as an
|
||||||
-- outline drawn on the page. These are still flat fills -- the depth comes
|
-- outline drawn on the page. These are still flat fills -- the depth comes
|
||||||
-- from the value step plus Theme.shadow, not from a gradient.
|
-- from the value step plus Theme.shadow, not from a gradient.
|
||||||
field = { 16, 8, 10 }, -- the page BEHIND the cards
|
field = { 20, 20, 20 }, -- the page BEHIND the cards
|
||||||
bg = { 0, 0, 0 }, -- true black: button rests, field interiors
|
bg = { 20, 20, 20 }, -- light black
|
||||||
surface = { 28, 21, 24 }, -- card interiors
|
surface = { 28, 21, 24 }, -- card interiors
|
||||||
rowBg = { 20, 14, 17 }, -- rows inside a card, one step below it
|
rowBg = { 20, 14, 17 }, -- rows inside a card, one step below it
|
||||||
raised = { 44, 34, 38 }, -- hover feedback
|
raised = { 44, 34, 38 }, -- hover feedback
|
||||||
@@ -82,6 +82,67 @@ Theme.A = {
|
|||||||
disabled = 0.30,
|
disabled = 0.30,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Theme.BUTTON = {
|
||||||
|
radius = 8,
|
||||||
|
ringPad = 2,
|
||||||
|
ringWidth = 2,
|
||||||
|
labelInset = 16,
|
||||||
|
labelPad = 10,
|
||||||
|
iconPad = 0.24,
|
||||||
|
letterGap = 4,
|
||||||
|
disabledA = 0.45,
|
||||||
|
embossHot = 1.3,
|
||||||
|
embossRest = 1,
|
||||||
|
embossDisabled = 0.4,
|
||||||
|
glowHz = 3,
|
||||||
|
glowBase = 0.25,
|
||||||
|
glowAmp = 0.75,
|
||||||
|
iconRestA = 0.85,
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme.CARD = {
|
||||||
|
radius = 14,
|
||||||
|
shadow = true,
|
||||||
|
fill = PAL.surface,
|
||||||
|
fillA = 1,
|
||||||
|
stroke = PAL.line,
|
||||||
|
strokeA = Theme.A.hairline,
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme.CARD_VARIANT = {
|
||||||
|
emphasis = { strokeA = Theme.A.focus },
|
||||||
|
muted = {
|
||||||
|
fill = PAL.bg, fillA = 0.8, stroke = PAL.muted, strokeA = 0.25, shadow = false,
|
||||||
|
},
|
||||||
|
mutedHot = {
|
||||||
|
fill = PAL.bg, fillA = 0.8, stroke = PAL.muted, strokeA = Theme.A.hover, shadow = false,
|
||||||
|
},
|
||||||
|
empty = { fillA = 0, strokeA = 0.22, shadow = false, radius = "ctl" },
|
||||||
|
warn = {
|
||||||
|
fill = PAL.rowBg, stroke = PAL.yellow, strokeA = Theme.A.hover,
|
||||||
|
shadow = false, radius = "ctl",
|
||||||
|
},
|
||||||
|
row = {
|
||||||
|
fill = PAL.rowBg, stroke = PAL.line, strokeA = Theme.A.hairline,
|
||||||
|
shadow = false, radius = "ctl",
|
||||||
|
},
|
||||||
|
rowHover = {
|
||||||
|
fill = PAL.raised, stroke = PAL.line, strokeA = Theme.A.hover,
|
||||||
|
shadow = false, radius = "ctl",
|
||||||
|
},
|
||||||
|
rowSelected = {
|
||||||
|
fill = PAL.ink, strokeA = 0, shadow = false, radius = "ctl",
|
||||||
|
},
|
||||||
|
hairline = {
|
||||||
|
fillA = 0, stroke = PAL.line, strokeA = Theme.A.hairline,
|
||||||
|
shadow = false, radius = "ctl",
|
||||||
|
},
|
||||||
|
badge = {
|
||||||
|
fill = PAL.bg, stroke = PAL.yellow, strokeA = Theme.A.hover,
|
||||||
|
shadow = false, radius = 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
local G = love and love.graphics or nil
|
local G = love and love.graphics or nil
|
||||||
|
|
||||||
local has = {}
|
local has = {}
|
||||||
@@ -123,11 +184,11 @@ end
|
|||||||
-- Controls get the smaller one, containers the larger, so a button never
|
-- Controls get the smaller one, containers the larger, so a button never
|
||||||
-- looks like a card and a card never looks like a button.
|
-- looks like a card and a card never looks like a button.
|
||||||
function Theme.radius()
|
function Theme.radius()
|
||||||
return 8
|
return Theme.BUTTON.radius
|
||||||
end
|
end
|
||||||
|
|
||||||
function Theme.cardRadius()
|
function Theme.cardRadius()
|
||||||
return 14
|
return Theme.CARD.radius
|
||||||
end
|
end
|
||||||
|
|
||||||
-- DROP SHADOW. Three stacked rounded rects at low alpha, each one step wider
|
-- DROP SHADOW. Three stacked rounded rects at low alpha, each one step wider
|
||||||
@@ -206,28 +267,42 @@ end
|
|||||||
-- The design's only container: a rounded surface a few values above the
|
-- The design's only container: a rounded surface a few values above the
|
||||||
-- field, its own drop shadow, and a white hairline. `emphasis` raises the
|
-- field, its own drop shadow, and a white hairline. `emphasis` raises the
|
||||||
-- outline to full white (used for the focused/active card).
|
-- outline to full white (used for the focused/active card).
|
||||||
function Theme.card(x, y, w, h, emphasis)
|
function Theme.card(x, y, w, h, variant)
|
||||||
local r = Theme.cardRadius()
|
local spec = Theme.CARD
|
||||||
Theme.shadow(x, y, w, h, r)
|
local v
|
||||||
Theme.fillRounded(x, y, w, h, PAL.surface, 1, r)
|
if variant == true then
|
||||||
Theme.strokeRounded(x, y, w, h, PAL.line,
|
v = Theme.CARD_VARIANT.emphasis
|
||||||
emphasis and Theme.A.focus or Theme.A.hairline, 1, r)
|
elseif type(variant) == "string" then
|
||||||
|
v = Theme.CARD_VARIANT[variant]
|
||||||
|
elseif type(variant) == "table" then
|
||||||
|
v = variant
|
||||||
|
end
|
||||||
|
local radius = (v and v.radius) or spec.radius
|
||||||
|
if radius == "ctl" then radius = Theme.BUTTON.radius end
|
||||||
|
local fill = (v and v.fill) or spec.fill
|
||||||
|
local fillA = spec.fillA
|
||||||
|
if v and v.fillA ~= nil then fillA = v.fillA end
|
||||||
|
local stroke = (v and v.stroke) or spec.stroke
|
||||||
|
local strokeA = spec.strokeA
|
||||||
|
if v and v.strokeA ~= nil then strokeA = v.strokeA end
|
||||||
|
local shadow = spec.shadow
|
||||||
|
if v and v.shadow ~= nil then shadow = v.shadow end
|
||||||
|
local strokeW = (v and v.strokeW) or 1
|
||||||
|
if fillA > 0 and fill then
|
||||||
|
if shadow then Theme.shadow(x, y, w, h, radius) end
|
||||||
|
Theme.fillRounded(x, y, w, h, fill, fillA, radius)
|
||||||
|
end
|
||||||
|
if strokeA > 0 and stroke then
|
||||||
|
Theme.strokeRounded(x, y, w, h, stroke, strokeA, strokeW, radius)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A list row. Three states, each one rect plus one outline:
|
|
||||||
-- normal one value below the card it sits in, hairline
|
|
||||||
-- hover lifted fill, brighter hairline
|
|
||||||
-- selected WHITE fill (callers print ink = PAL.inverse over it)
|
|
||||||
function Theme.row(x, y, w, h, state)
|
function Theme.row(x, y, w, h, state)
|
||||||
local r = Theme.radius()
|
|
||||||
if state == "selected" then
|
if state == "selected" then
|
||||||
Theme.fillRounded(x, y, w, h, PAL.ink, 1, r)
|
Theme.card(x, y, w, h, "rowSelected")
|
||||||
return PAL.inverse
|
return PAL.inverse
|
||||||
end
|
end
|
||||||
Theme.fillRounded(x, y, w, h,
|
Theme.card(x, y, w, h, state == "hover" and "rowHover" or "row")
|
||||||
state == "hover" and PAL.raised or PAL.rowBg, 1, r)
|
|
||||||
Theme.strokeRounded(x, y, w, h, PAL.line,
|
|
||||||
state == "hover" and Theme.A.hover or Theme.A.hairline, 1, r)
|
|
||||||
return PAL.text
|
return PAL.text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+15
-5
@@ -8,7 +8,7 @@ local SpriteRenderer = require("src.render.SpriteRenderer")
|
|||||||
local NPC = {}
|
local NPC = {}
|
||||||
NPC.__index = NPC
|
NPC.__index = NPC
|
||||||
|
|
||||||
local STEP_FRAMES = 16
|
local STEP_FRAMES = 32
|
||||||
|
|
||||||
local FACING_FROM_RANGE = {
|
local FACING_FROM_RANGE = {
|
||||||
DOWN = "down", UP = "up", LEFT = "left", RIGHT = "right",
|
DOWN = "down", UP = "up", LEFT = "left", RIGHT = "right",
|
||||||
@@ -52,7 +52,16 @@ function NPC:facePlayer(player)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function NPC:update(map, entities)
|
function NPC:update(map, entities)
|
||||||
-- self.stepFrames overrides the shared 16-frame walk for an object whose
|
-- An NPC tile is 32 frames, half the player's rate: TryWalking loads
|
||||||
|
-- WALKANIMATIONCOUNTER with $10 and UpdateSpriteInWalkingAnimation adds
|
||||||
|
-- the 1px step vector once per call (engine/overworld/movement.asm), but
|
||||||
|
-- UpdateSprites runs once per OverworldLoop pass and every pass opens
|
||||||
|
-- with two DelayFrame calls (home/overworld.asm) -- so those 16 ticks
|
||||||
|
-- cost 32 frames for one 16px cell, against AdvancePlayerSprite's 8
|
||||||
|
-- ticks of 2px. That halving is why pokeyellow's NormalPikachuFollow
|
||||||
|
-- needs TryDoubleAddPikachuStepVectorToScreenPixelCoords to keep up.
|
||||||
|
--
|
||||||
|
-- self.stepFrames overrides the shared walk for an object whose
|
||||||
-- step has to stay in phase with something else: Yellow's follower
|
-- step has to stay in phase with something else: Yellow's follower
|
||||||
-- Pikachu takes the player's own step length, halved while it is more
|
-- Pikachu takes the player's own step length, halved while it is more
|
||||||
-- than a cell behind (FastPikachuFollow, engine/pikachu/
|
-- than a cell behind (FastPikachuFollow, engine/pikachu/
|
||||||
@@ -78,7 +87,7 @@ function NPC:update(map, entities)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local d = Collision.DELTA[self.facing]
|
local d = Collision.DELTA[self.facing]
|
||||||
-- 1px per frame at the default length; a shortened step scales instead,
|
-- 1px per 2 frames at the default length; a shortened step scales instead,
|
||||||
-- so the cell still lands on a 16px boundary (Player:update does the
|
-- so the cell still lands on a 16px boundary (Player:update does the
|
||||||
-- same for the bicycle)
|
-- same for the bicycle)
|
||||||
local moved = math.floor(self.progress * 16 * span / stepLen)
|
local moved = math.floor(self.progress * 16 * span / stepLen)
|
||||||
@@ -113,8 +122,9 @@ end
|
|||||||
|
|
||||||
function NPC:walkPhase()
|
function NPC:walkPhase()
|
||||||
if not self.moving then return 0 end
|
if not self.moving then return 0 end
|
||||||
local p = self.progress % 16
|
local stepLen = self.stepFrames or STEP_FRAMES
|
||||||
return (p >= 4 and p < 12) and 1 or 0
|
local p = self.progress % stepLen
|
||||||
|
return (p >= stepLen / 4 and p < stepLen * 3 / 4) and 1 or 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Same contract as Player:pose -- the sheet, position, facing and step
|
-- Same contract as Player:pose -- the sheet, position, facing and step
|
||||||
|
|||||||
@@ -384,12 +384,21 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
|
|||||||
-- NPC instances persist across connection crossings in self.npcPool
|
-- NPC instances persist across connection crossings in self.npcPool
|
||||||
-- (keyed by NPC.id): a neighbor map's wandering ghosts ARE the
|
-- (keyed by NPC.id): a neighbor map's wandering ghosts ARE the
|
||||||
-- objects that become the real NPCs when the player crosses the
|
-- objects that become the real NPCs when the player crosses the
|
||||||
-- seam, so nothing snaps back to its spawn point in view of the
|
-- seam, so a map the player is not entering keeps its ghosts alive
|
||||||
-- survey zoom. Warps rebuild from scratch, like the original's
|
-- in view of the survey zoom. The map being entered does not --
|
||||||
-- per-entry sprite init (home/overworld.asm LoadMapHeader
|
-- crossing a seam runs .loadNewMap -> LoadMapHeader, whose
|
||||||
-- .loadSpriteData).
|
-- .loadSpriteData zeroes the sprite state data and re-seeds every
|
||||||
|
-- SPRITESTATEDATA2_MAPY/MAPX from the map header's object data
|
||||||
|
-- (home/overworld.asm), so an NPC who walked up to the player stands
|
||||||
|
-- on her spawn cell again the next time that map loads (#1028). Only
|
||||||
|
-- the save-side spawn flags survive, and those live in Game.save, not
|
||||||
|
-- here. Warps rebuild the whole pool from scratch.
|
||||||
if not (opts and opts.seamless and self.npcPool) then
|
if not (opts and opts.seamless and self.npcPool) then
|
||||||
self.npcPool = {}
|
self.npcPool = {}
|
||||||
|
elseif fromMapId ~= mapId then
|
||||||
|
for _, obj in ipairs(self.map.def.objects or {}) do
|
||||||
|
self.npcPool[mapId .. "_obj_" .. obj.index] = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
self.npcs = {}
|
self.npcs = {}
|
||||||
for _, obj in ipairs(self.map.def.objects or {}) do
|
for _, obj in ipairs(self.map.def.objects or {}) do
|
||||||
@@ -1986,10 +1995,12 @@ function OverworldState:tryHiddenObject(fx, fy)
|
|||||||
end
|
end
|
||||||
save.hiddenTaken[key] = true
|
save.hiddenTaken[key] = true
|
||||||
local name = Game.data.items[h.item] and Game.data.items[h.item].name or h.item
|
local name = Game.data.items[h.item] and Game.data.items[h.item].name or h.item
|
||||||
-- hidden items always play SFX_GET_ITEM_2 (hidden_items.asm)
|
-- hidden items always play SFX_GET_ITEM_2, and FoundHiddenItemText's
|
||||||
require("src.core.Sound").play(Game.data, "Get_Item2")
|
-- text_asm tail runs it as PlaySoundWaitForCurrent +
|
||||||
|
-- WaitForSoundToFinish once the box has printed (hidden_items.asm)
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s found\n%s!", save.player.name, name)))
|
Strings("%s found\n%s!", save.player.name, name),
|
||||||
|
nil, TextBox.soundOpts(Game, "Get_Item2")))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2001,9 +2012,9 @@ function OverworldState:tryHiddenObject(fx, fy)
|
|||||||
if not save.inventory.COIN_CASE then return false end
|
if not save.inventory.COIN_CASE then return false end
|
||||||
save.hiddenTaken[key] = true
|
save.hiddenTaken[key] = true
|
||||||
save.coins = math.min(9999, (save.coins or 0) + h.coins)
|
save.coins = math.min(9999, (save.coins or 0) + h.coins)
|
||||||
require("src.core.Sound").play(Game.data, "Get_Item2")
|
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s found\n%d coins!", save.player.name, h.coins)))
|
Strings("%s found\n%d coins!", save.player.name, h.coins),
|
||||||
|
nil, TextBox.soundOpts(Game, "Get_Item2")))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2650,10 +2661,11 @@ function OverworldState:talkTo(npc)
|
|||||||
end
|
end
|
||||||
local name = Game.data.items[d.item] and Game.data.items[d.item].name or d.item
|
local name = Game.data.items[d.item] and Game.data.items[d.item].name or d.item
|
||||||
local ddef = Game.data.items[d.item]
|
local ddef = Game.data.items[d.item]
|
||||||
require("src.core.Sound").play(Game.data,
|
-- FoundItemText: text_far, sound_get_item_1, text_end (pick_up_item.asm)
|
||||||
(ddef and ddef.keyItem) and "Get_Key_Item" or "Get_Item1")
|
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
Strings("%s found\n%s!", Game.save.player.name, name)))
|
Strings("%s found\n%s!", Game.save.player.name, name), nil,
|
||||||
|
TextBox.soundOpts(Game,
|
||||||
|
(ddef and ddef.keyItem) and "Get_Key_Item" or "Get_Item1")))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3182,6 +3194,58 @@ local function giveVictoryItem(reward)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- A gym's reward text is not one box. Every gym script carries a sound
|
||||||
|
-- command right after the FIRST label of each reward group
|
||||||
|
-- (scripts/PewterGym.asm PewterGymBrockReceivedBoulderBadgeText's
|
||||||
|
-- sound_level_up, PewterGymReceivedTM34Text's sound_get_item_1, and the
|
||||||
|
-- equivalents in the other seven), and home/text.asm TextCommand_SOUND
|
||||||
|
-- plays it only once that page has typed out, then blocks on
|
||||||
|
-- WaitForSoundToFinish before the next page prints. So the pages
|
||||||
|
-- accumulate and split into separate boxes at the sound points, each box
|
||||||
|
-- chained off the previous one's button press.
|
||||||
|
local function rewardChain()
|
||||||
|
local chain = { boxes = {}, pending = {} }
|
||||||
|
function chain.flush(sound)
|
||||||
|
if #chain.pending > 0 then
|
||||||
|
table.insert(chain.boxes,
|
||||||
|
{ text = table.concat(chain.pending, "\f"), sound = sound })
|
||||||
|
chain.pending = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- one reward group; `sound` rides its first page, where the scripts put it
|
||||||
|
function chain.add(labels, sound)
|
||||||
|
local text = Game.data.text or {}
|
||||||
|
local n = 0
|
||||||
|
for _, label in ipairs(labels or {}) do
|
||||||
|
if text[label] and text[label] ~= "" then
|
||||||
|
table.insert(chain.pending, text[label])
|
||||||
|
n = n + 1
|
||||||
|
if n == 1 and sound then chain.flush(sound) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- the synthetic stand-in line a reward with no `dialogue` shows
|
||||||
|
function chain.line(str, sound)
|
||||||
|
table.insert(chain.pending, str)
|
||||||
|
if sound then chain.flush(sound) end
|
||||||
|
end
|
||||||
|
function chain.push(done)
|
||||||
|
chain.flush()
|
||||||
|
local function step(i)
|
||||||
|
local box = chain.boxes[i]
|
||||||
|
if not box then
|
||||||
|
if done then done() end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local opts = box.sound and TextBox.soundOpts(Game, box.sound) or nil
|
||||||
|
Game.stack:push(TextBox.new(Game, box.text,
|
||||||
|
function() step(i + 1) end, opts))
|
||||||
|
end
|
||||||
|
step(1)
|
||||||
|
end
|
||||||
|
return chain
|
||||||
|
end
|
||||||
|
|
||||||
-- Badges/items awarded after specific battles (data/scripts/victories.lua).
|
-- Badges/items awarded after specific battles (data/scripts/victories.lua).
|
||||||
-- `deactivate` retires unfought gym/dojo trainers the way the originals'
|
-- `deactivate` retires unfought gym/dojo trainers the way the originals'
|
||||||
-- SetEvent / SetEventRange do after the leader victory.
|
-- SetEvent / SetEventRange do after the leader victory.
|
||||||
@@ -3218,44 +3282,31 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
|
|||||||
-- retries the hand-over later (offerGymTm via gyms.lua)
|
-- retries the hand-over later (offerGymTm via gyms.lua)
|
||||||
tmGiven = giveVictoryItem(reward)
|
tmGiven = giveVictoryItem(reward)
|
||||||
end
|
end
|
||||||
local lines = {}
|
local chain = rewardChain()
|
||||||
if reward.dialogue then
|
if reward.dialogue then
|
||||||
local text = Game.data.text or {}
|
chain.add(reward.dialogue, reward.badgeSound)
|
||||||
for _, label in ipairs(reward.dialogue) do
|
|
||||||
if text[label] and text[label] ~= "" then
|
|
||||||
table.insert(lines, text[label])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if reward.item then
|
if reward.item then
|
||||||
for _, label in ipairs(reward.tmPre or {}) do
|
chain.add(reward.tmPre)
|
||||||
if text[label] and text[label] ~= "" then
|
|
||||||
table.insert(lines, text[label])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if tmGiven then
|
if tmGiven then
|
||||||
for _, label in ipairs(reward.tmDialogue or {}) do
|
chain.add(reward.tmDialogue, reward.tmSound)
|
||||||
if text[label] and text[label] ~= "" then
|
else
|
||||||
table.insert(lines, text[label])
|
chain.add({ reward.noRoom })
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif reward.noRoom and text[reward.noRoom] and text[reward.noRoom] ~= "" then
|
|
||||||
table.insert(lines, text[reward.noRoom])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif reward.badge or reward.item then
|
elseif reward.badge or reward.item then
|
||||||
if reward.badge then
|
if reward.badge then
|
||||||
local name = Game.data.items[reward.badge] and Game.data.items[reward.badge].name
|
local name = Game.data.items[reward.badge] and Game.data.items[reward.badge].name
|
||||||
or reward.badge
|
or reward.badge
|
||||||
table.insert(lines, Strings("%s received\nthe %s!", Game.save.player.name, name))
|
chain.line(Strings("%s received\nthe %s!", Game.save.player.name, name),
|
||||||
|
reward.badgeSound)
|
||||||
end
|
end
|
||||||
if tmGiven then
|
if tmGiven then
|
||||||
local name = Game.stringBuffer or reward.item
|
local name = Game.stringBuffer or reward.item
|
||||||
table.insert(lines, Strings("%s received\n%s!", Game.save.player.name, name))
|
chain.line(Strings("%s received\n%s!", Game.save.player.name, name),
|
||||||
|
reward.tmSound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #lines > 0 then
|
chain.push()
|
||||||
Game.stack:push(TextBox.new(Game, table.concat(lines, "\f")))
|
|
||||||
end
|
|
||||||
self:runVictoryHook()
|
self:runVictoryHook()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3266,24 +3317,14 @@ end
|
|||||||
-- show again, then the same GiveItem check decides between the received
|
-- show again, then the same GiveItem check decides between the received
|
||||||
-- lines and the "make room" text.
|
-- lines and the "make room" text.
|
||||||
function OverworldState:offerGymTm(reward, done)
|
function OverworldState:offerGymTm(reward, done)
|
||||||
local text = Game.data.text or {}
|
local chain = rewardChain()
|
||||||
local lines = {}
|
chain.add(reward.tmPre)
|
||||||
local function addLine(label)
|
|
||||||
if label and text[label] and text[label] ~= "" then
|
|
||||||
table.insert(lines, text[label])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for _, label in ipairs(reward.tmPre or {}) do addLine(label) end
|
|
||||||
if giveVictoryItem(reward) then
|
if giveVictoryItem(reward) then
|
||||||
for _, label in ipairs(reward.tmDialogue or {}) do addLine(label) end
|
chain.add(reward.tmDialogue, reward.tmSound)
|
||||||
else
|
else
|
||||||
addLine(reward.noRoom)
|
chain.add({ reward.noRoom })
|
||||||
end
|
|
||||||
if #lines > 0 then
|
|
||||||
Game.stack:push(TextBox.new(Game, table.concat(lines, "\f"), done))
|
|
||||||
elseif done then
|
|
||||||
done()
|
|
||||||
end
|
end
|
||||||
|
chain.push(done)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- pokered reloads the map after every battle, re-running the map
|
-- pokered reloads the map after every battle, re-running the map
|
||||||
@@ -3661,8 +3702,9 @@ function OverworldState:onStepComplete()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- wild encounters in grass, on water while surfing, or -- on indoor
|
-- wild encounters in grass, on water while surfing (at the map's water
|
||||||
-- maps whose tileset is not FOREST -- on EVERY tile
|
-- rate, which is 0 on every indoor map), or -- on indoor maps whose
|
||||||
|
-- tileset is not FOREST -- on every other tile
|
||||||
-- (wild_encounters.asm: caves, towers, the Mansion, Power Plant)
|
-- (wild_encounters.asm: caves, towers, the Mansion, Power Plant)
|
||||||
-- The cooldown is checked after all other step processing so repel and
|
-- The cooldown is checked after all other step processing so repel and
|
||||||
-- movement systems continue to advance during the protected steps.
|
-- movement systems continue to advance during the protected steps.
|
||||||
@@ -3670,10 +3712,10 @@ function OverworldState:onStepComplete()
|
|||||||
local encDef = Game.data.encounters[self.map.id]
|
local encDef = Game.data.encounters[self.map.id]
|
||||||
local enc
|
local enc
|
||||||
local indoor = Game.data.field.indoorEncounters
|
local indoor = Game.data.field.indoorEncounters
|
||||||
if p.surfing and encDef and encDef.water and self.map:isWaterCell(p.cellX, p.cellY) then
|
if self.map:isGrassCell(p.cellX, p.cellY) then
|
||||||
enc = self:rollEncounter({ grass = encDef.water }, "water")
|
|
||||||
elseif self.map:isGrassCell(p.cellX, p.cellY) then
|
|
||||||
enc = self:rollEncounter(encDef, "grass")
|
enc = self:rollEncounter(encDef, "grass")
|
||||||
|
elseif p.surfing and self.map:isWaterCell(p.cellX, p.cellY) then
|
||||||
|
enc = self:rollEncounter({ grass = encDef and encDef.water }, "water")
|
||||||
elseif indoor and self.map.def.index >= indoor.firstIndoorMap
|
elseif indoor and self.map.def.index >= indoor.firstIndoorMap
|
||||||
and self.map.def.tileset ~= indoor.excludedTileset then
|
and self.map.def.tileset ~= indoor.excludedTileset then
|
||||||
enc = self:rollEncounter(encDef, "indoor")
|
enc = self:rollEncounter(encDef, "indoor")
|
||||||
@@ -3794,10 +3836,10 @@ function OverworldState:checkBadgeGate()
|
|||||||
if Game.save.inventory[g.badge] then
|
if Game.save.inventory[g.badge] then
|
||||||
if not Game.save.flags[passedFlag] then
|
if not Game.save.flags[passedFlag] then
|
||||||
Game.save.flags[passedFlag] = true
|
Game.save.flags[passedFlag] = true
|
||||||
-- Route22GateGuardGoRightAheadText plays sound_get_item_1
|
-- Route22GateGuardGoRightAheadText carries sound_get_item_1
|
||||||
require("src.core.Sound").play(Game.data, "Get_Item1")
|
|
||||||
Game.stack:push(TextBox.new(Game,
|
Game.stack:push(TextBox.new(Game,
|
||||||
t["_" .. g.passText] or Strings("Go right ahead!")))
|
t["_" .. g.passText] or Strings("Go right ahead!"),
|
||||||
|
nil, TextBox.soundOpts(Game, "Get_Item1")))
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -3822,11 +3864,11 @@ function OverworldState:checkBadgeGate()
|
|||||||
and Game.data.items[guard.badge].name or guard.badge
|
and Game.data.items[guard.badge].name or guard.badge
|
||||||
if Game.save.inventory[guard.badge] then
|
if Game.save.inventory[guard.badge] then
|
||||||
Game.save.flags[guard.event] = true
|
Game.save.flags[guard.event] = true
|
||||||
-- Route23OhThatIsTheBadgeText plays sound_get_item_1
|
-- Route23OhThatIsTheBadgeText carries sound_get_item_1
|
||||||
require("src.core.Sound").play(Game.data, "Get_Item1")
|
|
||||||
local text = (t["_" .. g.passText] or
|
local text = (t["_" .. g.passText] or
|
||||||
Strings("Oh! That is the\n{RAM}!")):gsub("{RAM:wNameBuffer}", badgeName)
|
Strings("Oh! That is the\n{RAM}!")):gsub("{RAM:wNameBuffer}", badgeName)
|
||||||
Game.stack:push(TextBox.new(Game, text))
|
Game.stack:push(TextBox.new(Game, text,
|
||||||
|
nil, TextBox.soundOpts(Game, "Get_Item1")))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
-- Route23YouDontHaveTheBadgeYetText plays SFX_DENIED
|
-- Route23YouDontHaveTheBadgeYetText plays SFX_DENIED
|
||||||
@@ -4470,7 +4512,7 @@ function OverworldState:scriptMove(entity, dir, tiles, onDone)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A step-in-place beat: the entity plays one walk-cycle animation (16
|
-- A step-in-place beat: the entity plays one walk-cycle animation (32
|
||||||
-- frames) without translating, keeping its current facing. Ports the
|
-- frames) without translating, keeping its current facing. Ports the
|
||||||
-- NPC_CHANGE_FACING movement byte (engine/overworld/movement.asm
|
-- NPC_CHANGE_FACING movement byte (engine/overworld/movement.asm
|
||||||
-- ChangeFacingDirection -> zero-delta TryWalking), used for Oak marching
|
-- ChangeFacingDirection -> zero-delta TryWalking), used for Oak marching
|
||||||
@@ -4483,7 +4525,7 @@ end
|
|||||||
|
|
||||||
-- Advance scripted moves in two phases so a chained step (a new move
|
-- Advance scripted moves in two phases so a chained step (a new move
|
||||||
-- queued by a completing move's onDone) begins the SAME frame the
|
-- queued by a completing move's onDone) begins the SAME frame the
|
||||||
-- previous one ends -- back-to-back 16-frame tiles like the GB's
|
-- previous one ends -- back-to-back 32-frame tiles like the GB's
|
||||||
-- simulated-joypad / NPC scripted movement, with no idle frame between
|
-- simulated-joypad / NPC scripted movement, with no idle frame between
|
||||||
-- tiles. Phase 1 retires finished moves (which may chain new ones);
|
-- tiles. Phase 1 retires finished moves (which may chain new ones);
|
||||||
-- phase 2 then starts every not-yet-moving move.
|
-- phase 2 then starts every not-yet-moving move.
|
||||||
|
|||||||
@@ -821,11 +821,13 @@ end
|
|||||||
function PikachuFollower.onFanClubEntered(game, ow)
|
function PikachuFollower.onFanClubEntered(game, ow)
|
||||||
if not (GameVersion.isYellow() and ow.map
|
if not (GameVersion.isYellow() and ow.map
|
||||||
and ow.map.id == "POKEMON_FAN_CLUB") then return end
|
and ow.map.id == "POKEMON_FAN_CLUB") then return end
|
||||||
|
local active = game.save.pikachuMapScriptActive
|
||||||
|
game.save.pikachuMapScriptActive = true
|
||||||
|
if active then return end
|
||||||
local starter = PikachuFollower.starterInParty(game.save)
|
local starter = PikachuFollower.starterInParty(game.save)
|
||||||
local npc = findFollower(ow)
|
local npc = findFollower(ow)
|
||||||
if not npc or (starter and starter.status) then return end
|
if not npc or (starter and starter.status) then return end
|
||||||
ow.pikachuFanClubScene = true
|
ow.pikachuFanClubScene = true
|
||||||
ow.pikachuMapScriptActive = true
|
|
||||||
ow.player.facing = "down"
|
ow.player.facing = "down"
|
||||||
for _, other in ipairs(ow.npcs or {}) do
|
for _, other in ipairs(ow.npcs or {}) do
|
||||||
if other.def and other.def.name == "POKEMONFANCLUB_SEEL" then
|
if other.def and other.def.name == "POKEMONFANCLUB_SEEL" then
|
||||||
@@ -844,6 +846,13 @@ 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
|
||||||
end
|
end
|
||||||
|
-- BillsHouse_CheckMetBill (scripts/BillsHouse.asm:22-40) sets
|
||||||
|
-- BIT_PIKACHU_MAP_SCRIPT_ACTIVE and rets nz before it looks at
|
||||||
|
-- EVENT_MET_BILL_2; the bit rides sMainData, so a reload inside the house
|
||||||
|
-- resumes at BillsHouseScript1's bare ret (#919).
|
||||||
|
local active = game.save.pikachuMapScriptActive
|
||||||
|
game.save.pikachuMapScriptActive = true
|
||||||
|
if active then return end
|
||||||
if game.save.flags.EVENT_MET_BILL_2 then return end
|
if game.save.flags.EVENT_MET_BILL_2 then return end
|
||||||
-- BillsHouseScript0 (scripts/BillsHouse.asm:41-47) only runs the confused
|
-- BillsHouseScript0 (scripts/BillsHouse.asm:41-47) only runs the confused
|
||||||
-- walk while CheckPikachuStatusCondition comes back clear
|
-- walk while CheckPikachuStatusCondition comes back clear
|
||||||
|
|||||||
@@ -191,12 +191,12 @@ return function(game)
|
|||||||
return cond()
|
return cond()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Jump the flash clock to just under FLASH_FRAMES (220) once the shot is
|
-- Jump the flash clock to just under FLASH_FRAMES (368) once the shot is
|
||||||
-- taken. EvolutionState:update only compares self.t against that constant,
|
-- taken. EvolutionState:update only compares self.t against that constant,
|
||||||
-- so this ends the animation early without touching the palette logic.
|
-- so this ends the animation early without touching the palette logic.
|
||||||
local function skipToEnd()
|
local function skipToEnd()
|
||||||
local st = evoTop()
|
local st = evoTop()
|
||||||
if st then st.t = 214 end
|
if st then st.t = 362 end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function startEvo(species, into)
|
local function startEvo(species, into)
|
||||||
@@ -212,7 +212,7 @@ return function(game)
|
|||||||
|
|
||||||
-- ---- case 1: the reported case, SGB, mid-flash --------------------------
|
-- ---- case 1: the reported case, SGB, mid-flash --------------------------
|
||||||
local weedle = startEvo("WEEDLE", "KAKUNA")
|
local weedle = startEvo("WEEDLE", "KAKUNA")
|
||||||
U.wait(24) -- into the flash, far short of FLASH_FRAMES = 220
|
U.wait(24) -- into the flash, far short of FLASH_FRAMES = 368
|
||||||
U.shot(game, DIR .. "/evo279_1_flash_sgb.png")
|
U.shot(game, DIR .. "/evo279_1_flash_sgb.png")
|
||||||
local c1 = probe("flash/SGB", {
|
local c1 = probe("flash/SGB", {
|
||||||
monYellow1 = YELLOW[2], monYellow2 = YELLOW[3],
|
monYellow1 = YELLOW[2], monYellow2 = YELLOW[3],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
-- LINK_STATE_TRADING) skip that poll and cannot be cancelled.
|
-- LINK_STATE_TRADING) skip that poll and cannot be cancelled.
|
||||||
--
|
--
|
||||||
-- Case 1 (level path, cancelable): open EvolutionState directly, wait past
|
-- Case 1 (level path, cancelable): open EvolutionState directly, wait past
|
||||||
-- the 80-frame pre-animLoop delay (still well under FLASH_FRAMES=220),
|
-- the 80-frame pre-animLoop delay (still well under FLASH_FRAMES=368),
|
||||||
-- press B, and assert the mon stays CATERPIE with the "stopped evolving"
|
-- press B, and assert the mon stays CATERPIE with the "stopped evolving"
|
||||||
-- text on screen.
|
-- text on screen.
|
||||||
-- Case 1b: after cancel, checkParty with no level-ups must not re-offer;
|
-- Case 1b: after cancel, checkParty with no level-ups must not re-offer;
|
||||||
@@ -81,7 +81,7 @@ return function(game)
|
|||||||
Evolution.evolve(game, mon, "METAPOD", function() done1 = true end)
|
Evolution.evolve(game, mon, "METAPOD", function() done1 = true end)
|
||||||
|
|
||||||
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end
|
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end
|
||||||
U.wait(100) -- past the 80-frame pre-animLoop delay, still under 220
|
U.wait(100) -- past the 80-frame pre-animLoop delay, still under 368
|
||||||
U.log("case1 flash", "t=", top().t, "species=", mon.species)
|
U.log("case1 flash", "t=", top().t, "species=", mon.species)
|
||||||
U.shot(game, DIR .. "/evo213_1_evolving.png")
|
U.shot(game, DIR .. "/evo213_1_evolving.png")
|
||||||
|
|
||||||
@@ -133,8 +133,8 @@ return function(game)
|
|||||||
local done2 = false
|
local done2 = false
|
||||||
Evolution.evolve(game, mon2, "METAPOD", function() done2 = true end)
|
Evolution.evolve(game, mon2, "METAPOD", function() done2 = true end)
|
||||||
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case2)") end
|
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case2)") end
|
||||||
-- let the full flash run (FLASH_FRAMES=220) without pressing B
|
-- let the full flash run (FLASH_FRAMES=368) without pressing B
|
||||||
waitFor(function() return not evoTop() end, 400)
|
waitFor(function() return not evoTop() end, 500)
|
||||||
if not waitFor(function() return findText("evolved into") ~= nil end, 120) then
|
if not waitFor(function() return findText("evolved into") ~= nil end, 120) then
|
||||||
error("Congratulations text not shown (case2)")
|
error("Congratulations text not shown (case2)")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ return function(game)
|
|||||||
Evolution.evolve(game, mon, "GYARADOS", function() done1 = true end)
|
Evolution.evolve(game, mon, "GYARADOS", function() done1 = true end)
|
||||||
|
|
||||||
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end
|
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end
|
||||||
-- let the full flash run (FLASH_FRAMES=220) with no input, then apply
|
-- let the full flash run (FLASH_FRAMES=368) with no input, then apply
|
||||||
waitFor(function() return not evoTop() end, 400)
|
waitFor(function() return not evoTop() end, 500)
|
||||||
if not waitFor(function() return findText("evolved into") ~= nil end, 120) then
|
if not waitFor(function() return findText("evolved into") ~= nil end, 120) then
|
||||||
error("Congratulations text not shown (case1)")
|
error("Congratulations text not shown (case1)")
|
||||||
end
|
end
|
||||||
@@ -124,7 +124,7 @@ return function(game)
|
|||||||
local done2 = false
|
local done2 = false
|
||||||
Evolution.evolve(game, mon2, "GYARADOS", function() done2 = true end)
|
Evolution.evolve(game, mon2, "GYARADOS", function() done2 = true end)
|
||||||
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case2)") end
|
if not waitFor(evoTop, 300) then error("EvolutionState never opened (case2)") end
|
||||||
waitFor(function() return not evoTop() end, 400)
|
waitFor(function() return not evoTop() end, 500)
|
||||||
if not waitFor(function() return findText("evolved into") ~= nil end, 120) then
|
if not waitFor(function() return findText("evolved into") ~= nil end, 120) then
|
||||||
error("Congratulations text not shown (case2)")
|
error("Congratulations text not shown (case2)")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
-- Driver: a trainer who walked up to the player must be back on her spawn
|
||||||
|
-- cell the next time her map loads, connection crossings included (#1028).
|
||||||
|
-- .loadNewMap calls LoadMapHeader for a seam crossing exactly as a warp does,
|
||||||
|
-- and .loadSpriteData zeroes the sprite state data and re-seeds every
|
||||||
|
-- SPRITESTATEDATA2_MAPY/MAPX from the map header's object data
|
||||||
|
-- (pokered home/overworld.asm), so only the save-side defeat flag survives.
|
||||||
|
--
|
||||||
|
-- Route 3's Lass (data/maps/objects/Route3.asm object_event 23, 4,
|
||||||
|
-- SPRITE_COOLTRAINER_F, STAY, LEFT, ..., OPP_LASS) has sight range 4, so
|
||||||
|
-- standing above the third Bug Catcher at (19,4) is inside her line and she
|
||||||
|
-- walks dist-1 = 3 cells west to (20,4) -- the reporter's exact setup.
|
||||||
|
--
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/trainer_reset_bug1028_test.lua \
|
||||||
|
-- POKEPORT_IDENTITY=bug1028 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
|
||||||
|
return function(game)
|
||||||
|
local U = dofile("tests/drivers/util.lua")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local Zoom = require("src.render.Zoom")
|
||||||
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||||
|
|
||||||
|
local MAP = "ROUTE_3"
|
||||||
|
local TARGET = 6
|
||||||
|
local SPAWN = { x = 23, y = 4 }
|
||||||
|
local STAND = { x = 19, y = 4 }
|
||||||
|
|
||||||
|
local pass = true
|
||||||
|
local function check(label, ok)
|
||||||
|
if not ok then pass = false end
|
||||||
|
U.log(ok and "PASS" or "FAIL", label)
|
||||||
|
return ok
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lass()
|
||||||
|
local ow = game.overworld
|
||||||
|
for _, n in ipairs(ow and ow.npcs or {}) do
|
||||||
|
if n.def and n.def.index == TARGET then return n end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function at(n) return n and (n.cellX .. "," .. n.cellY) or "absent" end
|
||||||
|
|
||||||
|
-- hold a direction until cond() or the budget runs out, then let the
|
||||||
|
-- half-finished step land
|
||||||
|
local function holdUntil(btn, cond, budget)
|
||||||
|
local first = true
|
||||||
|
for _ = 1, budget or 900 do
|
||||||
|
if cond() then break end
|
||||||
|
if first then table.insert(game.input.pressQueue, btn); first = false end
|
||||||
|
game.input.state[btn] = true
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
game.input.state[btn] = false
|
||||||
|
for _ = 1, 40 do
|
||||||
|
local ow = game.overworld
|
||||||
|
if ow and not ow.player.moving then break end
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
U.wait(4)
|
||||||
|
return cond()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function walkTo(btn, axis, want, budget)
|
||||||
|
return holdUntil(btn, function()
|
||||||
|
local p = game.overworld and game.overworld.player
|
||||||
|
return p and p[axis] == want and not p.moving
|
||||||
|
end, budget)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function onMap(id)
|
||||||
|
return game.overworld and game.overworld.map and game.overworld.map.id == id
|
||||||
|
end
|
||||||
|
|
||||||
|
-- mash A until the battle stack unwinds back to the overworld
|
||||||
|
local function mashToOverworld(budget)
|
||||||
|
for _ = 1, budget or 4000 do
|
||||||
|
if game.stack:top() == game.overworld and not game.overworld.engaging then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(3)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- absolute zoom: reset first so every shot frames the same amount of world
|
||||||
|
local function survey(n)
|
||||||
|
Zoom.reset()
|
||||||
|
U.wait(2)
|
||||||
|
for _ = 1, n do game:zoomStep(-1) end
|
||||||
|
U.wait(30)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- back up to the sighting cell so the before/after shots share a camera
|
||||||
|
local function returnToStand()
|
||||||
|
walkTo("right", "cellX", 11)
|
||||||
|
walkTo("up", "cellY", 4)
|
||||||
|
walkTo("right", "cellX", STAND.x)
|
||||||
|
end
|
||||||
|
|
||||||
|
game.save.party = {
|
||||||
|
Pokemon.new(game.data, "CHARIZARD", 80),
|
||||||
|
Pokemon.new(game.data, "SNORLAX", 80),
|
||||||
|
}
|
||||||
|
game.save.player.name = "MATT"
|
||||||
|
|
||||||
|
U.teleport(game, MAP, STAND.x, STAND.y, "right")
|
||||||
|
U.wait(20)
|
||||||
|
|
||||||
|
-- only the target Lass may engage: the reporter has already cleared the
|
||||||
|
-- Bug Catchers and Youngsters ahead of her
|
||||||
|
local ow = game.overworld
|
||||||
|
for _, n in ipairs(ow.npcs) do
|
||||||
|
if n.def.trainerClass and n.def.index ~= TARGET then
|
||||||
|
game.save.defeatedTrainers[n.id] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local l = lass()
|
||||||
|
check("Lass starts on her spawn cell " .. SPAWN.x .. "," .. SPAWN.y,
|
||||||
|
l ~= nil and l.cellX == SPAWN.x and l.cellY == SPAWN.y)
|
||||||
|
U.log("spawn:", at(l))
|
||||||
|
|
||||||
|
-- she sights the player and walks up, then the battle runs
|
||||||
|
for _ = 1, 600 do
|
||||||
|
if ow.engaging then break end
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
check("the Lass sighted the player at " .. STAND.x .. "," .. STAND.y, ow.engaging == true)
|
||||||
|
check("the battle ran to completion", mashToOverworld())
|
||||||
|
U.wait(30)
|
||||||
|
|
||||||
|
l = lass()
|
||||||
|
local movedX, movedY = l and l.cellX, l and l.cellY
|
||||||
|
U.log("after the walk-up she stands at", at(l))
|
||||||
|
check("she is off her spawn cell after walking up",
|
||||||
|
l ~= nil and not (movedX == SPAWN.x and movedY == SPAWN.y))
|
||||||
|
check("she is recorded as defeated", game.save.defeatedTrainers[l.id] == true)
|
||||||
|
|
||||||
|
survey(4)
|
||||||
|
U.shot(game, DIR .. "/bug1028_1_walked_up.png")
|
||||||
|
Zoom.reset()
|
||||||
|
U.wait(5)
|
||||||
|
|
||||||
|
-- west along row 4, down the single gap at column 11, then west on row 9
|
||||||
|
-- to the Pewter City seam
|
||||||
|
walkTo("left", "cellX", 11)
|
||||||
|
walkTo("down", "cellY", 9)
|
||||||
|
check("reached the descent column", game.overworld.player.cellX == 11
|
||||||
|
and game.overworld.player.cellY == 9)
|
||||||
|
|
||||||
|
-- crossing x=0 westward is the seam: crossConnection, not a warp
|
||||||
|
holdUntil("left", function() return onMap("PEWTER_CITY") end, 1200)
|
||||||
|
check("crossed the seam into Pewter City", onMap("PEWTER_CITY"))
|
||||||
|
|
||||||
|
survey(12)
|
||||||
|
U.shot(game, DIR .. "/bug1028_2_pewter_survey.png")
|
||||||
|
Zoom.reset()
|
||||||
|
U.wait(5)
|
||||||
|
|
||||||
|
-- and back east into Route 3, the map load that must re-seed her
|
||||||
|
holdUntil("right", function() return onMap(MAP) end, 1200)
|
||||||
|
check("crossed back into Route 3", onMap(MAP))
|
||||||
|
U.wait(30)
|
||||||
|
|
||||||
|
l = lass()
|
||||||
|
U.log("after the return crossing she stands at", at(l))
|
||||||
|
check("she is back on her spawn cell " .. SPAWN.x .. "," .. SPAWN.y,
|
||||||
|
l ~= nil and l.cellX == SPAWN.x and l.cellY == SPAWN.y)
|
||||||
|
check("she is still recorded as defeated",
|
||||||
|
l ~= nil and game.save.defeatedTrainers[l.id] == true)
|
||||||
|
|
||||||
|
-- same cell, same zoom as shot 1: she should now be three cells further
|
||||||
|
-- east, and being defeated she must not re-engage on the way back in
|
||||||
|
returnToStand()
|
||||||
|
check("walked back to the sighting cell",
|
||||||
|
game.overworld.player.cellX == STAND.x
|
||||||
|
and game.overworld.player.cellY == STAND.y)
|
||||||
|
check("a defeated trainer does not re-engage", game.overworld.engaging ~= true)
|
||||||
|
|
||||||
|
survey(4)
|
||||||
|
U.shot(game, DIR .. "/bug1028_3_reset.png")
|
||||||
|
|
||||||
|
Zoom.reset()
|
||||||
|
U.log(pass and "RESULT PASS" or "RESULT FAIL")
|
||||||
|
love.event.quit(pass and 0 or 1)
|
||||||
|
while true do coroutine.yield() end
|
||||||
|
end
|
||||||
@@ -20,7 +20,7 @@ local PartyMenu = require("src.ui.PartyMenu")
|
|||||||
|
|
||||||
-- minimal stack/input doubles matching the StateStack and Input surfaces,
|
-- minimal stack/input doubles matching the StateStack and Input surfaces,
|
||||||
-- plus an overworld stub: PALLET_TOWN's OVERWORLD tileset passes
|
-- plus an overworld stub: PALLET_TOWN's OVERWORLD tileset passes
|
||||||
-- CheckIfInOutsideMap, and the badges cover the list-time HM gates
|
-- CheckIfInOutsideMap
|
||||||
local function newGame(moves, inventory)
|
local function newGame(moves, inventory)
|
||||||
local game = {
|
local game = {
|
||||||
data = { pokemon = { LAPRAS = { name = "LAPRAS" },
|
data = { pokemon = { LAPRAS = { name = "LAPRAS" },
|
||||||
@@ -87,14 +87,15 @@ openSubmenu(pm2)
|
|||||||
same(actions(pm2.subItems), { "stats", "switch" },
|
same(actions(pm2.subItems), { "stats", "switch" },
|
||||||
"no field moves: the submenu is just STATS/SWITCH")
|
"no field moves: the submenu is just STATS/SWITCH")
|
||||||
|
|
||||||
-- the badge gates still filter the list: the same Lapras without the
|
-- GetMonFieldMoves is badge-blind: the same Lapras without the badges
|
||||||
-- badges keeps its moves but shows none of them
|
-- still lists both HMs, and .outOfBattleMovePointers refuses on
|
||||||
|
-- selection instead (#1022)
|
||||||
local noBadges = newGame(
|
local noBadges = newGame(
|
||||||
{ { id = "STRENGTH", pp = 15 }, { id = "SURF", pp = 15 } })
|
{ { id = "STRENGTH", pp = 15 }, { id = "SURF", pp = 15 } })
|
||||||
local pm3 = PartyMenu.new(noBadges, {})
|
local pm3 = PartyMenu.new(noBadges, {})
|
||||||
noBadges.stack:push(pm3)
|
noBadges.stack:push(pm3)
|
||||||
openSubmenu(pm3)
|
openSubmenu(pm3)
|
||||||
same(actions(pm3.subItems), { "stats", "switch" },
|
same(actions(pm3.subItems), { "strength", "surf", "stats", "switch" },
|
||||||
"ungated badges keep the HM moves out of the submenu")
|
"the HM moves are listed without the badges (#1022)")
|
||||||
|
|
||||||
T.finish("party_fieldmove_order_bug792")
|
T.finish("party_fieldmove_order_bug792")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ local T = require("tests.modkit")
|
|||||||
local boxes = {}
|
local boxes = {}
|
||||||
package.loaded["src.render.TextBox"] = {
|
package.loaded["src.render.TextBox"] = {
|
||||||
new = function(_, s, done) return { text = s, onDone = done } end,
|
new = function(_, s, done) return { text = s, onDone = done } end,
|
||||||
|
soundOpts = function() return {} end,
|
||||||
}
|
}
|
||||||
package.loaded["src.core.Sound"] = { play = function() end }
|
package.loaded["src.core.Sound"] = { play = function() end }
|
||||||
package.loaded["src.inventory.Bag"] = {
|
package.loaded["src.inventory.Bag"] = {
|
||||||
|
|||||||
@@ -187,6 +187,13 @@ local function runOne(seed)
|
|||||||
local function drive(side)
|
local function drive(side)
|
||||||
local bt = side.bt
|
local bt = side.bt
|
||||||
if bt.result then return end
|
if bt.result then return end
|
||||||
|
local top = side.game.stack:top()
|
||||||
|
if top and top.forceSwitch and top.party then
|
||||||
|
for i, mon in ipairs(top.party) do
|
||||||
|
if mon.hp > 0 then top.index = i break end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
if bt.phase ~= "menu" then side.menuFrames = 0 end
|
if bt.phase ~= "menu" then side.menuFrames = 0 end
|
||||||
if bt.phase == "moveSelect" then
|
if bt.phase == "moveSelect" then
|
||||||
local usable = {}
|
local usable = {}
|
||||||
|
|||||||
@@ -181,6 +181,13 @@ local function playMatch(hostEntry, guestEntry, rnd, label)
|
|||||||
local function drive(side)
|
local function drive(side)
|
||||||
local bt = side.bt
|
local bt = side.bt
|
||||||
if bt.result then return end
|
if bt.result then return end
|
||||||
|
local top = side.game.stack:top()
|
||||||
|
if top and top.forceSwitch and top.party then
|
||||||
|
for i, mon in ipairs(top.party) do
|
||||||
|
if mon.hp > 0 then top.index = i break end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
if bt.phase ~= "menu" then side.menuFrames = 0 end
|
if bt.phase ~= "menu" then side.menuFrames = 0 end
|
||||||
if bt.phase == "moveSelect" then
|
if bt.phase == "moveSelect" then
|
||||||
local usable = {}
|
local usable = {}
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ do
|
|||||||
local game = uiGame({ mon })
|
local game = uiGame({ mon })
|
||||||
Bag.add(game.save, "RARE_CANDY", 1)
|
Bag.add(game.save, "RARE_CANDY", 1)
|
||||||
game.stack:push(BagMenu.new(game))
|
game.stack:push(BagMenu.new(game))
|
||||||
for _ = 1, 600 do
|
for _ = 1, 800 do
|
||||||
local top = game.stack:top()
|
local top = game.stack:top()
|
||||||
if not top then break end
|
if not top then break end
|
||||||
pressed = { a = true }
|
pressed = { a = true }
|
||||||
|
|||||||
@@ -584,7 +584,9 @@ check(not fpm.submenu and forced == fgame.save.party[1],
|
|||||||
-- ------- issues #320/#385: the STRENGTH texts print over the party menu
|
-- ------- issues #320/#385: the STRENGTH texts print over the party menu
|
||||||
do
|
do
|
||||||
local owStub = { strengthActive = false,
|
local owStub = { strengthActive = false,
|
||||||
map = { def = { tileset = "OVERWORLD" } }, dark = false }
|
map = { def = { tileset = "OVERWORLD" } }, dark = false,
|
||||||
|
partyKnows = function(self, id) return self.knows == id end,
|
||||||
|
knows = "STRENGTH" }
|
||||||
local sgame = partyGame()
|
local sgame = partyGame()
|
||||||
sgame.overworld = owStub
|
sgame.overworld = owStub
|
||||||
sgame.data.text = {} -- the strength texts fall back to Strings sources
|
sgame.data.text = {} -- the strength texts fall back to Strings sources
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ function Link.prepare(data)
|
|||||||
return Input
|
return Input
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function steerReplacement(game)
|
||||||
|
local top = game.stack:top()
|
||||||
|
if not (top and top.forceSwitch and top.party) then return end
|
||||||
|
for i, mon in ipairs(top.party) do
|
||||||
|
if mon.hp > 0 then top.index = i return end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- run a full lockstep battle over a loopback pair, mashing A on both
|
-- run a full lockstep battle over a loopback pair, mashing A on both
|
||||||
-- sides, and report whether any turn's hashes disagreed
|
-- sides, and report whether any turn's hashes disagreed
|
||||||
function Link.lockstep(gameA, gameB, opts)
|
function Link.lockstep(gameA, gameB, opts)
|
||||||
@@ -80,7 +88,9 @@ function Link.lockstep(gameA, gameB, opts)
|
|||||||
while (resA == nil or resB == nil) and guard < limit do
|
while (resA == nil or resB == nil) and guard < limit do
|
||||||
guard = guard + 1
|
guard = guard + 1
|
||||||
Input.pressed = { a = true }
|
Input.pressed = { a = true }
|
||||||
|
steerReplacement(gameA)
|
||||||
gameA.stack:update(1 / 60)
|
gameA.stack:update(1 / 60)
|
||||||
|
steerReplacement(gameB)
|
||||||
gameB.stack:update(1 / 60)
|
gameB.stack:update(1 / 60)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -315,12 +315,19 @@ do
|
|||||||
local Renderer = require("src.render.Renderer")
|
local Renderer = require("src.render.Renderer")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
local OW = require("src.world.OverworldController")
|
local OW = require("src.world.OverworldController")
|
||||||
|
-- the reward text is a CHAIN of boxes split at each gym script's sound
|
||||||
|
-- command, so walk it: read a box's pages, close it, let its onDone push
|
||||||
|
-- the next one
|
||||||
local function stackedDialogue()
|
local function stackedDialogue()
|
||||||
local top = Game.stack:top()
|
|
||||||
if not (top and top.pages) then return "" end
|
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for _, page in ipairs(top.pages) do
|
local top = Game.stack:top()
|
||||||
parts[#parts + 1] = table.concat(page, "\n")
|
while top and top.pages do
|
||||||
|
for _, page in ipairs(top.pages) do
|
||||||
|
parts[#parts + 1] = table.concat(page, "\n")
|
||||||
|
end
|
||||||
|
Game.stack:pop()
|
||||||
|
if top.onDone then top.onDone() end
|
||||||
|
top = Game.stack:top()
|
||||||
end
|
end
|
||||||
return table.concat(parts, "\n")
|
return table.concat(parts, "\n")
|
||||||
end
|
end
|
||||||
|
|||||||
+10
-8
@@ -176,7 +176,8 @@ check(not onStack(pmSurf), "party menu closes after a successful SURF")
|
|||||||
check(sawText("got on"), "_SurfingGotOnText shown on a successful SURF")
|
check(sawText("got on"), "_SurfingGotOnText shown on a successful SURF")
|
||||||
|
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
-- I: list-time badge filter, CUT/SURF/STRENGTH absent without the badge
|
-- I: GetMonFieldMoves is badge-blind -- CUT/SURF/STRENGTH are listed with or
|
||||||
|
-- without the badge, and .outOfBattleMovePointers refuses on selection (#1022)
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
Game.save.party = { mkMon("SQUIRTLE", "CUT", "SURF", "STRENGTH") }
|
Game.save.party = { mkMon("SQUIRTLE", "CUT", "SURF", "STRENGTH") }
|
||||||
Game.save.inventory = {}
|
Game.save.inventory = {}
|
||||||
@@ -185,8 +186,8 @@ local pmNoBadge = PartyMenu.new(Game)
|
|||||||
Game.stack:push(pmNoBadge)
|
Game.stack:push(pmNoBadge)
|
||||||
frame({ "a" })
|
frame({ "a" })
|
||||||
local actsOff = submenuActions(pmNoBadge)
|
local actsOff = submenuActions(pmNoBadge)
|
||||||
check(not actsOff.cut and not actsOff.surf and not actsOff.strength,
|
check(actsOff.cut and actsOff.surf and actsOff.strength,
|
||||||
"no CUT/SURF/STRENGTH submenu entries without the required badges")
|
"CUT/SURF/STRENGTH submenu entries listed without the badges (#1022)")
|
||||||
popToOW()
|
popToOW()
|
||||||
Game.save.inventory = { CASCADEBADGE = true, SOULBADGE = true, RAINBOWBADGE = true }
|
Game.save.inventory = { CASCADEBADGE = true, SOULBADGE = true, RAINBOWBADGE = true }
|
||||||
local pmBadge = PartyMenu.new(Game)
|
local pmBadge = PartyMenu.new(Game)
|
||||||
@@ -194,7 +195,7 @@ Game.stack:push(pmBadge)
|
|||||||
frame({ "a" })
|
frame({ "a" })
|
||||||
local actsOn = submenuActions(pmBadge)
|
local actsOn = submenuActions(pmBadge)
|
||||||
check(actsOn.cut and actsOn.surf and actsOn.strength,
|
check(actsOn.cut and actsOn.surf and actsOn.strength,
|
||||||
"CUT/SURF/STRENGTH submenu entries appear once the badges are held")
|
"CUT/SURF/STRENGTH submenu entries still there once the badges are held")
|
||||||
|
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
-- I: CUT from the party menu. The Cerulean tree BLOCK (50, at block 9,14)
|
-- I: CUT from the party menu. The Cerulean tree BLOCK (50, at block 9,14)
|
||||||
@@ -471,8 +472,9 @@ local pmLobby = PartyMenu.new(Game)
|
|||||||
Game.stack:push(pmLobby)
|
Game.stack:push(pmLobby)
|
||||||
frame({ "a" })
|
frame({ "a" })
|
||||||
local actsLobby = submenuActions(pmLobby)
|
local actsLobby = submenuActions(pmLobby)
|
||||||
check(not actsLobby.fly, "FLY omitted inside Indigo Plateau lobby")
|
check(actsLobby.fly, "FLY still listed inside Indigo Plateau lobby (#1022)")
|
||||||
check(not actsLobby.escape, "TELEPORT omitted inside Indigo Plateau lobby")
|
check(actsLobby.escape,
|
||||||
|
"TELEPORT still listed inside Indigo Plateau lobby (#1022)")
|
||||||
popToOW()
|
popToOW()
|
||||||
|
|
||||||
-- restore fainted field-move mon for the STRENGTH/SURF cases below
|
-- restore fainted field-move mon for the STRENGTH/SURF cases below
|
||||||
@@ -486,8 +488,8 @@ Game.save.inventory = {
|
|||||||
ow = pushOW("SEAFOAM_ISLANDS_1F", 17, 10, "right")
|
ow = pushOW("SEAFOAM_ISLANDS_1F", 17, 10, "right")
|
||||||
clearCaptured()
|
clearCaptured()
|
||||||
local pmFaintStr = PartyMenu.new(Game)
|
local pmFaintStr = PartyMenu.new(Game)
|
||||||
-- Seafoam is not OVERWORLD, so FLY is omitted: CUT, STRENGTH, SURF, STATS, SWITCH
|
-- move order on the mon: FLY, CUT, STRENGTH, SURF, then STATS, SWITCH
|
||||||
selectSubItem(pmFaintStr, 2)
|
selectSubItem(pmFaintStr, 3)
|
||||||
eq(Game.overworld.strengthActive, true,
|
eq(Game.overworld.strengthActive, true,
|
||||||
"fainted mon can activate STRENGTH from the party menu")
|
"fainted mon can activate STRENGTH from the party menu")
|
||||||
check(sawText("used") and sawText("STRENGTH"),
|
check(sawText("used") and sawText("STRENGTH"),
|
||||||
|
|||||||
@@ -72,13 +72,19 @@ Game.input = Input; Input:init()
|
|||||||
Game.renderer = Renderer; Renderer:init()
|
Game.renderer = Renderer; Renderer:init()
|
||||||
Game.stack = StateStack; StateStack:init()
|
Game.stack = StateStack; StateStack:init()
|
||||||
|
|
||||||
-- concatenates the pages of the TextBox checkVictoryRewards pushed
|
-- concatenates the pages of the box CHAIN checkVictoryRewards pushed: the
|
||||||
|
-- reward text splits into a box per gym-script sound command, so close each
|
||||||
|
-- one and let its onDone push the next
|
||||||
local function stackedDialogue()
|
local function stackedDialogue()
|
||||||
local top = Game.stack:top()
|
|
||||||
if not (top and top.pages) then return "" end
|
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for _, page in ipairs(top.pages) do
|
local top = Game.stack:top()
|
||||||
parts[#parts + 1] = table.concat(page, "\n")
|
while top and top.pages do
|
||||||
|
for _, page in ipairs(top.pages) do
|
||||||
|
parts[#parts + 1] = table.concat(page, "\n")
|
||||||
|
end
|
||||||
|
Game.stack:pop()
|
||||||
|
if top.onDone then top.onDone() end
|
||||||
|
top = Game.stack:top()
|
||||||
end
|
end
|
||||||
return table.concat(parts, "\n")
|
return table.concat(parts, "\n")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ check(#moves == 0, "the parked Pikachu of the confused beat stays put")
|
|||||||
-- BillsHouseScript0 skips the whole entry beat for a statused starter
|
-- BillsHouseScript0 skips the whole entry beat for a statused starter
|
||||||
-- (CheckPikachuStatusCondition, scripts/BillsHouse.asm:45-46)
|
-- (CheckPikachuStatusCondition, scripts/BillsHouse.asm:45-46)
|
||||||
ow.pikachuBillsScene = nil
|
ow.pikachuBillsScene = nil
|
||||||
|
yellowGame.save.pikachuMapScriptActive = nil
|
||||||
moves = {}
|
moves = {}
|
||||||
yellowGame.save.party = { { species = "PIKACHU", hp = 12, status = "PAR" } }
|
yellowGame.save.party = { { species = "PIKACHU", hp = 12, status = "PAR" } }
|
||||||
PikachuFollower.onBillsHouseEnter(yellowGame, ow)
|
PikachuFollower.onBillsHouseEnter(yellowGame, ow)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ local ow = {
|
|||||||
|
|
||||||
Follower.onFanClubEntered({ save = save, data = Data }, ow)
|
Follower.onFanClubEntered({ save = save, data = Data }, ow)
|
||||||
check(ow.pikachuFanClubScene, "Fan Club disables normal Pikachu following")
|
check(ow.pikachuFanClubScene, "Fan Club disables normal Pikachu following")
|
||||||
check(ow.pikachuMapScriptActive, "Fan Club sets the map-script flag")
|
check(save.pikachuMapScriptActive, "Fan Club sets the map-script flag")
|
||||||
eq(ow.player.facing, "down", "Fan Club resets the player direction")
|
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][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[1] and moves[1][2], 1, "Fan Club slide-up spans one tile")
|
||||||
|
|||||||
+2
-2
@@ -2562,8 +2562,8 @@ do
|
|||||||
end
|
end
|
||||||
eq(fanfares, 1, "one caught fanfare per capture")
|
eq(fanfares, 1, "one caught fanfare per capture")
|
||||||
eq(tinks, 3, "three wobble tinks on a $43 capture")
|
eq(tinks, 3, "three wobble tinks on a $43 capture")
|
||||||
check(fanfareAt and caughtAt and fanfareAt < caughtAt,
|
check(fanfareAt and caughtAt and caughtAt < fanfareAt,
|
||||||
"Caught_Mon sounds with the caught text, not after its dismissal")
|
"Caught_Mon sounds once the caught text is out, before its prompt")
|
||||||
eq(cb4.result, "caught", "the capture resolved the battle")
|
eq(cb4.result, "caught", "the capture resolved the battle")
|
||||||
-- the nickname AskName that follows clears it (ClearSprites), so the
|
-- the nickname AskName that follows clears it (ClearSprites), so the
|
||||||
-- assertion is sampled while the caught text is up
|
-- assertion is sampled while the caught text is up
|
||||||
|
|||||||
Reference in New Issue
Block a user