From a446147787464a6cc3cf10f3e33bead816ad149d Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 22 Sep 2026 20:44:21 -0400 Subject: [PATCH] CLOSES #2408, CLOSES #2413, CLOSES #2414, CLOSES #2415, CLOSES #2416, CLOSES #2417, CLOSES #2419 --- mod_option_schemas.json | 2 +- src/core/FaithfulRes.lua | 3 + src/core/Game2.lua | 37 +++- src/core/game3/bag.lua | 17 +- src/core/game3/field.lua | 26 ++- src/core/game3/field_view.lua | 26 ++- src/core/game3/objects.lua | 52 ++++- src/core/game3/ow_sprites.lua | 89 ++++++++- src/core/game3/player.lua | 4 +- src/core/game3/scripting/adapters.lua | 44 ++--- src/core/game3/scripting/space.lua | 34 ++-- src/core/game3/scripting/stdscripts.lua | 1 + src/core/game3/vs_seeker.lua | 5 +- src/core/gen2/Save.lua | 3 +- src/import/LauncherSettings.lua | 10 + src/import/gba/versions.lua | 2 + src/mods/Gen3Compat.lua | 2 +- src/ui/game3/hud.lua | 1 + src/ui/game3/pc_menu.lua | 26 ++- src/ui/gen2/OptionsMenu.lua | 13 +- src/world/gen2/World.lua | 11 ++ tests/drivers/game3_badge_guard_names.lua | 105 ++++++++++ tests/drivers/game3_berry_powder_man_2416.lua | 90 +++++++++ tests/drivers/game3_fishing_pose_2419.lua | 152 ++++++++++++++ tests/drivers/game3_fuji_2417.lua | 107 ++++++++++ .../drivers/game3_invisible_journal_2414.lua | 66 +++++++ tests/drivers/game3_pc_se_2415.lua | 126 ++++++++++++ tests/drivers/game3_vs_seeker_bike_2419.lua | 108 ++++++++++ tests/drivers/gold_faithful_ratio.lua | 94 +++++++++ tests/engine/game2_faithful_res.lua | 178 +++++++++++++++++ tests/game3_berry_pouch_flag_test.lua | 87 ++++++++ tests/game3_bufferstdstring_test.lua | 99 ++++++++++ tests/game3_objects_flag_sync_test.lua | 186 ++++++++++++++++++ .../game3_objects_invisible_movement_test.lua | 98 +++++++++ tests/game3_ops_vars_test.lua | 21 ++ tests/game3_ow_fishing_pose_test.lua | 167 ++++++++++++++++ tests/game3_pc_sfx_2415_test.lua | 171 ++++++++++++++++ tests/game3_storage_test.lua | 2 +- tests/gen2_menus_test.lua | 5 +- 39 files changed, 2202 insertions(+), 68 deletions(-) create mode 100644 tests/drivers/game3_badge_guard_names.lua create mode 100644 tests/drivers/game3_berry_powder_man_2416.lua create mode 100644 tests/drivers/game3_fishing_pose_2419.lua create mode 100644 tests/drivers/game3_fuji_2417.lua create mode 100644 tests/drivers/game3_invisible_journal_2414.lua create mode 100644 tests/drivers/game3_pc_se_2415.lua create mode 100644 tests/drivers/game3_vs_seeker_bike_2419.lua create mode 100644 tests/drivers/gold_faithful_ratio.lua create mode 100644 tests/engine/game2_faithful_res.lua create mode 100644 tests/game3_berry_pouch_flag_test.lua create mode 100644 tests/game3_bufferstdstring_test.lua create mode 100644 tests/game3_objects_flag_sync_test.lua create mode 100644 tests/game3_objects_invisible_movement_test.lua create mode 100644 tests/game3_ow_fishing_pose_test.lua create mode 100644 tests/game3_pc_sfx_2415_test.lua diff --git a/mod_option_schemas.json b/mod_option_schemas.json index 592ffeba..4002f364 100644 --- a/mod_option_schemas.json +++ b/mod_option_schemas.json @@ -1 +1 @@ -{"mods":[],"schema_version":1} \ No newline at end of file +{"schema_version":1,"mods":[]} \ No newline at end of file diff --git a/src/core/FaithfulRes.lua b/src/core/FaithfulRes.lua index 9d42cdeb..843ebfa5 100644 --- a/src/core/FaithfulRes.lua +++ b/src/core/FaithfulRes.lua @@ -52,6 +52,7 @@ FaithfulRes.MIN_W, FaithfulRes.MIN_H = 480, 360 -- whether this module currently owns the window size FaithfulRes.locked = false FaithfulRes.prevSize = nil +FaithfulRes.modeSerial = 0 -- The highest level this display can actually show. -- @@ -210,6 +211,7 @@ function FaithfulRes.apply(v) flags.minheight = (prev and prev.minheight) or FaithfulRes.MIN_H love.window.setMode((prev and prev.w) or curW, (prev and prev.h) or curH, flags) + FaithfulRes.modeSerial = FaithfulRes.modeSerial + 1 FaithfulRes.prevSize = nil FaithfulRes.locked = false return false @@ -231,6 +233,7 @@ function FaithfulRes.apply(v) flags.resizable = false flags.minwidth, flags.minheight = w, h love.window.setMode(w, h, flags) + FaithfulRes.modeSerial = FaithfulRes.modeSerial + 1 FaithfulRes.locked = true return true end diff --git a/src/core/Game2.lua b/src/core/Game2.lua index a22a7f7f..629f375a 100644 --- a/src/core/Game2.lua +++ b/src/core/Game2.lua @@ -1520,7 +1520,9 @@ end function Game2:fxWorldOrigin(w, h, scale) local cam = self.world and self.world.camera if not cam then return nil end - local fx, fy = Playfield.rect(w, h) + local fx, fy, fw, fh = Playfield.rect(w, h) + local bx, by = Game2.faithfulBox(fw, fh) + if bx then fx, fy = fx + bx, fy + by end return fx + math.floor(-cam.x * scale), fy + math.floor(-cam.y * scale) end @@ -1554,6 +1556,13 @@ end -- the grid itself. function Game2:drawViewportFrame() local G = love.graphics + local serial = require("src.core.FaithfulRes").modeSerial + if serial ~= (self.modeSerial or 0) then + if self.world and self.world.map then + self.world:dropBakes() + end + self.modeSerial = serial + end local w, h = GameViewport.dimensions() local ShaderFX = require("src.render.ShaderFX") local GbcPalette = require("src.render.GbcPalette") @@ -1730,8 +1739,31 @@ function Game2:textboxPaper() return nil end +function Game2.faithfulBox(w, h) + if not require("src.core.FaithfulRes").scaleCap() then return nil end + local s = math.max(1, math.floor(math.min(w / 160, h / 144))) + local bw, bh = 160 * s, 144 * s + local lift = 0 + local ScreenPosition = require("src.core.ScreenPosition") + if not ScreenPosition.skinActive(w, h) then + lift = ScreenPosition.lift(h, bh, ScreenPosition.safeTop()) + end + return math.floor((w - bw) / 2), math.floor((h - bh) / 2) - lift, bw, bh +end + function Game2:drawContained(w, h) - local pw, ph = Playfield.push(w, h) + local pw, ph, px, py = Playfield.push(w, h) + local bx, by, bw, bh = Game2.faithfulBox(pw, ph) + if bx then + local G = love.graphics + G.setColor(0, 0, 0, 1) + G.rectangle("fill", 0, 0, pw, ph) + G.setColor(1, 1, 1, 1) + G.setScissor(px + bx, py + by, bw, bh) + G.translate(bx, by) + Playfield.enter(px + bx, py + by, bw, bh) + pw, ph = bw, bh + end local ok, err = pcall(self.drawScene, self, pw, ph) Playfield.pop() if not ok then error(err, 0) end @@ -2327,6 +2359,7 @@ function Game2:applyOptions() hotbar = options.hotbar, }) require("src.core.VideoMode").applyOptions(options) + require("src.core.FaithfulRes").applyOptions(options) require("src.core.ScreenPosition").applyOptions(options) require("src.core.VSync").applyOptions(options) require("src.core.FrameCap").applyOptions(options) diff --git a/src/core/game3/bag.lua b/src/core/game3/bag.lua index c32ab7ce..0e766d5c 100644 --- a/src/core/game3/bag.lua +++ b/src/core/game3/bag.lua @@ -258,6 +258,18 @@ function Bag.canAdd(bag, id, qty) end +local FLAG_SYS_GOT_BERRY_POUCH = 0x847 -- include/constants/flags.h:1405 + +local function mark_berry_pouch() + local Space = package.loaded["src.core.game3.scripting.space"] + local store = type(Space) == "table" and Space.store + if store then + require("src.core.game3.scripting.flags").setFlag(store, nil, FLAG_SYS_GOT_BERRY_POUCH, true) + end +end + +Bag.FLAG_SYS_GOT_BERRY_POUCH = FLAG_SYS_GOT_BERRY_POUCH + function Bag.add(bag, id, qty) bag = ensure(bag) qty = math.max(0, math.floor(tonumber(qty) or 1)) @@ -283,8 +295,9 @@ function Bag.add(bag, id, qty) return false, 0 end end - if num == ItemsData.ITEM_BERRY_POUCH or storeId == ItemsData.ITEM_BERRY_POUCH then - -- Flag handled by scripting later; bag just stores the key item. + -- src/item.c:242 + if pocket == "BERRY_POUCH" or num == ItemsData.ITEM_BERRY_POUCH or storeId == ItemsData.ITEM_BERRY_POUCH then + mark_berry_pouch() end local slots = bag.pockets[pocket] diff --git a/src/core/game3/field.lua b/src/core/game3/field.lua index 7f446cd7..8e14285b 100644 --- a/src/core/game3/field.lua +++ b/src/core/game3/field.lua @@ -1002,10 +1002,22 @@ function Field.startFishing(rod) Field.locked = true Player.fishing = true -- field_player_avatar.c:1667 - Field._fishing = { rod = tonumber(rod) or 0, step = "wait", timer = 0, dots = 0, required = 0, rounds = 0 } + Field._fishing = { rod = tonumber(rod) or 0, step = "wait", timer = 0, dots = 0, required = 0, rounds = 0, + anim = "takeout", animT = 0 } return true end +-- pokefirered/src/field_player_avatar.c:1954 AlignFishingAnimationFrames +function Field.fishingPose() + local f = Field._fishing + if not (f and Player.fishing) then return nil end + local OwSprites = require("src.core.game3.ow_sprites") + local facing = Player.facing or "down" + local g = OwSprites.fishingFrame(facing, f.anim, f.animT) + local x2, y2 = OwSprites.fishingOffset(OwSprites.fishingAbsFrame(facing, g), facing) + return g, x2, y2 +end + -- pokefirered/src/field_player_avatar.c:1936 Fishing16 local function fishingStop() Field._fishing = nil @@ -1050,6 +1062,14 @@ function Field.updateFishing() local Message = require("src.ui.game3.message") local Rng = require("src.core.game3.rng") f.timer = f.timer + 1 + f.animT = (f.animT or 0) + 1 + + if f.step == "result" and f.anim == "putaway" and Player.fishing then + local OwSprites = require("src.core.game3.ow_sprites") + local _, ended = OwSprites.fishingFrame(Player.facing, f.anim, f.animT) + -- pokefirered/src/field_player_avatar.c:1918 Fishing15 + if ended then Player.fishing = false end + end if f.step == "wait" then if f.timer >= FISHING_WAIT_FRAMES then @@ -1082,9 +1102,13 @@ function Field.updateFishing() local hasMons = okE and Encounters and Encounters.hasFishingMons and Encounters.hasFishingMons(fishingMapId()) or false if (not hasMons) or (Rng.Random() % 2 == 1) then + -- pokefirered/src/field_player_avatar.c:1890 Fishing12 + f.anim, f.animT = "putaway", 0 -- pokefirered/src/strings.c:1060 gText_NotEvenANibble Message.show(Strings("Not even a nibble…"), function() fishingStop() end) else + -- pokefirered/src/field_player_avatar.c:1791 + f.anim, f.animT = "hooked", 0 -- pokefirered/src/strings.c:1059 gText_PokemonOnHook local rod = f.rod Message.show(Strings("A POKéMON's on the hook!"), function() diff --git a/src/core/game3/field_view.lua b/src/core/game3/field_view.lua index a393fa0f..1288ac5d 100644 --- a/src/core/game3/field_view.lua +++ b/src/core/game3/field_view.lua @@ -351,6 +351,8 @@ local function collectNeighborActors(actors, baseIndex, hostMapId, hostDef) local ox, oy = tonumber(obj.x) or 0, tonumber(obj.y) or 0 local out = bounds and (ox < 0 or oy < 0 or ox >= bounds.w or oy >= bounds.h) + -- src/event_object_movement.c:8014 + if tonumber(obj.movementType) == 0x4C then out = true end if objectVisible(obj) and not out then baseIndex = baseIndex + 1 actors[#actors + 1] = { @@ -459,6 +461,9 @@ local function drawSingleActor(game, mapDef, a, camX, camY) local opts = { bow = a.bow, fieldMove = a.fieldMove, + fieldMoveFrame = a.fieldMoveFrame, + fishing = a.fishing, + fishFrame = a.fishFrame, frame = a.frame, } drew = OwSprites.draw( @@ -568,16 +573,31 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk if PlayerMod and PlayerMod.moving and PlayerMod.targetY and PlayerMod.targetY > (PlayerMod.cellY or 0) then playerSortY = math.max(playerSortY, PlayerMod.targetY * CELL) end + local fieldMove = (PlayerMod and PlayerMod.fieldMoveAnim and PlayerMod.fieldMoveAnim > 0) or false + local fieldMoveFrame + if fieldMove and useOw and OwSprites.fieldMoveFrame then + fieldMoveFrame = OwSprites.fieldMoveFrame( + (PlayerMod.fieldMoveTotal or PlayerMod.fieldMoveAnim) - PlayerMod.fieldMoveAnim, + PlayerMod.fieldMoveKind) + end + local FieldMod = package.loaded["src.core.game3.field"] + local fishFrame, fishX2, fishY2 + if not fieldMove and FieldMod and FieldMod.fishingPose then + fishFrame, fishX2, fishY2 = FieldMod.fishingPose() + end actors[#actors + 1] = { kind = "player", elevation = PlayerMod and PlayerMod.elevation or 3, - x = px + (playerXOff or 0), - y = py + (playerYOff or 0), + x = px + (playerXOff or 0) + (fishX2 or 0), + y = py + (playerYOff or 0) + (fishY2 or 0), sortY = playerSortY, facing = facing or "down", walkPhase = (walkPhase == 1 or walkPhase == true) and 1 or 0, stepFlip = stepFlip and true or false, - fieldMove = (PlayerMod and PlayerMod.fieldMoveAnim and PlayerMod.fieldMoveAnim > 0), + fieldMove = fieldMove, + fieldMoveFrame = fieldMoveFrame, + fishing = fishFrame ~= nil, + fishFrame = fishFrame, sprite = playerSpriteName(game), graphicsId = useOw and OwSprites.playerGraphicsId(game) or nil, } diff --git a/src/core/game3/objects.lua b/src/core/game3/objects.lua index 1f5667bc..d3a480da 100644 --- a/src/core/game3/objects.lua +++ b/src/core/game3/objects.lua @@ -18,6 +18,9 @@ local WALK_FRAMES = 16 local RUN_FRAMES = 8 -- pokefirered/src/event_object_movement.c:9029 UpdateRunSlowAnim local RUN_SLOW_FRAMES = 11 +-- include/constants/event_object_movement.h:81 +local MOVEMENT_TYPE_INVISIBLE = 0x4C +Objects.MOVEMENT_TYPE_INVISIBLE = MOVEMENT_TYPE_INVISIBLE local DELTA = { up = { 0, -1 }, down = { 0, 1 }, @@ -173,6 +176,8 @@ local function newEventObject(def) flag = def.flag, visible = objectVisible(def), hidden = not objectVisible(def), + -- src/event_object_movement.c:1569 + invisible = mt == MOVEMENT_TYPE_INVISIBLE, frozen = false, passable = def.passable and true or false, moving = false, @@ -287,6 +292,8 @@ local function applyPerm(eo, mapId) end local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[row.movementType] if face then eo.facing = face end + -- src/event_object_movement.c:1569 + eo.invisible = tonumber(row.movementType) == MOVEMENT_TYPE_INVISIBLE if eo.def then eo.def.movementType = row.movementType end end if row.facing ~= nil then @@ -435,6 +442,8 @@ function Objects.loadMap(game, mapId, mapDef) local tmt = Objects._templateMt[eo.localId] if tmt then Objects.setTrainerMovementType(eo, tmt) + -- src/event_object_movement.c:1569 + eo.invisible = tmt == MOVEMENT_TYPE_INVISIBLE local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[tmt] if face then eo.facing = face end end @@ -512,7 +521,8 @@ function Objects.forDraw() local list = {} for _, lid in ipairs(Objects._order) do local eo = Objects._byId[lid] - if eo and eo.visible and not eo.hidden + -- src/event_object_movement.c:8014 + if eo and eo.visible and not eo.hidden and not eo.invisible and not offMap(Objects._bounds, eo) then list[#list + 1] = eo end @@ -1050,7 +1060,8 @@ function Objects.poolForDraw(pool) if type(pool) ~= "table" then return list end for _, lid in ipairs(pool.order or {}) do local eo = pool.byId[lid] - if eo and eo.visible and not eo.hidden and not offMap(pool.bounds, eo) then + if eo and eo.visible and not eo.hidden and not eo.invisible + and not offMap(pool.bounds, eo) then list[#list + 1] = eo end end @@ -1086,6 +1097,10 @@ function Objects.addObject(localId) localId = tonumber(localId) or 0 local eo = Objects._byId[localId] if eo then + if eo.hidden then + -- src/event_object_movement.c:1569 + eo.invisible = Objects.templateMovementType(localId) == MOVEMENT_TYPE_INVISIBLE + end applyPerm(eo, Objects._mapId) eo.hidden = false eo.visible = true @@ -1119,9 +1134,26 @@ function Objects.refreshVisibility() end end +-- src/event_object_movement.c:1841 +local function inCameraView(eo) + local P = Player() + if not P then return false end + local px, py = tonumber(P.cellX), tonumber(P.cellY) + if not px or not py then return false end + local function inside(x, y) + x, y = tonumber(x), tonumber(y) + return x ~= nil and y ~= nil + and x >= px - 9 and x <= px + 10 and y >= py - 7 and y <= py + 9 + end + return inside(eo.cellX, eo.cellY) or inside(eo.homeX, eo.homeY) +end + +Objects.inCameraView = inCameraView + --- pret FlagClear/FlagSet on an object template hide flag. -- clearflag after removeobject must bring the NPC back at perm coords. -function Objects.syncFlagVisibility(flagId, hidden) +-- src/scrcmd.c:558 +function Objects.syncFlagVisibility(flagId, hidden, force) flagId = tonumber(flagId) or 0 if flagId == 0 or flagId == 0xFFFF or flagId == 65535 then return end for _, lid in ipairs(Objects._order) do @@ -1130,10 +1162,16 @@ function Objects.syncFlagVisibility(flagId, hidden) local f = tonumber(eo.flag) or (eo.def and tonumber(eo.def.flag or eo.def.flagId)) or 0 if f == flagId then if hidden then - eo.hidden = true - eo.visible = false - if eo.def then eo.def.hidden = true end + if force or not inCameraView(eo) then + eo.hidden = true + eo.visible = false + if eo.def then eo.def.hidden = true end + end else + if eo.hidden then + -- src/event_object_movement.c:1569 + eo.invisible = Objects.templateMovementType(lid) == MOVEMENT_TYPE_INVISIBLE + end applyPerm(eo, Objects._mapId) eo.hidden = false eo.visible = true @@ -1253,6 +1291,8 @@ function Objects.setTrainerMovementType(localId, mt) if hostMv.radius then eo.radius = hostMv.radius end end clearRaiseHand(eo) + -- src/event_object_movement.c:4543 + if mt == MOVEMENT_TYPE_INVISIBLE then eo.invisible = true end if eo.movement == "RAISE_HAND" then eo.facing = "down" end diff --git a/src/core/game3/ow_sprites.lua b/src/core/game3/ow_sprites.lua index bd184273..cc875aac 100644 --- a/src/core/game3/ow_sprites.lua +++ b/src/core/game3/ow_sprites.lua @@ -127,7 +127,76 @@ function OwSprites.get(graphicsId) return spr end -local FIELD_MOVE_POSE = { down = 3, up = 7, left = 4, right = 4 } +-- src/data/object_events/object_event_anims.h:633 +local FIELD_MOVE_SEQ = { 0, 4, 1, 4, 2, 4, 3, 4, 4, 8 } +-- src/data/object_events/object_event_anims.h:642 +local VS_SEEKER_SEQ = { 0, 4, 1, 4, 5, 4, 6, 4 } +for _ = 1, 7 do + VS_SEEKER_SEQ[#VS_SEEKER_SEQ + 1] = 7; VS_SEEKER_SEQ[#VS_SEEKER_SEQ + 1] = 4 + VS_SEEKER_SEQ[#VS_SEEKER_SEQ + 1] = 8; VS_SEEKER_SEQ[#VS_SEEKER_SEQ + 1] = 4 +end +for _, v in ipairs({ 6, 4, 1, 4, 0, 4 }) do VS_SEEKER_SEQ[#VS_SEEKER_SEQ + 1] = v end +-- src/data/object_events/object_event_anims.h:657 +local VS_SEEKER_BIKE_SEQ = { 0, 4, 1, 4, 2, 4, 3, 4 } +for _ = 1, 7 do + VS_SEEKER_BIKE_SEQ[#VS_SEEKER_BIKE_SEQ + 1] = 4; VS_SEEKER_BIKE_SEQ[#VS_SEEKER_BIKE_SEQ + 1] = 4 + VS_SEEKER_BIKE_SEQ[#VS_SEEKER_BIKE_SEQ + 1] = 5; VS_SEEKER_BIKE_SEQ[#VS_SEEKER_BIKE_SEQ + 1] = 4 +end +for _, v in ipairs({ 3, 4, 2, 4, 1, 4, 0, 4 }) do VS_SEEKER_BIKE_SEQ[#VS_SEEKER_BIKE_SEQ + 1] = v end + +-- src/data/object_events/object_event_anims.h:877 +local FISH_BASE = { down = 8, up = 4, left = 0, right = 0 } +local FISH_TAKE_OUT = { 0, 4, 1, 4, 2, 4, 3, 4 } +-- src/data/object_events/object_event_anims.h:909 +local FISH_PUT_AWAY_SN = { 3, 4, 2, 6, 1, 6, 0, 6 } +local FISH_PUT_AWAY_WE = { 3, 4, 2, 4, 1, 4, 0, 4 } +-- src/data/object_events/object_event_anims.h:941 +local FISH_HOOKED = { 2, 6, 3, 6, 2, 6, 3, 6, 3, 30 } + +local function seqFrame(seq, t, loop) + t = math.max(0, math.floor(tonumber(t) or 0)) + local total = 0 + for i = 2, #seq, 2 do total = total + seq[i] end + if loop and total > 0 then t = t % total end + local acc = 0 + for i = 1, #seq, 2 do + acc = acc + seq[i + 1] + if t < acc then return seq[i], false end + end + return seq[#seq - 1], true +end + +function OwSprites.fieldMoveFrame(elapsed, kind) + local seq = FIELD_MOVE_SEQ + if kind == "vs_seeker" then seq = VS_SEEKER_SEQ + elseif kind == "vs_seeker_bike" then seq = VS_SEEKER_BIKE_SEQ end + return (seqFrame(seq, elapsed)) +end + +function OwSprites.fishingFrame(facing, anim, t) + if anim == "hooked" then + return seqFrame(FISH_HOOKED, t, true) + elseif anim == "putaway" then + local seq = (facing == "left" or facing == "right") and FISH_PUT_AWAY_WE or FISH_PUT_AWAY_SN + return seqFrame(seq, t) + end + return seqFrame(FISH_TAKE_OUT, t) +end + +function OwSprites.fishingAbsFrame(facing, frameInGroup) + return (FISH_BASE[facing] or 8) + (tonumber(frameInGroup) or 3) +end + +-- src/field_player_avatar.c:1954 AlignFishingAnimationFrames +function OwSprites.fishingOffset(absFrame, facing) + local x2, y2 = 0, 0 + if absFrame == 1 or absFrame == 2 or absFrame == 3 then + x2 = (facing == "left") and -8 or 8 + end + if absFrame == 5 then y2 = -8 end + if absFrame == 10 or absFrame == 11 then y2 = 8 end + return x2, y2 +end -- ------------------------------------------------ runtime palette substitution -- pret recolours field objects by loading a new palette into their OBJ palette @@ -287,10 +356,15 @@ function OwSprites.pose(spr, facing, walkPhase, stepFlip, opts) return 9, false end - if opts and opts.fieldMove and spr.frameCount >= 9 then - local f = FIELD_MOVE_POSE[facing] or 3 + if opts and opts.fishing and spr.frameCount >= 12 then + local g = math.max(0, math.min(3, tonumber(opts.fishFrame) or 3)) + return OwSprites.fishingAbsFrame(facing, g), flip + end + + if opts and opts.fieldMove and spr.frameCount >= 6 then + local f = tonumber(opts.fieldMoveFrame) or 4 if f >= spr.frameCount then f = 0 end - return f, flip + return f, false end if spr.frameCount == 3 then @@ -323,7 +397,7 @@ end --- Draw at world pixel position (cell top-left). Feet at bottom of sprite. -- opts.bow: use nurse bow frame (ANIM_NURSE_BOW). --- opts.fieldMove: use directional arm-raise field move frame. +-- opts.fieldMove: use the arm-raise field move frame (opts.fieldMoveFrame). -- opts.frame: explicit frame index override. function OwSprites.draw(graphicsId, px, py, camX, camY, facing, walkPhase, stepFlip, opts) local spr = OwSprites.getDraw(graphicsId) @@ -352,6 +426,11 @@ function OwSprites.playerGraphicsId(game) if P then if P.fieldMoveAnim and P.fieldMoveAnim > 0 then + if P.fieldMoveKind == "vs_seeker_bike" then + -- src/field_player_avatar.c:1331 + return isFemale and (Versions.OW_PLAYER_FEMALE_VS_SEEKER_BIKE or 13) + or (Versions.OW_PLAYER_MALE_VS_SEEKER_BIKE or 6) + end return isFemale and (Versions.OW_PLAYER_FEMALE_FIELD_MOVE or 10) or (Versions.OW_PLAYER_MALE_FIELD_MOVE or 3) end diff --git a/src/core/game3/player.lua b/src/core/game3/player.lua index 49a37c8f..f95ef932 100644 --- a/src/core/game3/player.lua +++ b/src/core/game3/player.lua @@ -611,8 +611,10 @@ function Player.startSurfing(game, onDone) return true end -function Player.startFieldMove(duration) +function Player.startFieldMove(duration, kind) Player.fieldMoveAnim = duration or 28 + Player.fieldMoveTotal = Player.fieldMoveAnim + Player.fieldMoveKind = kind end local function finishStep(game) diff --git a/src/core/game3/scripting/adapters.lua b/src/core/game3/scripting/adapters.lua index cb74d9b1..a22746f4 100644 --- a/src/core/game3/scripting/adapters.lua +++ b/src/core/game3/scripting/adapters.lua @@ -17,6 +17,22 @@ local WARP_SLOT_FIELD = { setholewarp = "holeWarp", } +-- pokefirered/src/script_menu.c:574 +local STD_STRINGS = { + [0] = "COOL", "BEAUTY", "CUTE", "SMART", "TOUGH", + "COOL", "BEAUTY", "CUTE", "SMART", "TOUGH", + "ITEMS", "KEY ITEMS", "POKé BALLS", "TMs & HMs", "BERRIES", + "BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE", + "SOULBADGE", "MARSHBADGE", "VOLCANOBADGE", "EARTHBADGE", + "COINS", + "ITEMS POCKET", "KEY ITEMS POCKET", "POKé BALLS POCKET", "TM CASE", "BERRY POUCH", +} + +local function stdString(id) + local s = STD_STRINGS[tonumber(id) or -1] + return s and Strings(s) or nil +end + -- pokefirered/src/event_object_movement.c:5208 GetOppositeDirection local OPPOSITE_DIR = { down = "up", up = "down", left = "right", right = "left" } @@ -148,14 +164,7 @@ function Adapters.stub(opts) end end if op == "bufferstdstring" then - local STD = { - [24] = Strings("ITEMS POCKET"), - [25] = Strings("KEY ITEMS POCKET"), - [26] = Strings("POKé BALLS POCKET"), - [27] = Strings("TM CASE"), - [28] = Strings("BERRY POUCH"), - } - return STD[tonumber(src) or -1] + return stdString(src) end return nil end @@ -817,7 +826,9 @@ function Adapters.host(mod, game, world) onFlagChanged = function(flagId, hidden) local G3 = useGame3Objects() if G3 and G3.syncFlagVisibility then - G3.syncFlagVisibility(flagId, hidden and true or false) + local Space = package.loaded["src.core.game3.scripting.space"] + G3.syncFlagVisibility(flagId, hidden and true or false, + Space and Space._inTransition and true or nil) end end, hideObject = function(localId) @@ -1483,20 +1494,7 @@ function Adapters.host(mod, game, world) return ItemsData.displayName(src) end if op == "bufferstdstring" then - -- pret constants/menu.h STDSTRING_* - local STD = { - [10] = Strings("ITEMS"), - [11] = Strings("KEY ITEMS"), - [12] = Strings("POKé BALLS"), - [13] = Strings("TMs & HMs"), - [14] = Strings("BERRIES"), - [24] = Strings("ITEMS POCKET"), - [25] = Strings("KEY ITEMS POCKET"), - [26] = Strings("POKé BALLS POCKET"), - [27] = Strings("TM CASE"), - [28] = Strings("BERRY POUCH"), - } - return STD[tonumber(src) or -1] or tostring(src) + return stdString(src) or tostring(src) end if op == "bufferpartymonnick" then local Runtime = package.loaded["src.core.game3.runtime"] diff --git a/src/core/game3/scripting/space.lua b/src/core/game3/scripting/space.lua index 135c9da1..5232b6f3 100644 --- a/src/core/game3/scripting/space.lua +++ b/src/core/game3/scripting/space.lua @@ -7,6 +7,7 @@ local Vm = require("src.core.game3.scripting.vm") local Adapters = require("src.core.game3.scripting.adapters") local ExtractScripts = require("src.import.gba.extract_scripts") local GfxIds = require("src.core.game3.scripting.gfx_ids") +local ItemsData = require("src.core.game3.items_data") local Space = {} @@ -116,6 +117,10 @@ local function load_sidecar(mod, game) vars = session.vars, }) Flags.ensurePalletOakHidden(Space.store) + local Bag = require("src.core.game3.bag") + if session.bag and Bag.has(session.bag, ItemsData.ITEM_BERRY_POUCH, 1) then + Flags.setFlag(Space.store, nil, Bag.FLAG_SYS_GOT_BERRY_POUCH, true) -- src/item.c:249 + end return end game = resolve_game(mod, game) @@ -400,18 +405,25 @@ function Space.runEnterScripts(mod, mapId, game, world, opts) if not ev then return Space.vm end local vm = Space.vm local ms = ev.mapScripts or {} - if ms.onTransition and type(ms.onTransition) == "string" then - vm:start(ms.onTransition) - -- Drain short transition scripts so ON_FRAME can run this enter. - for _ = 1, 64 do - if not vm:isRunning() then break end - vm:tick() - end - -- VAR_OBJ_GFX_ID_* / setobjectxyperm applied — refresh NPC sprites. - Space.refreshObjectGraphics() - end -- pokefirered/src/overworld.c:807 - Space.runOnLoad(mapId) + -- pokefirered/src/event_object_movement.c:1813 + Space._inTransition = true + local ok, err = pcall(function() + if ms.onTransition and type(ms.onTransition) == "string" then + vm:start(ms.onTransition) + -- Drain short transition scripts so ON_FRAME can run this enter. + for _ = 1, 64 do + if not vm:isRunning() then break end + vm:tick() + end + -- VAR_OBJ_GFX_ID_* / setobjectxyperm applied — refresh NPC sprites. + Space.refreshObjectGraphics() + end + -- pokefirered/src/fieldmap.c:93 + Space.runOnLoad(mapId) + end) + Space._inTransition = false + if not ok then error(err, 0) end -- pokefirered/src/overworld.c:783 Space.runOnResume(mapId) -- pokefirered/src/overworld.c:2148 diff --git a/src/core/game3/scripting/stdscripts.lua b/src/core/game3/scripting/stdscripts.lua index 22e150c6..3a38fc4e 100644 --- a/src/core/game3/scripting/stdscripts.lua +++ b/src/core/game3/scripting/stdscripts.lua @@ -285,6 +285,7 @@ Std.SCRIPTS = { { op = "lockall" }, { op = "setvar", var = 0x8004, value = 0 }, { op = "special", id = Std.SPECIAL.AnimatePcTurnOn }, + { op = "playse", [1] = 4 }, -- data/scripts/pc.inc:9 { op = "loadword", dest = 0, value = "Text_BootedUpPC" }, { op = "message", ptr = 0 }, { op = "waitmessage" }, diff --git a/src/core/game3/vs_seeker.lua b/src/core/game3/vs_seeker.lua index 02306ae0..0a2009aa 100644 --- a/src/core/game3/vs_seeker.lua +++ b/src/core/game3/vs_seeker.lua @@ -568,7 +568,10 @@ function VsSeeker.use(session, game, onDone) fieldLock(true) freezeAll(Objects) local Player = package.loaded["src.core.game3.player"] - if Player and Player.startFieldMove then Player.startFieldMove(VsSeeker.EFFECT_FRAMES) end + -- src/field_player_avatar.c:1336 + if Player and Player.startFieldMove then + Player.startFieldMove(VsSeeker.EFFECT_FRAMES, Player.biking and "vs_seeker_bike" or "vs_seeker") + end end, tick = function() if seq.finished or seq.waitingText then return end diff --git a/src/core/gen2/Save.lua b/src/core/gen2/Save.lua index a95284da..d4fefc12 100644 --- a/src/core/gen2/Save.lua +++ b/src/core/gen2/Save.lua @@ -336,6 +336,7 @@ Save.DEFAULT_OPTIONS = { color = "gbc", palette = "", videoMode = "windowed", + faithfulRes = 0, fpsCap = 60, battleLayout = "og", battleHud = "standard", @@ -373,7 +374,7 @@ Save.OPTIONS_KEY = "gold" local SHARED_KEYS = { touchControls = true, haptics = true, screenPos = true, - videoMode = true, + videoMode = true, faithfulRes = true, mods = true, modsByVersion = true, modsGen2 = true, modOptions = true, modProfiles = true, modProfilesSeeded = true, activeProfile = true, diff --git a/src/import/LauncherSettings.lua b/src/import/LauncherSettings.lua index 7c62b99e..c66c6aad 100644 --- a/src/import/LauncherSettings.lua +++ b/src/import/LauncherSettings.lua @@ -671,6 +671,16 @@ local function gen2Rows(opts, hooks, shared) end) end + local okFr, FaithfulRes = pcall(require, "src.core.FaithfulRes") + if okFr then + add(Strings("FAITHFUL RATIO"), + function() return FaithfulRes.label(shared.faithfulRes) end, + function(dir) + shared.faithfulRes = FaithfulRes.cycle(shared.faithfulRes, dir) + return true + end) + end + local okCap, FrameCap = pcall(require, "src.core.FrameCap") if okCap then add(Strings("MAX FPS"), diff --git a/src/import/gba/versions.lua b/src/import/gba/versions.lua index dcf8801e..c4dd9868 100644 --- a/src/import/gba/versions.lua +++ b/src/import/gba/versions.lua @@ -76,11 +76,13 @@ Versions.OW_PLAYER_MALE_BIKE = 1 Versions.OW_PLAYER_MALE_SURF = 2 Versions.OW_PLAYER_MALE_FIELD_MOVE = 3 Versions.OW_PLAYER_MALE_FISH = 4 +Versions.OW_PLAYER_MALE_VS_SEEKER_BIKE = 6 Versions.OW_PLAYER_FEMALE = 7 Versions.OW_PLAYER_FEMALE_BIKE = 8 Versions.OW_PLAYER_FEMALE_SURF = 9 Versions.OW_PLAYER_FEMALE_FIELD_MOVE = 10 Versions.OW_PLAYER_FEMALE_FISH = 11 +Versions.OW_PLAYER_FEMALE_VS_SEEKER_BIKE = 13 -- FireRed USA 1.0 font (menu cursor = SelectorArrow2 / charmap ▶ = 0xEF). Versions.FONT_LATIN_NORMAL = 0x1FF300 -- sFontNormalLatinGlyphs Versions.FONT_LATIN_WIDTHS = 0x207300 -- sFontNormalLatinGlyphWidths diff --git a/src/mods/Gen3Compat.lua b/src/mods/Gen3Compat.lua index 35b4114d..0d43006d 100644 --- a/src/mods/Gen3Compat.lua +++ b/src/mods/Gen3Compat.lua @@ -334,7 +334,7 @@ function Gen3Compat.setFlag(name, value) Flags.setFlag(store, nil, id, value and true or false) local Objects = package.loaded["src.core.game3.objects"] if Objects and Objects.syncFlagVisibility then - Objects.syncFlagVisibility(id, value and true or false) + Objects.syncFlagVisibility(id, value and true or false, true) end return true end diff --git a/src/ui/game3/hud.lua b/src/ui/game3/hud.lua index f5b052a0..f4ccdbc1 100644 --- a/src/ui/game3/hud.lua +++ b/src/ui/game3/hud.lua @@ -368,6 +368,7 @@ function Hud.openMessageStay(game, text, opts) end function Hud.openPc(game, session) + pcall(function() require("src.core.game3.audio").playSe(4) end) -- data/scripts/pc.inc:9 PcMenu.show({ session = session or (require("src.core.game3.runtime").getSession()) }) end diff --git a/src/ui/game3/pc_menu.lua b/src/ui/game3/pc_menu.lua index a3666230..b7107178 100644 --- a/src/ui/game3/pc_menu.lua +++ b/src/ui/game3/pc_menu.lua @@ -54,6 +54,8 @@ local function se(id) end local FLAG_SYS_NOT_SOMEONES_PC = 0x834 -- pokefirered/include/constants/flags.h:1386 +local FLAG_SYS_POKEDEX_GET = 0x829 -- pokefirered/include/constants/flags.h:1375 +local FLAG_SYS_GAME_CLEAR = 0x82C -- pokefirered/include/constants/flags.h:1378 local function script_store(session) local Space = package.loaded["src.core.game3.scripting.space"] @@ -73,19 +75,26 @@ local function player_pc_name(session) return Strings("%s's PC", name) end +-- pokefirered/src/script_menu.c:1006 function PcMenu._rootEntries() local who = someone_or_bill_name(PcMenu._session) local player = player_pc_name(PcMenu._session) + local Flags = require("src.core.game3.scripting.flags") + local store = script_store(PcMenu._session) + local clear = Flags.getFlag(store, nil, FLAG_SYS_GAME_CLEAR) + local dex = clear or Flags.getFlag(store, nil, FLAG_SYS_POKEDEX_GET) local key = (Strings.active() and "t" or "e") .. "\0" .. who .. "\0" .. player + .. "\0" .. (dex and "d" or "") .. (clear and "c" or "") if PcMenu._rootKey ~= key then PcMenu._rootKey = key - PcMenu._rootRows = { + local rows = { { id = "storage", label = who }, { id = "player", label = player }, - { id = "oak", label = Strings("PROF. OAK's PC") }, - { id = "hall", label = Strings("HALL OF FAME") }, - { id = "quit", label = Strings("LOG OFF") }, } + if dex then rows[#rows + 1] = { id = "oak", label = Strings("PROF. OAK's PC") } end + if clear then rows[#rows + 1] = { id = "hall", label = Strings("HALL OF FAME") } end + rows[#rows + 1] = { id = "quit", label = Strings("LOG OFF") } + PcMenu._rootRows = rows end return PcMenu._rootRows end @@ -106,7 +115,6 @@ function PcMenu.show(opts) else PcMenu.mode = "root" PcMenu._status = Strings("Which PC would you like to access?") - se(2) -- SE_PC_ON / SE_PC_LOGIN end Stack.push("pc_menu", PcMenu, { hideBelow = false }) end @@ -201,20 +209,24 @@ function PcMenu.handleInput(input) elseif input:wasPressed("a") then local choice = entries[PcMenu.cursor] if choice.id == "quit" then + se(5) -- pokefirered/src/menu.c:347 PcMenu.close() elseif choice.id == "storage" then se(5) + se(2) -- data/scripts/pc.inc:47 PcMenu.mode = "storage_menu" PcMenu.storageCursor = PcMenu.storageCursor or 1 PcMenu.cursor = PcMenu.storageCursor PcMenu._status = STORAGE_OPTIONS[PcMenu.cursor].desc elseif choice.id == "player" then se(5) + se(2) -- data/scripts/pc.inc:39 PcMenu.mode = "player_pc" PcMenu.cursor = 1 PcMenu._status = Strings("What would you like to do?") elseif choice.id == "oak" then se(5) + se(2) -- data/scripts/pc.inc:88 local Dex = require("src.core.game3.dex") local dex = PcMenu._session and PcMenu._session.dex local caught = dex and Dex.countCaught(dex, "kanto") or 0 @@ -224,11 +236,13 @@ function PcMenu.handleInput(input) PcMenu.mode = "msg" elseif choice.id == "hall" then se(5) + se(2) -- data/scripts/pc.inc:76 PcMenu._status = Strings("No records in the HALL OF FAME.") PcMenu._prevMode = "root" PcMenu.mode = "msg" end elseif input:wasPressed("b") then + se(5) -- pokefirered/src/script_menu.c:831 PcMenu.close() end return @@ -585,7 +599,7 @@ function PcMenu.draw() -- Root Menu Box if PcMenu.mode == "root" then local entries = PcMenu._rootEntries() - Window.stdFrame(Window.template(1, 1, 14, 10)) + Window.stdFrame(Window.template(1, 1, 14, #entries * 2)) for i, e in ipairs(entries) do local yPx = 10 + (i - 1) * 16 if i == PcMenu.cursor then Window.cursorPx(12, yPx) end diff --git a/src/ui/gen2/OptionsMenu.lua b/src/ui/gen2/OptionsMenu.lua index d2ad12b6..df9fb7a5 100644 --- a/src/ui/gen2/OptionsMenu.lua +++ b/src/ui/gen2/OptionsMenu.lua @@ -61,6 +61,7 @@ local FINITE_VALUE_SOURCES = { Strings.source("LIGHT"), Strings.source("STRONG"), Strings.source("ADAPTIVE"), Strings.source("UNAVAILABLE"), Strings.source("DISPLAY"), Strings.source("DISPLAY (%dHZ)"), + Strings.source("ON"), Strings.source("4X"), } local function volLabel(v) @@ -345,6 +346,15 @@ local ROWS = { return VideoMode.normalize(options.videoMode) == "borderless" and Strings("FULL") or Strings("WINDOWED") end }, + { label = Strings.source("FAITHFUL RATIO"), key = "faithfulRes", port = true, + cycle = function(options, delta) + local FaithfulRes = require("src.core.FaithfulRes") + options.faithfulRes = FaithfulRes.cycle(options.faithfulRes, delta) + FaithfulRes.apply(options.faithfulRes) + end, + text = function(options) + return Strings(require("src.core.FaithfulRes").label(options.faithfulRes)) + end }, { label = Strings.source("SCREEN POS"), key = "screenPos", port = true, cycle = function(options, delta) local ScreenPosition = require("src.core.ScreenPosition") @@ -486,7 +496,8 @@ local GROUPS = { { id = "group.speed", label = Strings.source("SPEED"), members = { "textSpeed", "speed" } }, { id = "group.video", label = Strings.source("VIDEO"), - members = { "videoMode", "screenPos", "fpsCap", "vsync", "logicClock" } }, + members = { "videoMode", "faithfulRes", "screenPos", "fpsCap", "vsync", + "logicClock" } }, { id = "group.graphics", label = Strings.source("GRAPHICS"), members = { "color", "uiLetterbox", "shaderfx", "shaderfx2", "frame" } }, { id = "group.audio", label = Strings.source("AUDIO"), diff --git a/src/world/gen2/World.lua b/src/world/gen2/World.lua index a4284763..4babaa12 100644 --- a/src/world/gen2/World.lua +++ b/src/world/gen2/World.lua @@ -5888,6 +5888,17 @@ function World:release() self.connectionMaps = nil end +function World:dropBakes() + for _, img in pairs(self.mapImages or {}) do safeRelease(img) end + for _, strip in pairs(self.scrollStrips or {}) do safeRelease(strip) end + self.mapImages = {} + self.scrollStrips = {} + if self.map and self.map.id then + self.mapImage = self:imageFor(self.map.id) + self:rebuildNeighbors() + end +end + -- LoadMapAttributes' refill, for every map the session has edited. Neighbour -- strips share the same buffer on the cart, so a connection crossing reloads -- them too: this runs on any setMap, seamless or not. diff --git a/tests/drivers/game3_badge_guard_names.lua b/tests/drivers/game3_badge_guard_names.lua new file mode 100644 index 00000000..c5f6e2d8 --- /dev/null +++ b/tests/drivers/game3_badge_guard_names.lua @@ -0,0 +1,105 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_badge_guard_names" + +local GATE = "FR_ROUTE_22_NORTH_ENTRANCE" +local ROUTE23 = "FR_ROUTE_23" +-- pokefirered/include/constants/flags.h:1364 +local FLAG_BADGE01_GET = 0x820 + +local failures = 0 +local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + if failures == 0 then + print("PASS badge_guard_names") + love.event.quit(0) + else + print("FAIL badge_guard_names failures=" .. failures) + love.event.quit(1) + end +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(240) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local Player = require("src.core.game3.player") + local Message = require("src.ui.game3.message") + + if not result(Runtime.getSession() ~= nil, "new game reached the game3 field") then return finish() end + + for i = 0, 7 do + Flags.setFlag(Space.store, Space.vm and Space.vm.ctx, FLAG_BADGE01_GET + i, true) + end + + local function goTo(mapId, x, y, facing) + Map.load(nil, game, mapId, { x = x, y = y, facing = facing or "up" }) + if game.session then + game.session.x, game.session.y, game.session.facing = x, y, facing or "up" + end + Player.cellX, Player.cellY = x, y + Player.px, Player.py = x * 16, y * 16 + Player.targetX, Player.targetY = x, y + Player.facing = facing or "up" + U.wait(90) + end + + local function talkAndFind(want, shotName) + U.tap(game, "a") + local seen = "" + for _ = 1, 20 do + U.wait(20) + if Message.isOpen() then + Message.skipReveal() + local page = Message.currentPage() or "" + seen = seen .. " | " .. page + if page:find(want, 1, true) then + U.wait(2) + U.shot(game, DIR .. "/" .. shotName) + return true, page + end + U.tap(game, "a") + end + end + return false, seen + end + + local function closeAll() + for _ = 1, 30 do + if not (Message.isOpen() or (Space.vm and Space.vm:isRunning())) then break end + U.tap(game, "a") + U.wait(10) + end + end + + -- pokefirered/data/maps/Route22_NorthEntrance/map.json:21 + goTo(GATE, 8, 3, "up") + local ok, text = talkAndFind("BOULDERBADGE", "2413_boulder_guard_badge_name.png") + print("[driver] gate text: " .. tostring(text)) + result(ok, "route22 gate guard names BOULDERBADGE") + result(not tostring(text):find("the 15", 1, true), "route22 gate guard text has no raw id 15") + closeAll() + + -- pokefirered/data/maps/Route23/map.json:33 + goTo(ROUTE23, 15, 150, "up") + ok, text = talkAndFind("CASCADEBADGE", "2413_cascade_guard_badge_name.png") + print("[driver] route23 text: " .. tostring(text)) + result(ok, "route23 guard names CASCADEBADGE") + result(not tostring(text):find("the 16", 1, true), "route23 guard text has no raw id 16") + closeAll() + + finish() +end diff --git a/tests/drivers/game3_berry_powder_man_2416.lua b/tests/drivers/game3_berry_powder_man_2416.lua new file mode 100644 index 00000000..aada1a4e --- /dev/null +++ b/tests/drivers/game3_berry_powder_man_2416.lua @@ -0,0 +1,90 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/bug2416" + +local failures = 0 +local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + if failures == 0 then + print("PASS game3_berry_powder_man_2416") + love.event.quit(0) + else + print("FAIL game3_berry_powder_man_2416 failures=" .. failures) + love.event.quit(1) + end +end + +return function(game) + for _ = 1, 600 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(180) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local Bag = require("src.core.game3.bag") + local Objects = require("src.core.game3.objects") + local Field = require("src.core.game3.field") + local Message = package.loaded["src.ui.game3.message"] or require("src.ui.game3.message") + + local session = Runtime.getSession() + if not result(session ~= nil, "new game reached the field") then return finish() end + + Bag.add(session.bag, 139, 1) + result(Bag.has(session.bag, 365, 1), "ORAN BERRY auto-granted the BERRY POUCH") + + local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog") + local mapId = (okC and MapCatalog.pretToEngine and MapCatalog.pretToEngine("CeruleanCity_House5")) + or "FR_CERULEAN_CITY_HOUSE5" + Map.load(nil, game, mapId, { x = 8, y = 4, facing = "left" }) + game.session.x, game.session.y, game.session.facing = 8, 4, "left" + U.wait(30) + + result(Space.store and Flags.getFlag(Space.store, nil, 0x847) == true, + "FLAG_SYS_GOT_BERRY_POUCH set in the house") + local man = Objects.find(1) + if not result(man ~= nil, "Berry Powder man (localId 1) loaded") then return finish() end + + local function vmBusy() + return Space.vm and Space.vm.isRunning and Space.vm:isRunning() + end + local function page() + return string.upper(Message.currentPage and Message.currentPage() or "") + end + + U.tap(game, "a") + local hit, liar = false, false + for _ = 1, 400 do + local p = page() + if p:find("JUST THE THING", 1, true) then hit = true break end + if p:find("LIE TO ME", 1, true) then liar = true break end + if Message.isOpen() and Message.isTyping() then Message.skipReveal() end + if Message.isOpen() and Message.isWaiting() then U.tap(game, "a") end + U.wait(3) + end + result(not liar, "man did not take the NoBerries branch") + if not result(hit, "man says he has just the thing") then return finish() end + Message.skipReveal() + U.wait(10) + result(U.shot(game, DIR .. "/2416_powder_man_just_the_thing.png"), "screenshot 2416_powder_man_just_the_thing") + + for _ = 1, 400 do + if not Message.isOpen() and not vmBusy() and not Field.locked then break end + if Message.isOpen() and Message.isTyping() then Message.skipReveal() end + if Message.isOpen() then U.tap(game, "a") end + U.wait(3) + end + result(Bag.has(session.bag, 372, 1), "POWDER JAR landed in the bag") + result(Flags.getFlag(Space.store, nil, "FLAG_GOT_POWDER_JAR") == true, "FLAG_GOT_POWDER_JAR set") + + return finish() +end diff --git a/tests/drivers/game3_fishing_pose_2419.lua b/tests/drivers/game3_fishing_pose_2419.lua new file mode 100644 index 00000000..be0316f3 --- /dev/null +++ b/tests/drivers/game3_fishing_pose_2419.lua @@ -0,0 +1,152 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_fishing_pose_2419" + +-- pokefirered/src/item_use.c:286 FieldUseFunc_Rod +local ITEM_OLD_ROD = 262 +local MAPS = { "FR_PALLET_TOWN", "FR_VIRIDIAN_CITY", "FR_ROUTE_21_NORTH", "FR_VERMILION_CITY", + "FR_CINNABAR_ISLAND", "FR_ROUTE_22", "FR_ROUTE_20" } + +local failures = 0 +local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + if failures == 0 then + print("PASS fishing_pose_2419") + love.event.quit(0) + else + print("FAIL fishing_pose_2419 failures=" .. failures) + love.event.quit(1) + end +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(240) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Player = require("src.core.game3.player") + local Field = require("src.core.game3.field") + local Bag = require("src.core.game3.bag") + local ItemUse = require("src.core.game3.item_use") + local Collision = require("src.core.game3.collision") + local Objects = require("src.core.game3.objects") + local Encounters = require("src.core.game3.encounters") + local Message = require("src.ui.game3.message") + local OwSprites = require("src.core.game3.ow_sprites") + + local session = Runtime.getSession() + if not result(session ~= nil, "new game reached the game3 field") then return finish() end + + local Party = require("src.core.game3.party") + if not (session.party and session.party[1]) then + session.party = {} + Party.giveMon(session, 7, 15) + end + session.bag = session.bag or {} + Bag.add(session.bag, ITEM_OLD_ROD, 1) + session.registeredItem = ITEM_OLD_ROD + + local function place(x, y, facing) + Player.moving = false + Player.progress = 0 + Player.cellX, Player.cellY = x, y + Player.px, Player.py = x * 16, y * 16 + Player.targetX, Player.targetY = x, y + Player.facing = facing + if game.session then + game.session.x, game.session.y, game.session.facing = x, y, facing + end + end + + local function findSpot(facing) + for _, mapId in ipairs(MAPS) do + local ok = pcall(Map.load, nil, game, mapId, { x = 5, y = 5, facing = facing }) + if ok then + U.wait(20) + local w, h = Collision._widthCells or 0, Collision._heightCells or 0 + for y = 4, h - 5 do + for x = 5, w - 6 do + if Collision.isWalkable(x, y) and not Collision.isWater(x, y) + and not (Objects.blocks and Objects.blocks(x, y)) then + place(x, y, facing) + if ItemUse.canFish() then return mapId, x, y end + end + end + end + end + end + return nil + end + + local realHasMons = Encounters.hasFishingMons + Encounters.hasFishingMons = function() return false end + + local EXPECT = { down = 11, up = 7, left = 3, right = 3 } + + local function cast(facing, tag) + local mapId, x, y = findSpot(facing) + if not result(mapId ~= nil, tag .. " found fishable water facing " .. facing) then return end + place(x, y, facing) + U.wait(30) + U.tap(game, "select") + U.wait(2) + if not result(Field.isFishing() == true, tag .. " cast started on " .. mapId .. " (" .. x .. "," .. y .. ")") then + return + end + U.wait(24) + local g, x2, y2 = Field.fishingPose() + local abs = g and OwSprites.fishingAbsFrame(facing, g) + result(abs == EXPECT[facing], string.format("%s rod out on frame %s (want %d)", tag, tostring(abs), EXPECT[facing])) + local wx2, wy2 = OwSprites.fishingOffset(EXPECT[facing], facing) + result(x2 == wx2 and y2 == wy2, string.format("%s aligned by (%s,%s)", tag, tostring(x2), tostring(y2))) + U.shot(game, DIR .. "/2419_" .. tag .. "_rod_out.png") + + for _ = 1, 900 do + U.wait(1) + if Message.isOpen() and Message.currentPage():find("nibble", 1, true) then break end + end + result(Message.isOpen() and Message.currentPage():find("nibble", 1, true) ~= nil, + tag .. " no-bite text is up") + U.wait(8) + U.shot(game, DIR .. "/2419_" .. tag .. "_put_away.png") + U.wait(30) + result(Player.fishing == false and Field.isFishing() == true, + tag .. " rod put away while the text is still up") + for _ = 1, 60 do + if not Field.isFishing() then break end + U.tap(game, "a") + U.wait(6) + end + result(Field.isFishing() == false and Field.locked == false, tag .. " fishing ended, controls free") + end + + for _, facing in ipairs({ "down", "up", "left", "right" }) do + cast(facing, "male_" .. facing) + end + + session.gender = "female" + if game.save then game.save.gender = "female" end + for _, facing in ipairs({ "down", "up", "left", "right" }) do + cast(facing, "female_" .. facing) + end + local gid + Field.startFishing(0) + gid = OwSprites.playerGraphicsId(game) + Field._fishing = nil + Player.fishing = false + Field.locked = false + result(gid == 11, "female rod sheet is gid 11 (got " .. tostring(gid) .. ")") + + Encounters.hasFishingMons = realHasMons + finish() +end diff --git a/tests/drivers/game3_fuji_2417.lua b/tests/drivers/game3_fuji_2417.lua new file mode 100644 index 00000000..134c7583 --- /dev/null +++ b/tests/drivers/game3_fuji_2417.lua @@ -0,0 +1,107 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_fuji_2417" + +-- pokefirered/include/constants/flags.h:68 +local FLAG_HIDE_TOWER_FUJI = 0x034 +local FLAG_HIDE_POKEHOUSE_FUJI = 0x035 +-- pokefirered/include/constants/flags.h:110 +local FLAG_HIDE_TOWER_ROCKET_1 = 0x05E +-- pokefirered/include/constants/flags.h:147 +local FLAG_HIDE_TOWER_ROCKET_2 = 0x083 +local FLAG_HIDE_TOWER_ROCKET_3 = 0x084 +-- pokefirered/include/constants/flags.h:597 +local FLAG_RESCUED_MR_FUJI = 0x23C +-- pokefirered/include/constants/items.h:422 +local ITEM_POKE_FLUTE = 350 +-- pokefirered/data/maps/PokemonTower_7F/map.json:18 +local FUJI = 1 + +return function(game) + local failures = 0 + local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok + end + local function finish() love.event.quit(failures == 0 and 0 or 1) end + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(240) + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Player = require("src.core.game3.player") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local Objects = require("src.core.game3.objects") + local Bag = require("src.core.game3.bag") + local Message = require("src.ui.game3.message") + local session = Runtime.getSession() + if not check(session ~= nil, "fuji_new_game") then return finish() end + local function place(x, y, facing) + Player.moving, Player.progress = false, 0 + Player.cellX, Player.cellY, Player.targetX, Player.targetY = x, y, x, y + Player.px, Player.py, Player.facing = x * 16, y * 16, facing + session.x, session.y, session.facing = x, y, facing + end + local function getFlag(id) return Flags.getFlag(Space.store, Space.vm and Space.vm.ctx, id) end + for _, f in ipairs({ FLAG_HIDE_TOWER_ROCKET_1, FLAG_HIDE_TOWER_ROCKET_2, FLAG_HIDE_TOWER_ROCKET_3, + FLAG_HIDE_POKEHOUSE_FUJI }) do + Flags.setFlag(Space.store, Space.vm and Space.vm.ctx, f, true) + end + Map.load(nil, game, "FR_POKEMON_TOWER_7F", { x = 11, y = 5, facing = "up" }) + place(11, 5, "up") + U.wait(90) + local fuji = Objects.find(FUJI) + if not check(fuji ~= nil and fuji.visible, "fuji_present_on_7f") then return finish() end + U.tap(game, "a") + local opened = false + for _ = 1, 240 do + if Message.isOpen() then opened = true break end + U.wait(1) + end + if not check(opened, "fuji_text_opened") then return finish() end + U.wait(40) + fuji = Objects.find(FUJI) + check(getFlag(FLAG_HIDE_TOWER_FUJI), "fuji_hide_flag_set_before_text") + check(fuji ~= nil and fuji.visible and not fuji.hidden, "fuji_visible_during_text") + check(U.shot(game, DIR .. "/2417_fuji_text_visible.png"), "fuji_text_screenshot_written") + local arrived = false + for _ = 1, 400 do + if Runtime.getSession().map == "FR_LAVENDER_TOWN_VOLUNTEER_POKEMON_HOUSE" + and not Space.vm:isRunning() then + arrived = true + break + end + if Message.isOpen() then U.tap(game, "a") end + U.wait(6) + end + if not check(arrived, "fuji_warp_to_volunteer_house") then return finish() end + U.wait(90) + session = Runtime.getSession() + check(Player.cellX == 4 and Player.cellY == 7, "fuji_player_at_house_4_7") + local houseFuji = Objects.find(FUJI) + check(houseFuji ~= nil and houseFuji.visible and houseFuji.cellX == 3 and houseFuji.cellY == 3, + "fuji_present_in_house") + check(getFlag(FLAG_RESCUED_MR_FUJI) and not getFlag(FLAG_HIDE_POKEHOUSE_FUJI), "fuji_rescue_flags") + check(U.shot(game, DIR .. "/2417_fuji_in_volunteer_house.png"), "fuji_house_screenshot_written") + place(3, 4, "up") + U.wait(10) + U.tap(game, "a") + local gotFlute = false + for _ = 1, 400 do + if Bag.has(session.bag, ITEM_POKE_FLUTE, 1) then gotFlute = true end + if gotFlute and not Message.isOpen() and not Space.vm:isRunning() then break end + if Message.isOpen() then U.tap(game, "a") end + U.wait(6) + end + check(gotFlute, "fuji_gives_poke_flute") + Map.load(nil, game, "FR_POKEMON_TOWER_7F", { x = 11, y = 5, facing = "up" }) + place(11, 5, "up") + U.wait(90) + local gone = Objects.find(FUJI) + check(gone == nil or not gone.visible, "fuji_absent_on_7f_reload") + finish() +end diff --git a/tests/drivers/game3_invisible_journal_2414.lua b/tests/drivers/game3_invisible_journal_2414.lua new file mode 100644 index 00000000..fab0d23f --- /dev/null +++ b/tests/drivers/game3_invisible_journal_2414.lua @@ -0,0 +1,66 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_invisible_journal_2414" + +-- pokefirered/include/constants/flags.h:176 +local FLAG_HIDE_FAME_CHECKER_LT_SURGE_JOURNAL = 0x0A0 + +return function(game) + local failures = 0 + local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok + end + local function finish() love.event.quit(failures == 0 and 0 or 1) end + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(240) + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Player = require("src.core.game3.player") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local Objects = require("src.core.game3.objects") + local Message = require("src.ui.game3.message") + local session = Runtime.getSession() + if not check(session ~= nil, "journal_new_game") then return finish() end + local function place(x, y, facing) + Player.moving, Player.progress = false, 0 + Player.cellX, Player.cellY, Player.targetX, Player.targetY = x, y, x, y + Player.px, Player.py, Player.facing = x * 16, y * 16, facing + session.x, session.y, session.facing = x, y, facing + end + Flags.setFlag(Space.store, Space.vm and Space.vm.ctx, FLAG_HIDE_FAME_CHECKER_LT_SURGE_JOURNAL, false) + Map.load(nil, game, "FR_VERMILION_CITY_POKEMON_CENTER_1F", { x = 3, y = 2, facing = "up" }) + place(3, 2, "up") + U.wait(90) + local j6, j7 = Objects.find(6), Objects.find(7) + check(j6 ~= nil and j7 ~= nil and j6.visible and j7.visible, "journal_objects_live") + check(j6 and j6.invisible and j7 and j7.invisible, "journal_objects_marked_invisible") + local drawn = false + for _, eo in ipairs(Objects.forDraw()) do + if eo.localId == 6 or eo.localId == 7 then drawn = true end + end + check(not drawn, "journal_objects_not_drawn") + check(U.shot(game, DIR .. "/2414_vermilion_pc_shelf_empty.png"), "journal_shelf_screenshot_written") + U.tap(game, "a") + local opened = false + for _ = 1, 120 do + if Message.isOpen() then opened = true break end + U.wait(1) + end + check(opened, "journal_readable_on_a") + if opened then + U.wait(60) + check(U.shot(game, DIR .. "/2414_vermilion_journal_text.png"), "journal_text_screenshot_written") + end + for _ = 1, 120 do + if not Message.isOpen() and not Space.vm:isRunning() then break end + U.tap(game, "a") + U.wait(8) + end + finish() +end diff --git a/tests/drivers/game3_pc_se_2415.lua b/tests/drivers/game3_pc_se_2415.lua new file mode 100644 index 00000000..37d46785 --- /dev/null +++ b/tests/drivers/game3_pc_se_2415.lua @@ -0,0 +1,126 @@ +-- pokefirered/data/scripts/pc.inc:1 +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp" + +local PC_X, PC_Y = 11, 2 +local SE_PC_LOGIN, SE_PC_OFF, SE_PC_ON, SE_SELECT = 2, 3, 4, 5 -- include/constants/songs.h:6 + +local failures = 0 +local function result(ok, label) + if ok then + print("PASS " .. label) + else + failures = failures + 1 + print("FAIL " .. label) + end + return ok +end + +local function finish() + if failures == 0 then + print("PASS game3_pc_se_2415") + love.event.quit(0) + else + print("FAIL game3_pc_se_2415 failures=" .. failures) + love.event.quit(1) + end +end + +local function waitFor(pred, n) + for _ = 1, n do + if pred() then return true end + U.wait(1) + end + return pred() +end + +local function PcMenu() return package.loaded["src.ui.game3.pc_menu"] end +local function Space() return package.loaded["src.core.game3.scripting.space"] end +local function Message() return package.loaded["src.ui.game3.message"] end +local function menuOpen() + local M = PcMenu() + return M and M.isOpen and M.isOpen() and M.mode == "root" +end + +return function(game) + U.wait(30) + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(120) + local Map = require("src.core.game3.map") + Map.load(nil, game, "FR_VIRIDIAN_CITY_POKEMON_CENTER_1F", { x = PC_X, y = PC_Y, facing = "up" }) + game.session.x, game.session.y, game.session.facing = PC_X, PC_Y, "up" + U.wait(90) + + local Audio = require("src.core.game3.audio") + local orig = Audio.playSe + local log = {} + Audio.playSe = function(id, ...) + log[#log + 1] = id + print("SE " .. tostring(id)) + return orig(id, ...) + end + local function since(n) + local t = {} + for i = n + 1, #log do t[#t + 1] = tostring(log[i]) end + return table.concat(t, ",") + end + + U.tap(game, "a") + local booted = waitFor(function() + local Msg = Message() + return Msg and Msg.isOpen and Msg.isOpen() + end, 60) + U.wait(10) + result(booted, "interact opens the booted-up message") + result(log[1] == SE_PC_ON and #log == 1, + "SE_PC_ON (4) plays on interact before the menu (got " .. since(0) .. ")") + U.shot(game, DIR .. "/2415_01_booted_up_pc.png") + + local mark = #log + for _ = 1, 8 do + if menuOpen() then break end + U.tap(game, "a") + U.wait(20) + end + if not result(menuOpen(), "Center PC opens the root menu") then return finish() end + result(since(mark) == "", "root menu open plays no SE (got " .. since(mark) .. ")") + + mark = #log + U.tap(game, "a") + U.wait(20) + result(since(mark) == SE_SELECT .. "," .. SE_PC_LOGIN, + "SOMEONE'S PC selection plays SE_SELECT then SE_PC_LOGIN (got " .. since(mark) .. ")") + U.shot(game, DIR .. "/2415_02_storage_menu_after_login.png") + + local M = PcMenu() + for _ = 1, 8 do + if M.mode == "root" then break end + U.tap(game, "b") + U.wait(15) + end + if not result(M.mode == "root", "back to root menu") then return finish() end + local rows = M._rootEntries() + local ids = {} + for _, r in ipairs(rows) do ids[#ids + 1] = r.id end + result(table.concat(ids, ",") == "storage,player,quit", + "new game root menu has 3 rows (got " .. table.concat(ids, ",") .. ")") + for _ = 1, #rows + 1 do + if rows[M.cursor] and rows[M.cursor].id == "quit" then break end + U.tap(game, "down") + U.wait(8) + end + mark = #log + U.tap(game, "a") + local released = waitFor(function() + local S = Space() + local running = S and S.vm and S.vm.isRunning and S.vm:isRunning() + local Msg = Message() + return not M.isOpen() and not running and not (Msg and Msg.isOpen and Msg.isOpen()) + end, 180) + result(released, "LOG OFF releases the player") + result(since(mark) == SE_SELECT .. "," .. SE_PC_OFF, + "LOG OFF plays SE_SELECT then SE_PC_OFF once (got " .. since(mark) .. ")") + + Audio.playSe = orig + finish() +end diff --git a/tests/drivers/game3_vs_seeker_bike_2419.lua b/tests/drivers/game3_vs_seeker_bike_2419.lua new file mode 100644 index 00000000..9e635232 --- /dev/null +++ b/tests/drivers/game3_vs_seeker_bike_2419.lua @@ -0,0 +1,108 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_vs_seeker_bike_2419" + +-- pokefirered/include/constants/items.h:434 +local ITEM_VS_SEEKER = 362 +local LASS_MEGAN, TWINS_ELI_ANNE = 130, 484 +local IVYSAUR = 2 +local FLAG_GOT_VS_SEEKER, FLAG_CELADON, FLAG_FUCHSIA = 0x292, 0x896, 0x897 + +local failures = 0 +local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + if failures == 0 then + print("PASS vs_seeker_bike_2419") + love.event.quit(0) + else + print("FAIL vs_seeker_bike_2419 failures=" .. failures) + love.event.quit(1) + end +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + + game:_handleBootAction({ action = "new_game", name = "RED" }) + U.wait(240) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local Party = require("src.core.game3.party") + local Bag = require("src.core.game3.bag") + local Player = require("src.core.game3.player") + local VsSeeker = require("src.core.game3.vs_seeker") + local OwSprites = require("src.core.game3.ow_sprites") + local Message = package.loaded["src.ui.game3.message"] or require("src.ui.game3.message") + + local session = Runtime.getSession() + if not result(session ~= nil, "new game reached the field") then return finish() end + + session.party = {} + Party.giveMon(session, IVYSAUR, 45) + session.bag = session.bag or Bag.new() + Bag.add(session.bag, ITEM_VS_SEEKER, 1) + local function setFlag(id) Flags.setFlag(Space.store, nil, id, true) end + setFlag(FLAG_GOT_VS_SEEKER) + setFlag(FLAG_CELADON) + setFlag(FLAG_FUCHSIA) + + local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog") + local mapId = (okC and MapCatalog.pretToEngine and MapCatalog.pretToEngine("Route8")) or "FR_ROUTE_8" + Map.load(nil, game, mapId, { x = 40, y = 6, facing = "up" }) + game.session.x, game.session.y, game.session.facing = 40, 6, "up" + U.wait(90) + session = Runtime.getSession() + setFlag(Flags.trainerFlagId(LASS_MEGAN)) + setFlag(Flags.trainerFlagId(TWINS_ELI_ANNE)) + + Player.biking = true + U.wait(10) + result(OwSprites.playerGraphicsId(game) == 1, "riding the bike (gid 1)") + + VsSeeker.setBattery(session, VsSeeker.MAX_CHARGE) + local done = false + local used, code = VsSeeker.use(session, game, function() done = true end) + print("DIAG use=" .. tostring(used) .. " code=" .. tostring(code) .. " battery=" .. tostring(VsSeeker.getBattery(session))) + + local function poseNow() + local gid = OwSprites.playerGraphicsId(game) + local spr = OwSprites.getDraw(gid) + local mf = OwSprites.fieldMoveFrame((Player.fieldMoveTotal or 0) - (Player.fieldMoveAnim or 0), Player.fieldMoveKind) + local f = spr and OwSprites.pose(spr, Player.facing, 0, false, { fieldMove = true, fieldMoveFrame = mf }) + return gid, f, spr and spr.frameCount + end + + U.wait(22) + local gid, f, n = poseNow() + result(Player.fieldMoveKind == "vs_seeker_bike", "VS Seeker started the bike anim (kind " .. tostring(Player.fieldMoveKind) .. ")") + result(gid == 6 and n == 6, "male bike VS Seeker sheet gid 6 (got " .. tostring(gid) .. ", " .. tostring(n) .. " frames)") + result(f == 4 or f == 5, "male raising the seeker on frame 4/5 (got " .. tostring(f) .. ")") + U.shot(game, DIR .. "/2419_male_bike_vs_seeker.png") + + session.gender = "female" + if game.save then game.save.gender = "female" end + U.wait(8) + gid, f, n = poseNow() + result(gid == 13 and n == 6, "female bike VS Seeker sheet gid 13 (got " .. tostring(gid) .. ", " .. tostring(n) .. " frames)") + result(f == 4 or f == 5, "female raising the seeker on frame 4/5 (got " .. tostring(f) .. ")") + U.shot(game, DIR .. "/2419_female_bike_vs_seeker.png") + + for _ = 1, 600 do + if done then break end + if Message.isOpen and Message.isOpen() then U.tap(game, "a") end + U.wait(1) + end + result(done, "VS Seeker sequence finished") + result(OwSprites.playerGraphicsId(game) == 8, "back on the bike sheet afterwards (got " .. tostring(OwSprites.playerGraphicsId(game)) .. ")") + finish() +end diff --git a/tests/drivers/gold_faithful_ratio.lua b/tests/drivers/gold_faithful_ratio.lua new file mode 100644 index 00000000..afa3bb91 --- /dev/null +++ b/tests/drivers/gold_faithful_ratio.lua @@ -0,0 +1,94 @@ +local U = require("tests.drivers.util") +local FaithfulRes = require("src.core.FaithfulRes") +local OptionsMenu = require("src.ui.gen2.OptionsMenu") + +local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots" + +return function(game) + local fails = 0 + local function ok(cond, line) + if not cond then fails = fails + 1 end + print("[ratio] " .. (cond and "PASS " or "FAIL ") .. line) + end + local mobile = FaithfulRes.fixedDisplay() + local tag = mobile and "mobile" or "desktop" + + local row + for _, r in ipairs(OptionsMenu.ROWS) do + if r.key == "faithfulRes" then row = r end + end + ok(row ~= nil, "gen 2 OPTIONS has FAITHFUL RATIO") + + U.wait(45) + local world = game.world + if not (row and world and world.map) then + print("[ratio] FAIL the gen 2 world did not boot") + love.event.quit(1) + return + end + + game.options.faithfulRes = 0 + FaithfulRes.apply(0) + local _, _, flags = love.window.getMode() + flags.resizable, flags.fullscreen = true, false + flags.minwidth, flags.minheight = 1, 1 + if mobile then + love.window.setMode(540, 1170, flags) + else + love.window.setMode(900, 600, flags) + end + world:warpToMapId("NEW_BARK_TOWN", 6, 6, "down") + U.wait(45) + U.shot(game, SHOT_DIR .. "/" .. tag .. "_0_off.png") + + local menu = OptionsMenu.new(game, { options = game.options }) + game.stack:push(menu) + U.wait(2) + menu:focusRow("faithfulRes") + U.wait(3) + U.shot(game, SHOT_DIR .. "/" .. tag .. "_1_row_off.png") + U.tap(game, "right") + U.wait(10) + ok(FaithfulRes.normalize(game.options.faithfulRes) == 1, + "RIGHT on the row steps it to " .. FaithfulRes.label(1)) + U.shot(game, SHOT_DIR .. "/" .. tag .. "_2_row_on.png") + for _ = 1, 2 do + U.tap(game, "b") + U.wait(10) + end + ok(game.stack:top() == nil or game.stack:top() ~= menu, "B closes OPTIONS") + U.wait(20) + + if mobile then + ok(FaithfulRes.scaleCap() ~= nil, "mobile lock engaged") + U.shot(game, SHOT_DIR .. "/" .. tag .. "_3_on.png") + else + for level = 1, 4 do + game.options.faithfulRes = level + FaithfulRes.apply(level) + U.wait(20) + local w, h = love.graphics.getPixelDimensions() + ok(w == 160 * level and h == 144 * level, + string.format("%dX window is %dx%d", level, w, h)) + U.shot(game, SHOT_DIR .. "/" .. tag .. "_3_" .. level .. "x.png") + end + game.options.faithfulRes = 2 + FaithfulRes.apply(2) + U.wait(10) + end + + game:persistOptions() + local text = love.filesystem.read("options.lua") or "" + ok(text:find("faithfulRes = " .. (mobile and 1 or 2), 1, true) ~= nil, + "options.lua holds faithfulRes on the shared key") + + game.options.faithfulRes = 0 + FaithfulRes.apply(0) + U.wait(10) + ok(not FaithfulRes.locked, "OFF releases the lock") + U.shot(game, SHOT_DIR .. "/" .. tag .. "_4_off_again.png") + game:persistOptions() + + print("[ratio] " .. (fails == 0 and "ALL PASS" or (fails .. " FAILED"))) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/engine/game2_faithful_res.lua b/tests/engine/game2_faithful_res.lua new file mode 100644 index 00000000..f720c9ed --- /dev/null +++ b/tests/engine/game2_faithful_res.lua @@ -0,0 +1,178 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local T = require("tests.harness") +local check, eq = T.check, T.eq + +local FaithfulRes = require("src.core.FaithfulRes") +local OptionsMenu = require("src.ui.gen2.OptionsMenu") +local Save = require("src.core.gen2.Save") +local Playfield = require("src.render.Playfield") + +local g = love.graphics +local realDims, realPixelDims = g.getDimensions, g.getPixelDimensions +love.system = love.system or {} +local realOS = love.system.getOS +local realWindow = love.window + +local function pose(w, h, osName) + love.system.getOS = function() return osName end + g.getDimensions = function() return w, h end + g.getPixelDimensions = function() return w, h end +end + +local function reset() + FaithfulRes.locked, FaithfulRes.prevSize = false, nil + FaithfulRes.mobileScale = 0 +end + +local calls = {} +local function stubWindow(w, h) + local cur = { w = w, h = h, flags = { resizable = true } } + love.window = setmetatable({ + getMode = function() return cur.w, cur.h, cur.flags end, + setMode = function(nw, nh, flags) + calls[#calls + 1] = { w = nw, h = nh, flags = flags } + cur.w, cur.h, cur.flags = nw, nh, flags + return true + end, + }, { __index = realWindow }) +end + +local rowAt, row +for i, r in ipairs(OptionsMenu.ROWS) do + if r.key == "faithfulRes" then rowAt, row = i, r end +end +check(row ~= nil, "Gen 2 OPTIONS has a FAITHFUL RATIO row") +eq(row and row.label, "FAITHFUL RATIO", "labelled as on Gen 1 and Gen 3") +eq(OptionsMenu.ROWS[rowAt - 1].key, "videoMode", "right after VIDEO MODE") +eq(OptionsMenu.ROWS[rowAt + 1].key, "screenPos", "and ahead of SCREEN POS") +eq(Save.DEFAULT_OPTIONS.faithfulRes, 0, "defaults to OFF") + +do + reset() + pose(800, 720, "OS X") + stubWindow(800, 720) + calls = {} + local options = Save.defaultOptions() + eq(row.text(options), "OFF", "reads OFF by default") + local seen = {} + for _ = 1, 5 do + row.cycle(options, 1) + seen[#seen + 1] = row.text(options) + end + eq(table.concat(seen, ","), "1X,2X,3X,4X,OFF", "cycles the desktop ladder") + row.cycle(options, -1) + eq(options.faithfulRes, 4, "stepping back from OFF lands on 4X") + local last = calls[#calls] + eq(last and last.w, 640, "4X locks the window to 640 wide") + eq(last and last.h, 576, "and 576 tall") + eq(FaithfulRes.locked, true, "the row applies the lock live") + row.cycle(options, 1) + eq(FaithfulRes.locked, false, "OFF releases it") + reset() +end + +do + local files = {} + local fs = { + write = function(p, c) files[p] = c return true end, + read = function(p) return files[p] end, + remove = function(p) files[p] = nil return true end, + getInfo = function(p) return files[p] ~= nil and { type = "file" } or nil end, + } + local options = Save.defaultOptions() + options.faithfulRes = 3 + Save.saveOptions(options, fs) + local SaveData = require("src.core.SaveData") + local flat = SaveData.loadOptions(fs) + eq(flat and flat.faithfulRes, 3, "persists on the flat key Gen 1 and Gen 3 read") + eq(flat and flat.gold and flat.gold.faithfulRes, nil, "not inside the gold block") + eq(Save.loadOptions(fs).faithfulRes, 3, "and Gen 2 reads it back") + flat.faithfulRes = 2 + SaveData.saveOptions(flat, fs) + eq(Save.loadOptions(fs).faithfulRes, 2, "a Gen 1 edit carries into Gen 2") +end + +local Game2 = require("src.core.Game2") + +do + reset() + pose(800, 720, "OS X") + stubWindow(800, 720) + calls = {} + local seen + local real = FaithfulRes.applyOptions + FaithfulRes.applyOptions = function(opts) + seen = opts and opts.faithfulRes + return real(opts) + end + local fake = setmetatable({ options = Save.defaultOptions() }, Game2) + fake.options.faithfulRes = 2 + local ok, err = pcall(Game2.applyOptions, fake) + FaithfulRes.applyOptions = real + check(ok, "Game2:applyOptions runs headless: " .. tostring(err)) + eq(seen, 2, "Game2:applyOptions pushes faithfulRes into FaithfulRes") + eq(FaithfulRes.locked, true, "so a boot restores the lock") + eq(calls[#calls] and calls[#calls].w, 320, "at 2X") + FaithfulRes.apply(0) + reset() +end + +do + reset() + pose(1080, 2400, "Android") + eq(Game2.faithfulBox(1080, 2400), nil, "no box while the lock is off") + FaithfulRes.apply(1) + local x, y, w, h = Game2.faithfulBox(1080, 2400) + eq(w, 960, "the mobile lock clips Gen 2 to a whole 6x multiple wide") + eq(h, 864, "and 10:9 tall") + eq(x, 60, "centred across") + eq(y, 768, "and down") + + local drawn + local fake = setmetatable({}, Game2) + fake.drawScene = function(_, sw, sh) + local dw, dh = Playfield.dimensions() + drawn = { w = sw, h = sh, dw = dw, dh = dh } + end + fake:drawContained(1080, 2400) + eq(drawn and drawn.w, 960, "drawScene is handed the locked width") + eq(drawn and drawn.h, 864, "and height") + eq(drawn and drawn.dw, 960, "the world sizes its view off the locked box") + eq(drawn and drawn.dh, 864, "on both axes") + eq(Playfield.entered, false, "and the playfield is released afterwards") + + FaithfulRes.apply(0) + fake:drawContained(1080, 2400) + eq(drawn and drawn.w, 1080, "OFF draws edge to edge again") + reset() +end + +do + pose(800, 720, "OS X") + local LauncherSettings = require("src.import.LauncherSettings") + for _, version in ipairs({ "gold", "silver", "crystal" }) do + local model = LauncherSettings.open({}, version) + local found, prev + for _, section in ipairs(model.sections) do + for _, r in ipairs(section.rows) do + if r.label == "FAITHFUL RATIO" then found = r end + if not found then prev = r.label end + end + end + check(found ~= nil, version .. " launcher gear offers FAITHFUL RATIO") + eq(prev, "VIDEO MODE", version .. " next to VIDEO MODE") + if found then + local before = found.value() + found.step(1) + check(found.value() ~= before, version .. " launcher row steps") + end + end +end + +g.getDimensions, g.getPixelDimensions = realDims, realPixelDims +love.system.getOS = realOS +love.window = realWindow +reset() +T.finish("game2 faithful res") diff --git a/tests/game3_berry_pouch_flag_test.lua b/tests/game3_berry_pouch_flag_test.lua new file mode 100644 index 00000000..7cf7dfa6 --- /dev/null +++ b/tests/game3_berry_pouch_flag_test.lua @@ -0,0 +1,87 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local Bag = require("src.core.game3.bag") +local Flags = require("src.core.game3.scripting.flags") +local Space = require("src.core.game3.scripting.space") + +local FLAG = 0x847 +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +print("=== game3 FLAG_SYS_GOT_BERRY_POUCH ===") + +check(Bag.FLAG_SYS_GOT_BERRY_POUCH == FLAG, "flag id is 0x847") + +do + Space.store = Flags.newStore() + local bag = Bag.new() + local ok = Bag.add(bag, 139, 1) + check(ok, "ORAN BERRY added") + check(Bag.has(bag, 365, 1), "BERRY POUCH auto-granted") + check(Flags.getFlag(Space.store, nil, FLAG), "berry add sets FLAG_SYS_GOT_BERRY_POUCH") +end + +do + Space.store = Flags.newStore() + local bag = Bag.new() + Bag.add(bag, 365, 1) + check(Flags.getFlag(Space.store, nil, FLAG), "adding BERRY POUCH directly sets the flag") +end + +do + Space.store = Flags.newStore() + local bag = Bag.new() + Bag.add(bag, 13, 1) + check(not Flags.getFlag(Space.store, nil, FLAG), "non-berry add leaves the flag clear") +end + +local function activate(session) + Space.active = false + Space.mapId = nil + Space.bundle = Space.bundle or { scripts = {}, text = {}, movements = {}, events = {} } + local game = { session = session, data = { maps = {} } } + local ok, err = pcall(Space.activate, nil, "CERULEAN_CITY_HOUSE5", game, nil) + if not ok then print(" activate error: " .. tostring(err)) end + local on = Space.store and Flags.getFlag(Space.store, nil, FLAG) + pcall(Space.deactivate, nil) + Space.store = nil + return on +end + +do + Space.store = nil + local bag = Bag.new() + bag.pockets.KEY_ITEMS[1] = { id = 365, qty = 1 } + bag.pockets.BERRY_POUCH[1] = { id = 139, qty = 3 } + local session = { bag = bag, flags = {}, vars = {} } + check(activate(session) == true, "old save holding BERRY POUCH gets the flag on map load") +end + +do + local session = { bag = Bag.new(), flags = {}, vars = {} } + check(not activate(session), "save without BERRY POUCH keeps the flag clear on map load") +end + +do + Space.store = Flags.newStore() + local session = { bag = Bag.new(), flags = {}, vars = {} } + Bag.add(session.bag, 139, 1) + Space.persistSession(nil, { session = session }) + Space.store = nil + check(session.flags[tostring(FLAG)] == true, "persistSession writes the flag into session.flags") + local reloaded = { bag = Bag.new(), flags = session.flags, vars = session.vars } + check(activate(reloaded) == true, "flag survives save and reload without the bag backfill") +end + +if failed > 0 then + print(string.format("%d check(s) failed", failed)) + os.exit(1) +end +print("all passed") diff --git a/tests/game3_bufferstdstring_test.lua b/tests/game3_bufferstdstring_test.lua new file mode 100644 index 00000000..d00e7218 --- /dev/null +++ b/tests/game3_bufferstdstring_test.lua @@ -0,0 +1,99 @@ +#!/usr/bin/env luajit + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local GameVersion = require("src.core.GameVersion") +GameVersion.set("firered") + +local Flags = require("src.core.game3.scripting.flags") +local Vm = require("src.core.game3.scripting.vm") +local Adapters = require("src.core.game3.scripting.adapters") + +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +local function eq(a, b, msg) + check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b))) +end + +local function slurp(path) + local f = io.open(path, "rb") + if not f then return nil end + local d = f:read("*a") + f:close() + return d +end + +local host = Adapters.host(nil, nil, nil) +local stub = Adapters.stub({}) + +local function runVm(src, pre) + local rows = {} + for _, r in ipairs(pre or {}) do rows[#rows + 1] = r end + rows[#rows + 1] = { op = "bufferstdstring", dest = 0, src = src } + rows[#rows + 1] = { op = "end" } + local vm = Vm.new({ + store = Flags.newStore(), + scripts = { t = rows }, + adapters = Adapters.host(nil, nil, nil), + }) + vm:start("t") + return vm.ctx.stringVars and vm.ctx.stringVars[1] +end + +-- pokefirered/include/constants/menu.h:99 +eq(host.bufferName("bufferstdstring", 15), "BOULDERBADGE", "host 15 is BOULDERBADGE") +eq(host.bufferName("bufferstdstring", 22), "EARTHBADGE", "host 22 is EARTHBADGE") +eq(stub.bufferName("bufferstdstring", 15), "BOULDERBADGE", "stub 15 is BOULDERBADGE") +eq(stub.bufferName("bufferstdstring", 22), "EARTHBADGE", "stub 22 is EARTHBADGE") + +eq(runVm(15), "BOULDERBADGE", "VM STR_VAR_1 for STDSTRING_BOULDER_BADGE") +eq(runVm(22), "EARTHBADGE", "VM STR_VAR_1 for STDSTRING_EARTH_BADGE") +eq(runVm(23), "COINS", "VM STR_VAR_1 for STDSTRING_COINS") +eq(runVm(0x8004, { { op = "setvar", [1] = 0x8004, [2] = 17 } }), "THUNDERBADGE", + "VM STR_VAR_1 via VAR_0x8004 = STDSTRING_THUNDER_BADGE") + +local menuH = slurp("../pokefirered/include/constants/menu.h") +local menuC = slurp("../pokefirered/src/script_menu.c") +local stringsC = slurp("../pokefirered/src/strings.c") +if not (menuH and menuC and stringsC) then + print("[skip] ../pokefirered not present; pret table parity skipped") +else + local texts = {} + for sym, body in stringsC:gmatch("const u8 (gText_[%w_]+)%[%] = _%(\"(.-)\"%);") do + texts[sym] = body + end + local ptrs = {} + local block = menuC:match("gStdStringPtrs%[%] = {(.-)};") + check(block ~= nil, "found gStdStringPtrs in script_menu.c") + for name, sym in (block or ""):gmatch("%[(STDSTRING_[%w_]+)%]%s*=%s*([%w_]+)") do + ptrs[name] = sym + end + local n = 0 + for name, id in menuH:gmatch("#define (STDSTRING_[%w_]+)%s+(%d+)") do + n = n + 1 + id = tonumber(id) + local want = texts[ptrs[name] or ""] + check(want ~= nil, name .. " has a pret string") + for label, a in pairs({ host = host, stub = stub }) do + local got = a.bufferName("bufferstdstring", id) + check(got ~= nil and not tostring(got):match("^%d+$"), + string.format("%s %s (%d) is text: %s", label, name, id, tostring(got))) + eq(got, want, string.format("%s %s matches pret", label, name)) + end + end + check(n == 29, "menu.h defines 29 STDSTRING ids (" .. n .. ")") +end + +if failed > 0 then + print("[test] FAILED " .. failed) + os.exit(1) +end +print("[test] all passed") diff --git a/tests/game3_objects_flag_sync_test.lua b/tests/game3_objects_flag_sync_test.lua new file mode 100644 index 00000000..9f755b2a --- /dev/null +++ b/tests/game3_objects_flag_sync_test.lua @@ -0,0 +1,186 @@ +#!/usr/bin/env luajit + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local GameVersion = require("src.core.GameVersion") +GameVersion.set("firered") + +local Flags = require("src.core.game3.scripting.flags") +local Vm = require("src.core.game3.scripting.vm") +local Adapters = require("src.core.game3.scripting.adapters") +local Player = require("src.core.game3.player") +local Objects = require("src.core.game3.objects") + +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +local TOWER = "FR_POKEMON_TOWER_7F" +local FUJI = 10 +local FLAG_HIDE_TOWER_FUJI = 0x34 +local FLAG_HIDE_POKEHOUSE_FUJI = 0x35 +local FLAG_RESCUED_MR_FUJI = 0x23C + +local function towerDef() + return { + midLayout = { width = 20, height = 40 }, + objects = { + { localId = FUJI, graphicsId = 30, x = 11, y = 4, movementType = 8, flag = FLAG_HIDE_TOWER_FUJI }, + { localId = 2, graphicsId = 40, x = 11, y = 34, movementType = 8, flag = 0x3F0 }, + { localId = 3, graphicsId = 40, x = 5, y = 5, movementType = 8, flag = 0x3F1 }, + }, + } +end + +local function placePlayer(x, y) + Player.cellX, Player.cellY = x, y + Player.targetX, Player.targetY = x, y + Player.px, Player.py = x * 16, y * 16 + Player.moving = false +end + +local function fresh() + Objects.reset() + Objects.loadMap(nil, TOWER, towerDef()) +end + +print("[test] 1. setflag on an on-screen object's hide flag leaves it visible") +fresh() +placePlayer(11, 5) +Objects.syncFlagVisibility(FLAG_HIDE_TOWER_FUJI, true) +local fuji = Objects.find(FUJI) +check(fuji and fuji.visible and not fuji.hidden, "Fuji still visible after his hide flag is set") + +print("[test] 2. an object outside the camera box is culled and stays gone") +Objects.syncFlagVisibility(0x3F0, true) +local far = Objects.find(2) +check(far and far.hidden and not far.visible, "far object (11,34) hidden") + +print("[test] 3. clearflag still respawns a removed object") +Objects.removeObject(3) +check(Objects.find(3).hidden, "object 3 removed") +Objects.syncFlagVisibility(0x3F1, false) +check(Objects.find(3).visible and not Objects.find(3).hidden, "clearflag brings object 3 back") + +print("[test] 4. forced sync (mod SDK) still hides on-screen objects") +fresh() +placePlayer(11, 5) +Objects.syncFlagVisibility(FLAG_HIDE_TOWER_FUJI, true, true) +check(Objects.find(FUJI).hidden, "forced hide removes Fuji") + +print("[test] 5. Mr Fuji's script leaves him on screen through the msgbox") +fresh() +placePlayer(11, 5) +local store = Flags.newStore() +Flags.setFlag(store, nil, FLAG_HIDE_POKEHOUSE_FUJI, true) +local vm = Vm.new({ + store = store, + adapters = Adapters.host(nil, nil, nil), + scripts = { + fuji = { + { op = "setflag", [1] = FLAG_HIDE_TOWER_FUJI }, + { op = "clearflag", [1] = FLAG_HIDE_POKEHOUSE_FUJI }, + { op = "setflag", [1] = FLAG_RESCUED_MR_FUJI }, + { op = "end" }, + }, + }, +}) +vm:start("fuji") +for _ = 1, 16 do + if not vm:isRunning() then break end + vm:tick() +end +check(Flags.getFlag(store, vm.ctx, FLAG_HIDE_TOWER_FUJI), "FLAG_HIDE_TOWER_FUJI set") +check(Flags.getFlag(store, vm.ctx, FLAG_RESCUED_MR_FUJI), "FLAG_RESCUED_MR_FUJI set") +check(not Flags.getFlag(store, vm.ctx, FLAG_HIDE_POKEHOUSE_FUJI), "FLAG_HIDE_POKEHOUSE_FUJI cleared") +fuji = Objects.find(FUJI) +check(fuji and fuji.visible and not fuji.hidden, "Fuji still drawn after the script's setflag") +local drawn = false +for _, eo in ipairs(Objects.forDraw()) do + if eo.localId == FUJI then drawn = true end +end +check(drawn, "Fuji in forDraw") + +print("[test] 6. ON_TRANSITION / ON_LOAD hides run before spawn: in-view objects are gone") +local Space = require("src.core.game3.scripting.space") +local VERMILION = "FR_VERMILION_CITY" +local AIDE = 8 +local FLAG_HIDE_AIDE = 0x0A1 +local FLAG_HIDE_LOAD = 0x3F2 +local function vermilionDef() + return { + midLayout = { width = 50, height = 40 }, + objects = { + { localId = AIDE, graphicsId = 30, x = 25, y = 7, movementType = 8, flag = FLAG_HIDE_AIDE }, + { localId = 9, graphicsId = 40, x = 16, y = 8, movementType = 8, flag = FLAG_HIDE_LOAD }, + }, + } +end +local enterStore = Flags.newStore() +local saved = { + bundle = Space.bundle, vm = Space.vm, store = Space.store, + mapId = Space.mapId, active = Space.active, +} +Space.bundle = { + events = { + [VERMILION] = { mapScripts = { onTransition = "vermilion_transition", onLoad = "vermilion_load" } }, + }, +} +Space.store = enterStore +Space.vm = Vm.new({ + store = enterStore, + adapters = Adapters.host(nil, nil, nil), + scripts = { + -- data/maps/VermilionCity/scripts.inc:28 + vermilion_transition = { + { op = "setflag", [1] = FLAG_HIDE_AIDE }, + { op = "end" }, + }, + vermilion_load = { + { op = "setflag", [1] = FLAG_HIDE_LOAD }, + { op = "end" }, + }, + later = { + { op = "setflag", [1] = FLAG_HIDE_AIDE }, + { op = "end" }, + }, + }, +}) +Space.mapId = VERMILION +Space.active = true +Objects.reset() +Objects.loadMap(nil, VERMILION, vermilionDef()) +placePlayer(15, 7) +local okEnter, errEnter = pcall(Space.runEnterScripts, nil, VERMILION, nil, nil, { enterVia = "warp" }) +check(okEnter, "runEnterScripts ran" .. (okEnter and "" or (": " .. tostring(errEnter)))) +check(Flags.getFlag(enterStore, nil, FLAG_HIDE_AIDE), "ON_TRANSITION set the aide hide flag") +local aide = Objects.find(AIDE) +check(aide and aide.hidden and not aide.visible, "aide at (25,7) hidden though in view of (15,7)") +check(Objects.find(9) and Objects.find(9).hidden, "ON_LOAD setflag hides an in-view object") +check(not Space._inTransition, "transition window closed after the drain") + +print("[test] 7. setflag after the entry drain keeps cart semantics") +Objects.syncFlagVisibility(FLAG_HIDE_AIDE, false) +check(Objects.find(AIDE).visible, "aide respawned by clearflag") +Flags.setFlag(enterStore, nil, FLAG_HIDE_AIDE, false) +Space.vm:start("later") +for _ = 1, 16 do + if not Space.vm:isRunning() then break end + Space.vm:tick() +end +check(Objects.find(AIDE).visible, "mid-map setflag leaves the in-view aide drawn") + +Space.bundle, Space.vm, Space.store = saved.bundle, saved.vm, saved.store +Space.mapId, Space.active = saved.mapId, saved.active + +if failed > 0 then + print("[test] FAILED " .. failed) + os.exit(1) +end +print("[test] all passed") diff --git a/tests/game3_objects_invisible_movement_test.lua b/tests/game3_objects_invisible_movement_test.lua new file mode 100644 index 00000000..fff9f714 --- /dev/null +++ b/tests/game3_objects_invisible_movement_test.lua @@ -0,0 +1,98 @@ +#!/usr/bin/env luajit + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +local Objects = require("src.core.game3.objects") + +local MAP = "FR_VERMILION_CITY_POKEMON_CENTER_1F" +local INVISIBLE = 0x4C + +local function pcDef() + return { + midLayout = { width = 15, height = 9 }, + objects = { + { localId = 1, graphicsId = 64, x = 7, y = 2, movementType = 8 }, + { localId = 6, graphicsId = 0, x = 2, y = 1, movementType = INVISIBLE, scriptKey = "J1" }, + { localId = 7, graphicsId = 0, x = 3, y = 1, movementType = INVISIBLE, scriptKey = "J2" }, + }, + } +end + +local function has(list, lid) + for _, v in ipairs(list) do + if v == lid or (type(v) == "table" and v.localId == lid) then return true end + end + return false +end + +print("[test] 1. MOVEMENT_TYPE_INVISIBLE objects are never drawn") +Objects.reset() +Objects.loadMap(nil, MAP, pcDef()) +local draw = Objects.forDraw() +check(has(draw, 1), "the nurse is drawn") +check(not has(draw, 6), "journal 6 is not drawn") +check(not has(draw, 7), "journal 7 is not drawn") + +print("[test] 2. they stay solid and talkable") +local j = Objects.find(6) +check(j ~= nil and j.visible and not j.hidden, "journal 6 is live") +check(has(Objects.listActive(), 6) and has(Objects.listActive(), 7), "journals are in listActive") +check(Objects.at(2, 1) == j, "Objects.at finds the journal for talk") +check(Objects.blocks(3, 1), "the journal cell blocks movement") + +print("[test] 3. ghost pools skip them too") +local pool = Objects.spawnFromDefs(pcDef().objects, pcDef()) +local pdraw = Objects.poolForDraw(pool) +check(has(pdraw, 1), "pool draws the nurse") +check(not has(pdraw, 6) and not has(pdraw, 7), "pool skips the journals") + +print("[test] 4. addobject respawn keeps it invisible") +Objects.removeObject(6) +Objects.addObject(6) +check(not has(Objects.forDraw(), 6), "respawned journal is still not drawn") +check(has(Objects.listActive(), 6), "respawned journal is live") + +print("[test] 5. switching a live object to the invisible type hides its sprite") +Objects.setTrainerMovementType(1, INVISIBLE) +check(not has(Objects.forDraw(), 1), "nurse no longer drawn") +check(has(Objects.listActive(), 1), "nurse still live") + +print("[test] 6. respawn from the template drops a live-only invisible flag") +Objects.removeObject(1) +Objects.addObject(1) +check(has(Objects.forDraw(), 1), "respawned nurse drawn again") +Objects.setTrainerMovementType(1, INVISIBLE) +Objects.removeObject(1) +Objects.syncFlagVisibility(0, false) +Objects.addObject(1) +check(has(Objects.forDraw(), 1), "addobject after removeobject respawns the nurse visible") + +print("[test] 7. a template override away from the invisible type spawns drawn") +Objects.reset() +Objects.loadMap(nil, MAP, pcDef()) +Objects.overrideTemplateMovementType(6, 8) +Objects.loadMap(nil, MAP, pcDef()) +check(has(Objects.forDraw(), 6), "journal 6 with an overridden template type is drawn") +check(not has(Objects.forDraw(), 7), "journal 7 still invisible") +Objects.reset() + +print("[test] 8. addobject on a live invisible object leaves it invisible") +Objects.loadMap(nil, MAP, pcDef()) +Objects.addObject(6) +check(not has(Objects.forDraw(), 6), "journal 6 still not drawn") + +if failed > 0 then + print("[test] FAILED " .. failed) + os.exit(1) +end +print("[test] all passed") diff --git a/tests/game3_ops_vars_test.lua b/tests/game3_ops_vars_test.lua index fa8f1833..3f711b6b 100644 --- a/tests/game3_ops_vars_test.lua +++ b/tests/game3_ops_vars_test.lua @@ -349,6 +349,27 @@ w = warpArgs(0x4100, 0x8015) eq(w[4], 0x4100, "an id past VARS_END is a literal") eq(w[5], 0x8015, "an id past SPECIAL_VARS_END is a literal") +print("[test] 11. buffernumberstring VarGets a literal requirement") +-- pokefirered/data/maps/Route10_PokemonCenter_1F/scripts.inc:59 +-- pokefirered/src/event_data.c:235-241 +store = Flags.newStore() +eq(Flags.getVar(store, nil, 20), 20, "getVar(20) returns the literal 20") +Flags.setVar(store, nil, 0x4050, 3) +eq(Flags.getVar(store, nil, 0x4050), 3, "a var at 0x4050 still reads the store") +vm, store = run({ + t = { + { op = "setvar", [1] = 0x8006, [2] = 7 }, + { op = "setvar", [1] = VAR_TEMP_1, [2] = 42 }, + { op = "buffernumberstring", [1] = 0, [2] = 20 }, + { op = "buffernumberstring", [1] = 1, [2] = VAR_TEMP_1 }, + { op = "buffernumberstring", [1] = 2, [2] = 0x8006 }, + { op = "end" }, + }, +}, "t") +eq(vm.ctx.stringVars[1], "20", "buffernumberstring STR_VAR_1, 20 buffers \"20\"") +eq(vm.ctx.stringVars[2], "42", "buffernumberstring of a save var buffers its value") +eq(vm.ctx.stringVars[3], "7", "buffernumberstring of VAR_0x8006 buffers the caught count") + if failed > 0 then print("[test] FAILED " .. failed) os.exit(1) diff --git a/tests/game3_ow_fishing_pose_test.lua b/tests/game3_ow_fishing_pose_test.lua new file mode 100644 index 00000000..e8b9588c --- /dev/null +++ b/tests/game3_ow_fishing_pose_test.lua @@ -0,0 +1,167 @@ +#!/usr/bin/env luajit +-- pokefirered/src/data/object_events/object_event_anims.h:877 +-- pokefirered/src/field_player_avatar.c:1954 AlignFishingAnimationFrames + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +local GameVersion = require("src.core.GameVersion") +GameVersion.set("firered") + +local OwSprites = require("src.core.game3.ow_sprites") + +local function pose(spr, facing, opts) + local f, flip = OwSprites.pose(spr, facing, 0, false, opts) + return f, flip +end + +print("[test] 1. fishing sheet layout W 0-3 / N 4-7 / S 8-11, east = west hflip") +for _, n in ipairs({ 12, 18 }) do + local spr = { frameCount = n, quads = {} } + local want = { + { "down", 3, 11, false }, { "up", 3, 7, false }, + { "left", 3, 3, false }, { "right", 3, 3, true }, + { "down", 0, 8, false }, { "up", 0, 4, false }, + { "left", 0, 0, false }, { "right", 0, 0, true }, + } + for _, w in ipairs(want) do + local f, flip = pose(spr, w[1], { fishing = true, fishFrame = w[2] }) + check(f == w[3] and flip == w[4], string.format( + "frameCount %d %s fishFrame %d -> %d%s (got %s%s)", n, w[1], w[2], w[3], + w[4] and " hflip" or "", tostring(f), flip and " hflip" or "")) + end +end + +print("[test] 2. take-out / hooked / put-away timelines") +local function seq(facing, anim, upTo) + local out = {} + for t = 0, upTo do out[#out + 1] = (OwSprites.fishingFrame(facing, anim, t)) end + return table.concat(out, ",") +end +check(seq("down", "takeout", 17) == "0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,3,3", + "take-out rod: 0,1,2,3 at 4 ticks, then holds 3") +local _, ended = OwSprites.fishingFrame("down", "takeout", 500) +check(ended == true and (OwSprites.fishingFrame("down", "takeout", 500)) == 3, + "take-out holds the last frame through the dot game") +local sn = {} +for t = 0, 21 do sn[#sn + 1] = (OwSprites.fishingFrame("up", "putaway", t)) end +check(table.concat(sn, ",") == "3,3,3,3,2,2,2,2,2,2,1,1,1,1,1,1,0,0,0,0,0,0", + "put away north/south: 3@4, 2@6, 1@6, 0@6") +local _, e21 = OwSprites.fishingFrame("up", "putaway", 21) +local _, e22 = OwSprites.fishingFrame("up", "putaway", 22) +check(e21 == false and e22 == true, "north/south put-away ends after 22 ticks") +local _, w15 = OwSprites.fishingFrame("left", "putaway", 15) +local _, w16 = OwSprites.fishingFrame("right", "putaway", 16) +check(w15 == false and w16 == true, "west/east put-away ends after 16 ticks") +check((OwSprites.fishingFrame("down", "hooked", 0)) == 2 + and (OwSprites.fishingFrame("down", "hooked", 6)) == 3 + and (OwSprites.fishingFrame("down", "hooked", 12)) == 2 + and (OwSprites.fishingFrame("down", "hooked", 18)) == 3 + and (OwSprites.fishingFrame("down", "hooked", 53)) == 3 + and (OwSprites.fishingFrame("down", "hooked", 54)) == 2, + "hooked: 2,3 twice at 6 ticks, hold 3 for 30, then loop") + +print("[test] 3. AlignFishingAnimationFrames offsets") +local function off(abs, facing) + local x2, y2 = OwSprites.fishingOffset(abs, facing) + return x2 .. "," .. y2 +end +check(off(0, "left") == "0,0", "frame 0: no offset") +check(off(3, "left") == "-8,0", "frame 3 facing west: x2 -8") +check(off(3, "right") == "8,0", "frame 3 facing east: x2 +8") +check(off(1, "right") == "8,0" and off(2, "left") == "-8,0", "frames 1-2 shift sideways") +check(off(4, "up") == "0,0" and off(5, "up") == "0,-8" and off(7, "up") == "0,0", + "north: only frame 5 lifts 8") +check(off(9, "down") == "0,0" and off(10, "down") == "0,8" and off(11, "down") == "0,8", + "south: frames 10-11 drop 8") + +print("[test] 4. field move is sAnim_FieldMove 0-4, south only, never flipped") +local item = { frameCount = 18, quads = {} } +for _, facing in ipairs({ "down", "up", "left", "right" }) do + local f, flip = pose(item, facing, { fieldMove = true }) + check(f == 4 and flip == false, facing .. " holds frame 4, no flip (got " .. tostring(f) .. ")") +end +local fm = {} +for t = 0, 20 do fm[#fm + 1] = OwSprites.fieldMoveFrame(t) end +check(table.concat(fm, ",") == "0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,4", + "field move 0,1,2,3 at 4 ticks then 4") +local vs = {} +for t = 0, 88, 4 do vs[#vs + 1] = OwSprites.fieldMoveFrame(t, "vs_seeker") end +check(table.concat(vs, ",") == "0,1,5,6,7,8,7,8,7,8,7,8,7,8,7,8,7,8,6,1,0,0,0", + "VS Seeker: 0,1,5,6, (7,8)x7, 6,1,0") + +print("[test] 4b. VS Seeker on a bike uses gid 6/13 and sAnim_VSSeekerBike") +local vb = {} +for t = 0, 92, 4 do vb[#vb + 1] = OwSprites.fieldMoveFrame(t, "vs_seeker_bike") end +check(table.concat(vb, ",") == "0,1,2,3,4,5,4,5,4,5,4,5,4,5,4,5,4,5,3,2,1,0,0,0", + "VS Seeker bike: 0,1,2,3, (4,5)x7, 3,2,1,0 (got " .. table.concat(vb, ",") .. ")") +local bikeSheet = { frameCount = 6, quads = {} } +for _, facing in ipairs({ "down", "up", "left", "right" }) do + local f, flip = pose(bikeSheet, facing, { fieldMove = true, fieldMoveFrame = 5 }) + check(f == 5 and flip == false, "bike VS Seeker sheet " .. facing .. " frame 5, no flip (got " + .. tostring(f) .. (flip and " hflip" or "") .. ")") +end +do + local P = require("src.core.game3.player") + local VsSeeker = require("src.core.game3.vs_seeker") + local saved = { P.fieldMoveAnim, P.fieldMoveTotal, P.fieldMoveKind, P.biking } + local function gid(gender) + return OwSprites.playerGraphicsId({ session = { gender = gender } }) + end + P.biking = true + P.startFieldMove(VsSeeker.EFFECT_FRAMES, P.biking and "vs_seeker_bike" or "vs_seeker") + check(gid("male") == 6, "male on bike using VS Seeker draws gid 6 (got " .. tostring(gid("male")) .. ")") + check(gid("female") == 13, "female on bike using VS Seeker draws gid 13 (got " .. tostring(gid("female")) .. ")") + P.biking = false + P.startFieldMove(VsSeeker.EFFECT_FRAMES, "vs_seeker") + check(gid("male") == 3 and gid("female") == 10, "on foot VS Seeker keeps the item sheet 3/10") + P.fieldMoveAnim, P.fieldMoveTotal, P.fieldMoveKind, P.biking = saved[1], saved[2], saved[3], saved[4] +end +do + local src = io.open("src/core/game3/vs_seeker.lua"):read("*a") + check(src:find('"vs_seeker_bike"', 1, true) ~= nil and src:find("Player.biking", 1, true) ~= nil, + "vs_seeker.lua picks the bike anim from Player.biking") +end + +print("[test] 5. Field.fishingPose drives the pose from the task") +local okF, Field = pcall(require, "src.core.game3.field") +local Player = require("src.core.game3.player") +if okF and Field and Field.fishingPose then + Field._fishing = nil + Player.fishing = false + Field.startFishing(0) + Player.facing = "down" + local g, x2, y2 = Field.fishingPose() + check(g == 0 and x2 == 0 and y2 == 0, "cast starts on frame 8 with no offset") + Field._fishing.animT = 12 + g, x2, y2 = Field.fishingPose() + check(g == 3 and x2 == 0 and y2 == 8, "facing south, frame 11 drops 8") + Player.facing = "left" + g, x2, y2 = Field.fishingPose() + check(g == 3 and x2 == -8 and y2 == 0, "facing west, frame 3 shifts -8") + Player.facing = "up" + Field._fishing.animT = 5 + g, x2, y2 = Field.fishingPose() + check(g == 1 and x2 == 0 and y2 == -8, "facing north, frame 5 lifts 8") + Field._fishing = nil + Player.fishing = false + check(Field.fishingPose() == nil, "no pose once fishing ends") +else + check(false, "src.core.game3.field loads: " .. tostring(Field)) +end + +if failed > 0 then + print("[test] FAILED " .. failed) + os.exit(1) +end +print("[test] all passed") +os.exit(0) diff --git a/tests/game3_pc_sfx_2415_test.lua b/tests/game3_pc_sfx_2415_test.lua new file mode 100644 index 00000000..af7bcc5a --- /dev/null +++ b/tests/game3_pc_sfx_2415_test.lua @@ -0,0 +1,171 @@ +#!/usr/bin/env luajit +-- pokefirered/data/scripts/pc.inc:1 + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +local events = {} +package.loaded["src.ui.game3.stack"] = { + push = function() end, + pop = function() end, + busy = function() return false end, + drawOrder = function() return {} end, +} +package.loaded["src.core.game3.audio"] = { + playSe = function(id) events[#events + 1] = "se" .. tostring(id) end, + playCry = function() end, +} + +local function input(key) + return { wasPressed = function(_, k) return k == key end, isDown = function() return false end } +end + +local function seOnly() + local out = {} + for _, e in ipairs(events) do + if e:sub(1, 2) == "se" then out[#out + 1] = e end + end + return table.concat(out, ",") +end + +local Player = require("src.core.game3.player") +Player.cellX, Player.cellY, Player.facing = 5, 10, "up" +local PcAnim = require("src.core.game3.pc_anim") +PcAnim.setter(function() end) +local realTurnOn = PcAnim.turnOn +PcAnim.turnOn = function(...) + events[#events + 1] = "anim_on" + return realTurnOn(...) +end + +local Std = require("src.core.game3.scripting.stdscripts") +local PcMenu = require("src.ui.game3.pc_menu") +local Bag = require("src.core.game3.bag") +local Storage = require("src.core.game3.storage") +local function session() return { bag = Bag.new(), storage = Storage.new(), name = "RED" } end + +print("[test] 1. EventScript_PC carries playse SE_PC_ON right after AnimatePcTurnOn") +local rows = Std.SCRIPTS.EventScript_PC +local onIdx, animIdx +for i, r in ipairs(rows) do + if r.op == "special" and r.id == Std.SPECIAL.AnimatePcTurnOn then animIdx = i end + if r.op == "playse" and r[1] == 4 then onIdx = i end +end +check(animIdx ~= nil and onIdx == animIdx + 1, "playse 4 follows AnimatePcTurnOn") + +print("[test] 2. Center PC flow: ON, message, menu, storage LOGIN, LOG OFF") +local Vm = require("src.core.game3.scripting.vm") +local Adapters = require("src.core.game3.scripting.adapters") +local vm = Vm.new({ + scripts = Std.SCRIPTS, + text = Std.TEXT, + adapters = Adapters.stub({ + onMessage = function() events[#events + 1] = "msg" end, + openPc = function(done) + events[#events + 1] = "menu" + PcMenu.show({ session = session() }) + PcMenu.handleInput(input("a")) + PcMenu.close() + done() + end, + }), +}) +check(vm:start("EventScript_PC") == true, "vm started EventScript_PC") +local steps = 0 +while vm:isRunning() and steps < 80 do vm:step(); steps = steps + 1 end +check(not vm:isRunning(), "EventScript_PC completed") +local seq = table.concat(events, ",") +check(seq == "anim_on,se4,msg,menu,se5,se2,se3", + "ordered events anim_on,se4,msg,menu,se5,se2,se3 (got " .. seq .. ")") + +local function flagged(dex, clear) + local s = session() + s.flags = {} + local Space = package.loaded["src.core.game3.scripting.space"] + local stores = { s } + if Space and Space.store then Space.store.flags = Space.store.flags or {}; stores[2] = Space.store end + for _, st in ipairs(stores) do + st.flags[0x829] = dex or nil + st.flags[0x82C] = clear or nil + end + return s +end + +local function rowIds() + local ids = {} + for _, r in ipairs(PcMenu._rootEntries()) do ids[#ids + 1] = r.id end + return table.concat(ids, ",") +end + +print("[test] 3. root rows gate on POKEDEX_GET / GAME_CLEAR (script_menu.c:1006)") +for _, c in ipairs({ + { false, false, "storage,player,quit" }, + { true, false, "storage,player,oak,quit" }, + { true, true, "storage,player,oak,hall,quit" }, + { false, true, "storage,player,oak,hall,quit" }, +}) do + PcMenu.show({ session = flagged(c[1], c[2]) }) + local got = rowIds() + check(got == c[3], string.format("dex=%s clear=%s rows %s (got %s)", tostring(c[1]), tostring(c[2]), c[3], got)) + PcMenu.close() +end + +print("[test] 4. root menu open plays nothing; each access row plays SELECT then LOGIN") +for i, id in ipairs({ "storage", "player", "oak", "hall" }) do + local s = flagged(true, true) + events = {} + PcMenu.show({ session = s }) + check(seOnly() == "", id .. ": root open plays no SE") + PcMenu.cursor = i + check(PcMenu._rootEntries()[i].id == id, id .. " is root row " .. i) + PcMenu.handleInput(input("a")) + check(seOnly() == "se5,se2", id .. ": A plays se5,se2 (got " .. seOnly() .. ")") + PcMenu.close() +end + +print("[test] 5. LOG OFF and B play SE_SELECT then SE_PC_OFF") +for _, c in ipairs({ { false, false, 3 }, { true, false, 4 }, { true, true, 5 } }) do + events = {} + PcMenu.show({ session = flagged(c[1], c[2]) }) + PcMenu.cursor = c[3] + check(PcMenu._rootEntries()[c[3]].id == "quit", "LOG OFF is row " .. c[3]) + PcMenu.handleInput(input("a")) + check(seOnly() == "se5,se3", "LOG OFF row " .. c[3] .. " plays se5,se3 (got " .. seOnly() .. ")") +end +events = {} +PcMenu.show({ session = flagged(false, false) }) +PcMenu.handleInput(input("b")) +check(seOnly() == "se5,se3", "B plays se5,se3 (got " .. seOnly() .. ")") + +print("[test] 6. bedroom player_pc open plays nothing") +events = {} +PcMenu.show({ session = session(), startMode = "player_pc", closeOnExit = true }) +check(seOnly() == "", "bedroom open plays no SE") +events = {} +PcMenu.handleInput(input("b")) +check(seOnly() == "se5,se3", "bedroom TURN OFF plays se5,se3 once (got " .. seOnly() .. ")") + +print("[test] 7. Hud.openPc boots with SE_PC_ON") +local okHud, Hud = pcall(require, "src.ui.game3.hud") +check(okHud, "hud loads (" .. tostring(okHud and "" or Hud) .. ")") +if okHud then + events = {} + Hud.openPc(nil, session()) + check(seOnly() == "se4", "Hud.openPc plays se4 (got " .. seOnly() .. ")") + PcMenu.close() +end + +if failed > 0 then + print(string.format("[FAIL] %d check(s) failed", failed)) + os.exit(1) +end +print("[ok] all checks passed") diff --git a/tests/game3_storage_test.lua b/tests/game3_storage_test.lua index 0ac8fe74..f516b48c 100644 --- a/tests/game3_storage_test.lua +++ b/tests/game3_storage_test.lua @@ -343,7 +343,7 @@ print("[ok] BoxStorageUI navigation and hover bounce passed") print("=== [TEST 11] PcMenu Root Navigation & Submenu Lifecycle ===") local pcSession = { name = "RED", - flags = { [0x828] = true }, -- BILL'S PC unlocked + flags = { [0x828] = true, [0x82C] = true }, -- BILL'S PC unlocked party = { { species = 25, level = 10, hp = 30, maxHp = 30 } }, storage = Storage.new(), bag = Bag.new(), diff --git a/tests/gen2_menus_test.lua b/tests/gen2_menus_test.lua index 5554aa6e..dbf93c8b 100644 --- a/tests/gen2_menus_test.lua +++ b/tests/gen2_menus_test.lua @@ -366,7 +366,7 @@ local options = OptionsMenu.new(optionsGame, { }) -- The cart's seven rows, then the port's: CONTROLS, audio, PERFORMANCE, -- speed, display, SHADER FX + SHADER FX 2 (the second slot added alongside -check("thirty-four rows", #OptionsMenu.ROWS, 34) +check("thirty-five rows", #OptionsMenu.ROWS, 35) check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame") check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls") check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol") @@ -1049,7 +1049,8 @@ check("and TILT follows VOID FILL", OptionsMenu.ROWS[zoomIndex + 2].label, -- moves the whole run instead of breaking six separate index assertions. do local run = { "TILT", "COLOR", "UI LETTERBOX", "SHADER FX", - "SHADER FX 2", "VIDEO MODE", "SCREEN POS", "TOUCH PAD" } + "SHADER FX 2", "VIDEO MODE", "FAITHFUL RATIO", "SCREEN POS", + "TOUCH PAD" } for at, want in ipairs(run) do check("display block order: " .. want, OptionsMenu.ROWS[tiltIndex + at - 1].label, want)