mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
more buggies
This commit is contained in:
+85
-36
@@ -616,7 +616,8 @@ M.SILPH_CO_11F = {
|
|||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
-- Victory Road boulder switches (scripts/VictoryRoad1F/2F/3F.asm):
|
-- Victory Road boulder switches (scripts/VictoryRoad1F/2F/3F.asm):
|
||||||
-- a boulder resting on a switch removes a barrier block; the 3F hole
|
-- a boulder resting on a switch removes a barrier block; the 3F hole
|
||||||
-- drops a boulder down to the 2F switch.
|
-- drops a boulder down to the 2F switch, and also dungeon-warps the
|
||||||
|
-- player (IsPlayerOnDungeonWarp + DungeonWarpList/Data -> 2F at 22,16).
|
||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
|
|
||||||
local function boulderAt(ow, x, y)
|
local function boulderAt(ow, x, y)
|
||||||
@@ -682,6 +683,18 @@ M.VICTORY_ROAD_3F = {
|
|||||||
Commands.show_object(ctx, "VICTORY_ROAD_2F", "VICTORYROAD2F_BOULDER")
|
Commands.show_object(ctx, "VICTORY_ROAD_2F", "VICTORYROAD2F_BOULDER")
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
-- scripts/VictoryRoad3F.asm VictoryRoad3FDefaultScript: the same hole
|
||||||
|
-- is a dungeon warp for the player (wDungeonWarpDestinationMap =
|
||||||
|
-- VICTORY_ROAD_2F, wWhichDungeonWarp = 2 -> DungeonWarpData 22,16).
|
||||||
|
-- Mirrors POKEMON_MANSION_3F.onStep; CAVERN $22 is walkable so the
|
||||||
|
-- fall is onStep, not a collision block.
|
||||||
|
onStep = function(game, ow, x, y)
|
||||||
|
if x == 23 and y == 15 then
|
||||||
|
ow:startWarpTo("VICTORY_ROAD_2F", 22, 16, ow.player.facing)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
@@ -694,6 +707,16 @@ M.VICTORY_ROAD_3F = {
|
|||||||
-- IndigoPlateauLobby.asm) so the champion is re-fightable on rematches,
|
-- IndigoPlateauLobby.asm) so the champion is re-fightable on rematches,
|
||||||
-- like pokered's re-entry cutscene. EVENT_BEAT_CHAMPION_RIVAL stays set
|
-- like pokered's re-entry cutscene. EVENT_BEAT_CHAMPION_RIVAL stays set
|
||||||
-- forever (postgame gates like the Cerulean cave guard read it).
|
-- forever (postgame gates like the Cerulean cave guard read it).
|
||||||
|
--
|
||||||
|
-- pokered forces the fight on map entry: Agatha's victory arms
|
||||||
|
-- SCRIPT_CHAMPIONSROOM_PLAYER_ENTERS (scripts/AgathasRoom.asm), and
|
||||||
|
-- ChampionsRoomPlayerEntersScript then runs RivalEntrance_RLEMovement
|
||||||
|
-- (up 1, right 1, up 3) before ChampionsRoomRivalReadyToBattleScript.
|
||||||
|
-- The rival object has no trainer header / sight range, so without that
|
||||||
|
-- entrance script the player can walk past (issue #99). We arm on the
|
||||||
|
-- run flag instead of Agatha's victory bit: same observable effect for
|
||||||
|
-- first clear and Indigo rematches, without a cross-map script pointer.
|
||||||
|
--
|
||||||
-- scripts/ChampionsRoom.asm ChampionsRoomRivalDefeatedScript ->
|
-- scripts/ChampionsRoom.asm ChampionsRoomRivalDefeatedScript ->
|
||||||
-- OakArrivesScript -> OakCongratulatesPlayerScript ->
|
-- OakArrivesScript -> OakCongratulatesPlayerScript ->
|
||||||
-- OakDisappointedWithRivalScript -> OakComeWithMeScript -> OakExitsScript,
|
-- OakDisappointedWithRivalScript -> OakComeWithMeScript -> OakExitsScript,
|
||||||
@@ -702,43 +725,69 @@ M.VICTORY_ROAD_3F = {
|
|||||||
-- itself is NOT run here: it belongs to the HALL_OF_FAME room script
|
-- itself is NOT run here: it belongs to the HALL_OF_FAME room script
|
||||||
-- (scripts/HallOfFame.asm), so we set a one-shot marker and warp; the room
|
-- (scripts/HallOfFame.asm), so we set a one-shot marker and warp; the room
|
||||||
-- onEnter (M.HALL_OF_FAME below) drives the HoF Oak speech + record.
|
-- onEnter (M.HALL_OF_FAME below) drives the HoF Oak speech + record.
|
||||||
|
local championsRoomRivalScript = {
|
||||||
|
{ "face_player" }, -- 1
|
||||||
|
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
|
||||||
|
{ "jump_if_true", 25 }, -- 3 past end
|
||||||
|
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
||||||
|
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
|
||||||
|
{ "jump_if_false", 25 }, -- 6 past end
|
||||||
|
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
|
||||||
|
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
|
||||||
|
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
|
||||||
|
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
||||||
|
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
||||||
|
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
||||||
|
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
|
||||||
|
-- ChampionsRoomOakArrivesScript: Music_Cities1AlternateTempo
|
||||||
|
-- (Cities1, kept into HALL_OF_FAME like BIT_NO_MAP_MUSIC after
|
||||||
|
-- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in
|
||||||
|
{ "play_music", "Music_Cities1", { keep = true } }, -- 10
|
||||||
|
{ "show_text", "_ChampionsRoomOakText" }, -- 11
|
||||||
|
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 12
|
||||||
|
{ "move_npc", 2, "up", 5 }, -- 13 OakEntranceAfterVictoryMovement
|
||||||
|
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
|
||||||
|
{ "face_object", 1, "left" }, -- 14
|
||||||
|
{ "face_object", 2, "down" }, -- 15
|
||||||
|
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 16
|
||||||
|
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
||||||
|
{ "face_object", 2, "right" }, -- 17
|
||||||
|
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 18
|
||||||
|
-- OakComeWithMeScript: Oak faces down again, then exits up
|
||||||
|
{ "face_object", 2, "down" }, -- 19
|
||||||
|
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 20
|
||||||
|
{ "move_npc", 2, "up", 2 }, -- 21 OakExitChampionsRoomMovement
|
||||||
|
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 22
|
||||||
|
-- hand the induction off to the HALL_OF_FAME room (consumed by its
|
||||||
|
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
|
||||||
|
{ "set_field", "pendingHallOfFame", true }, -- 23
|
||||||
|
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 24
|
||||||
|
}
|
||||||
|
|
||||||
M.CHAMPIONS_ROOM = {
|
M.CHAMPIONS_ROOM = {
|
||||||
|
onEnter = function(game, ow)
|
||||||
|
if game.save.flags.EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN then return end
|
||||||
|
-- Lance entrance warps land at y=7; HoF return warps land at y=0.
|
||||||
|
-- Only the south entry should run ChampionsRoomPlayerEntersScript.
|
||||||
|
if ow.player.cellY < 7 then return end
|
||||||
|
local rival
|
||||||
|
for _, npc in ipairs(ow.npcs) do
|
||||||
|
if npc.def and npc.def.name == "CHAMPIONSROOM_RIVAL" then
|
||||||
|
rival = npc
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- RivalEntrance_RLEMovement, then the battle/Oak script (queued
|
||||||
|
-- separately so talk-script jump indices stay 1-based as written).
|
||||||
|
ow:queueScript({
|
||||||
|
{ "move_player", "up", 1 },
|
||||||
|
{ "move_player", "right", 1 },
|
||||||
|
{ "move_player", "up", 3 },
|
||||||
|
})
|
||||||
|
ow:queueScript(championsRoomRivalScript, { npc = rival })
|
||||||
|
end,
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_CHAMPIONSROOM_RIVAL = {
|
TEXT_CHAMPIONSROOM_RIVAL = championsRoomRivalScript,
|
||||||
{ "face_player" }, -- 1
|
|
||||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
|
|
||||||
{ "jump_if_true", 24 }, -- 3
|
|
||||||
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
|
||||||
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
|
|
||||||
{ "jump_if_false", 24 }, -- 6
|
|
||||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
|
|
||||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
|
|
||||||
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
|
|
||||||
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
|
||||||
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
|
||||||
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
|
||||||
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
|
|
||||||
-- ChampionsRoomOakArrivesScript: Oak's "{PLAYER}!" then reveal + walk in
|
|
||||||
{ "show_text", "_ChampionsRoomOakText" }, -- 10
|
|
||||||
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 11
|
|
||||||
{ "move_npc", 2, "up", 5 }, -- 12 OakEntranceAfterVictoryMovement (3,7)->(3,2)
|
|
||||||
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
|
|
||||||
{ "face_object", 1, "left" }, -- 13
|
|
||||||
{ "face_object", 2, "down" }, -- 14
|
|
||||||
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 15
|
|
||||||
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
|
||||||
{ "face_object", 2, "right" }, -- 16
|
|
||||||
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 17
|
|
||||||
-- OakComeWithMeScript: Oak faces down again, then exits up
|
|
||||||
{ "face_object", 2, "down" }, -- 18
|
|
||||||
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 19
|
|
||||||
{ "move_npc", 2, "up", 2 }, -- 20 OakExitChampionsRoomMovement (3,2)->(3,0)
|
|
||||||
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 21
|
|
||||||
-- hand the induction off to the HALL_OF_FAME room (consumed by its
|
|
||||||
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
|
|
||||||
{ "set_field", "pendingHallOfFame", true }, -- 22
|
|
||||||
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 23
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-18
@@ -672,13 +672,21 @@ M.AGATHAS_ROOM = e4ExitSeal("EVENT_BEAT_AGATHAS_ROOM_TRAINER_0", 0x3b, 0x0e,
|
|||||||
-- EVENT_LANCES_ROOM_LOCK_DOOR: first crossing seals
|
-- EVENT_LANCES_ROOM_LOCK_DOOR: first crossing seals
|
||||||
-- the door behind the player with SFX_GO_INSIDE
|
-- the door behind the player with SFX_GO_INSIDE
|
||||||
-- (24,16) the entrance staircase -> WalkToLance: an auto-walk
|
-- (24,16) the entrance staircase -> WalkToLance: an auto-walk
|
||||||
-- (up 12, left 12, down 7, left 6) landing on (6,11).
|
-- landing on (6,11). Vanilla WalkToLance_RLEList is
|
||||||
-- It marches straight across the room's water decor:
|
-- up 12 / left 12 / down 7 / left 6 and skips collision
|
||||||
-- pokered's CollisionCheckOnLand skips collision
|
-- through void tiles; our camera follows that literally
|
||||||
-- entirely while simulated joypad states run, and our
|
-- and briefly shows the empty upper chamber (reads as
|
||||||
-- scriptMove is collision-free the same way.
|
-- "Gary's room"). We keep the same landing cell but
|
||||||
|
-- route along the open-door floor corridor instead.
|
||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Floor corridor (24,16) -> (6,11) with EVENT_LANCES_ROOM_LOCK_DOOR
|
||||||
|
-- unset (entrance blocks $31/$32). Exposed for parity tests.
|
||||||
|
local LANCE_WALK_IN = {
|
||||||
|
{ "down", 2 }, { "left", 6 }, { "down", 5 }, { "left", 10 },
|
||||||
|
{ "up", 9 }, { "left", 2 }, { "up", 3 },
|
||||||
|
}
|
||||||
|
|
||||||
local function lanceEntranceBlocks(game, ow)
|
local function lanceEntranceBlocks(game, ow)
|
||||||
local locked = game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR
|
local locked = game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR
|
||||||
ow:replaceBlock(2, 6, locked and 0x72 or 0x31)
|
ow:replaceBlock(2, 6, locked and 0x72 or 0x31)
|
||||||
@@ -693,21 +701,24 @@ local function lanceLockDoor(game, ow)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function lanceWalkIn(game, ow)
|
local function lanceWalkIn(game, ow)
|
||||||
ow:scriptMove(ow.player, "up", 12, function()
|
local i = 0
|
||||||
ow:scriptMove(ow.player, "left", 12, function()
|
local function step()
|
||||||
ow:scriptMove(ow.player, "down", 7, function()
|
i = i + 1
|
||||||
ow:scriptMove(ow.player, "left", 6, function()
|
local seg = LANCE_WALK_IN[i]
|
||||||
-- the walk lands on (6,11); vanilla's per-frame coord poll
|
if not seg then
|
||||||
-- then locks the door at once. scriptMove landings do not
|
-- lands on (6,11); vanilla's per-frame coord poll then locks the
|
||||||
-- fire onStep, so lock here.
|
-- door at once. scriptMove landings do not fire onStep, so lock
|
||||||
lanceLockDoor(game, ow)
|
-- here.
|
||||||
end)
|
lanceLockDoor(game, ow)
|
||||||
end)
|
return
|
||||||
end)
|
end
|
||||||
end)
|
ow:scriptMove(ow.player, seg[1], seg[2], step)
|
||||||
|
end
|
||||||
|
step()
|
||||||
end
|
end
|
||||||
|
|
||||||
M.LANCES_ROOM = {
|
M.LANCES_ROOM = {
|
||||||
|
walkInRoute = LANCE_WALK_IN,
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
lanceEntranceBlocks(game, ow)
|
lanceEntranceBlocks(game, ow)
|
||||||
-- the warp arrival lands ON the staircase trigger, and onStep only
|
-- the warp arrival lands ON the staircase trigger, and onStep only
|
||||||
@@ -727,7 +738,14 @@ M.LANCES_ROOM = {
|
|||||||
end
|
end
|
||||||
if not lance or ow:trainerDefeated(lance) then return false end
|
if not lance or ow:trainerDefeated(lance) then return false end
|
||||||
lance:facePlayer(ow.player)
|
lance:facePlayer(ow.player)
|
||||||
ow:engageTrainer(lance, function() end)
|
-- LancesRoomLanceEndBattleScript DisplayTextID -> TalkToTrainer
|
||||||
|
-- after-battle text (rival became champion first). engageTrainer
|
||||||
|
-- only shows the won line, so push the after text on a win.
|
||||||
|
ow:engageTrainer(lance, function()
|
||||||
|
if not ow:trainerDefeated(lance) then return end
|
||||||
|
local after = text(game)._LancesRoomLanceAfterBattleText
|
||||||
|
if after then push(game, after) end
|
||||||
|
end)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if (x == 5 or x == 6) and y == 11 then
|
if (x == 5 or x == 6) and y == 11 then
|
||||||
|
|||||||
+124
-30
@@ -52,6 +52,10 @@ local imageCache = {}
|
|||||||
-- fully transparent rows below a pic's content (the extracted 32x32 back
|
-- fully transparent rows below a pic's content (the extracted 32x32 back
|
||||||
-- pics carry baked-in padding); used to sit the pic flush on the text box
|
-- pics carry baked-in padding); used to sit the pic flush on the text box
|
||||||
local imagePadBottom = {}
|
local imagePadBottom = {}
|
||||||
|
-- fully transparent columns left of a pic's content; at 2x (back pics)
|
||||||
|
-- this is subtracted from hlcoord 1,5 so opaque pixels match hardware,
|
||||||
|
-- where those columns were white-on-white rather than shifted content
|
||||||
|
local imagePadLeft = {}
|
||||||
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be
|
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be
|
||||||
-- rebuilt for any battle pic, whatever code loaded it
|
-- rebuilt for any battle pic, whatever code loaded it
|
||||||
local imageMeta = {}
|
local imageMeta = {}
|
||||||
@@ -63,7 +67,7 @@ local function getImage(path, pal, trueColor)
|
|||||||
if trueColor then pal = nil end
|
if trueColor then pal = nil end
|
||||||
local key = pal and (path .. "#" .. pal.name) or path
|
local key = pal and (path .. "#" .. pal.name) or path
|
||||||
if not imageCache[key] then
|
if not imageCache[key] then
|
||||||
local img, pad = nil, 0
|
local img, pad, padL = nil, 0, 0
|
||||||
if love.image and love.image.newImageData then
|
if love.image and love.image.newImageData then
|
||||||
local id = Assets.imageData(path)
|
local id = Assets.imageData(path)
|
||||||
if pal then
|
if pal then
|
||||||
@@ -86,13 +90,25 @@ local function getImage(path, pal, trueColor)
|
|||||||
if opaque then break end
|
if opaque then break end
|
||||||
bottom = bottom - 1
|
bottom = bottom - 1
|
||||||
end
|
end
|
||||||
|
local left = 0
|
||||||
|
while left < w do
|
||||||
|
local opaque = false
|
||||||
|
for y = 0, h - 1 do
|
||||||
|
local _, _, _, a = id:getPixel(left, y)
|
||||||
|
if a > 0 then opaque = true break end
|
||||||
|
end
|
||||||
|
if opaque then break end
|
||||||
|
left = left + 1
|
||||||
|
end
|
||||||
img = love.graphics.newImage(id)
|
img = love.graphics.newImage(id)
|
||||||
pad = h - 1 - bottom
|
pad = h - 1 - bottom
|
||||||
|
padL = left
|
||||||
else
|
else
|
||||||
img = Assets.image(path) -- headless stub: no pixel access
|
img = Assets.image(path) -- headless stub: no pixel access
|
||||||
end
|
end
|
||||||
imageCache[key] = img
|
imageCache[key] = img
|
||||||
imagePadBottom[img] = pad
|
imagePadBottom[img] = pad
|
||||||
|
imagePadLeft[img] = padL
|
||||||
imageMeta[img] = { path = path, pal = pal, trueColor = trueColor or nil }
|
imageMeta[img] = { path = path, pal = pal, trueColor = trueColor or nil }
|
||||||
end
|
end
|
||||||
return imageCache[key]
|
return imageCache[key]
|
||||||
@@ -101,7 +117,7 @@ end
|
|||||||
-- hot reload: the next getImage re-resolves every pic through the asset
|
-- hot reload: the next getImage re-resolves every pic through the asset
|
||||||
-- search path and re-measures its ground padding
|
-- search path and re-measures its ground padding
|
||||||
function BattleState.invalidate()
|
function BattleState.invalidate()
|
||||||
imageCache, imagePadBottom, imageMeta = {}, {}, {}
|
imageCache, imagePadBottom, imagePadLeft, imageMeta = {}, {}, {}, {}
|
||||||
end
|
end
|
||||||
|
|
||||||
Assets.register(BattleState.invalidate)
|
Assets.register(BattleState.invalidate)
|
||||||
@@ -1667,10 +1683,19 @@ end
|
|||||||
-- only after MoveHitTest / the effect lands (HandleIfPlayerMoveMissed skips
|
-- only after MoveHitTest / the effect lands (HandleIfPlayerMoveMissed skips
|
||||||
-- it on a miss unless EXPLODE_EFFECT); we insert early for blink attachment
|
-- it on a miss unless EXPLODE_EFFECT); we insert early for blink attachment
|
||||||
-- and peel it back on miss/fail paths.
|
-- and peel it back on miss/fail paths.
|
||||||
|
-- Dig/Fly charge leaves the user pic hidden (SLIDE_DOWN / TELEPORT); the
|
||||||
|
-- second-turn DIG/FLY anim restores it via SE_SLIDE_MON_UP / SE_SHOW_MON_PIC.
|
||||||
|
-- Cancelling that row on miss/immune would otherwise leave the digger
|
||||||
|
-- invisible until another anim's resetPicFx (#100).
|
||||||
function BattleState:cancelMoveAnim()
|
function BattleState:cancelMoveAnim()
|
||||||
local row = self.moveAnimRow
|
local row = self.moveAnimRow
|
||||||
if not row then return end
|
if not row then return end
|
||||||
self.moveAnimRow = nil
|
self.moveAnimRow = nil
|
||||||
|
if row.anim == "DIG" or row.anim == "FLY" then
|
||||||
|
local user = row.attackerIsPlayer and self.player or self.enemy
|
||||||
|
local pf = user and self.picFx and self.picFx[user]
|
||||||
|
if pf then pf.hidden = nil end
|
||||||
|
end
|
||||||
for i, item in ipairs(self.queue) do
|
for i, item in ipairs(self.queue) do
|
||||||
if item == row then
|
if item == row then
|
||||||
table.remove(self.queue, i)
|
table.remove(self.queue, i)
|
||||||
@@ -1734,14 +1759,23 @@ end
|
|||||||
|
|
||||||
-- transient pic effects reset when a new animation row starts (each
|
-- transient pic effects reset when a new animation row starts (each
|
||||||
-- PlayAnimation redraws from a clean slate); `minimized` survives --
|
-- PlayAnimation redraws from a clean slate); `minimized` survives --
|
||||||
-- the minimize sprite replaces the pic DATA, so redraws keep it until
|
-- the minimize sprite replaces the pic DATA until reload. Dig/Fly's
|
||||||
-- the pic is reloaded (switch/Transform/ChangeMonPic)
|
-- charge hide is a cleared tilemap that must survive until SE_SHOW_* /
|
||||||
|
-- SE_SLIDE_MON_UP (or cancelMoveAnim on a missed Dig/Fly release):
|
||||||
|
-- clearing it here made Dig pop in before emerge and wrap/bounce (#100).
|
||||||
|
-- Other hides (Acid Armor, etc.) still clear so the next anim restores.
|
||||||
function BattleState:resetPicFx()
|
function BattleState:resetPicFx()
|
||||||
if not self.picFx then return end
|
if not self.picFx then return end
|
||||||
for _, pf in pairs(self.picFx) do
|
local digFly = self.animName == "DIG" or self.animName == "FLY"
|
||||||
|
local digFlyUser = digFly and (self.animAttackerIsPlayer
|
||||||
|
and self.player or self.enemy) or nil
|
||||||
|
for battler, pf in pairs(self.picFx) do
|
||||||
pf.kind, pf.t = nil, nil
|
pf.kind, pf.t = nil, nil
|
||||||
pf.ox, pf.oy = 0, 0
|
pf.ox, pf.oy = 0, 0
|
||||||
pf.hidden = nil
|
local keepHide = battler.invulnerable or battler == digFlyUser
|
||||||
|
if not keepHide then
|
||||||
|
pf.hidden = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2194,6 +2228,43 @@ function BattleState:executeAction(user, target, action)
|
|||||||
self:performMove(user, target, action, false)
|
self:performMove(user, target, action, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Sleep / confusion onomatopoeia from Check*StatusConditions
|
||||||
|
-- (core.asm): side-specific SLP_*/CONF_* anims, not the Rest/Amnesia
|
||||||
|
-- move rows. Player sleep plays the anim before FastAsleepText;
|
||||||
|
-- enemy sleep and both confusion sides print the text first.
|
||||||
|
function BattleState:statusOnomatopoeia(user, kind)
|
||||||
|
local isPlayer = user.isPlayer
|
||||||
|
local anim
|
||||||
|
if kind == "sleep" then
|
||||||
|
anim = isPlayer and "SLP_PLAYER_ANIM" or "SLP_ANIM"
|
||||||
|
else
|
||||||
|
anim = isPlayer and "CONF_PLAYER_ANIM" or "CONF_ANIM"
|
||||||
|
end
|
||||||
|
local text = kind == "sleep"
|
||||||
|
and (displayName(user) .. "\nis fast asleep!")
|
||||||
|
or (displayName(user) .. "\nis confused!")
|
||||||
|
if kind == "sleep" and isPlayer then
|
||||||
|
self:animNext(anim, isPlayer)
|
||||||
|
self:sayNext(text)
|
||||||
|
else
|
||||||
|
self:sayNext(text)
|
||||||
|
self:animNext(anim, isPlayer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Queue status text (+ sleep/confusion FX when the line matches).
|
||||||
|
-- Wake / snap-out / flinch / etc. stay text-only.
|
||||||
|
function BattleState:sayStatusMsg(user, msg)
|
||||||
|
local text = prefixEnemy(msg, user)
|
||||||
|
if msg:find("is fast asleep!", 1, true) then
|
||||||
|
self:statusOnomatopoeia(user, "sleep")
|
||||||
|
elseif msg:find("is confused!", 1, true) then
|
||||||
|
self:statusOnomatopoeia(user, "confused")
|
||||||
|
else
|
||||||
|
self:sayNext(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- The pre-recharge slice of CheckPlayerStatusConditions (core.asm:
|
-- The pre-recharge slice of CheckPlayerStatusConditions (core.asm:
|
||||||
-- 3328-3382): sleep -> freeze -> held-in-place -> flinch, each losing
|
-- 3328-3382): sleep -> freeze -> held-in-place -> flinch, each losing
|
||||||
-- the turn WITHOUT consuming the recharge flag. The disable/confusion/
|
-- the turn WITHOUT consuming the recharge flag. The disable/confusion/
|
||||||
@@ -2212,7 +2283,7 @@ function BattleState:preRechargeChecks(user, target)
|
|||||||
mon.status = nil
|
mon.status = nil
|
||||||
self:sayNext(displayName(user) .. "\nwoke up!")
|
self:sayNext(displayName(user) .. "\nwoke up!")
|
||||||
else
|
else
|
||||||
self:sayNext(displayName(user) .. "\nis fast asleep!")
|
self:statusOnomatopoeia(user, "sleep")
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -2239,7 +2310,7 @@ end
|
|||||||
-- returns true when the user's action is interrupted.
|
-- returns true when the user's action is interrupted.
|
||||||
function BattleState:statusInterrupt(user, target)
|
function BattleState:statusInterrupt(user, target)
|
||||||
local canMove, msgs, selfHit = Status.beforeMove(user, self.rng, self)
|
local canMove, msgs, selfHit = Status.beforeMove(user, self.rng, self)
|
||||||
for _, m in ipairs(msgs) do self:sayNext(prefixEnemy(m, user)) end
|
for _, m in ipairs(msgs) do self:sayStatusMsg(user, m) end
|
||||||
if selfHit then
|
if selfHit then
|
||||||
-- confusion self-hit (core.asm:3428-3434): clears everything in
|
-- confusion self-hit (core.asm:3428-3434): clears everything in
|
||||||
-- status1 except CONFUSED, then HandleSelfConfusionDamage deals a
|
-- status1 except CONFUSED, then HandleSelfConfusionDamage deals a
|
||||||
@@ -2316,11 +2387,16 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
|||||||
user.charging, user.chargeReady, user.invulnerable = nil, nil, nil
|
user.charging, user.chargeReady, user.invulnerable = nil, nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PP: not for continuations, struggle, or called moves
|
-- PP: not for continuations, struggle, called moves, or (under
|
||||||
|
-- gen1_faithful) wild/trainer enemies — pokered DecrementPP only ever
|
||||||
|
-- mutates wBattleMonPP / party PP (engine/battle/decrement_pp.asm).
|
||||||
local isContinuation = releasing
|
local isContinuation = releasing
|
||||||
or (user.thrashTurns and user.thrashTurns > 0 and moveInst == user.thrashMove)
|
or (user.thrashTurns and user.thrashTurns > 0 and moveInst == user.thrashMove)
|
||||||
or moveInst == user.rageMove
|
or moveInst == user.rageMove
|
||||||
if not isContinuation and not moveInst.struggle and not isCalled then
|
local enemyUnlimited = not user.isPlayer
|
||||||
|
and self.ruleset and self.ruleset.enemyUnlimitedPP
|
||||||
|
if not isContinuation and not moveInst.struggle and not isCalled
|
||||||
|
and not enemyUnlimited then
|
||||||
moveInst.pp = math.max(0, moveInst.pp - 1)
|
moveInst.pp = math.max(0, moveInst.pp - 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3540,17 +3616,16 @@ function BattleState:drawBattlerPic(battler, x, y, scale)
|
|||||||
love.graphics.draw(img, quad, x + ox, y + oy, 0, scale, scale)
|
love.graphics.draw(img, quad, x + ox, y + oy, 0, scale, scale)
|
||||||
end
|
end
|
||||||
elseif k == "slideUp" then
|
elseif k == "slideUp" then
|
||||||
-- AnimationSlideMonUp: cyclic upward wrap, one row per 2 frames
|
-- AnimationSlideMonUp (animations.asm): 7 row steps x 2f. After Dig's
|
||||||
local scroll = 8 * math.min(7, math.floor(t / 2) + 1)
|
-- SLIDE_DOWN the tilemap is blank; each step fills the next bottom
|
||||||
local src = math.floor(scroll / scale) % h
|
-- row so the mon emerges from underground. A cyclic wrap of a full
|
||||||
if src == 0 then
|
-- pic looked like a bounce at Dig's end (#100).
|
||||||
love.graphics.draw(img, x + ox, y, 0, scale, scale)
|
local step = math.min(7, math.floor((t - 1) / 2) + 1)
|
||||||
else
|
local visible = math.floor(h * step / 7)
|
||||||
local top = love.graphics.newQuad(0, src, w, h - src, w, h)
|
if visible > 0 then
|
||||||
love.graphics.draw(img, top, x + ox, y, 0, scale, scale)
|
local quad = love.graphics.newQuad(0, h - visible, w, visible, w, h)
|
||||||
local bottom = love.graphics.newQuad(0, 0, w, src, w, h)
|
love.graphics.draw(img, quad, x + ox,
|
||||||
love.graphics.draw(img, bottom, x + ox, y + (h - src) * scale,
|
y + (h - visible) * scale, 0, scale, scale)
|
||||||
0, scale, scale)
|
|
||||||
end
|
end
|
||||||
elseif xscale < 1 then
|
elseif xscale < 1 then
|
||||||
-- AnimationSquishMonPic: columns collapse toward the middle
|
-- AnimationSquishMonPic: columns collapse toward the middle
|
||||||
@@ -3762,6 +3837,20 @@ function BattleState:drawAnimLayer(colorized)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Front/trainer pics: LoadUncompressedSpriteData centers the sprite in
|
||||||
|
-- a 7x7 tile buffer, then CopyUncompressedPicToTilemap places that
|
||||||
|
-- buffer at hlcoord 12,0. Horizontal pad is floor((8-w)/2) tiles;
|
||||||
|
-- vertical pad is (7-h) -- bottom-aligned inside the 7x7.
|
||||||
|
local function enemyPicXY(img, slide, sx, sy)
|
||||||
|
local tw = math.floor(img:getWidth() / 8)
|
||||||
|
local th = math.floor(img:getHeight() / 8)
|
||||||
|
if tw < 1 then tw = 1 elseif tw > 7 then tw = 7 end
|
||||||
|
if th < 1 then th = 1 elseif th > 7 then th = 7 end
|
||||||
|
local hPad = math.floor((8 - tw) / 2)
|
||||||
|
local vPad = 7 - th
|
||||||
|
return 96 + 8 * hPad - slide + sx, 8 * vPad + sy
|
||||||
|
end
|
||||||
|
|
||||||
-- the two mon pics (or the trainer/back pics), offset by the window
|
-- the two mon pics (or the trainer/back pics), offset by the window
|
||||||
-- shake -- on the GB the pics are BG tiles, so they move with it
|
-- shake -- on the GB the pics are BG tiles, so they move with it
|
||||||
function BattleState:drawPicsLayer(slide, sx, sy)
|
function BattleState:drawPicsLayer(slide, sx, sy)
|
||||||
@@ -3779,19 +3868,18 @@ function BattleState:drawPicsLayer(slide, sx, sy)
|
|||||||
g.intersectScissor(0, 0, 160, clipY)
|
g.intersectScissor(0, 0, 160, clipY)
|
||||||
clipped = true
|
clipped = true
|
||||||
end
|
end
|
||||||
-- Enemy: front sprite top-right (GB: pic at hlcoord 12,0).
|
-- Enemy: front sprite in the 7x7 slot at hlcoord 12,0.
|
||||||
if self.showEnemyTrainer and self.trainerPic then
|
if self.showEnemyTrainer and self.trainerPic then
|
||||||
-- the enemy trainer pic holds the mon slot until the send-out
|
-- the enemy trainer pic holds the mon slot until the send-out
|
||||||
local img = self:picImage(self.trainerPic)
|
local img = self:picImage(self.trainerPic)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(img, 160 - 8 - img:getWidth() - slide + sx,
|
local ex, ey = enemyPicXY(img, slide, sx, sy)
|
||||||
math.max(0, 48 - img:getHeight()) + sy)
|
love.graphics.draw(img, ex, ey)
|
||||||
elseif self.enemy and self.enemy.sprite and not self.enemyHidden
|
elseif self.enemy and self.enemy.sprite and not self.enemyHidden
|
||||||
and not self.enemySendingOut and not self:fxHidden(self.enemy) then
|
and not self.enemySendingOut and not self:fxHidden(self.enemy) then
|
||||||
local img = self:picImage(self.enemy.sprite)
|
local img = self:picImage(self.enemy.sprite)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
local ex = 160 - 8 - img:getWidth() - slide + sx
|
local ex, ey = enemyPicXY(img, slide, sx, sy)
|
||||||
local ey = math.max(0, 48 - img:getHeight()) + sy
|
|
||||||
local gs = self:growInScale(self.enemy)
|
local gs = self:growInScale(self.enemy)
|
||||||
if gs then
|
if gs then
|
||||||
-- AnimateSendingOutMon: the downscaled pic keeps its bottom edge
|
-- AnimateSendingOutMon: the downscaled pic keeps its bottom edge
|
||||||
@@ -3806,14 +3894,17 @@ function BattleState:drawPicsLayer(slide, sx, sy)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Player: back sprite at hlcoord 1,5 (x=8), 2x like the GB, feet at y=96.
|
-- Player: back sprite at hlcoord 1,5 (x=8), 2x like the GB, feet at y=96.
|
||||||
|
-- Left transparent columns (matted white) are pulled back so opaque
|
||||||
|
-- pixels land where hardware's white-on-white columns left them.
|
||||||
local hidePlayer = self.safari or self.demo
|
local hidePlayer = self.safari or self.demo
|
||||||
if self.showPlayerBack and self.playerBackPic then
|
if self.showPlayerBack and self.playerBackPic then
|
||||||
-- Red's (or the old man's) back pic until "Go!"; it stays up for
|
-- Red's (or the old man's) back pic until "Go!"; it stays up for
|
||||||
-- the whole safari / catch-demo battle like the original
|
-- the whole safari / catch-demo battle like the original
|
||||||
local img = self:picImage(self.playerBackPic)
|
local img = self:picImage(self.playerBackPic)
|
||||||
local pad = imagePadBottom[self.playerBackPic] or 0
|
local pad = imagePadBottom[self.playerBackPic] or 0
|
||||||
|
local padL = imagePadLeft[self.playerBackPic] or 0
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(img, 8 + slide + sx,
|
love.graphics.draw(img, 8 - padL * 2 + slide + sx,
|
||||||
96 - (img:getHeight() - pad) * 2 + sy, 0, 2, 2)
|
96 - (img:getHeight() - pad) * 2 + sy, 0, 2, 2)
|
||||||
elseif self.player and self.player.sprite and not hidePlayer
|
elseif self.player and self.player.sprite and not hidePlayer
|
||||||
and not self.sendingOut and not self:fxHidden(self.player) then
|
and not self.sendingOut and not self:fxHidden(self.player) then
|
||||||
@@ -3821,17 +3912,19 @@ function BattleState:drawPicsLayer(slide, sx, sy)
|
|||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
-- feet flush on the text box top (y=96), ignoring baked-in padding
|
-- feet flush on the text box top (y=96), ignoring baked-in padding
|
||||||
local pad = imagePadBottom[self.player.sprite] or 0
|
local pad = imagePadBottom[self.player.sprite] or 0
|
||||||
|
local padL = imagePadLeft[self.player.sprite] or 0
|
||||||
|
local px = 8 - padL * 2 + sx
|
||||||
local gs = self:growInScale(self.player)
|
local gs = self:growInScale(self.player)
|
||||||
if gs then
|
if gs then
|
||||||
-- the player-side AnimateSendingOutMon grow (after the poof,
|
-- the player-side AnimateSendingOutMon grow (after the poof,
|
||||||
-- core.asm:1757-1762): feet pinned at y=96, center at x=8+w
|
-- core.asm:1757-1762): feet pinned at y=96, center at x=8+w
|
||||||
if gs > 0 then
|
if gs > 0 then
|
||||||
love.graphics.draw(img, 8 + img:getWidth() * (1 - gs) + sx,
|
love.graphics.draw(img, px + img:getWidth() * (1 - gs),
|
||||||
96 - (img:getHeight() - pad) * 2 * gs + sy,
|
96 - (img:getHeight() - pad) * 2 * gs + sy,
|
||||||
0, 2 * gs, 2 * gs)
|
0, 2 * gs, 2 * gs)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self:drawBattlerPic(self.player, 8 + sx,
|
self:drawBattlerPic(self.player, px,
|
||||||
96 - (img:getHeight() - pad) * 2 + sy, 2)
|
96 - (img:getHeight() - pad) * 2 + sy, 2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3935,7 +4028,8 @@ function BattleState:drawTextArea()
|
|||||||
if self.phase == "messages" and self.current then
|
if self.phase == "messages" and self.current then
|
||||||
local shown = 0
|
local shown = 0
|
||||||
for li, codes in ipairs(self.lines) do
|
for li, codes in ipairs(self.lines) do
|
||||||
local y = 104 + li * 8
|
-- battle text uses every other tile row (hlcoord *,14 / *,16)
|
||||||
|
local y = 112 + (li - 1) * 16
|
||||||
for i = 1, #codes do
|
for i = 1, #codes do
|
||||||
if shown >= self.charIndex then break end
|
if shown >= self.charIndex then break end
|
||||||
Font.drawCode(codes[i], 8 + (i - 1) * 8, y)
|
Font.drawCode(codes[i], 8 + (i - 1) * 8, y)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
-- (super-effective) or adds 1 (not-effective when a better move exists);
|
-- (super-effective) or adds 1 (not-effective when a better move exists);
|
||||||
-- the MINIMUM-scored move is chosen, ties broken uniformly among the
|
-- the MINIMUM-scored move is chosen, ties broken uniformly among the
|
||||||
-- tied minima (core.asm:2971-3002). A non-minimal move is never
|
-- tied minima (core.asm:2971-3002). A non-minimal move is never
|
||||||
-- selectable. Respects PP, Disable and Transform/Mimic move overrides.
|
-- selectable. Respects Disable (and PP only when the ruleset depletes
|
||||||
|
-- enemy PP — Gen 1 AI never reads wEnemyMonPP).
|
||||||
|
|
||||||
local TypeChart = require("src.battle.TypeChart")
|
local TypeChart = require("src.battle.TypeChart")
|
||||||
|
|
||||||
@@ -212,9 +213,13 @@ end
|
|||||||
|
|
||||||
function TrainerAI.chooseMove(battler, rng, battle)
|
function TrainerAI.chooseMove(battler, rng, battle)
|
||||||
rng = rng or love.math.random
|
rng = rng or love.math.random
|
||||||
|
-- Gen 1: SelectEnemyMove never consults wEnemyMonPP; Struggle only when
|
||||||
|
-- every move slot is missing/disabled (core.asm:2957-2999). modern_clean
|
||||||
|
-- depletes enemy PP and falls back to Struggle when none remain.
|
||||||
|
local unlimited = battle and battle.ruleset and battle.ruleset.enemyUnlimitedPP
|
||||||
local usable = {}
|
local usable = {}
|
||||||
for i, mv in ipairs(battler.curMoves) do
|
for i, mv in ipairs(battler.curMoves) do
|
||||||
if mv.pp > 0 and battler.disabledSlot ~= i then
|
if battler.disabledSlot ~= i and (unlimited or mv.pp > 0) then
|
||||||
table.insert(usable, mv)
|
table.insert(usable, mv)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ return {
|
|||||||
randMax = 255,
|
randMax = 255,
|
||||||
-- Focus Energy famously QUARTERS the crit rate instead of x4
|
-- Focus Energy famously QUARTERS the crit rate instead of x4
|
||||||
focusEnergyBug = true,
|
focusEnergyBug = true,
|
||||||
|
-- Wild/trainer enemies never spend PP (DecrementPP only touches the
|
||||||
|
-- player side in pokered). They therefore never Struggle from empty PP.
|
||||||
|
enemyUnlimitedPP = true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ return {
|
|||||||
randMin = 217,
|
randMin = 217,
|
||||||
randMax = 255,
|
randMax = 255,
|
||||||
focusEnergyBug = false,
|
focusEnergyBug = false,
|
||||||
|
-- Gen 2+ style: AI opponents deplete PP and can Struggle when empty.
|
||||||
|
enemyUnlimitedPP = false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,12 @@ function FixedStep:update(dt)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Drop any pending catch-up steps. A hitch inside one logic step (map
|
||||||
|
-- seam setMap / song start) makes the next real-time dt huge; without this
|
||||||
|
-- the while-loop above would advance many walk frames before the next
|
||||||
|
-- draw, which looks like a slide with no leg animation (issue #93).
|
||||||
|
function FixedStep:discardCatchup()
|
||||||
|
self.accum = 0
|
||||||
|
end
|
||||||
|
|
||||||
return FixedStep
|
return FixedStep
|
||||||
|
|||||||
+49
-15
@@ -23,6 +23,29 @@ Renderer.HEIGHT = 144
|
|||||||
-- endFrame composites the padded canvas back with a matching offset.
|
-- endFrame composites the padded canvas back with a matching offset.
|
||||||
Renderer.UPRIGHT_MARGIN = 160
|
Renderer.UPRIGHT_MARGIN = 160
|
||||||
|
|
||||||
|
-- LOVE units + framebuffer pixels + the live unit→pixel ratio.
|
||||||
|
-- Android's DisplayMetrics.density (love.graphics.getDPIScale) is often
|
||||||
|
-- non-integer (1.5, 2.75, …). Integer scaling in units then maps each GB
|
||||||
|
-- pixel to a fractional number of framebuffer pixels → shimmer, uneven /
|
||||||
|
-- non-square "pixels", and movement judder (issue #87). Always derive the
|
||||||
|
-- crisp integer scale from the drawable pixel size; draw with (pixels/dpi)
|
||||||
|
-- so the GPU lands on whole framebuffer pixels. Desktop dpi=1 is unchanged.
|
||||||
|
local function displayMetrics()
|
||||||
|
local ww, wh = love.graphics.getDimensions()
|
||||||
|
local pw, ph = ww, wh
|
||||||
|
if love.graphics.getPixelDimensions then
|
||||||
|
pw, ph = love.graphics.getPixelDimensions()
|
||||||
|
end
|
||||||
|
local dpi = 1
|
||||||
|
if ww > 0 and pw > 0 then
|
||||||
|
dpi = pw / ww
|
||||||
|
elseif love.graphics.getDPIScale then
|
||||||
|
dpi = love.graphics.getDPIScale()
|
||||||
|
end
|
||||||
|
if not dpi or dpi < 1e-6 then dpi = 1 end
|
||||||
|
return ww, wh, pw, ph, dpi
|
||||||
|
end
|
||||||
|
|
||||||
function Renderer:init()
|
function Renderer:init()
|
||||||
self.canvas = love.graphics.newCanvas(self.WIDTH, self.HEIGHT)
|
self.canvas = love.graphics.newCanvas(self.WIDTH, self.HEIGHT)
|
||||||
self.canvas:setFilter("nearest", "nearest")
|
self.canvas:setFilter("nearest", "nearest")
|
||||||
@@ -36,10 +59,12 @@ function Renderer:init()
|
|||||||
self.uprightActive = false
|
self.uprightActive = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- integer scale that fits the GB UI viewport in the window
|
-- Integer framebuffer pixels per GB pixel that fit the window. Zoom /
|
||||||
|
-- GBCFX / callers treat this as the crisp scale; endFrame converts to LOVE
|
||||||
|
-- units via / dpi when drawing.
|
||||||
function Renderer:fitScale()
|
function Renderer:fitScale()
|
||||||
local ww, wh = love.graphics.getDimensions()
|
local _, _, pw, ph = displayMetrics()
|
||||||
return math.max(1, math.floor(math.min(ww / self.WIDTH, wh / self.HEIGHT)))
|
return math.max(1, math.floor(math.min(pw / self.WIDTH, ph / self.HEIGHT)))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- world-pass canvas size in world pixels: enough to fill the window at s'.
|
-- world-pass canvas size in world pixels: enough to fill the window at s'.
|
||||||
@@ -48,9 +73,13 @@ end
|
|||||||
-- background peeking at the receded top/bottom corners; flat mode returns
|
-- background peeking at the receded top/bottom corners; flat mode returns
|
||||||
-- exactly today's size (growth factor is 1 when tilt is inactive).
|
-- exactly today's size (growth factor is 1 when tilt is inactive).
|
||||||
function Renderer:worldViewSize()
|
function Renderer:worldViewSize()
|
||||||
local ww, wh = love.graphics.getDimensions()
|
local ww, wh, _, _, dpi = displayMetrics()
|
||||||
local s = Zoom.scale(self:fitScale())
|
local s = Zoom.scale(self:fitScale()) / dpi
|
||||||
local vw, vh = Zoom.fillViewSize(s, ww, wh)
|
local vw, vh = Zoom.fillViewSize(s, ww, wh)
|
||||||
|
-- Even sizes keep Camera:follow on integer pixels (viewW/2 is integral),
|
||||||
|
-- so unfloored FX/sprite math cannot phase-shimmer against the tile layer.
|
||||||
|
if vw % 2 ~= 0 then vw = vw + 1 end
|
||||||
|
if vh % 2 ~= 0 then vh = vh + 1 end
|
||||||
if Tilt.active() then
|
if Tilt.active() then
|
||||||
local g = Tilt.viewGrowth()
|
local g = Tilt.viewGrowth()
|
||||||
vw, vh = math.ceil(vw * g), math.ceil(vh * g)
|
vw, vh = math.ceil(vw * g), math.ceil(vh * g)
|
||||||
@@ -289,11 +318,14 @@ end
|
|||||||
-- presented through the GBC FX shader as a final pass.
|
-- presented through the GBC FX shader as a final pass.
|
||||||
function Renderer:endFrame(zones, worldZones)
|
function Renderer:endFrame(zones, worldZones)
|
||||||
love.graphics.setCanvas()
|
love.graphics.setCanvas()
|
||||||
local ww, wh = love.graphics.getDimensions()
|
local ww, wh, pw, ph, dpi = displayMetrics()
|
||||||
local S = self:fitScale()
|
-- Sp = integer framebuffer pixels per GB pixel; S = LOVE-unit draw scale.
|
||||||
|
local Sp = self:fitScale()
|
||||||
|
local S = Sp / dpi
|
||||||
local vpw, vph = self.WIDTH * S, self.HEIGHT * S
|
local vpw, vph = self.WIDTH * S, self.HEIGHT * S
|
||||||
local ox = math.floor((ww - vpw) / 2)
|
-- Snap the letterbox origin to a framebuffer pixel, then convert to units.
|
||||||
local oy = math.floor((wh - vph) / 2)
|
local ox = math.floor((pw - self.WIDTH * Sp) / 2) / dpi
|
||||||
|
local oy = math.floor((ph - self.HEIGHT * Sp) / 2) / dpi
|
||||||
local GBCFX = require("src.render.GBCFX")
|
local GBCFX = require("src.render.GBCFX")
|
||||||
-- Forced mono/Classic modes still need a whole-screen zone when a state
|
-- Forced mono/Classic modes still need a whole-screen zone when a state
|
||||||
-- exposes no SGB packets (raw DMG canvas), so sendColors can remap.
|
-- exposes no SGB packets (raw DMG canvas), so sendColors can remap.
|
||||||
@@ -321,9 +353,9 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
love.graphics.rectangle("fill", 0, 0, ww, wh)
|
love.graphics.rectangle("fill", 0, 0, ww, wh)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
|
||||||
-- blit `canvas` at integer `scale` into origin (bx, by), scissored to
|
-- blit `canvas` at `scale` (LOVE units) into origin (bx, by), scissored to
|
||||||
-- the (boxX, boxY, boxW, boxH) screen rect. zoneScale converts zone
|
-- the (boxX, boxY, boxW, boxH) screen rect. zoneScale converts zone
|
||||||
-- coords (canvas-space) into screen pixels.
|
-- coords (canvas-space) into screen units.
|
||||||
local function blit(canvas, scale, zoneList, zoneScale, bx, by, boxX, boxY, boxW, boxH)
|
local function blit(canvas, scale, zoneList, zoneScale, bx, by, boxX, boxY, boxW, boxH)
|
||||||
local shader = zoneList and zoneList[1] and PaletteFX.shader() or nil
|
local shader = zoneList and zoneList[1] and PaletteFX.shader() or nil
|
||||||
if not shader then
|
if not shader then
|
||||||
@@ -355,11 +387,12 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if self.worldActive then
|
if self.worldActive then
|
||||||
local s = Zoom.scale(S)
|
local sp = Zoom.scale(Sp)
|
||||||
|
local s = sp / dpi
|
||||||
local wvw = self.worldCanvas:getWidth()
|
local wvw = self.worldCanvas:getWidth()
|
||||||
local wvh = self.worldCanvas:getHeight()
|
local wvh = self.worldCanvas:getHeight()
|
||||||
local wox = math.floor((ww - wvw * s) / 2)
|
local wox = math.floor((pw - wvw * sp) / 2) / dpi
|
||||||
local woy = math.floor((wh - wvh * s) / 2)
|
local woy = math.floor((ph - wvh * sp) / 2) / dpi
|
||||||
-- Tilt mode projects the ground world pass through the perspective mesh
|
-- Tilt mode projects the ground world pass through the perspective mesh
|
||||||
-- (SGB zones baked in beforehand -- see drawTiltedWorld -- so no zone
|
-- (SGB zones baked in beforehand -- see drawTiltedWorld -- so no zone
|
||||||
-- scissoring here). drawTiltedWorld returns false when tilt is off or
|
-- scissoring here). drawTiltedWorld returns false when tilt is off or
|
||||||
@@ -433,7 +466,8 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
|
|
||||||
if present then
|
if present then
|
||||||
love.graphics.setCanvas()
|
love.graphics.setCanvas()
|
||||||
GBCFX.present(present, S)
|
-- shader grid/shadow math is in framebuffer pixels
|
||||||
|
GBCFX.present(present, Sp)
|
||||||
end
|
end
|
||||||
self.worldActive = false
|
self.worldActive = false
|
||||||
self.uprightActive = false
|
self.uprightActive = false
|
||||||
|
|||||||
+3
-1
@@ -216,7 +216,9 @@ local function useOn(game, battle, id, target, list, moveIndex)
|
|||||||
consume(game, id)
|
consume(game, id)
|
||||||
require("src.core.Sound").play(game.data, "Teleport_Exit1")
|
require("src.core.Sound").play(game.data, "Teleport_Exit1")
|
||||||
ow.player.surfing = false
|
ow.player.surfing = false
|
||||||
ow:warpToHealPoint()
|
-- EnterMapAnim on arrival (BIT_ESCAPE_WARP / special warp path);
|
||||||
|
-- blackouts omit arrive="teleport" (HandleBlackOut has no LeaveMapAnim)
|
||||||
|
ow:warpToHealPoint(nil, { arrive = "teleport" })
|
||||||
else
|
else
|
||||||
showMessages(game, { "OAK: " .. game.save.player.name
|
showMessages(game, { "OAK: " .. game.save.player.name
|
||||||
.. "!\nThis isn't the\ntime to use that!" })
|
.. "!\nThis isn't the\ntime to use that!" })
|
||||||
|
|||||||
+204
-79
@@ -1,14 +1,16 @@
|
|||||||
-- Hall of Fame induction (engine/movie/hall_of_fame.asm): each party
|
-- Hall of Fame induction (engine/movie/hall_of_fame.asm): each party
|
||||||
-- member's front sprite scrolls onto the screen (HoFShowMonOrPlayer's
|
-- member's front sprite scrolls onto the right side of the screen
|
||||||
-- .ScrollPic), then its name/level shows and its cry plays
|
-- (HoFShowMonOrPlayer's .ScrollPic), then HoFDisplayMonInfo draws the
|
||||||
-- (HoFDisplayAndRecordMonInfo). After the last mon, HoFDisplayPlayerStats
|
-- left-side LEVEL/TYPE box, plays the cry, holds, and pops the bottom
|
||||||
-- shows the trainer name, play time, money and Prof. Oak's dex rating.
|
-- "HALL OF FAME" text box before fading to the next mon. After the
|
||||||
-- Plays Music_HallOfFame when the audio data has it. Calls onDone() after
|
-- party, the player pic scrolls in and HoFDisplayPlayerStats shows the
|
||||||
-- popping itself.
|
-- name/time/money boxes plus the dex rating. Plays Music_HallOfFame
|
||||||
|
-- when the audio data has it. Calls onDone() after popping itself.
|
||||||
|
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
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 TypeChart = require("src.battle.TypeChart")
|
||||||
|
|
||||||
local HallOfFame = {}
|
local HallOfFame = {}
|
||||||
HallOfFame.__index = HallOfFame
|
HallOfFame.__index = HallOfFame
|
||||||
@@ -17,6 +19,10 @@ HallOfFame.isOpaque = true
|
|||||||
-- SGB: SetPal_PokemonWholeScreen for the mon on display
|
-- SGB: SetPal_PokemonWholeScreen for the mon on display
|
||||||
function HallOfFame:sgbPalettes(game)
|
function HallOfFame:sgbPalettes(game)
|
||||||
local P = require("src.render.PaletteFX")
|
local P = require("src.render.PaletteFX")
|
||||||
|
if self.phase == "player" or self.phase == "player_stats"
|
||||||
|
or self.phase == "player_dex" or self.phase == "player_rating" then
|
||||||
|
return P.wholeNamed(game.data, "MEWMON")
|
||||||
|
end
|
||||||
local mon = game.save.party[self.index or 0]
|
local mon = game.save.party[self.index or 0]
|
||||||
if mon then
|
if mon then
|
||||||
local c = P.monPal(game.data, mon.species)
|
local c = P.monPal(game.data, mon.species)
|
||||||
@@ -26,17 +32,20 @@ function HallOfFame:sgbPalettes(game)
|
|||||||
return P.wholeNamed(game.data, "MEWMON")
|
return P.wholeNamed(game.data, "MEWMON")
|
||||||
end
|
end
|
||||||
|
|
||||||
local MON_FRAMES = 150 -- ~2.5s per inductee (A advances early)
|
|
||||||
|
|
||||||
-- HoFShowMonOrPlayer's .ScrollPic: hSCX is nudged by e = 4px per
|
-- HoFShowMonOrPlayer's .ScrollPic: hSCX is nudged by e = 4px per
|
||||||
-- DelayFrame (doubled on SGB) until it settles. The back pic (an
|
-- DelayFrame (doubled on SGB) until it settles. The front pic rests at
|
||||||
-- enlarged, blurred 2x scale of the back sprite) sweeps right-to-left
|
-- hlcoord 12,5 (engine/movie/hall_of_fame.asm HoFLoadMonPlayerPicTileIDs).
|
||||||
-- and off the left edge first; tracing the actual hSCX/hSCY math shows
|
|
||||||
-- the real front pic that follows enters from the *left* edge and
|
|
||||||
-- slides *right* into its resting tile, at that same 4px/frame rate --
|
|
||||||
-- that's the half we port here (the back-pic wipe is a VRAM/scroll-
|
|
||||||
-- register trick with no equivalent in this sprite-based renderer).
|
|
||||||
local SCROLL_SPEED = 4 -- px/frame @ 60fps
|
local SCROLL_SPEED = 4 -- px/frame @ 60fps
|
||||||
|
local PIC_X, PIC_Y = 12 * 8, 5 * 8
|
||||||
|
|
||||||
|
-- After HoFDisplayAndRecordMonInfo: 80 DelayFrames, then the bottom
|
||||||
|
-- HALL OF FAME box for 180 DelayFrames, then GBFadeOutToWhite.
|
||||||
|
local INFO_HOLD = 80
|
||||||
|
local HOF_HOLD = 180
|
||||||
|
local FADE_FRAMES = 20
|
||||||
|
|
||||||
|
-- HoFPrintTextAndDelay after each dex line
|
||||||
|
local DEX_HOLD = 120
|
||||||
|
|
||||||
local function tryImage(path)
|
local function tryImage(path)
|
||||||
if not path then return nil end
|
if not path then return nil end
|
||||||
@@ -57,7 +66,7 @@ local function drawTextBlock(text, x, y, maxY)
|
|||||||
for line in (text:gsub("\v", "\n"):gsub("\f", "\n") .. "\n"):gmatch("(.-)\n") do
|
for line in (text:gsub("\v", "\n"):gsub("\f", "\n") .. "\n"):gmatch("(.-)\n") do
|
||||||
if maxY and y > maxY then break end
|
if maxY and y > maxY then break end
|
||||||
Font.draw(line, x, y)
|
Font.draw(line, x, y)
|
||||||
y = y + 10
|
y = y + 8
|
||||||
end
|
end
|
||||||
return y
|
return y
|
||||||
end
|
end
|
||||||
@@ -70,6 +79,10 @@ function HallOfFame.new(game, onDone)
|
|||||||
self.timer = 0
|
self.timer = 0
|
||||||
self.phase = "mons"
|
self.phase = "mons"
|
||||||
self.sprites = {} -- species -> image or false
|
self.sprites = {} -- species -> image or false
|
||||||
|
self.playerPic = tryImage("assets/generated/trainer_card/red.png")
|
||||||
|
self.scrollX = PIC_X
|
||||||
|
self.showHofBanner = false
|
||||||
|
self.fade = 0
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -84,16 +97,21 @@ end
|
|||||||
function HallOfFame:nextMon()
|
function HallOfFame:nextMon()
|
||||||
self.index = self.index + 1
|
self.index = self.index + 1
|
||||||
local mon = self.game.save.party[self.index]
|
local mon = self.game.save.party[self.index]
|
||||||
|
self.showHofBanner = false
|
||||||
|
self.fade = 0
|
||||||
if mon then
|
if mon then
|
||||||
self.timer = MON_FRAMES
|
self.phase = "mons"
|
||||||
|
self.timer = INFO_HOLD
|
||||||
Sound.playCry(self.game.data, mon.species)
|
Sound.playCry(self.game.data, mon.species)
|
||||||
-- scroll the new inductee's pic in from the left (see SCROLL_SPEED)
|
|
||||||
local sprite = self:spriteFor(mon.species)
|
local sprite = self:spriteFor(mon.species)
|
||||||
local w = sprite and sprite:getWidth() or 0
|
local w = sprite and sprite:getWidth() or 56
|
||||||
self.scrollRestX = math.floor((160 - w) / 2)
|
|
||||||
self.scrollX = -w
|
self.scrollX = -w
|
||||||
else
|
else
|
||||||
self.phase = "congrats"
|
-- HoFShowMonOrPlayer with wHoFMonOrPlayer = player
|
||||||
|
self.phase = "player"
|
||||||
|
self.timer = 0
|
||||||
|
local w = self.playerPic and self.playerPic:getWidth() or 56
|
||||||
|
self.scrollX = -w
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -117,77 +135,184 @@ function HallOfFame:dexSeenOwned()
|
|||||||
return seen, owned
|
return seen, owned
|
||||||
end
|
end
|
||||||
|
|
||||||
function HallOfFame:update(dt)
|
function HallOfFame:advanceMonPhase()
|
||||||
local input = self.game.input
|
if self.phase == "mons" and not self.showHofBanner then
|
||||||
if self.phase == "mons" then
|
-- 80-frame info hold done: TextBoxBorder at (2,13) + "HALL OF FAME"
|
||||||
if self.scrollX and self.scrollX < self.scrollRestX then
|
self.showHofBanner = true
|
||||||
self.scrollX = math.min(self.scrollRestX, self.scrollX + SCROLL_SPEED)
|
self.timer = HOF_HOLD
|
||||||
end
|
elseif self.phase == "mons" then
|
||||||
self.timer = self.timer - 1
|
self.phase = "fade"
|
||||||
if input:wasPressed("a") or self.timer <= 0 then
|
self.timer = FADE_FRAMES
|
||||||
self:nextMon()
|
self.fade = 0
|
||||||
end
|
elseif self.phase == "fade" then
|
||||||
elseif input:wasPressed("a") then
|
self:nextMon()
|
||||||
Sound.play(self.game.data, "Press_AB")
|
|
||||||
self.game.stack:pop()
|
|
||||||
if self.onDone then self.onDone() end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function HallOfFame:draw()
|
function HallOfFame:update(dt)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
local input = self.game.input
|
||||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
local skip = input:wasPressed("a")
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
if self.phase == "mons" then
|
|
||||||
Font.draw("HALL OF FAME", (160 - 12 * 8) / 2, 8)
|
|
||||||
local mon = self.game.save.party[self.index]
|
|
||||||
if mon then
|
|
||||||
local def = self.game.data.pokemon[mon.species]
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
local sprite = self:spriteFor(mon.species)
|
|
||||||
if sprite then
|
|
||||||
local w, h = sprite:getDimensions()
|
|
||||||
love.graphics.draw(sprite, self.scrollX or math.floor((160 - w) / 2), 96 - h)
|
|
||||||
end
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
local name = mon.nickname or (def and def.name) or mon.species
|
|
||||||
Font.draw(name, 32, 108)
|
|
||||||
Font.draw((":L%d"):format(mon.level), 112, 108)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- HoFDisplayPlayerStats (no "HALL OF FAME" banner here -- the real
|
|
||||||
-- screen is a fresh ClearScreen): trainer name, play time, money,
|
|
||||||
-- then the POKéDEX seen/owned tally and Prof. Oak's rating text,
|
|
||||||
-- using the same real save-data fields as TrainerCard.lua
|
|
||||||
-- (save.player.name/playTime/money) and PokedexMenu.lua/
|
|
||||||
-- OverworldController:dexRating (save.pokedex.seen/owned).
|
|
||||||
local save = self.game.save
|
|
||||||
local text = self.game.data.text or {}
|
|
||||||
local y = 8
|
|
||||||
Font.draw(save.player.name or "RED", 8, y)
|
|
||||||
y = y + 16
|
|
||||||
local t = math.floor(save.playTime or 0)
|
|
||||||
Font.draw(("PLAY TIME %3d:%02d"):format(math.floor(t / 3600),
|
|
||||||
math.floor(t / 60) % 60), 8, y)
|
|
||||||
y = y + 12
|
|
||||||
Font.draw(("MONEY ¥%d"):format(save.money or 0), 8, y)
|
|
||||||
y = y + 16
|
|
||||||
|
|
||||||
|
if self.phase == "mons" or self.phase == "fade" then
|
||||||
|
if self.phase == "mons" and self.scrollX < PIC_X then
|
||||||
|
self.scrollX = math.min(PIC_X, self.scrollX + SCROLL_SPEED)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self.phase == "fade" then
|
||||||
|
self.timer = self.timer - 1
|
||||||
|
self.fade = 1 - math.max(0, self.timer) / FADE_FRAMES
|
||||||
|
if self.timer <= 0 or skip then self:advanceMonPhase() end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.timer = self.timer - 1
|
||||||
|
if skip or self.timer <= 0 then
|
||||||
|
self:advanceMonPhase()
|
||||||
|
end
|
||||||
|
elseif self.phase == "player" then
|
||||||
|
if self.scrollX < PIC_X then
|
||||||
|
self.scrollX = math.min(PIC_X, self.scrollX + SCROLL_SPEED)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.phase = "player_stats"
|
||||||
|
self.timer = DEX_HOLD
|
||||||
|
elseif self.phase == "player_stats" then
|
||||||
|
-- name / play time / money boxes are up; then DexSeenOwnedText
|
||||||
|
self.timer = self.timer - 1
|
||||||
|
if skip or self.timer <= 0 then
|
||||||
|
self.phase = "player_dex"
|
||||||
|
self.timer = DEX_HOLD
|
||||||
|
end
|
||||||
|
elseif self.phase == "player_dex" then
|
||||||
|
self.timer = self.timer - 1
|
||||||
|
if skip or self.timer <= 0 then
|
||||||
|
self.phase = "player_rating"
|
||||||
|
self.timer = DEX_HOLD
|
||||||
|
end
|
||||||
|
elseif self.phase == "player_rating" then
|
||||||
|
self.timer = self.timer - 1
|
||||||
|
if skip or self.timer <= 0 then
|
||||||
|
-- HoFFadeOutScreenAndMusic -> Credits lead-in (no A wait here)
|
||||||
|
self.game.stack:pop()
|
||||||
|
if self.onDone then self.onDone() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HoFDisplayMonInfo: TextBoxBorder (0,2) b=9,c=10 + LEVEL/TYPE labels
|
||||||
|
function HallOfFame:drawMonInfo(mon)
|
||||||
|
local def = self.game.data.pokemon[mon.species]
|
||||||
|
Font.drawBox(0, 2, 12, 11)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
local name = mon.nickname or (def and def.name) or mon.species
|
||||||
|
Font.draw(name, 1 * 8, 4 * 8)
|
||||||
|
Font.draw("LEVEL/", 2 * 8, 6 * 8)
|
||||||
|
Font.draw("TYPE1/", 2 * 8, 7 * 8)
|
||||||
|
local t1 = def and def.types and def.types[1]
|
||||||
|
local t2 = def and def.types and def.types[2]
|
||||||
|
local dual = t2 and t2 ~= t1
|
||||||
|
if dual then
|
||||||
|
Font.draw("TYPE2/", 2 * 8, 8 * 8)
|
||||||
|
end
|
||||||
|
-- PrintLevelCommon at (8,7): bare level digits (no <LV> tile here)
|
||||||
|
Font.draw(tostring(mon.level), 8 * 8, 7 * 8)
|
||||||
|
-- PrintMonType at (3,9) / +2 rows for type 2
|
||||||
|
if t1 then
|
||||||
|
Font.draw(TypeChart.displayName(t1), 3 * 8, 9 * 8)
|
||||||
|
end
|
||||||
|
if dual then
|
||||||
|
Font.draw(TypeChart.displayName(t2), 3 * 8, 11 * 8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Bottom HALL OF FAME banner: TextBoxBorder (2,13) b=3,c=14
|
||||||
|
function HallOfFame:drawHofBanner()
|
||||||
|
Font.drawBox(2, 13, 16, 5)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
Font.draw("HALL OF FAME", 4 * 8, 15 * 8)
|
||||||
|
end
|
||||||
|
|
||||||
|
function HallOfFame:drawPic(img)
|
||||||
|
if not img then return end
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.draw(img, self.scrollX or PIC_X, PIC_Y)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HoFDisplayPlayerStats boxes + labels (player pic already on the right)
|
||||||
|
function HallOfFame:drawPlayerStats()
|
||||||
|
local save = self.game.save
|
||||||
|
-- name box: TextBoxBorder (5,0) b=2,c=9 → drawBox(5,0,11,4)
|
||||||
|
Font.drawBox(5, 0, 11, 4)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
Font.draw(save.player.name or "RED", 7 * 8, 2 * 8)
|
||||||
|
|
||||||
|
-- play time / money box: TextBoxBorder (0,4) b=6,c=10 → drawBox(0,4,12,8)
|
||||||
|
Font.drawBox(0, 4, 12, 8)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
Font.draw("PLAY TIME", 1 * 8, 6 * 8)
|
||||||
|
local t = math.floor(save.playTime or 0)
|
||||||
|
Font.draw(("%3d:%02d"):format(math.floor(t / 3600), math.floor(t / 60) % 60),
|
||||||
|
5 * 8, 7 * 8)
|
||||||
|
Font.draw("MONEY", 1 * 8, 9 * 8)
|
||||||
|
-- PrintBCDNumber with MONEY_SIGN; port uses ¥ like TrainerCard
|
||||||
|
Font.draw(("¥%d"):format(save.money or 0), 4 * 8, 10 * 8)
|
||||||
|
end
|
||||||
|
|
||||||
|
function HallOfFame:drawDexBox(kind)
|
||||||
|
local save = self.game.save
|
||||||
|
local text = self.game.data.text or {}
|
||||||
|
Font.drawBox(0, 12, 20, 6)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
if kind == "seen" then
|
||||||
local seen, owned = self:dexSeenOwned()
|
local seen, owned = self:dexSeenOwned()
|
||||||
local seenOwned = text._DexSeenOwnedText
|
local seenOwned = text._DexSeenOwnedText
|
||||||
or "POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}"
|
or "POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}"
|
||||||
seenOwned = seenOwned
|
seenOwned = seenOwned
|
||||||
:gsub("{NUM:wDexRatingNumMonsSeen[^}]*}", tostring(seen))
|
:gsub("{NUM:wDexRatingNumMonsSeen[^}]*}", tostring(seen))
|
||||||
:gsub("{NUM:wDexRatingNumMonsOwned[^}]*}", tostring(owned))
|
:gsub("{NUM:wDexRatingNumMonsOwned[^}]*}", tostring(owned))
|
||||||
y = drawTextBlock(seenOwned, 8, y) + 6
|
drawTextBlock(seenOwned, 1 * 8, 14 * 8, 17 * 8)
|
||||||
|
else
|
||||||
|
local _, owned = self:dexSeenOwned()
|
||||||
local ratingHeader = (text._DexRatingText or "POKéDEX Rating{COLON}"):gsub("{COLON}", ":")
|
local ratingHeader = (text._DexRatingText or "POKéDEX Rating{COLON}"):gsub("{COLON}", ":")
|
||||||
Font.draw(ratingHeader, 8, y)
|
Font.draw(ratingHeader, 1 * 8, 14 * 8)
|
||||||
y = y + 12
|
|
||||||
|
|
||||||
local rating = text[dexRatingKey(owned)] or "Keep it up!"
|
local rating = text[dexRatingKey(owned)] or "Keep it up!"
|
||||||
drawTextBlock(rating, 8, y, 136)
|
drawTextBlock(rating, 1 * 8, 15 * 8, 17 * 8)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function HallOfFame:draw()
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||||
|
|
||||||
|
if self.phase == "mons" or self.phase == "fade" then
|
||||||
|
local mon = self.game.save.party[self.index]
|
||||||
|
if mon then
|
||||||
|
self:drawPic(self:spriteFor(mon.species))
|
||||||
|
if self.scrollX >= PIC_X then
|
||||||
|
self:drawMonInfo(mon)
|
||||||
|
if self.showHofBanner then
|
||||||
|
self:drawHofBanner()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.phase == "fade" and self.fade > 0 then
|
||||||
|
love.graphics.setColor(1, 1, 1, self.fade)
|
||||||
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||||
|
end
|
||||||
|
elseif self.phase == "player" then
|
||||||
|
self:drawPic(self.playerPic)
|
||||||
|
elseif self.phase == "player_stats" then
|
||||||
|
self:drawPic(self.playerPic)
|
||||||
|
self:drawPlayerStats()
|
||||||
|
elseif self.phase == "player_dex" then
|
||||||
|
self:drawPic(self.playerPic)
|
||||||
|
self:drawPlayerStats()
|
||||||
|
self:drawDexBox("seen")
|
||||||
|
elseif self.phase == "player_rating" then
|
||||||
|
self:drawPic(self.playerPic)
|
||||||
|
self:drawPlayerStats()
|
||||||
|
self:drawDexBox("rating")
|
||||||
|
end
|
||||||
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,9 @@ function PartyMenu:update(dt)
|
|||||||
if ow and heal then
|
if ow and heal then
|
||||||
self.game.stack:push(Transition.whiteFlash(self.game, nil, function()
|
self.game.stack:push(Transition.whiteFlash(self.game, nil, function()
|
||||||
require("src.core.Sound").play(self.game.data, "Teleport_Exit1")
|
require("src.core.Sound").play(self.game.data, "Teleport_Exit1")
|
||||||
ow:warpToHealPoint()
|
-- EnterMapAnim on arrival (HandleFlyWarpOrDungeonWarp sets
|
||||||
|
-- BIT_FLY_WARP); blackouts must not pass arrive="teleport"
|
||||||
|
ow:warpToHealPoint(nil, { arrive = "teleport" })
|
||||||
end))
|
end))
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -303,6 +303,9 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
|
|||||||
else
|
else
|
||||||
self.player = Player.new(Game.data, x, y, facing)
|
self.player = Player.new(Game.data, x, y, facing)
|
||||||
end
|
end
|
||||||
|
-- crossConnection re-arms this after setMap; clear so a warp/reload
|
||||||
|
-- cannot leave a stale deferred PlayMapMusic pending
|
||||||
|
self.pendingSeamMusic = nil
|
||||||
self.entities = { self.player }
|
self.entities = { self.player }
|
||||||
for _, n in ipairs(self.npcs) do table.insert(self.entities, n) end
|
for _, n in ipairs(self.npcs) do table.insert(self.entities, n) end
|
||||||
|
|
||||||
@@ -808,6 +811,15 @@ function OverworldState:update(dt)
|
|||||||
if entry and (self.player.cellX ~= entry.x or self.player.cellY ~= entry.y) then
|
if entry and (self.player.cellX ~= entry.x or self.player.cellY ~= entry.y) then
|
||||||
self.warpEntryCell = nil
|
self.warpEntryCell = nil
|
||||||
end
|
end
|
||||||
|
-- deferred PlayMapMusic from crossConnection (issue #93)
|
||||||
|
if stepped and self.pendingSeamMusic then
|
||||||
|
local mapId = self.pendingSeamMusic
|
||||||
|
self.pendingSeamMusic = nil
|
||||||
|
if mapId == self.map.id then
|
||||||
|
require("src.core.Music").playMap(Game.data, mapId, Game.save.onBike,
|
||||||
|
self.player.surfing)
|
||||||
|
end
|
||||||
|
end
|
||||||
if stepped and not scripted then
|
if stepped and not scripted then
|
||||||
self:onStepComplete()
|
self:onStepComplete()
|
||||||
end
|
end
|
||||||
@@ -1056,7 +1068,15 @@ function OverworldState:crossConnection(dir, conn)
|
|||||||
if not Map.defPassable(dest, ts, x, y, p.surfing) then
|
if not Map.defPassable(dest, ts, x, y, p.surfing) then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
self:setMap(conn.map, x, y, p.facing, { seamless = true })
|
-- keepMusic: defer PlayMapMusic until the seam step lands. Starting a
|
||||||
|
-- new chip song inside setMap used to hitch the render thread (~200ms)
|
||||||
|
-- so FixedStep catch-up ate the walk frames (issue #93). Threaded synth
|
||||||
|
-- removed most of that hitch; discarding catch-up + deferring the song
|
||||||
|
-- still protects the visible step when neighbor rebuild or the sync
|
||||||
|
-- fallback stalls, and avoids the rare one-frame volume spike from a
|
||||||
|
-- song swap mid-step.
|
||||||
|
self:setMap(conn.map, x, y, p.facing, { seamless = true, keepMusic = true })
|
||||||
|
self.pendingSeamMusic = conn.map
|
||||||
-- place the player one cell before the seam (their old world spot,
|
-- place the player one cell before the seam (their old world spot,
|
||||||
-- which the neighbor strip renders identically) and start the step
|
-- which the neighbor strip renders identically) and start the step
|
||||||
-- into the new map RIGHT NOW so there is no one-frame stall at the
|
-- into the new map RIGHT NOW so there is no one-frame stall at the
|
||||||
@@ -1070,9 +1090,13 @@ function OverworldState:crossConnection(dir, conn)
|
|||||||
p.targetX, p.targetY = x, y
|
p.targetX, p.targetY = x, y
|
||||||
p.moving = true
|
p.moving = true
|
||||||
p.progress = 0
|
p.progress = 0
|
||||||
|
-- fresh walk-cycle clock so the seam step always shows leg frames
|
||||||
|
-- (mid-cycle stand phase would otherwise look like a slide)
|
||||||
|
p.animClock = 0
|
||||||
p.stepFramesCur = Game.save.onBike
|
p.stepFramesCur = Game.save.onBike
|
||||||
and (FieldDefaults.world(Game.data, "bikeStepFrames") or 8)
|
and (FieldDefaults.world(Game.data, "bikeStepFrames") or 8)
|
||||||
or (FieldDefaults.world(Game.data, "stepFrames") or 16)
|
or (FieldDefaults.world(Game.data, "stepFrames") or 16)
|
||||||
|
require("src.core.FixedStep"):discardCatchup()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3175,16 +3199,20 @@ end
|
|||||||
-- Warp to the last heal point (blackout, ESCAPE ROPE, DIG/TELEPORT).
|
-- Warp to the last heal point (blackout, ESCAPE ROPE, DIG/TELEPORT).
|
||||||
-- The heal point is usually an interior, so LAST_MAP exits are re-pointed
|
-- The heal point is usually an interior, so LAST_MAP exits are re-pointed
|
||||||
-- at its remembered town door rather than wherever the player left from.
|
-- at its remembered town door rather than wherever the player left from.
|
||||||
function OverworldState:warpToHealPoint(onDone)
|
--
|
||||||
|
-- opts.arrive = "teleport" for Dig/Teleport/Escape Rope (LeaveMapAnim /
|
||||||
|
-- EnterMapAnim). Blackouts omit it: pret HandleBlackOut only
|
||||||
|
-- GBFadeOutToBlack + PrepareForSpecialWarp + SpecialEnterMap, and never
|
||||||
|
-- sets BIT_FLY_WARP / BIT_DUNGEON_WARP, so EnterMap never runs EnterMapAnim.
|
||||||
|
function OverworldState:warpToHealPoint(onDone, opts)
|
||||||
local heal = self:healPoint()
|
local heal = self:healPoint()
|
||||||
self.player.surfing = false
|
self.player.surfing = false
|
||||||
-- HandleFlyWarpOrDungeonWarp + DisplayPlayerBlackedOutText both clear
|
-- HandleFlyWarpOrDungeonWarp + DisplayPlayerBlackedOutText both clear
|
||||||
-- BIT_ALWAYS_ON_BIKE (home/overworld.asm / home/text_script.asm)
|
-- BIT_ALWAYS_ON_BIKE (home/overworld.asm / home/text_script.asm)
|
||||||
Game.save.forcedBike = nil
|
Game.save.forcedBike = nil
|
||||||
-- rematerializing plays the teleport-in poof (EnterMapAnim in
|
if opts and opts.arrive == "teleport" then
|
||||||
-- engine/overworld/player_animations.asm: SFX_TELEPORT_ENTER_1, then
|
self.arriveWarp = "teleport"
|
||||||
-- ENTER_2 after the spin-down); blackouts take this path too
|
end
|
||||||
self.arriveWarp = "teleport"
|
|
||||||
self:startWarpTo(heal.map, heal.x, heal.y, "down", onDone)
|
self:startWarpTo(heal.map, heal.x, heal.y, "down", onDone)
|
||||||
if heal.outdoor then
|
if heal.outdoor then
|
||||||
self:rememberOutdoor(heal.outdoor.id, heal.outdoor.x, heal.outdoor.y)
|
self:rememberOutdoor(heal.outdoor.id, heal.outdoor.x, heal.outdoor.y)
|
||||||
@@ -3222,9 +3250,9 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
|
|||||||
-- one-step justWarped guard, which only skipped the very next frame's
|
-- one-step justWarped guard, which only skipped the very next frame's
|
||||||
-- check and so let a mon walked back onto the pad re-trigger it.
|
-- check and so let a mon walked back onto the pad re-trigger it.
|
||||||
self.warpEntryCell = { x = x, y = y }
|
self.warpEntryCell = { x = x, y = y }
|
||||||
-- Fly/Teleport/Dig/Escape-Rope/blackout landings poof the player
|
-- Fly/Teleport/Dig/Escape-Rope landings poof the player back in
|
||||||
-- back in (player_animations.asm EnterMapAnim); ordinary door
|
-- (player_animations.asm EnterMapAnim). Blackouts and ordinary
|
||||||
-- warps never take this branch
|
-- door warps never take this branch.
|
||||||
if arriveWarp == "fly" then
|
if arriveWarp == "fly" then
|
||||||
require("src.core.Sound").play(Game.data, "Fly")
|
require("src.core.Sound").play(Game.data, "Fly")
|
||||||
elseif arriveWarp == "teleport" then
|
elseif arriveWarp == "teleport" then
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ stub.graphics = {
|
|||||||
push = noop, pop = noop, translate = noop, scale = noop,
|
push = noop, pop = noop, translate = noop, scale = noop,
|
||||||
rotate = noop, origin = noop, setShader = noop, setScissor = noop,
|
rotate = noop, origin = noop, setShader = noop, setScissor = noop,
|
||||||
getDimensions = function() return 640, 576 end,
|
getDimensions = function() return 640, 576 end,
|
||||||
|
-- dpi=1 desktop default; issue #87 tests override these for Android density
|
||||||
|
getPixelDimensions = function() return 640, 576 end,
|
||||||
|
getDPIScale = function() return 1 end,
|
||||||
}
|
}
|
||||||
|
|
||||||
stub.math = {
|
stub.math = {
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ love.graphics = {
|
|||||||
translate = noop, scale = noop, rotate = noop, origin = noop,
|
translate = noop, scale = noop, rotate = noop, origin = noop,
|
||||||
setScissor = noop, getColor = function() return 1, 1, 1, 1 end,
|
setScissor = noop, getColor = function() return 1, 1, 1, 1 end,
|
||||||
getDimensions = function() return 640, 576 end,
|
getDimensions = function() return 640, 576 end,
|
||||||
|
getPixelDimensions = function() return 640, 576 end,
|
||||||
|
getDPIScale = function() return 1 end,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Fresh copies of the modules that cache a compiled shader or a page set
|
-- Fresh copies of the modules that cache a compiled shader or a page set
|
||||||
|
|||||||
+49
-1
@@ -21,12 +21,17 @@ check(hof and type(hof.onEnter) == "function", "HALL_OF_FAME.onEnter is a functi
|
|||||||
|
|
||||||
local champ = init.get("CHAMPIONS_ROOM")
|
local champ = init.get("CHAMPIONS_ROOM")
|
||||||
check(champ ~= nil, "CHAMPIONS_ROOM map script registered")
|
check(champ ~= nil, "CHAMPIONS_ROOM map script registered")
|
||||||
|
check(champ and type(champ.onEnter) == "function",
|
||||||
|
"CHAMPIONS_ROOM.onEnter is a function (forced rival entrance)")
|
||||||
local rows = champ and champ.talk and champ.talk.TEXT_CHAMPIONSROOM_RIVAL
|
local rows = champ and champ.talk and champ.talk.TEXT_CHAMPIONSROOM_RIVAL
|
||||||
check(type(rows) == "table", "CHAMPIONS_ROOM.talk.TEXT_CHAMPIONSROOM_RIVAL exists")
|
check(type(rows) == "table", "CHAMPIONS_ROOM.talk.TEXT_CHAMPIONSROOM_RIVAL exists")
|
||||||
|
|
||||||
-- (2) the rival cutscene rows contain the pokered beats in order
|
-- (2) the rival cutscene rows contain the pokered beats in order
|
||||||
rows = rows or {}
|
rows = rows or {}
|
||||||
local preds = {
|
local preds = {
|
||||||
|
{ "play_music Music_Cities1 (OakArrives / Music_Cities1AlternateTempo)",
|
||||||
|
function(r) return r[1] == "play_music" and r[2] == "Music_Cities1"
|
||||||
|
and type(r[3]) == "table" and r[3].keep == true end },
|
||||||
{ "show_object CHAMPIONSROOM_OAK",
|
{ "show_object CHAMPIONSROOM_OAK",
|
||||||
function(r) return r[1] == "show_object" and r[3] == "CHAMPIONSROOM_OAK" end },
|
function(r) return r[1] == "show_object" and r[3] == "CHAMPIONSROOM_OAK" end },
|
||||||
{ "move_npc(2,'up',5) OakEntranceAfterVictoryMovement",
|
{ "move_npc(2,'up',5) OakEntranceAfterVictoryMovement",
|
||||||
@@ -82,7 +87,13 @@ check(champToHof, "CHAMPIONS_ROOM has a warp up into HALL_OF_FAME")
|
|||||||
-- (5) functional: HALL_OF_FAME.onEnter consumes the one-shot marker and
|
-- (5) functional: HALL_OF_FAME.onEnter consumes the one-shot marker and
|
||||||
-- queues (does not directly run) the room cutscene.
|
-- queues (does not directly run) the room cutscene.
|
||||||
local queued
|
local queued
|
||||||
local fakeOw = { queueScript = function(self, script, extra) queued = script; self.pendingScript = { script = script } end }
|
local fakeOw = {
|
||||||
|
map = { def = { signs = {} }, widthCells = 10, signAt = {} },
|
||||||
|
queueScript = function(self, script, extra)
|
||||||
|
queued = script
|
||||||
|
self.pendingScript = { script = script }
|
||||||
|
end,
|
||||||
|
}
|
||||||
local fakeGame = { save = { pendingHallOfFame = true } }
|
local fakeGame = { save = { pendingHallOfFame = true } }
|
||||||
hof.onEnter(fakeGame, fakeOw)
|
hof.onEnter(fakeGame, fakeOw)
|
||||||
check(queued ~= nil, "HALL_OF_FAME.onEnter queues a cutscene script when marker set")
|
check(queued ~= nil, "HALL_OF_FAME.onEnter queues a cutscene script when marker set")
|
||||||
@@ -102,4 +113,41 @@ fakeGame.save.pendingHallOfFame = false
|
|||||||
hof.onEnter(fakeGame, fakeOw)
|
hof.onEnter(fakeGame, fakeOw)
|
||||||
check(queued == nil, "HALL_OF_FAME.onEnter does not replay once the marker is consumed")
|
check(queued == nil, "HALL_OF_FAME.onEnter does not replay once the marker is consumed")
|
||||||
|
|
||||||
|
-- (6) Champions Room forced entrance (ChampionsRoomPlayerEntersScript):
|
||||||
|
-- from Lance (y=7), queue RivalEntrance walk then the rival battle script.
|
||||||
|
local champQueued = {}
|
||||||
|
local champOw = {
|
||||||
|
player = { cellX = 3, cellY = 7 },
|
||||||
|
npcs = { { def = { name = "CHAMPIONSROOM_RIVAL" } } },
|
||||||
|
queueScript = function(_, script, extra)
|
||||||
|
champQueued[#champQueued + 1] = { script = script, extra = extra }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local champGame = { save = { flags = {} } }
|
||||||
|
champ.onEnter(champGame, champOw)
|
||||||
|
eq(#champQueued, 2, "CHAMPIONS_ROOM.onEnter queues entrance walk + rival script")
|
||||||
|
local walk = champQueued[1] and champQueued[1].script
|
||||||
|
check(walk and walk[1][1] == "move_player" and walk[1][2] == "up" and walk[1][3] == 1
|
||||||
|
and walk[2][1] == "move_player" and walk[2][2] == "right" and walk[2][3] == 1
|
||||||
|
and walk[3][1] == "move_player" and walk[3][2] == "up" and walk[3][3] == 3,
|
||||||
|
"entrance walk is RivalEntrance_RLEMovement (up 1, right 1, up 3)")
|
||||||
|
check(champQueued[2] and champQueued[2].script == rows,
|
||||||
|
"second queue is TEXT_CHAMPIONSROOM_RIVAL script rows")
|
||||||
|
check(champQueued[2] and champQueued[2].extra
|
||||||
|
and champQueued[2].extra.npc == champOw.npcs[1],
|
||||||
|
"rival script receives the CHAMPIONSROOM_RIVAL npc")
|
||||||
|
|
||||||
|
-- already beaten this run: no forced entrance
|
||||||
|
champQueued = {}
|
||||||
|
champGame.save.flags.EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN = true
|
||||||
|
champ.onEnter(champGame, champOw)
|
||||||
|
eq(#champQueued, 0, "CHAMPIONS_ROOM.onEnter idle after champion beaten this run")
|
||||||
|
|
||||||
|
-- Hall of Fame return (y=0) must not re-trigger even if the run flag is clear
|
||||||
|
champQueued = {}
|
||||||
|
champGame.save.flags.EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN = nil
|
||||||
|
champOw.player.cellY = 0
|
||||||
|
champ.onEnter(champGame, champOw)
|
||||||
|
eq(#champQueued, 0, "CHAMPIONS_ROOM.onEnter ignores Hall of Fame return landing")
|
||||||
|
|
||||||
S.finish()
|
S.finish()
|
||||||
|
|||||||
+26
-1
@@ -221,10 +221,35 @@ do
|
|||||||
check(fe.chooser == nil, "enemy Mimic never opens a chooser")
|
check(fe.chooser == nil, "enemy Mimic never opens a chooser")
|
||||||
eq(tbe.enemy.curMoves[1].id, "SAND_ATTACK",
|
eq(tbe.enemy.curMoves[1].id, "SAND_ATTACK",
|
||||||
"enemy Mimic copies a random player move immediately")
|
"enemy Mimic copies a random player move immediately")
|
||||||
eq(tbe.enemy.curMoves[1].pp, 9, "enemy Mimic also keeps the slot's PP")
|
-- Gen 1 never decrements enemy PP, so the copied move inherits Mimic's
|
||||||
|
-- full remaining PP (still 10). Player Mimic would leave 9.
|
||||||
|
eq(tbe.enemy.curMoves[1].pp, 10, "enemy Mimic keeps full slot PP (no enemy drain)")
|
||||||
check(fe.anim and hasText(fe, "learned"),
|
check(fe.anim and hasText(fe, "learned"),
|
||||||
"enemy Mimic still plays the animation and learned text")
|
"enemy Mimic still plays the animation and learned text")
|
||||||
|
|
||||||
|
-- === #94: gen1_faithful enemies never deplete PP; player still does ===
|
||||||
|
do
|
||||||
|
local tb = freshBattle()
|
||||||
|
check(tb.ruleset.enemyUnlimitedPP,
|
||||||
|
"default ruleset grants enemy unlimited PP")
|
||||||
|
local enemyMove = { id = "TACKLE", pp = 5 }
|
||||||
|
local playerMove = { id = "TACKLE", pp = 5 }
|
||||||
|
tb.queue, tb.nextInsert = {}, 0
|
||||||
|
tb:performMove(tb.enemy, tb.player, enemyMove)
|
||||||
|
eq(enemyMove.pp, 5, "gen1_faithful: enemy move PP is not decremented")
|
||||||
|
tb.queue, tb.nextInsert = {}, 0
|
||||||
|
tb:performMove(tb.player, tb.enemy, playerMove)
|
||||||
|
eq(playerMove.pp, 4, "gen1_faithful: player move PP still decrements")
|
||||||
|
|
||||||
|
-- modern_clean tracks enemy PP (Gen 2+ style)
|
||||||
|
local modern = require("src.battle.rulesets.modern_clean")
|
||||||
|
tb.ruleset = modern
|
||||||
|
local enemyModern = { id = "TACKLE", pp = 5 }
|
||||||
|
tb.queue, tb.nextInsert = {}, 0
|
||||||
|
tb:performMove(tb.enemy, tb.player, enemyModern)
|
||||||
|
eq(enemyModern.pp, 4, "modern_clean: enemy move PP decrements")
|
||||||
|
end
|
||||||
|
|
||||||
-- link battle: the player's Mimic rolls random too (no chooser)
|
-- link battle: the player's Mimic rolls random too (no chooser)
|
||||||
local tbl = freshBattle()
|
local tbl = freshBattle()
|
||||||
tbl.kind = "link"
|
tbl.kind = "link"
|
||||||
|
|||||||
+31
-3
@@ -115,11 +115,39 @@ do
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- === STRUGGLE fallback when nothing is usable ===
|
-- === STRUGGLE fallback when nothing is usable ===
|
||||||
|
-- modern_clean / no unlimited flag: empty PP forces Struggle.
|
||||||
do
|
do
|
||||||
local aiMon = { curMoves = { { id = "TACKLE", pp = 0 } } }
|
local aiMon = { curMoves = { { id = "TACKLE", pp = 0 } } }
|
||||||
local pick = TrainerAI.chooseMove(aiMon, rngLo, { enemyAIMods = { 1 }, data = Data,
|
local pick = TrainerAI.chooseMove(aiMon, rngLo, {
|
||||||
player = { mon = {}, curTypes = {} } })
|
enemyAIMods = { 1 }, data = Data,
|
||||||
check(pick and pick.struggle and pick.id == "STRUGGLE", "STRUGGLE fallback when no PP")
|
player = { mon = {}, curTypes = {} },
|
||||||
|
ruleset = { enemyUnlimitedPP = false },
|
||||||
|
})
|
||||||
|
check(pick and pick.struggle and pick.id == "STRUGGLE",
|
||||||
|
"STRUGGLE fallback when no PP (enemy PP tracked)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- gen1_faithful: AI ignores PP; a 0-PP move is still selectable.
|
||||||
|
do
|
||||||
|
local aiMon = { curMoves = { { id = "TACKLE", pp = 0 } } }
|
||||||
|
local pick = TrainerAI.chooseMove(aiMon, rngLo, {
|
||||||
|
enemyAIMods = { 1 }, data = Data,
|
||||||
|
player = { mon = {}, curTypes = {} },
|
||||||
|
ruleset = { enemyUnlimitedPP = true },
|
||||||
|
})
|
||||||
|
eq(pick and pick.id, "TACKLE",
|
||||||
|
"gen1_faithful: enemy still picks 0-PP moves (no Struggle)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Disable with unlimited PP: sole disabled move still yields Struggle.
|
||||||
|
do
|
||||||
|
local aiMon = { curMoves = { { id = "TACKLE", pp = 10 } }, disabledSlot = 1 }
|
||||||
|
local pick = TrainerAI.chooseMove(aiMon, rngLo, {
|
||||||
|
enemyAIMods = {}, data = Data,
|
||||||
|
ruleset = { enemyUnlimitedPP = true },
|
||||||
|
})
|
||||||
|
check(pick and pick.struggle and pick.id == "STRUGGLE",
|
||||||
|
"gen1_faithful: Struggle only when every move is disabled")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- === switchAction off-by-one fix (matches AISwitchIfEnoughMons cp 2) ===
|
-- === switchAction off-by-one fix (matches AISwitchIfEnoughMons cp 2) ===
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- Parity test (#96): blackouts must not play the Dig/Teleport EnterMapAnim.
|
||||||
|
--
|
||||||
|
-- pret HandleBlackOut fades to black and SpecialEnterMap's without setting
|
||||||
|
-- BIT_FLY_WARP / BIT_DUNGEON_WARP, so EnterMap skips EnterMapAnim. Dig,
|
||||||
|
-- Teleport and Escape Rope go through HandleFlyWarpOrDungeonWarp and do
|
||||||
|
-- spin. warpToHealPoint used to always set arriveWarp="teleport", so a
|
||||||
|
-- loss (notably Elite Four -> Indigo lobby) rematerialized with the
|
||||||
|
-- teleport spin instead of a plain whiteout warp.
|
||||||
|
-- Self-contained; run via `luajit tests/parity_blackout_warp.lua`.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
local Data = require("src.core.Data")
|
||||||
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||||
|
local S = require("tests.harness").suite("parity blackout warp")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
local Input = require("src.core.Input")
|
||||||
|
local StateStack = require("src.core.StateStack")
|
||||||
|
local Renderer = require("src.render.Renderer")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local OW = require("src.world.OverworldController")
|
||||||
|
|
||||||
|
Game.data = Data
|
||||||
|
Game.input = Input; Input:init()
|
||||||
|
Game.renderer = Renderer; Renderer:init()
|
||||||
|
Game.stack = StateStack; StateStack:init()
|
||||||
|
Game.save = SaveData.newGame()
|
||||||
|
Game.save.party = { Pokemon.new(Data, "SQUIRTLE", 5) }
|
||||||
|
Game.save.party[1].hp = 0
|
||||||
|
Game.save.lastHeal = { map = "INDIGO_PLATEAU_LOBBY", x = 7, y = 6,
|
||||||
|
outdoor = { id = "INDIGO_PLATEAU", x = 9, y = 5 } }
|
||||||
|
|
||||||
|
Game.stack:push(OW, "LORELEIS_ROOM", 4, 5, "up")
|
||||||
|
local ow = Game.stack:top()
|
||||||
|
Game.overworld = ow
|
||||||
|
|
||||||
|
local captured
|
||||||
|
local realStart = ow.startWarpTo
|
||||||
|
ow.startWarpTo = function(self, mapId, x, y, facing, onDone, opts)
|
||||||
|
captured = self.arriveWarp
|
||||||
|
-- skip Transition; only the arrive flag matters here
|
||||||
|
self.arriveWarp = nil
|
||||||
|
self.transitioning = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Battle blackout (afterBattle -> warpToHealPoint): no EnterMapAnim.
|
||||||
|
captured = "sentinel"
|
||||||
|
ow:afterBattle("lose", { oppClass = "OPP_LORELEI" })
|
||||||
|
eq(captured, nil,
|
||||||
|
"Elite Four blackout does not set arriveWarp=teleport (#96)")
|
||||||
|
|
||||||
|
-- Poison / field blackout path uses the same helper with no opts.
|
||||||
|
captured = "sentinel"
|
||||||
|
ow.arriveWarp = nil
|
||||||
|
ow:warpToHealPoint()
|
||||||
|
eq(captured, nil, "plain warpToHealPoint has no teleport arrive FX")
|
||||||
|
|
||||||
|
-- Dig / Teleport / Escape Rope keep EnterMapAnim.
|
||||||
|
captured = "sentinel"
|
||||||
|
ow:warpToHealPoint(nil, { arrive = "teleport" })
|
||||||
|
eq(captured, "teleport",
|
||||||
|
"escape warps still request EnterMapAnim on arrival")
|
||||||
|
|
||||||
|
ow.startWarpTo = realStart
|
||||||
|
S.finish()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
-- Parity: Dig / Fly semi-invulnerable pic hide (#100).
|
||||||
|
-- Dig charge SLIDE_DOWN hides the user; a cancelled Dig release (miss /
|
||||||
|
-- type immunity) must restore the pic; a successful Dig release keeps the
|
||||||
|
-- user hidden through the dirt subanim then emerges via SE_SLIDE_MON_UP
|
||||||
|
-- (not a cyclic bounce). Self-contained: `luajit tests/parity_dig_pic.lua`.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
local Data = require("src.core.Data")
|
||||||
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||||
|
local Font = require("src.render.Font")
|
||||||
|
Font.load(Data)
|
||||||
|
local S = require("tests.harness").suite("parity dig pic")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local TypeChart = require("src.battle.TypeChart")
|
||||||
|
TypeChart.load(Data)
|
||||||
|
|
||||||
|
local function makeGame(species, level, moves)
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
local mon = Pokemon.new(Data, species, level)
|
||||||
|
mon.moves = moves
|
||||||
|
save.party = { mon }
|
||||||
|
local stack = { states = {} }
|
||||||
|
function stack:push(state) self.states[#self.states + 1] = state end
|
||||||
|
function stack:pop() return table.remove(self.states) end
|
||||||
|
function stack:top() return self.states[#self.states] end
|
||||||
|
return { data = Data, save = save, stack = stack,
|
||||||
|
input = { wasPressed = function() return true end } }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pumpToMenu(battle)
|
||||||
|
local kinds, anims = {}, {}
|
||||||
|
local guard = 0
|
||||||
|
while guard < 20000 do
|
||||||
|
guard = guard + 1
|
||||||
|
battle.frame = (battle.frame or 0) + 1
|
||||||
|
battle:updateFx()
|
||||||
|
local pf = battle.picFx and battle.picFx[battle.player]
|
||||||
|
if pf and pf.kind and not kinds[pf.kind] then
|
||||||
|
kinds[pf.kind] = true
|
||||||
|
end
|
||||||
|
if battle.animPlaying and battle.animName
|
||||||
|
and anims[#anims] ~= battle.animName then
|
||||||
|
anims[#anims + 1] = battle.animName
|
||||||
|
end
|
||||||
|
if not battle:updateQueue() then
|
||||||
|
if battle.phase == "messages" and battle.afterQueue == "menu"
|
||||||
|
and not battle.animPlaying and not battle.current
|
||||||
|
and #battle.queue == 0 then
|
||||||
|
battle.phase = "menu"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if battle.phase == "menu" then break end
|
||||||
|
if not battle.animPlaying and not battle.current
|
||||||
|
and #battle.queue == 0 then
|
||||||
|
if battle.afterQueue == "menu" then battle.phase = "menu" end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _ = 1, 200 do
|
||||||
|
battle.frame = battle.frame + 1
|
||||||
|
battle:updateFx()
|
||||||
|
end
|
||||||
|
return kinds, anims
|
||||||
|
end
|
||||||
|
|
||||||
|
local function picHidden(battle)
|
||||||
|
local pf = battle.picFx and battle.picFx[battle.player]
|
||||||
|
return pf and pf.hidden or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dig charge hides; Dig miss on release restores the pic (#100 vanish).
|
||||||
|
do
|
||||||
|
local game = makeGame("SANDSHREW", 40, { { id = "DIG", pp = 10 } })
|
||||||
|
local battle = BattleState.newWild(game, "RATTATA", 5)
|
||||||
|
battle.player.curMoves = game.save.party[1].moves
|
||||||
|
local dig = battle.player.curMoves[1]
|
||||||
|
battle.enemyAction = function() return { id = "TACKLE", pp = 35 } end
|
||||||
|
battle.rng = function(a) return a or 0 end
|
||||||
|
battle:resolveTurn(dig)
|
||||||
|
pumpToMenu(battle)
|
||||||
|
check(battle.player.invulnerable == true, "Dig charge sets invulnerable")
|
||||||
|
check(picHidden(battle), "Dig charge leaves the user pic hidden")
|
||||||
|
|
||||||
|
battle.rng = function(a, b)
|
||||||
|
if a == 0 and b == 255 then return 255 end -- force Dig accuracy miss
|
||||||
|
return a or 0
|
||||||
|
end
|
||||||
|
battle:resolveTurn(dig)
|
||||||
|
local kinds, anims = pumpToMenu(battle)
|
||||||
|
check(not picHidden(battle),
|
||||||
|
"Dig miss on release restores the user pic (#100)")
|
||||||
|
check(not kinds.bounce, "Dig release miss never starts a bounce pic fx")
|
||||||
|
local sawDig = false
|
||||||
|
for _, name in ipairs(anims) do
|
||||||
|
if name == "DIG" then sawDig = true end
|
||||||
|
end
|
||||||
|
check(not sawDig, "Dig miss cancels the DIG release anim")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dig hit: stay hidden through DIG start, emerge via slideUp (not bounce).
|
||||||
|
do
|
||||||
|
local game = makeGame("SANDSHREW", 40, { { id = "DIG", pp = 10 } })
|
||||||
|
local battle = BattleState.newWild(game, "SNORLAX", 40)
|
||||||
|
battle.player.curMoves = game.save.party[1].moves
|
||||||
|
local dig = battle.player.curMoves[1]
|
||||||
|
battle.enemyAction = function() return { id = "TACKLE", pp = 35 } end
|
||||||
|
battle.rng = function(a) return a or 0 end
|
||||||
|
battle:resolveTurn(dig)
|
||||||
|
pumpToMenu(battle)
|
||||||
|
|
||||||
|
battle:resolveTurn(dig)
|
||||||
|
local hiddenAtDigStart = nil
|
||||||
|
local kinds = {}
|
||||||
|
local guard = 0
|
||||||
|
while guard < 20000 do
|
||||||
|
guard = guard + 1
|
||||||
|
battle.frame = (battle.frame or 0) + 1
|
||||||
|
battle:updateFx()
|
||||||
|
if battle.animPlaying and battle.animName == "DIG"
|
||||||
|
and hiddenAtDigStart == nil then
|
||||||
|
-- right after DIG row starts (resetPicFx already ran)
|
||||||
|
hiddenAtDigStart = picHidden(battle)
|
||||||
|
end
|
||||||
|
local pf = battle.picFx and battle.picFx[battle.player]
|
||||||
|
if pf and pf.kind then kinds[pf.kind] = true end
|
||||||
|
if not battle:updateQueue() then
|
||||||
|
if battle.phase == "messages" and battle.afterQueue == "menu"
|
||||||
|
and not battle.animPlaying and not battle.current
|
||||||
|
and #battle.queue == 0 then
|
||||||
|
battle.phase = "menu"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if battle.phase == "menu" then break end
|
||||||
|
if not battle.animPlaying and not battle.current
|
||||||
|
and #battle.queue == 0 then
|
||||||
|
if battle.afterQueue == "menu" then battle.phase = "menu" end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _ = 1, 200 do
|
||||||
|
battle.frame = battle.frame + 1
|
||||||
|
battle:updateFx()
|
||||||
|
end
|
||||||
|
check(hiddenAtDigStart == true,
|
||||||
|
"DIG release keeps the digger hidden until SE_SLIDE_MON_UP")
|
||||||
|
check(kinds.slideUp == true, "Dig release uses slideUp emerge")
|
||||||
|
check(not kinds.bounce, "Dig release must not bounce (#100)")
|
||||||
|
check(not picHidden(battle), "Dig hit leaves the user pic shown")
|
||||||
|
end
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -113,6 +113,22 @@ eq(#game2.save.hallOfFame, 1, "winning team recorded (SaveHallOfFameTeams)")
|
|||||||
local HallOfFame = require("src.ui.HallOfFame")
|
local HallOfFame = require("src.ui.HallOfFame")
|
||||||
check(getmetatable(stack2:top()) == HallOfFame, "induction showcase pushed")
|
check(getmetatable(stack2:top()) == HallOfFame, "induction showcase pushed")
|
||||||
|
|
||||||
|
-- Gen1 layout (issue #102): pic rests at hlcoord (12,5); mon phase starts
|
||||||
|
-- with the LEVEL/TYPE info box (not a top "HALL OF FAME" banner alone)
|
||||||
|
local hofUi = stack2:top()
|
||||||
|
eq(hofUi.phase, "mons", "induction opens on the mon showcase phase")
|
||||||
|
eq(hofUi.scrollX < 12 * 8, true, "front pic starts off-screen left of (12,5)")
|
||||||
|
-- drive past the scroll so the info box is armed
|
||||||
|
local scrollGuard = 0
|
||||||
|
while hofUi.scrollX < 12 * 8 and scrollGuard < 200 do
|
||||||
|
scrollGuard = scrollGuard + 1
|
||||||
|
hofUi:update(1 / 60)
|
||||||
|
end
|
||||||
|
eq(hofUi.scrollX, 12 * 8, "front pic settles at hlcoord (12,5)")
|
||||||
|
eq(hofUi.showHofBanner, false, "bottom HALL OF FAME banner waits for the 80-frame hold")
|
||||||
|
check(hofUi.timer == 80 or hofUi.timer < 80,
|
||||||
|
"info hold uses the pokered 80 DelayFrames window")
|
||||||
|
|
||||||
-- drive induction + full credits with A held (pages are unskippable; A
|
-- drive induction + full credits with A held (pages are unskippable; A
|
||||||
-- only advances the induction and the final THE END wait)
|
-- only advances the induction and the final THE END wait)
|
||||||
pressed.a = true
|
pressed.a = true
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
-- Parity test: Lance room walk-in stays on the floor, and defeat dialogue
|
||||||
|
-- includes the rival-became-champion after-battle text.
|
||||||
|
--
|
||||||
|
-- Sources: scripts/LancesRoom.asm (WalkToLance / LancesRoomLanceEndBattleScript),
|
||||||
|
-- text/LancesRoom.asm (_LancesRoomLanceAfterBattleText).
|
||||||
|
-- Self-contained: run via `luajit tests/parity_lance.lua`; also dofile'd
|
||||||
|
-- by tests/run_tests.lua's aggregator.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
local Data = require("src.core.Data")
|
||||||
|
if not (Data.maps and Data.maps.LANCES_ROOM) then Data:load() end
|
||||||
|
local Font = require("src.render.Font")
|
||||||
|
if not pcall(Font.encode, "A") then Font.load(Data) end
|
||||||
|
local S = require("tests.harness").suite("parity lance")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
local mapScripts = require("data.scripts.init")
|
||||||
|
local hooks = mapScripts.get("LANCES_ROOM")
|
||||||
|
check(hooks and hooks.walkInRoute, "LANCES_ROOM exposes walkInRoute")
|
||||||
|
|
||||||
|
local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
||||||
|
|
||||||
|
-- === (1) walk-in RLE lands on the door-lock trigger (6,11) ===
|
||||||
|
do
|
||||||
|
local x, y = 24, 16
|
||||||
|
local steps = 0
|
||||||
|
for _, seg in ipairs(hooks.walkInRoute) do
|
||||||
|
local d = D[seg[1]]
|
||||||
|
check(d ~= nil, "walk segment direction " .. tostring(seg[1]))
|
||||||
|
for _ = 1, seg[2] do
|
||||||
|
x, y = x + d[1], y + d[2]
|
||||||
|
steps = steps + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
eq(x, 6, "walk-in ends at x=6")
|
||||||
|
eq(y, 11, "walk-in ends at y=11 (door-lock / arena entry)")
|
||||||
|
check(steps > 0, "walk-in has steps")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- === (2) every cell of the route is walkable with the entrance open ===
|
||||||
|
do
|
||||||
|
local Map = require("src.world.Map")
|
||||||
|
local def = Data.maps.LANCES_ROOM
|
||||||
|
local tileset = Data.tilesets[def.tileset]
|
||||||
|
-- mutate a copy of the block list so we don't poison the registry
|
||||||
|
local blocks = {}
|
||||||
|
for i, b in ipairs(def.blocks) do blocks[i] = b end
|
||||||
|
local copy = {}
|
||||||
|
for k, v in pairs(def) do copy[k] = v end
|
||||||
|
copy.blocks = blocks
|
||||||
|
-- LanceShowOrHideEntranceBlocks with the door unlocked
|
||||||
|
blocks[6 * def.width + 2 + 1] = 0x31
|
||||||
|
blocks[6 * def.width + 3 + 1] = 0x32
|
||||||
|
local map = Map.new(copy, tileset)
|
||||||
|
local x, y = 24, 16
|
||||||
|
check(map:isWalkableCell(x, y), "start (24,16) walkable")
|
||||||
|
for _, seg in ipairs(hooks.walkInRoute) do
|
||||||
|
local d = D[seg[1]]
|
||||||
|
for _ = 1, seg[2] do
|
||||||
|
x, y = x + d[1], y + d[2]
|
||||||
|
check(map:isWalkableCell(x, y),
|
||||||
|
("walk-in cell (%d,%d) is floor, not void/wall"):format(x, y))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- === (3) after-battle text names the rival as the real champion ===
|
||||||
|
do
|
||||||
|
local after = Data.text._LancesRoomLanceAfterBattleText
|
||||||
|
check(after ~= nil, "_LancesRoomLanceAfterBattleText extracted")
|
||||||
|
check(after:find("ELITE", 1, true) or after:find("{RIVAL}", 1, true),
|
||||||
|
"after text mentions rival / Elite Four")
|
||||||
|
check(after:find("champion", 1, true) or after:find("CHAMPION", 1, true),
|
||||||
|
"after text has the champion reveal")
|
||||||
|
check(after:find("before you", 1, true) or after:find("before you!", 1, true)
|
||||||
|
or after:find("FOUR before", 1, true),
|
||||||
|
"after text says rival beat the Elite Four first")
|
||||||
|
local header = Data:trainerHeader("LancesRoom", 1)
|
||||||
|
check(header and header.after == "_LancesRoomLanceAfterBattleText",
|
||||||
|
"trainer header wires the after-battle label")
|
||||||
|
check(header and header.won == "_LancesRoomLanceEndBattleText",
|
||||||
|
"trainer header wires the won label")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- === (4) onStep win callback pushes the after text (not only won text) ===
|
||||||
|
do
|
||||||
|
local pushed = {}
|
||||||
|
local game = {
|
||||||
|
data = Data,
|
||||||
|
save = {
|
||||||
|
flags = {},
|
||||||
|
defeatedTrainers = {},
|
||||||
|
player = { name = "RED", rival = "BLUE" },
|
||||||
|
},
|
||||||
|
stack = {
|
||||||
|
push = function(_, state)
|
||||||
|
pushed[#pushed + 1] = state
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local lance = {
|
||||||
|
def = { name = "LANCESROOM_LANCE", index = 1,
|
||||||
|
trainerClass = "OPP_LANCE", trainerParty = 1, text = 1 },
|
||||||
|
id = "LANCES_ROOM:1",
|
||||||
|
facePlayer = function() end,
|
||||||
|
}
|
||||||
|
local engaged = false
|
||||||
|
local ow = {
|
||||||
|
npcs = { lance },
|
||||||
|
player = { cellX = 6, cellY = 2 },
|
||||||
|
trainerDefeated = function(_, npc)
|
||||||
|
return game.save.defeatedTrainers[npc.id] == true
|
||||||
|
end,
|
||||||
|
engageTrainer = function(_, npc, onDone)
|
||||||
|
engaged = npc == lance
|
||||||
|
-- simulate engageTrainer's win path: flag the trainer, then onDone
|
||||||
|
game.save.defeatedTrainers[npc.id] = true
|
||||||
|
game.save.flags.EVENT_BEAT_LANCES_ROOM_TRAINER_0 = true
|
||||||
|
if onDone then onDone() end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local handled = hooks.onStep(game, ow, 6, 2)
|
||||||
|
check(handled == true, "Lance coord trigger engages")
|
||||||
|
check(engaged, "engageTrainer called for Lance")
|
||||||
|
check(#pushed == 1 and pushed[1].pages ~= nil, "after-battle TextBox pushed")
|
||||||
|
-- TextBox.substitute already expanded {RIVAL}/{PLAYER}; page glyphs
|
||||||
|
-- are opaque, so re-check the source label via the header + that a
|
||||||
|
-- box was queued on win only.
|
||||||
|
check(#pushed[1].pages > 0, "after-battle TextBox has pages")
|
||||||
|
-- loss path must not show after text
|
||||||
|
pushed = {}
|
||||||
|
game.save.defeatedTrainers = {}
|
||||||
|
ow.engageTrainer = function(_, npc, onDone)
|
||||||
|
-- lose: do not mark defeated
|
||||||
|
if onDone then onDone() end
|
||||||
|
end
|
||||||
|
hooks.onStep(game, ow, 6, 2)
|
||||||
|
eq(#pushed, 0, "loss does not push after-battle text")
|
||||||
|
end
|
||||||
|
|
||||||
|
print("parity_lance: ok")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
-- Regression: map-connection seam steps must show walk frames (issue #93).
|
||||||
|
--
|
||||||
|
-- A hitch inside setMap (neighbor rebuild / map-song start) made the next
|
||||||
|
-- real-time dt huge; FixedStep catch-up then advanced many walk frames
|
||||||
|
-- before the next draw, which looked like a slide with no leg animation.
|
||||||
|
-- crossConnection now discards that catch-up, starts a fresh animClock,
|
||||||
|
-- and defers PlayMapMusic until the seam step lands.
|
||||||
|
--
|
||||||
|
-- Self-contained; run via `luajit tests/parity_seam_walk_anim.lua`.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
|
||||||
|
local Data = require("src.core.Data")
|
||||||
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||||
|
|
||||||
|
local FixedStep = require("src.core.FixedStep")
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
local Input = require("src.core.Input")
|
||||||
|
local Music = require("src.core.Music")
|
||||||
|
local Renderer = require("src.render.Renderer")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local StateStack = require("src.core.StateStack")
|
||||||
|
local OW = require("src.world.OverworldController")
|
||||||
|
local S = require("tests.harness").suite("parity seam walk anim")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
Game.data = Data
|
||||||
|
Game.input = Input; Input:init()
|
||||||
|
Game.renderer = Renderer; Renderer:init()
|
||||||
|
Game.stack = StateStack
|
||||||
|
StateStack:init()
|
||||||
|
Game.save = SaveData.newGame()
|
||||||
|
Game.overworld = OW
|
||||||
|
|
||||||
|
while Game.stack:top() do Game.stack:pop() end
|
||||||
|
Game.stack:push(OW, "PALLET_TOWN", 10, 0, "up")
|
||||||
|
local ow = Game.stack:top()
|
||||||
|
local north = ow.map:connection("north")
|
||||||
|
check(north and north.map == "ROUTE_1", "Pallet north connects to ROUTE_1")
|
||||||
|
|
||||||
|
-- simulate a post-hitch catch-up budget waiting in the accumulator
|
||||||
|
FixedStep.accum = 0.24
|
||||||
|
local played = {}
|
||||||
|
local realPlayMap = Music.playMap
|
||||||
|
Music.playMap = function(data, mapId, onBike, surfing)
|
||||||
|
played[#played + 1] = mapId
|
||||||
|
return realPlayMap(data, mapId, onBike, surfing)
|
||||||
|
end
|
||||||
|
|
||||||
|
check(ow:crossConnection("up", north) == true, "Pallet -> Route 1 crosses")
|
||||||
|
eq(ow.map.id, "ROUTE_1", "landed on ROUTE_1")
|
||||||
|
eq(FixedStep.accum, 0, "seam cross discards FixedStep catch-up")
|
||||||
|
eq(ow.pendingSeamMusic, "ROUTE_1", "map music deferred across the seam step")
|
||||||
|
eq(#played, 0, "PlayMapMusic not called inside setMap for the seam")
|
||||||
|
eq(ow.player.animClock, 0, "seam step starts a fresh walk-cycle clock")
|
||||||
|
check(ow.player.moving, "seam step is in progress")
|
||||||
|
|
||||||
|
local sawWalk = false
|
||||||
|
local phases = {}
|
||||||
|
for _ = 1, 20 do
|
||||||
|
ow:update(1 / 60)
|
||||||
|
phases[#phases + 1] = ow.player:walkPhase()
|
||||||
|
if ow.player:walkPhase() == 1 then sawWalk = true end
|
||||||
|
if not ow.player.moving and not ow.pendingSeamMusic then break end
|
||||||
|
end
|
||||||
|
check(sawWalk, "seam step shows at least one walk frame")
|
||||||
|
-- mid-cycle: frames 4..11 of a fresh animClock are walk
|
||||||
|
local midWalk = false
|
||||||
|
for i = 4, 11 do
|
||||||
|
if phases[i] == 1 then midWalk = true break end
|
||||||
|
end
|
||||||
|
check(midWalk, "fresh animClock puts walk frames in the middle of the seam step")
|
||||||
|
eq(ow.pendingSeamMusic, nil, "deferred music flushed after the seam step")
|
||||||
|
eq(played[1], "ROUTE_1", "PlayMapMusic runs once the seam step lands")
|
||||||
|
|
||||||
|
Music.playMap = realPlayMap
|
||||||
|
S.finish()
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
-- Parity: sleep/confusion onomatopoeia on status-check text
|
||||||
|
-- (core.asm CheckPlayerStatusConditions / CheckEnemyStatusConditions).
|
||||||
|
-- Self-contained: `luajit tests/parity_status_onomatopoeia.lua`; also
|
||||||
|
-- dofile'd by tests/run_tests.lua's parity_* aggregator.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
local Data = require("src.core.Data")
|
||||||
|
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||||
|
local S = require("tests.harness").suite("parity status onomatopoeia")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
Game.data = Data
|
||||||
|
Game.save = require("src.core.SaveData").newGame()
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
|
||||||
|
local function freshBattle()
|
||||||
|
Game.save.party = { Pokemon.new(Data, "NIDOKING", 40) }
|
||||||
|
return BattleState.newWild(Game, "DEWGONG", 30)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Capture queue order of anim/text rows inserted via *Next helpers.
|
||||||
|
local function capture(battle)
|
||||||
|
local seq = {}
|
||||||
|
battle.nextInsert = 0
|
||||||
|
battle.queue = {}
|
||||||
|
battle.animNext = function(_, name, isPlayer)
|
||||||
|
seq[#seq + 1] = { kind = "anim", name = name, isPlayer = isPlayer }
|
||||||
|
end
|
||||||
|
battle.sayNext = function(_, text)
|
||||||
|
seq[#seq + 1] = { kind = "text", text = text }
|
||||||
|
end
|
||||||
|
return seq
|
||||||
|
end
|
||||||
|
|
||||||
|
-- --- sleep: player anim-before-text, enemy text-before-anim -------------
|
||||||
|
do
|
||||||
|
local b = freshBattle()
|
||||||
|
b.rng = function() return 255 end
|
||||||
|
local seq = capture(b)
|
||||||
|
b.player.mon.status = "SLP"
|
||||||
|
b.player.sleepTurns = 3
|
||||||
|
check(b:statusInterrupt(b.player, b.enemy) == true, "sleep interrupts the turn")
|
||||||
|
eq(#seq, 2, "sleep queues anim + text")
|
||||||
|
eq(seq[1].kind, "anim", "player sleep: SLP_PLAYER_ANIM before text")
|
||||||
|
eq(seq[1].name, "SLP_PLAYER_ANIM", "player sleep uses SLP_PLAYER_ANIM")
|
||||||
|
eq(seq[1].isPlayer, true, "player sleep anim faces the player")
|
||||||
|
check(seq[2].text:find("is fast asleep!", 1, true),
|
||||||
|
"player sleep text follows the anim")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local b = freshBattle()
|
||||||
|
b.rng = function() return 255 end
|
||||||
|
local seq = capture(b)
|
||||||
|
b.enemy.mon.status = "SLP"
|
||||||
|
b.enemy.sleepTurns = 3
|
||||||
|
check(b:statusInterrupt(b.enemy, b.player) == true, "enemy sleep interrupts")
|
||||||
|
eq(seq[1].kind, "text", "enemy sleep: FastAsleepText before anim")
|
||||||
|
check(seq[1].text:find("Enemy ", 1, true),
|
||||||
|
"enemy sleep text carries the Enemy prefix")
|
||||||
|
eq(seq[2].name, "SLP_ANIM", "enemy sleep uses SLP_ANIM (enemy Z coords)")
|
||||||
|
eq(seq[2].isPlayer, false, "enemy sleep anim faces the enemy")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local b = freshBattle()
|
||||||
|
local seq = capture(b)
|
||||||
|
b.player.mon.status = "SLP"
|
||||||
|
b.player.sleepTurns = 3
|
||||||
|
check(b:preRechargeChecks(b.player, b.enemy) == true,
|
||||||
|
"pre-recharge sleep still loses the turn")
|
||||||
|
eq(seq[1].name, "SLP_PLAYER_ANIM",
|
||||||
|
"pre-recharge sleep plays the same onomatopoeia")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- --- confusion: text then CONF_*_ANIM (both sides) ---------------------
|
||||||
|
-- Status.beforeMove: rng(0,255) < 128 -> hurt itself; else can still move.
|
||||||
|
do
|
||||||
|
local b = freshBattle()
|
||||||
|
b.rng = function() return 200 end -- no self-hit
|
||||||
|
local seq = capture(b)
|
||||||
|
b.player.confusedTurns = 3
|
||||||
|
b.player.mon.status = nil
|
||||||
|
local stopped = b:statusInterrupt(b.player, b.enemy)
|
||||||
|
check(stopped == false, "confusion can still allow a move")
|
||||||
|
eq(seq[1].kind, "text", "player confusion: IsConfusedText before anim")
|
||||||
|
check(seq[1].text:find("is confused!", 1, true), "player confusion text")
|
||||||
|
eq(seq[2].name, "CONF_PLAYER_ANIM", "player confusion uses CONF_PLAYER_ANIM")
|
||||||
|
eq(seq[2].isPlayer, true, "player confusion anim faces the player")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local b = freshBattle()
|
||||||
|
b.rng = function() return 0 end -- self-hit
|
||||||
|
local seq = capture(b)
|
||||||
|
b.computeDamage = function() return 1 end
|
||||||
|
b.applyDamage = function() end
|
||||||
|
b.onFaint = function() end
|
||||||
|
b.enemy.confusedTurns = 3
|
||||||
|
b.enemy.mon.status = nil
|
||||||
|
local stopped = b:statusInterrupt(b.enemy, b.player)
|
||||||
|
check(stopped == true, "confusion self-hit interrupts")
|
||||||
|
eq(seq[1].kind, "text", "enemy confusion text first")
|
||||||
|
check(seq[1].text:find("is confused!", 1, true), "enemy confusion text")
|
||||||
|
eq(seq[2].name, "CONF_ANIM", "enemy confusion uses CONF_ANIM")
|
||||||
|
eq(seq[2].isPlayer, false, "enemy confusion anim faces the enemy")
|
||||||
|
check(seq[3] and seq[3].text:find("hurt itself", 1, true),
|
||||||
|
"hurt-itself text follows the confusion anim")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- wake stays text-only (no onomatopoeia)
|
||||||
|
do
|
||||||
|
local b = freshBattle()
|
||||||
|
b.rng = function() return 0 end
|
||||||
|
local seq = capture(b)
|
||||||
|
b.player.mon.status = "SLP"
|
||||||
|
b.player.sleepTurns = 1
|
||||||
|
b:statusInterrupt(b.player, b.enemy)
|
||||||
|
eq(#seq, 1, "waking up is text-only")
|
||||||
|
check(seq[1].text:find("woke up!", 1, true), "wake text")
|
||||||
|
end
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
-- Parity test: Victory Road 3F hole at (23,15) dungeon-warps the player
|
||||||
|
-- to VICTORY_ROAD_2F (22,16).
|
||||||
|
--
|
||||||
|
-- scripts/VictoryRoad3F.asm VictoryRoad3FDefaultScript feeds
|
||||||
|
-- .SwitchOrHoleCoords into IsPlayerOnDungeonWarp with destination
|
||||||
|
-- VICTORY_ROAD_2F; data/maps/special_warps.asm DungeonWarpList entry
|
||||||
|
-- (VICTORY_ROAD_2F, 2) lands at DungeonWarpData (22, 16). The same cell
|
||||||
|
-- also drops a boulder (onBoulderMoved), but the player fall was missing
|
||||||
|
-- -- CAVERN $22 is walkable, so without onStep Red stood on the hole
|
||||||
|
-- (GitHub #86).
|
||||||
|
--
|
||||||
|
-- Self-contained; run via `luajit tests/parity_victory_road_hole.lua`.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
local S = require("tests.harness").suite("parity victory road hole")
|
||||||
|
local check, eq = S.check, S.eq
|
||||||
|
|
||||||
|
local M = dofile("data/scripts/story.lua")
|
||||||
|
local vr3 = M.VICTORY_ROAD_3F
|
||||||
|
check(vr3 ~= nil and vr3.onStep ~= nil, "VICTORY_ROAD_3F has an onStep hole trigger")
|
||||||
|
|
||||||
|
local function owRecording()
|
||||||
|
local warps = {}
|
||||||
|
return {
|
||||||
|
player = { facing = "right" },
|
||||||
|
startWarpTo = function(_, mapId, x, y, facing)
|
||||||
|
warps[#warps + 1] = { mapId = mapId, x = x, y = y, facing = facing }
|
||||||
|
end,
|
||||||
|
_warps = warps,
|
||||||
|
}, warps
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stepping onto the hole falls through to 2F at the dungeon-warp landing.
|
||||||
|
do
|
||||||
|
local ow, warps = owRecording()
|
||||||
|
local handled = vr3.onStep({}, ow, 23, 15)
|
||||||
|
check(handled, "stepping on (23,15) is consumed")
|
||||||
|
eq(#warps, 1, "exactly one dungeon warp fires")
|
||||||
|
eq(warps[1].mapId, "VICTORY_ROAD_2F", "destination is VICTORY_ROAD_2F")
|
||||||
|
eq(warps[1].x, 22, "lands at x=22")
|
||||||
|
eq(warps[1].y, 16, "lands at y=16")
|
||||||
|
eq(warps[1].facing, "right", "facing is preserved across the fall")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Any other cell is ignored (switch at 3,5 is boulder-only).
|
||||||
|
do
|
||||||
|
local ow, warps = owRecording()
|
||||||
|
eq(vr3.onStep({}, ow, 3, 5), false, "the switch cell does not dungeon-warp")
|
||||||
|
eq(vr3.onStep({}, ow, 22, 15), false, "a neighboring floor cell is ignored")
|
||||||
|
eq(#warps, 0, "no warp fires off the hole")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The hole collision tile stays walkable (fall, do not block).
|
||||||
|
local Data = require("src.core.Data")
|
||||||
|
if not (Data.maps and Data.maps.VICTORY_ROAD_3F) then Data:load() end
|
||||||
|
local MapLoader = require("src.world.MapLoader")
|
||||||
|
local map = MapLoader.load(Data, "VICTORY_ROAD_3F")
|
||||||
|
check(map:isWalkableCell(23, 15), "CAVERN hole tile at (23,15) is walkable")
|
||||||
|
eq(map:warpPadOrHoleAt(23, 15), "hole", "collision tile is the CAVERN hole ($22)")
|
||||||
|
check(map:warpAtCell(23, 15) == nil,
|
||||||
|
"the hole is a dungeon warp, not a map warp event")
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -1659,6 +1659,47 @@ do
|
|||||||
eq(cam.y, 160 - (288 / 2 - 8), "wide view keeps player centered y")
|
eq(cam.y, 160 - (288 / 2 - 8), "wide view keeps player centered y")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------- dpi fit scale (#87)
|
||||||
|
-- Android density is often non-integer; fitScale must use framebuffer
|
||||||
|
-- pixels so each GB pixel maps to a whole number of physical pixels.
|
||||||
|
do
|
||||||
|
local Renderer = require("src.render.Renderer")
|
||||||
|
local Zoom = require("src.render.Zoom")
|
||||||
|
local g = love.graphics
|
||||||
|
local oldDim, oldPix, oldDpi = g.getDimensions, g.getPixelDimensions, g.getDPIScale
|
||||||
|
|
||||||
|
-- desktop / dpi=1: identical to the pre-fix unit-based floor scale
|
||||||
|
g.getDimensions = function() return 1920, 1080 end
|
||||||
|
g.getPixelDimensions = function() return 1920, 1080 end
|
||||||
|
g.getDPIScale = function() return 1 end
|
||||||
|
eq(Renderer:fitScale(), 7, "dpi=1 1080p fitScale is floor(1080/144)=7")
|
||||||
|
|
||||||
|
-- density 1.5 on a 1920x1080 panel → LOVE units 1280x720
|
||||||
|
g.getDimensions = function() return 1280, 720 end
|
||||||
|
g.getPixelDimensions = function() return 1920, 1080 end
|
||||||
|
g.getDPIScale = function() return 1.5 end
|
||||||
|
eq(Renderer:fitScale(), 7,
|
||||||
|
"non-integer density still picks integer framebuffer pixels (7, not 5)")
|
||||||
|
-- old unit-only math would have returned floor(min(1280/160,720/144))=5
|
||||||
|
-- and 5*1.5=7.5 physical px/GB px (shimmer). 7 physical is crisp.
|
||||||
|
|
||||||
|
Zoom.reset()
|
||||||
|
local vw, vh = Renderer:worldViewSize()
|
||||||
|
check(vw % 2 == 0 and vh % 2 == 0, "world view sizes are even (integer camera)")
|
||||||
|
-- ceil(pw/Sp)=ceil(1920/7)=275 → even 276; ceil(1080/7)=155 → even 156
|
||||||
|
eq(vw, 276, "world fill width covers the unit window at pixel scale 7")
|
||||||
|
eq(vh, 156, "world fill height covers the unit window at pixel scale 7")
|
||||||
|
|
||||||
|
-- missing pixel API falls back to getDimensions (headless / old stub)
|
||||||
|
g.getPixelDimensions = nil
|
||||||
|
g.getDPIScale = nil
|
||||||
|
g.getDimensions = function() return 640, 576 end
|
||||||
|
eq(Renderer:fitScale(), 4, "no pixel API: fitScale uses unit dimensions")
|
||||||
|
|
||||||
|
g.getDimensions, g.getPixelDimensions, g.getDPIScale = oldDim, oldPix, oldDpi
|
||||||
|
Zoom.reset()
|
||||||
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------------------------- spawn filter
|
-- ---------------------------------------------------------------- spawn filter
|
||||||
do
|
do
|
||||||
local OW = require("src.world.OverworldController")
|
local OW = require("src.world.OverworldController")
|
||||||
@@ -2486,6 +2527,51 @@ do
|
|||||||
love.graphics.draw = origDraw
|
love.graphics.draw = origDraw
|
||||||
eq(xs[1], 8, "player/old-man back pic also rests at x=8")
|
eq(xs[1], 8, "player/old-man back pic also rests at x=8")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Front pics: LoadUncompressedSpriteData centers in a 7x7 buffer at
|
||||||
|
-- hlcoord 12,0. A 5x5 (40x40) Squirtle rests at (104,16), not
|
||||||
|
-- right/bottom-aligned to (112,8).
|
||||||
|
do
|
||||||
|
local front = {
|
||||||
|
getWidth = function() return 40 end,
|
||||||
|
getHeight = function() return 40 end,
|
||||||
|
}
|
||||||
|
local battle = setmetatable({
|
||||||
|
showEnemyTrainer = false,
|
||||||
|
enemyHidden = false,
|
||||||
|
enemySendingOut = false,
|
||||||
|
phase = "command",
|
||||||
|
enemy = { sprite = front, isPlayer = false },
|
||||||
|
}, BattleState)
|
||||||
|
function battle:picImage(i) return i end
|
||||||
|
function battle:growInScale() return nil end
|
||||||
|
function battle:fxHidden() return false end
|
||||||
|
function battle:drawBattlerPic(b, x, y, scale)
|
||||||
|
battle._ex, battle._ey, battle._scale = x, y, scale
|
||||||
|
end
|
||||||
|
battle:drawPicsLayer(0, 0, 0)
|
||||||
|
eq(battle._ex, 104, "enemy 5x5 front rests at hlcoord 12,0 + hPad 1 (x=104)")
|
||||||
|
eq(battle._ey, 16, "enemy 5x5 front bottom-aligned in 7x7 (y=16)")
|
||||||
|
eq(battle._scale, 1, "enemy front draws at 1x")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Battle message lines skip a tile row (14 then 16), matching the menu.
|
||||||
|
do
|
||||||
|
local Font = require("src.render.Font")
|
||||||
|
local ys, origCode, origBox = {}, Font.drawCode, Font.drawBox
|
||||||
|
Font.drawBox = function() end
|
||||||
|
Font.drawCode = function(_, _, y) ys[#ys + 1] = y end
|
||||||
|
local battle = setmetatable({
|
||||||
|
phase = "messages",
|
||||||
|
current = true,
|
||||||
|
charIndex = 999,
|
||||||
|
lines = { { 0x80 }, { 0x81 } },
|
||||||
|
}, BattleState)
|
||||||
|
battle:drawTextArea()
|
||||||
|
Font.drawCode, Font.drawBox = origCode, origBox
|
||||||
|
eq(ys[1], 112, "battle text line 1 at row 14 (y=112)")
|
||||||
|
eq(ys[2], 128, "battle text line 2 at row 16 (y=128)")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ================= BUGS.md batch: ledge-shadow =================
|
-- ================= BUGS.md batch: ledge-shadow =================
|
||||||
|
|||||||
Reference in New Issue
Block a user