mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-06 17:10:55 -04:00
Place object sprites by their ROM subsprite offsets and drop the sailor stand-ins for the Seagallop and SS Anne
This commit is contained in:
@@ -455,8 +455,11 @@ function OwSprites.draw(graphicsId, px, py, camX, camY, facing, walkPhase, stepF
|
||||
local frame, flip = OwSprites.pose(spr, facing, walkPhase, stepFlip, opts)
|
||||
local q = spr.quads[frame]
|
||||
if not q then return false end
|
||||
local sx = px - camX + (16 - spr.width) / 2
|
||||
local sy = py - camY + 16 - spr.height
|
||||
local offX = spr.drawOffX or 0
|
||||
-- src/sprite.c:1669
|
||||
if flip then offX = -offX end
|
||||
local sx = px - camX + (16 - spr.width) / 2 + offX
|
||||
local sy = py - camY + 16 - spr.height + (spr.drawOffY or 0)
|
||||
love.graphics.setColor(1, 1, 1, opts and opts.alpha or 1)
|
||||
if flip then
|
||||
love.graphics.draw(spr.image, q, sx + spr.width, sy, 0, -1, 1)
|
||||
|
||||
@@ -44,8 +44,6 @@ GfxIds.TO_SPRITE = {
|
||||
[88] = "SPRITE_MOM", -- player's PC / mom-adjacent
|
||||
[89] = "SPRITE_SUPER_NERD", -- CELIO
|
||||
[92] = "SPRITE_POKE_BALL",
|
||||
[108] = "SPRITE_SAILOR", -- OBJ_EVENT_GFX_SEAGALLOP fallback
|
||||
[109] = "SPRITE_SAILOR", -- OBJ_EVENT_GFX_SS_ANNE fallback
|
||||
}
|
||||
|
||||
local S, N, W, E = "down", "up", "left", "right"
|
||||
|
||||
@@ -11,7 +11,6 @@ local SE = require("src.core.game3.se_ids")
|
||||
local INIT_FRAMES = 50 -- Task_SSAnneInit countdown
|
||||
local SMOKE_INTERVAL = 70 -- Task_SSAnneRun smoke puff period
|
||||
local SLIDE_SPEED_DIV = 5 -- 1 pixel movement every 5 frames (x = data[2] / 5)
|
||||
local TRAVEL_DISTANCE = 216 -- pixels to travel until boat is fully off-screen
|
||||
local FINISH_FRAMES = 40 -- Task_SSAnneFinish delay after exit horn
|
||||
|
||||
SSAnneCutscene._active = false
|
||||
@@ -58,6 +57,18 @@ local function loadGfx()
|
||||
end
|
||||
end
|
||||
|
||||
local function screenOrigin()
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local px = P and (P.cellX or 0) * 16 or 0
|
||||
local py = P and (P.cellY or 0) * 16 or 0
|
||||
return px - 112, py - 72
|
||||
end
|
||||
|
||||
local function boatScreenX(eo)
|
||||
local ox = screenOrigin()
|
||||
return (eo.px or (eo.cellX * 16)) + 8 - ox
|
||||
end
|
||||
|
||||
function SSAnneCutscene.isActive()
|
||||
return SSAnneCutscene._active
|
||||
end
|
||||
@@ -117,33 +128,41 @@ function SSAnneCutscene.step()
|
||||
SSAnneCutscene._runTimer1 = SSAnneCutscene._runTimer1 + 1
|
||||
SSAnneCutscene._runTimer2 = SSAnneCutscene._runTimer2 + 1
|
||||
|
||||
-- Smoke puff creation every 70 frames
|
||||
-- pokefirered/src/ss_anne.c:112
|
||||
if SSAnneCutscene._runTimer1 == SMOKE_INTERVAL then
|
||||
SSAnneCutscene._runTimer1 = 0
|
||||
table.insert(SSAnneCutscene._smokes, {
|
||||
timer = 0,
|
||||
x2 = 0,
|
||||
frame = 0,
|
||||
animEnded = false,
|
||||
boatOffsetAtSpawn = SSAnneCutscene._boatOffset,
|
||||
})
|
||||
if not eo or boatScreenX(eo) - SSAnneCutscene._boatOffset + 49 >= -32 then
|
||||
table.insert(SSAnneCutscene._smokes, {
|
||||
timer = 0,
|
||||
x2 = 0,
|
||||
frame = 0,
|
||||
animEnded = false,
|
||||
boatOffsetAtSpawn = SSAnneCutscene._boatOffset,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Boat movement: x = data[2] / 5 (1 pixel every 5 frames)
|
||||
SSAnneCutscene._boatOffset = math.floor(SSAnneCutscene._runTimer2 / SLIDE_SPEED_DIV)
|
||||
if eo then
|
||||
eo.raiseX = -SSAnneCutscene._boatOffset
|
||||
-- pokefirered/src/ss_anne.c:119
|
||||
local departed = eo and boatScreenX(eo) - SSAnneCutscene._boatOffset < -120
|
||||
if not departed then
|
||||
SSAnneCutscene._boatOffset = math.floor(SSAnneCutscene._runTimer2 / SLIDE_SPEED_DIV)
|
||||
if eo then
|
||||
eo.raiseX = -SSAnneCutscene._boatOffset
|
||||
end
|
||||
end
|
||||
|
||||
-- Update wake sprite
|
||||
-- pokefirered/src/ss_anne.c:159
|
||||
if SSAnneCutscene._wake then
|
||||
local w = SSAnneCutscene._wake
|
||||
if math.floor(w.timer / 6) < 22 then
|
||||
w.timer = w.timer + 1
|
||||
end
|
||||
w.x2 = math.floor(w.timer / 6)
|
||||
-- 12 ticks per frame, looping between frame 0 and frame 1
|
||||
w.frame = (math.floor(w.timer / 12) % 2 == 0) and 0 or 1
|
||||
w.anim = (w.anim or 0) + 1
|
||||
w.frame = (math.floor((w.anim - 1) / 12) % 2 == 0) and 0 or 1
|
||||
if eo and boatScreenX(eo) - SSAnneCutscene._boatOffset + 80 + w.x2 < -18 then
|
||||
SSAnneCutscene._wake = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Update smoke sprites
|
||||
@@ -168,8 +187,7 @@ function SSAnneCutscene.step()
|
||||
end
|
||||
SSAnneCutscene._smokes = activeSmokes
|
||||
|
||||
-- Exit check: when boat moves completely off-screen
|
||||
if SSAnneCutscene._boatOffset >= TRAVEL_DISTANCE then
|
||||
if departed then
|
||||
-- Final horn sound
|
||||
playSe(SE.SE_SS_ANNE_HORN)
|
||||
SSAnneCutscene._phase = "finish"
|
||||
@@ -204,16 +222,13 @@ function SSAnneCutscene.drawWake(camX, camY)
|
||||
local eo = Objects and Objects.find and Objects.find(1)
|
||||
if not eo then return end
|
||||
|
||||
local curBoatPx = (eo.px or (eo.cellX * 16)) - SSAnneCutscene._boatOffset
|
||||
local boatPy = eo.py or (eo.cellY * 16)
|
||||
local boatLeft = curBoatPx - camX - 56
|
||||
local boatTop = boatPy - camY - 48
|
||||
|
||||
local _, oy = screenOrigin()
|
||||
local w = SSAnneCutscene._wake
|
||||
local q = SSAnneCutscene._wakeQuads[w.frame or 0]
|
||||
if q then
|
||||
local wx = boatLeft + 106 + (w.x2 or 0)
|
||||
local wy = boatTop + 24
|
||||
-- pokefirered/src/ss_anne.c:153
|
||||
local wx = (eo.px or (eo.cellX * 16)) - SSAnneCutscene._boatOffset + 80 + (w.x2 or 0) - camX
|
||||
local wy = oy + 109 - 16 - camY
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(SSAnneCutscene._wakeImage, q, wx, wy)
|
||||
end
|
||||
@@ -228,15 +243,13 @@ function SSAnneCutscene.drawSmoke(camX, camY)
|
||||
local eo = Objects and Objects.find and Objects.find(1)
|
||||
if not eo then return end
|
||||
|
||||
local boatPy = eo.py or (eo.cellY * 16)
|
||||
local boatTop = boatPy - camY - 48
|
||||
|
||||
local _, oy = screenOrigin()
|
||||
for _, s in ipairs(SSAnneCutscene._smokes) do
|
||||
local q = SSAnneCutscene._smokeQuads[s.frame or 0]
|
||||
if q then
|
||||
local spawnBoatLeft = (eo.px or (eo.cellX * 16)) - s.boatOffsetAtSpawn - camX - 56
|
||||
local sx = spawnBoatLeft + 78 + (s.x2 or 0)
|
||||
local sy = boatTop + 2
|
||||
-- pokefirered/src/ss_anne.c:185
|
||||
local sx = (eo.px or (eo.cellX * 16)) - s.boatOffsetAtSpawn + 49 + (s.x2 or 0) - camX
|
||||
local sy = oy + 78 - 8 - camY
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(SSAnneCutscene._smokeImage, q, sx, sy)
|
||||
end
|
||||
|
||||
@@ -22,12 +22,12 @@ CacheContract.VERSION_FORMAT = {
|
||||
-- data/pikachu/pikachu_pic_animation.asm:340
|
||||
yellow = "rom-cache-v12-yellow2:",
|
||||
-- v8: M4A tracks retain reachable patterns and explicit entry offsets.
|
||||
firered = "rom-cache-v23-firered:",
|
||||
leafgreen = "rom-cache-v8-leafgreen:",
|
||||
emerald = "rom-cache-v3-emerald:",
|
||||
firered = "rom-cache-v24-firered:",
|
||||
leafgreen = "rom-cache-v9-leafgreen:",
|
||||
emerald = "rom-cache-v4-emerald:",
|
||||
-- pokeruby/src/string_util.c:408
|
||||
ruby = "rom-cache-v5-ruby:",
|
||||
sapphire = "rom-cache-v5-sapphire:",
|
||||
ruby = "rom-cache-v6-ruby:",
|
||||
sapphire = "rom-cache-v6-sapphire:",
|
||||
}
|
||||
CacheContract.MARKER_PATH = "rom-cache.complete"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ local ExtractMapEvents = require("src.import.gba.extract_map_events")
|
||||
local OwExtract = {}
|
||||
|
||||
OwExtract.MAGIC = "SVOW"
|
||||
OwExtract.FORMAT_VERSION = 3
|
||||
OwExtract.FORMAT_VERSION = 4
|
||||
|
||||
OwExtract.REQUIRED = { "ow/manifest.lua", "ow/palette_manifest.lua" }
|
||||
|
||||
@@ -130,6 +130,7 @@ local function read_graphics_info(rom, infoOff)
|
||||
local tracks = rom:get(infoOff + 13)
|
||||
local imagesPtr = rom:u32(infoOff + 0x1C)
|
||||
local animsPtr = rom:u32(infoOff + 0x18)
|
||||
local subspriteTablesPtr = rom:u32(infoOff + 0x14)
|
||||
return {
|
||||
tileTag = tileTag,
|
||||
paletteTag = paletteTag,
|
||||
@@ -143,6 +144,59 @@ local function read_graphics_info(rom, infoOff)
|
||||
tracks = tracks,
|
||||
imagesPtr = imagesPtr,
|
||||
animsPtr = animsPtr,
|
||||
subspriteTablesPtr = subspriteTablesPtr,
|
||||
}
|
||||
end
|
||||
|
||||
-- src/sprite.c:246
|
||||
local OAM_DIMS = {
|
||||
[0] = { { 8, 8 }, { 16, 16 }, { 32, 32 }, { 64, 64 } },
|
||||
[1] = { { 16, 8 }, { 32, 8 }, { 32, 16 }, { 64, 32 } },
|
||||
[2] = { { 8, 16 }, { 8, 32 }, { 16, 32 }, { 32, 64 } },
|
||||
}
|
||||
|
||||
local function signed(v, bits)
|
||||
local half = 2 ^ (bits - 1)
|
||||
if v >= half then return v - 2 * half end
|
||||
return v
|
||||
end
|
||||
|
||||
-- src/sprite.c:1669
|
||||
local function read_subsprite_layout(rom, tablesPtr, width, height)
|
||||
local tableOff = tablesPtr ~= 0 and gba_off(tablesPtr)
|
||||
if not tableOff then return nil end
|
||||
local count = rom:get(tableOff)
|
||||
local subsOff = gba_off(rom:u32(tableOff + 4))
|
||||
if not subsOff or count < 1 or count > 64 then return nil end
|
||||
local game = Versions.GAME
|
||||
-- pokeruby/include/sprite.h:153
|
||||
local wide = game == "ruby" or game == "sapphire"
|
||||
local stride = wide and 8 or 4
|
||||
local subs = {}
|
||||
local minX, minY, maxX, maxY = math.huge, math.huge, -math.huge, -math.huge
|
||||
for i = 0, count - 1 do
|
||||
local off = subsOff + i * stride
|
||||
local x, y, bits
|
||||
if wide then
|
||||
x, y, bits = signed(rom:u16(off), 16), signed(rom:u16(off + 2), 16), rom:u16(off + 4)
|
||||
else
|
||||
x, y, bits = signed(rom:get(off), 8), signed(rom:get(off + 1), 8), rom:u16(off + 2)
|
||||
end
|
||||
local dims = OAM_DIMS[bits % 4]
|
||||
dims = dims and dims[math.floor(bits / 4) % 4 + 1]
|
||||
if not dims then return nil end
|
||||
subs[#subs + 1] = { x = x, y = y, w = dims[1], h = dims[2], tile = math.floor(bits / 16) % 1024 }
|
||||
minX, minY = math.min(minX, x), math.min(minY, y)
|
||||
maxX, maxY = math.max(maxX, x + dims[1]), math.max(maxY, y + dims[2])
|
||||
end
|
||||
if maxX - minX ~= width or maxY - minY ~= height then return nil end
|
||||
for _, sub in ipairs(subs) do
|
||||
sub.x, sub.y = sub.x - minX, sub.y - minY
|
||||
end
|
||||
return {
|
||||
subs = subs,
|
||||
offX = minX + math.floor(width / 2),
|
||||
offY = minY + math.floor(height / 2),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -205,7 +259,7 @@ local function sym_frame_count(imagesOff)
|
||||
end
|
||||
|
||||
--- Decode one 4bpp sprite frame (tile order: L→R, T→B 8×8) → indexed [w*h].
|
||||
local function decode_frame_4bpp(raw, width, height)
|
||||
local function decode_frame_4bpp(raw, width, height, layout)
|
||||
local pixels = {}
|
||||
local tilesX = math.floor(width / 8)
|
||||
local tilesY = math.floor(height / 8)
|
||||
@@ -221,18 +275,24 @@ local function decode_frame_4bpp(raw, width, height)
|
||||
if high then return math.floor(b / 16) % 16 end
|
||||
return b % 16
|
||||
end
|
||||
local function tileAt(tx, ty)
|
||||
if not layout then return ty * tilesX + tx end
|
||||
local px, py = tx * 8, ty * 8
|
||||
for _, sub in ipairs(layout.subs) do
|
||||
if px >= sub.x and px < sub.x + sub.w and py >= sub.y and py < sub.y + sub.h then
|
||||
return sub.tile + ((py - sub.y) / 8) * (sub.w / 8) + (px - sub.x) / 8
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
for ty = 0, tilesY - 1 do
|
||||
for tx = 0, tilesX - 1 do
|
||||
local tileIndex = ty * tilesX + tx
|
||||
if width == 128 and height == 64 then
|
||||
tileIndex = math.floor(ty / 4) * 64 + math.floor(tx / 8) * 32
|
||||
+ (ty % 4) * 8 + tx % 8
|
||||
end
|
||||
local tileOff = tileIndex * 32
|
||||
local tileIndex = tileAt(tx, ty)
|
||||
local tileOff = (tileIndex or 0) * 32
|
||||
for y = 0, 7 do
|
||||
for x = 0, 7 do
|
||||
local byteIndex = tileOff + y * 4 + math.floor(x / 2)
|
||||
local idx = nybble(byteIndex, x % 2 == 1)
|
||||
local idx = tileIndex and nybble(byteIndex, x % 2 == 1) or 0
|
||||
local px = tx * 8 + x
|
||||
local py = ty * 8 + y
|
||||
pixels[py * width + px + 1] = idx
|
||||
@@ -305,6 +365,7 @@ function OwExtract.extractOne(rom, graphicsId, palsByTag, version, reflectionMap
|
||||
|
||||
local imagesOff = gba_off(info.imagesPtr)
|
||||
if not imagesOff then return nil, "bad images ptr" end
|
||||
local layout = read_subsprite_layout(rom, info.subspriteTablesPtr, w, h)
|
||||
|
||||
local expected = math.floor(w * h / 2)
|
||||
local frames = {}
|
||||
@@ -320,7 +381,7 @@ function OwExtract.extractOne(rom, graphicsId, palsByTag, version, reflectionMap
|
||||
frames[i + 1] = {}
|
||||
for p = 1, w * h do frames[i + 1][p] = 0 end
|
||||
else
|
||||
frames[i + 1] = decode_frame_4bpp(bytes, w, h)
|
||||
frames[i + 1] = decode_frame_4bpp(bytes, w, h, layout)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -347,6 +408,8 @@ function OwExtract.extractOne(rom, graphicsId, palsByTag, version, reflectionMap
|
||||
reflectionPalette = rawReflectPal,
|
||||
mappedReflectionPalette = reflectPal,
|
||||
inanimate = info.inanimate,
|
||||
drawOffX = layout and layout.offX or 0,
|
||||
drawOffY = layout and layout.offY or 0,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -398,6 +461,8 @@ function OwExtract.encodeMeta(sprite)
|
||||
for c = 0, 15 do out[#out + 1] = u16le(sprite.palette and sprite.palette[c] or 0) end
|
||||
for c = 0, 15 do out[#out + 1] = u16le(sprite.reflectionPalette and sprite.reflectionPalette[c] or 0) end
|
||||
for c = 0, 15 do out[#out + 1] = u16le(sprite.mappedReflectionPalette and sprite.mappedReflectionPalette[c] or 0) end
|
||||
out[#out + 1] = u8(sprite.drawOffX or 0)
|
||||
out[#out + 1] = u8(sprite.drawOffY or 0)
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
@@ -431,6 +496,11 @@ function OwExtract.decodeMeta(blob)
|
||||
if meta.reflectionPaletteTag == PALETTE_TAG_NONE then meta.reflectionPaletteTag = nil end
|
||||
if meta.reflectionPaletteMappedTag == PALETTE_TAG_NONE then meta.reflectionPaletteMappedTag = nil end
|
||||
end
|
||||
meta.drawOffX, meta.drawOffY = 0, 0
|
||||
if meta.formatVersion >= 4 and #blob >= 120 then
|
||||
local function s8(v) return v >= 128 and v - 256 or v end
|
||||
meta.drawOffX, meta.drawOffY = s8(blob:byte(119)), s8(blob:byte(120))
|
||||
end
|
||||
return meta
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_cinnabar_seagallop_arrival"
|
||||
|
||||
-- pokefirered/include/constants/vars.h:165
|
||||
local VAR_MAP_SCENE_CINNABAR_ISLAND = 0x4071
|
||||
-- pokefirered/include/constants/flags.h:114
|
||||
local FLAG_HIDE_CINNABAR_BILL = 0x062
|
||||
-- pokefirered/include/constants/event_objects.h:114
|
||||
local OBJ_EVENT_GFX_SEAGALLOP = 108
|
||||
local CINNABAR = "FR_CINNABAR_ISLAND"
|
||||
local SEAGALLOP_LOCALID = 4
|
||||
|
||||
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()
|
||||
print(failures == 0 and "PASS cinnabar_seagallop_arrival"
|
||||
or ("FAIL cinnabar_seagallop_arrival failures=" .. failures))
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
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")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local OwSprites = require("src.core.game3.ow_sprites")
|
||||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
|
||||
if not result(Runtime.getSession() ~= nil, "new game reached the game3 field") then return finish() end
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
|
||||
local function goTo(x, y)
|
||||
Map.load(nil, game, CINNABAR, { x = x, y = y, facing = "down" })
|
||||
Player.cellX, Player.cellY = x, y
|
||||
Player.px, Player.py = x * 16, y * 16
|
||||
Player.targetX, Player.targetY = x, y
|
||||
Player.facing = "down"
|
||||
U.wait(90)
|
||||
end
|
||||
|
||||
goTo(20, 5)
|
||||
-- pokefirered/data/maps/CinnabarIsland_Gym/scripts.inc:61
|
||||
Flags.setVar(Space.store, ctx(), VAR_MAP_SCENE_CINNABAR_ISLAND, 1)
|
||||
Flags.setFlag(Space.store, ctx(), FLAG_HIDE_CINNABAR_BILL, false)
|
||||
goTo(20, 5)
|
||||
|
||||
result(OwSprites.ready(), "FRLG OW sprites are ready")
|
||||
result(GfxIds.TO_SPRITE[OBJ_EVENT_GFX_SEAGALLOP] == nil, "no host sprite fallback for the Seagallop")
|
||||
|
||||
local shots, boatSeen, arrived, arrivedMsg = 0, false, false, false
|
||||
local lastX
|
||||
for _ = 1, 2400 do
|
||||
local boat = Objects.find(SEAGALLOP_LOCALID)
|
||||
local visible = boat and boat.visible and not boat.hidden and not boat.invisible
|
||||
if visible and not boatSeen then
|
||||
boatSeen = true
|
||||
local spr = OwSprites.getDraw(boat.graphicsId or (boat.def and boat.def.graphicsId))
|
||||
print(("[driver] boat added gid=%s cell=(%s,%s) spr=%sx%s frames=%s slot=%s"):format(
|
||||
tostring(boat.graphicsId or (boat.def and boat.def.graphicsId)), tostring(boat.cellX), tostring(boat.cellY),
|
||||
tostring(spr and spr.width), tostring(spr and spr.height), tostring(spr and spr.frameCount),
|
||||
tostring(spr and spr.paletteSlot)))
|
||||
result(spr and spr.width == 64 and spr.height == 64, "Seagallop draws the 64x64 ROM sheet")
|
||||
end
|
||||
if boatSeen and not arrived and boat and boat.px ~= lastX then
|
||||
lastX = boat.px
|
||||
if shots < 12 and (boat.px % 8 == 0) then
|
||||
shots = shots + 1
|
||||
U.shot(game, ("%s/arrive_%02d_px%d.png"):format(DIR, shots, boat.px))
|
||||
end
|
||||
end
|
||||
if boatSeen and not arrived and boat and boat.cellX == 25 and not boat.moving then
|
||||
arrived = true
|
||||
U.wait(2)
|
||||
U.shot(game, DIR .. "/arrive_docked.png")
|
||||
print(("[driver] boat docked cell=(%d,%d) px=(%s,%s)"):format(boat.cellX, boat.cellY,
|
||||
tostring(boat.px), tostring(boat.py)))
|
||||
end
|
||||
if arrived and Message.isOpen and Message.isOpen() and not arrivedMsg then
|
||||
arrivedMsg = true
|
||||
U.wait(30)
|
||||
U.shot(game, DIR .. "/arrive_message.png")
|
||||
break
|
||||
end
|
||||
if not boatSeen and Message.isOpen and Message.isOpen() then
|
||||
U.tap(game, "a")
|
||||
U.wait(8)
|
||||
else
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
result(boatSeen, "the Seagallop was added by the Bill scene")
|
||||
result(arrived, "the Seagallop docked at x=25")
|
||||
result(arrivedMsg, "Bill's boat-arrived message opened")
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_ss_anne_exterior_departure"
|
||||
|
||||
-- pokefirered/include/constants/vars.h:178
|
||||
local VAR_MAP_SCENE_VERMILION_CITY = 0x407E
|
||||
-- pokefirered/include/constants/event_objects.h:157
|
||||
local OBJ_EVENT_GFX_SS_ANNE = 151
|
||||
local EXTERIOR = "FR_SSANNE_EXTERIOR"
|
||||
|
||||
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()
|
||||
print(failures == 0 and "PASS ss_anne_exterior_departure"
|
||||
or ("FAIL ss_anne_exterior_departure failures=" .. failures))
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
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 Objects = require("src.core.game3.objects")
|
||||
local OwSprites = require("src.core.game3.ow_sprites")
|
||||
local Cutscene = require("src.core.game3.ss_anne_cutscene")
|
||||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
|
||||
if not result(Runtime.getSession() ~= nil, "new game reached the game3 field") then return finish() end
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
|
||||
local function goTo(x, y)
|
||||
Map.load(nil, game, EXTERIOR, { x = x, y = y, facing = "down" })
|
||||
Player.cellX, Player.cellY = x, y
|
||||
Player.px, Player.py = x * 16, y * 16
|
||||
Player.targetX, Player.targetY = x, y
|
||||
Player.facing = "down"
|
||||
end
|
||||
|
||||
goTo(32, 14)
|
||||
U.wait(30)
|
||||
-- pokefirered/data/maps/SSAnne_Exterior/scripts.inc:11
|
||||
Flags.setVar(Space.store, ctx(), VAR_MAP_SCENE_VERMILION_CITY, 1)
|
||||
goTo(32, 14)
|
||||
|
||||
result(GfxIds.TO_SPRITE[OBJ_EVENT_GFX_SS_ANNE] == nil, "no host sprite fallback for the SS Anne")
|
||||
local ship = Objects.find(1)
|
||||
local gid = ship and (ship.graphicsId or (ship.def and ship.def.graphicsId))
|
||||
local spr = OwSprites.getDraw(gid)
|
||||
print(("[driver] ship gid=%s cell=(%s,%s) spr=%sx%s slot=%s"):format(tostring(gid),
|
||||
tostring(ship and ship.cellX), tostring(ship and ship.cellY), tostring(spr and spr.width),
|
||||
tostring(spr and spr.height), tostring(spr and spr.paletteSlot)))
|
||||
result(gid == OBJ_EVENT_GFX_SS_ANNE, "SS Anne object uses OBJ_EVENT_GFX_SS_ANNE")
|
||||
result(spr and spr.width == 128 and spr.height == 64, "SS Anne draws the 128x64 ROM sheet")
|
||||
|
||||
local started = false
|
||||
for _ = 1, 600 do
|
||||
if Cutscene.isActive() then started = true break end
|
||||
U.wait(1)
|
||||
end
|
||||
result(started, "the departure cutscene started")
|
||||
U.shot(game, DIR .. "/ssanne_00_start.png")
|
||||
local shots = 0
|
||||
for _ = 1, 1800 do
|
||||
if not Cutscene.isActive() then break end
|
||||
local off = Cutscene._boatOffset or 0
|
||||
if off >= (shots + 1) * 40 and shots < 5 then
|
||||
shots = shots + 1
|
||||
U.shot(game, ("%s/ssanne_%02d_off%d.png"):format(DIR, shots, off))
|
||||
else
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
result(not Cutscene.isActive(), "the departure cutscene finished")
|
||||
finish()
|
||||
end
|
||||
@@ -16,8 +16,8 @@ local function digest(list)
|
||||
return h
|
||||
end
|
||||
|
||||
eq(CacheContract.markerFor("emerald", SHA), "rom-cache-v3-emerald:" .. SHA, "emerald marker")
|
||||
check(CacheContract.markerMatches("emerald", "rom-cache-v3-emerald:" .. SHA), "emerald marker matches")
|
||||
eq(CacheContract.markerFor("emerald", SHA), "rom-cache-v4-emerald:" .. SHA, "emerald marker")
|
||||
check(CacheContract.markerMatches("emerald", "rom-cache-v4-emerald:" .. SHA), "emerald marker matches")
|
||||
check(not CacheContract.markerMatches("emerald", "rom-cache-v17-firered:" .. SHA), "a FireRed marker does not")
|
||||
|
||||
eq(#CacheContract.requiredFiles("firered"), 456, "FireRed required list size")
|
||||
@@ -83,7 +83,7 @@ check(not CacheContract.cacheVersionCurrent("emerald", stale), "an emerald meta
|
||||
|
||||
local complete = {}
|
||||
for _, p in ipairs(em) do complete["emerald/" .. p] = "x" end
|
||||
complete["emerald/" .. CacheContract.MARKER_PATH] = "rom-cache-v3-emerald:" .. SHA
|
||||
complete["emerald/" .. CacheContract.MARKER_PATH] = "rom-cache-v4-emerald:" .. SHA
|
||||
complete["emerald/data/generated/gba/meta.json"] = emMeta
|
||||
check(CacheContract.isReady("emerald", memfs(complete)), "a complete emerald cache is ready")
|
||||
|
||||
|
||||
@@ -86,10 +86,14 @@ local blob = OwExtract.encodeMeta({
|
||||
palette = source,
|
||||
reflectionPalette = rawReflection,
|
||||
mappedReflectionPalette = mappedReflection,
|
||||
drawOffX = 32,
|
||||
drawOffY = -4,
|
||||
})
|
||||
local meta = OwExtract.decodeMeta(blob)
|
||||
check(meta ~= nil, "reflection metadata decodes")
|
||||
eq(meta.formatVersion, 3, "new metadata format is explicit")
|
||||
eq(meta.formatVersion, OwExtract.FORMAT_VERSION, "new metadata format is explicit")
|
||||
eq(meta.drawOffX, 32, "subsprite draw x offset is retained")
|
||||
eq(meta.drawOffY, -4, "negative subsprite draw y offset is retained")
|
||||
eq(meta.paletteTag, 0x1100, "base palette tag is retained")
|
||||
eq(meta.reflectionPaletteTag, 0x1102, "ROM reflection palette tag is retained")
|
||||
eq(meta.paletteSlot, 0, "ROM palette slot is retained")
|
||||
@@ -109,4 +113,5 @@ check(unavailable ~= nil, "metadata without a mapped reflection palette decodes"
|
||||
eq(unavailable.reflectionPaletteMappedTag, nil, "missing mapped palette stays unavailable")
|
||||
eq(unavailable.reflectionPalette, nil, "missing palette colors are not synthesized")
|
||||
eq(unavailable.mappedReflectionPalette, nil, "missing mapped colors are not synthesized")
|
||||
eq(unavailable.drawOffX, 0, "missing draw offset is centered")
|
||||
T.finish("game3_ow_reflection_meta_test")
|
||||
|
||||
@@ -16,9 +16,47 @@ local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
|
||||
print("[test] 1. GfxIds maps OBJ_EVENT_GFX_SEAGALLOP (108)")
|
||||
local sprName = GfxIds.spriteFor(108)
|
||||
check(sprName == "SPRITE_SAILOR", "108 maps to SPRITE_SAILOR fallback (got " .. tostring(sprName) .. ")")
|
||||
print("[test] 1. OBJ_EVENT_GFX_SEAGALLOP (108) / OBJ_EVENT_GFX_SS_ANNE (151) come from the ROM")
|
||||
check(GfxIds.TO_SPRITE[108] == nil, "108 has no host sprite fallback")
|
||||
check(GfxIds.TO_SPRITE[109] == nil, "109 has no host sprite fallback")
|
||||
check(GfxIds.TO_SPRITE[151] == nil, "151 has no host sprite fallback")
|
||||
|
||||
local FR_ROOT = os.getenv("POKEFIRERED") or "../pokefirered"
|
||||
local f = io.open(FR_ROOT .. "/pokefirered.gba", "rb")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
local GV = require("src.core.GameVersion")
|
||||
local Rom = require("src.import.gba.rom")
|
||||
local OwExtract = require("src.import.gba.ow_extract")
|
||||
local sha = GV.VERSIONS.firered.sha1
|
||||
local rom = assert(Rom.open({
|
||||
info = function() return { size = #data, md5 = sha } end,
|
||||
read = function(_, _, off, len) return data:sub(off + 1, off + len) end,
|
||||
}, GV.forSha1(sha)))
|
||||
local pals = OwExtract.loadPaletteTable(rom, {})
|
||||
-- src/data/object_events/object_event_graphics_info.h:2889
|
||||
local boat = assert(OwExtract.extractOne(rom, 108, pals, {}))
|
||||
check(boat.width == 64 and boat.height == 64, "Seagallop is a 64x64 sheet")
|
||||
check(boat.paletteTag == 0x1114 and boat.paletteSlot == 10, "Seagallop uses OBJ_EVENT_PAL_TAG_SEAGALLOP in PALSLOT_NPC_SPECIAL")
|
||||
check(boat.drawOffX == 0 and boat.drawOffY == 0, "Seagallop 64x64 subsprite is centered")
|
||||
check(pals[0x1114] ~= nil, "Seagallop palette loaded from sObjectEventSpritePalettes")
|
||||
local ext = OwExtract.decodeMeta(OwExtract.encodeMeta(boat))
|
||||
check(ext.width == 64 and ext.drawOffX == 0, "Seagallop meta round-trips")
|
||||
-- src/data/object_events/object_event_graphics_info.h:2908
|
||||
local anne = assert(OwExtract.extractOne(rom, 151, pals, {}))
|
||||
check(anne.width == 128 and anne.height == 64, "SS Anne is a 128x64 sheet")
|
||||
check(anne.paletteTag == 0x1115 and anne.paletteSlot == 10, "SS Anne uses OBJ_EVENT_PAL_TAG_SS_ANNE in PALSLOT_NPC_SPECIAL")
|
||||
-- src/data/object_events/object_event_subsprites.h:999
|
||||
check(anne.drawOffX == 32 and anne.drawOffY == 16, "SS Anne draws +32,+16 off center (got "
|
||||
.. tostring(anne.drawOffX) .. "," .. tostring(anne.drawOffY) .. ")")
|
||||
local meta = OwExtract.decodeMeta(OwExtract.encodeMeta(anne))
|
||||
check(meta.drawOffX == 32 and meta.drawOffY == 16, "SS Anne draw offset round-trips through the meta")
|
||||
local player = assert(OwExtract.extractOne(rom, 0, pals, {}))
|
||||
check(player.drawOffX == 0 and player.drawOffY == 0, "16x32 player subsprite stays centered")
|
||||
else
|
||||
print("[skip] pokefirered.gba not found, ROM sprite checks skipped")
|
||||
end
|
||||
|
||||
print("[test] 2. offMap bounds checking")
|
||||
local bounds = { w = 24, h = 20 }
|
||||
|
||||
@@ -8,6 +8,7 @@ local Std = require("src.core.game3.scripting.stdscripts")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local SSAnneCutscene = require("src.core.game3.ss_anne_cutscene")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local Player = require("src.core.game3.player")
|
||||
|
||||
local playedSEs = {}
|
||||
local Audio = require("src.core.game3.audio")
|
||||
@@ -50,6 +51,7 @@ local mockBoat = {
|
||||
}
|
||||
Objects._byId[1] = mockBoat
|
||||
Objects._order = { 1 }
|
||||
Player.cellX, Player.cellY = 32, 14
|
||||
|
||||
local task = SSAnneCutscene.start()
|
||||
assert_true(SSAnneCutscene.isActive(), "cutscene is active")
|
||||
@@ -89,16 +91,16 @@ assert_eq(mockBoat.raiseX, -14, "boat object raiseX is -14")
|
||||
-- Verify wake animation
|
||||
assert_true(SSAnneCutscene._wake.x2 > 0, "wake drifting right relative to boat")
|
||||
|
||||
-- Advance to near exit (travel distance 216 px = 216 * 5 = 1080 frames)
|
||||
local prevSmokesCount = #SSAnneCutscene._smokes
|
||||
for f = 71, 1079 do
|
||||
-- pokefirered/src/ss_anne.c:119
|
||||
for f = 71, 1045 do
|
||||
task()
|
||||
end
|
||||
assert_eq(SSAnneCutscene._boatOffset, 209, "boat offset is 209 px at frame 1045")
|
||||
|
||||
assert_eq(SSAnneCutscene._phase, "run", "still run phase before distance reached")
|
||||
assert_true(#playedSEs == 1, "only 1 horn so far")
|
||||
|
||||
-- Frame 1080 reaches 216 px offset and triggers exit horn
|
||||
-- pokefirered/src/ss_anne.c:119
|
||||
task()
|
||||
assert_eq(SSAnneCutscene._phase, "finish", "switched to finish phase")
|
||||
assert_eq(#playedSEs, 2, "second horn played on departure")
|
||||
|
||||
Reference in New Issue
Block a user