mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-05 08:57:49 -04:00
+4
-1
@@ -823,7 +823,10 @@ function Game:_cycleSpeed(dir)
|
||||
if busy then return end
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local key = GameSpeed.optionKey(Game.speedCategoryInStack(self.stack))
|
||||
self.save.options[key] = GameSpeed.cycle(self.save.options[key], dir)
|
||||
local nextSpeed = GameSpeed.cycle(self.save.options[key], dir)
|
||||
for _, c in ipairs(GameSpeed.CATEGORIES) do
|
||||
self.save.options[GameSpeed.optionKey(c)] = nextSpeed
|
||||
end
|
||||
self:writeOptions()
|
||||
end
|
||||
|
||||
|
||||
+4
-1
@@ -677,7 +677,10 @@ function Game3:_cycleSpeed(dir)
|
||||
if self:speedLocked() then return end
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local key = GameSpeed.optionKey(self:speedCategory())
|
||||
self.options[key] = GameSpeed.cycle(self.options[key], dir)
|
||||
local nextSpeed = GameSpeed.cycle(self.options[key], dir)
|
||||
for _, c in ipairs(GameSpeed.CATEGORIES) do
|
||||
self.options[GameSpeed.optionKey(c)] = nextSpeed
|
||||
end
|
||||
self:writeOptions()
|
||||
end
|
||||
|
||||
|
||||
@@ -93,6 +93,17 @@ local function stop_low_hp_song()
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_low_hp(on)
|
||||
if on and not Battle._lowHpSong then
|
||||
Battle._lowHpSong = true
|
||||
if Audio and Audio.playSe and SE then
|
||||
Audio.playSe(SE.SE_LOW_HEALTH, { loop = true })
|
||||
end
|
||||
elseif not on then
|
||||
stop_low_hp_song()
|
||||
end
|
||||
end
|
||||
|
||||
--- pret HandleLowHpMusicChange / HandleBattleLowHpMusicChange
|
||||
local function update_low_hp_music()
|
||||
local st = Battle._st
|
||||
@@ -106,16 +117,17 @@ local function update_low_hp_music()
|
||||
local hp = math.floor(tonumber(pres and pres.displayHp) or tonumber(mon.hp) or 0)
|
||||
local maxHp = math.floor(tonumber(pres and pres.displayMaxHp) or tonumber(mon.maxHp) or 0)
|
||||
local red = hp_bar_red(hp, maxHp)
|
||||
if red and not Battle._lowHpSong then
|
||||
Battle._lowHpSong = true
|
||||
if Audio and Audio.playSe and SE then
|
||||
Audio.playSe(SE.SE_LOW_HEALTH, { loop = true })
|
||||
end
|
||||
elseif not red then
|
||||
stop_low_hp_song()
|
||||
if ModRuntime.wantsHook("battle.low_health_alarm") then
|
||||
ModRuntime.call("battle.low_health_alarm", function(ctx)
|
||||
apply_low_hp(ctx.on)
|
||||
end, { on = red, battle = st })
|
||||
else
|
||||
apply_low_hp(red)
|
||||
end
|
||||
end
|
||||
|
||||
Battle._updateLowHpMusic = update_low_hp_music
|
||||
|
||||
local function party_menu_input(PartyMenu, input)
|
||||
local SummaryMenu = package.loaded["src.ui.game3.summary_menu"]
|
||||
if SummaryMenu and SummaryMenu.isOpen and SummaryMenu.isOpen() then
|
||||
|
||||
@@ -250,6 +250,18 @@ function Collision.behavior(cx,cy)
|
||||
return Collision.behaviorOn(Collision._mapDef, cx, cy)
|
||||
end
|
||||
|
||||
local worldMap
|
||||
|
||||
-- pokefirered/src/fieldmap.c:129
|
||||
function Collision.worldBehavior(cx, cy)
|
||||
local def = Collision._mapDef
|
||||
if not (def and def.midLayout) then return nil end
|
||||
worldMap = worldMap or require("src.core.game3.map")
|
||||
local mid, pair = worldMap.worldMidAt(cx, cy, def)
|
||||
local behaviors = InteractionScripts.behaviors[pair]
|
||||
return behaviors and behaviors[mid]
|
||||
end
|
||||
|
||||
-- pokefirered/include/constants/metatile_behaviors.h:39
|
||||
local MB_IMPASSABLE_EAST = 0x30
|
||||
local MB_IMPASSABLE_WEST = 0x31
|
||||
|
||||
@@ -46,6 +46,7 @@ local FLY_BIRD_W, FLY_BIRD_H, FLY_BIRD_FRAMES = 64, 64, 5
|
||||
local frlgReflective = nil
|
||||
local frlgReflectionQuad = nil
|
||||
local frlgModules = nil
|
||||
local FRLG_REFL_CULL = 64
|
||||
local objectPoseOpts = {}
|
||||
local playerPoseOpts = {}
|
||||
local function isFrlgReflective(behavior)
|
||||
@@ -99,6 +100,7 @@ local function drawFrlgReflections(camX, camY)
|
||||
if not modules then return end
|
||||
local Collision, Ow = modules.Collision, modules.Ow
|
||||
local Objects, P, Runtime = modules.Objects, modules.Player, modules.Runtime
|
||||
local behaviorAt = Collision.worldBehavior or Collision.behavior
|
||||
local drawn = 0
|
||||
if Objects.forDraw then
|
||||
for _, obj in ipairs(Objects.forDraw()) do
|
||||
@@ -109,7 +111,45 @@ local function drawFrlgReflections(camX, camY)
|
||||
local frame, flip = Ow.pose(spr, obj.facing, Objects.walkPhase(obj), obj.stepFlip,
|
||||
objectPoseOpts)
|
||||
if drawFrlgReflection(obj, obj.graphicsId, frame, flip, camX, camY,
|
||||
Collision.behavior, Ow) then drawn = drawn + 1 end
|
||||
behaviorAt, Ow) then drawn = drawn + 1 end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
local Ghosts = package.loaded["src.core.game3.ghosts"]
|
||||
local world = Map and Map.world
|
||||
if Ghosts and Ghosts.forDraw and type(world) == "table" then
|
||||
local host = Collision._mapDef
|
||||
local FV = package.loaded["src.core.game3.field_view"]
|
||||
local Display = require("src.core.game3.display")
|
||||
local vw = FV and FV._viewW or Display.W
|
||||
local vh = FV and FV._viewH or Display.H
|
||||
local x0, y0 = camX - FRLG_REFL_CULL, camY - FRLG_REFL_CULL
|
||||
local x1, y1 = camX + vw + FRLG_REFL_CULL, camY + vh + FRLG_REFL_CULL
|
||||
for i = 1, #world do
|
||||
local entry = world[i]
|
||||
local ox, oy = entry.ox or 0, entry.oy or 0
|
||||
local L = entry.def and entry.def.midLayout
|
||||
local ex, ey = ox * CELL, oy * CELL
|
||||
if entry.def ~= host and L
|
||||
and ex + (L.width or 0) * CELL > x0 and ex < x1
|
||||
and ey + (L.height or 0) * CELL > y0 and ey < y1 then
|
||||
local live = Ghosts.forDraw(entry.id)
|
||||
if live then
|
||||
for j = 1, #live do
|
||||
local obj = live[j]
|
||||
if not obj.hideReflection and obj.graphicsId and not obj.virtualId then
|
||||
local spr = Ow.getDraw(obj.graphicsId)
|
||||
if spr then
|
||||
objectPoseOpts.frame = obj.customFrame
|
||||
local frame, flip = Ow.pose(spr, obj.facing, Objects.walkPhase(obj), obj.stepFlip,
|
||||
objectPoseOpts)
|
||||
if drawFrlgReflection(obj, obj.graphicsId, frame, flip, camX, camY,
|
||||
behaviorAt, Ow, ox, oy) then drawn = drawn + 1 end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -122,22 +162,23 @@ local function drawFrlgReflections(camX, camY)
|
||||
local frame, flip = Ow.pose(spr, P.facing, P.walkPhase and P.walkPhase() or 0,
|
||||
P.drawFlip and P.drawFlip() or false, playerPoseOpts)
|
||||
if drawFrlgReflection(P, gid, frame, flip, camX, camY,
|
||||
Collision.behavior, Ow) then drawn = drawn + 1 end
|
||||
behaviorAt, Ow) then drawn = drawn + 1 end
|
||||
end
|
||||
end
|
||||
FieldEffects.lastFrlgReflections = drawn
|
||||
end
|
||||
|
||||
drawFrlgReflection = function(obj, graphicsId, frame, hflip, camX, camY, behaviorAt, Ow)
|
||||
drawFrlgReflection = function(obj, graphicsId, frame, hflip, camX, camY, behaviorAt, Ow, ox, oy)
|
||||
local spr = Ow.getReflectionDraw and Ow.getReflectionDraw(graphicsId)
|
||||
if not (spr and spr.quads and spr.quads[frame]) then return false end
|
||||
local cx = obj.moving and obj.targetX or obj.cellX
|
||||
local cy = obj.moving and obj.targetY or obj.cellY
|
||||
local pcx, pcy = obj.cellX, obj.cellY
|
||||
ox, oy = ox or 0, oy or 0
|
||||
local cx = (obj.moving and obj.targetX or obj.cellX) + ox
|
||||
local cy = (obj.moving and obj.targetY or obj.cellY) + oy
|
||||
local pcx, pcy = obj.cellX + ox, obj.cellY + oy
|
||||
if not frlgReflectionType(cx, cy, pcx, pcy, spr.width, spr.height, behaviorAt) then return false end
|
||||
local w, h = spr.width, spr.height
|
||||
local left = (obj.px or cx * CELL) + (16 - w) / 2
|
||||
local top = (obj.py or cy * CELL) + 14
|
||||
local left = (obj.px or (cx - ox) * CELL) + ox * CELL + (16 - w) / 2
|
||||
local top = (obj.py or (cy - oy) * CELL) + oy * CELL + 14
|
||||
local x0, x1 = math.floor(left / CELL), math.floor((left + w - 1) / CELL)
|
||||
local y0, y1 = math.floor(top / CELL), math.floor((top + h - 1) / CELL)
|
||||
local q = frlgReflectionQuad or love.graphics.newQuad(0, 0, 1, 1, spr.width, spr.height * spr.frameCount)
|
||||
|
||||
@@ -144,6 +144,12 @@ local function behaviorAt(x, y)
|
||||
return C.behavior(x, y)
|
||||
end
|
||||
|
||||
local function worldBehaviorAt(x, y)
|
||||
local C = Collision()
|
||||
if not (C and C.worldBehavior) or x == nil or y == nil then return nil end
|
||||
return C.worldBehavior(x, y)
|
||||
end
|
||||
|
||||
|
||||
function FxRse.animCmds(name, idx)
|
||||
local o = FE().manifestObject(name)
|
||||
@@ -574,7 +580,7 @@ end
|
||||
|
||||
-- pokeemerald/src/event_object_movement.c:7625
|
||||
local function reflectionAt(x, y)
|
||||
local b = behaviorAt(x, y)
|
||||
local b = worldBehaviorAt(x, y)
|
||||
if b == nil then return 0 end
|
||||
if B.ice(b) then return 1 end
|
||||
if B.reflective(b) then return 2 end
|
||||
@@ -645,19 +651,23 @@ end
|
||||
|
||||
local reflQuad
|
||||
local coverCells, coverN = {}, 0
|
||||
local coverBuckets, coverPairs = {}, {}
|
||||
local REFL_CULL = 64
|
||||
local ghostPose = {}
|
||||
|
||||
local function drawReflection(obj, gid, frame, hflip, x2, y2, camX, camY)
|
||||
local function drawReflection(obj, gid, frame, hflip, x2, y2, camX, camY, ox, oy)
|
||||
local Ow = package.loaded["src.core.game3.ow_sprites"]
|
||||
local spr = Ow and Ow.getDraw and Ow.getDraw(gid)
|
||||
if not (spr and spr.quads and spr.quads[frame]) then return false end
|
||||
local cx = obj.moving and obj.targetX or obj.cellX
|
||||
local cy = obj.moving and obj.targetY or obj.cellY
|
||||
local pcx, pcy = obj.cellX, obj.cellY
|
||||
if obj == Player() then pcx, pcy = obj.prevCellX or cx, obj.prevCellY or cy end
|
||||
ox, oy = ox or 0, oy or 0
|
||||
local cx = (obj.moving and obj.targetX or obj.cellX) + ox
|
||||
local cy = (obj.moving and obj.targetY or obj.cellY) + oy
|
||||
local pcx, pcy = obj.cellX + ox, obj.cellY + oy
|
||||
if obj == Player() then pcx, pcy = (obj.prevCellX or cx - ox) + ox, (obj.prevCellY or cy - oy) + oy end
|
||||
local rtype = FxRse.reflectionType(cx, cy, pcx, pcy, spr.width, spr.height)
|
||||
if rtype == 0 then return false end
|
||||
local bridge = B.bridgeType(behaviorAt(pcx, pcy))
|
||||
if bridge == 0 then bridge = B.bridgeType(behaviorAt(cx, cy)) end
|
||||
local bridge = B.bridgeType(worldBehaviorAt(pcx, pcy))
|
||||
if bridge == 0 then bridge = B.bridgeType(worldBehaviorAt(cx, cy)) end
|
||||
local info = fcGfx(gid)
|
||||
local tag = FxRse.reflectionPaletteTag(gid, bridge)
|
||||
local src = (tag and recolored(gid, spr, tag)) or spr
|
||||
@@ -667,8 +677,8 @@ local function drawReflection(obj, gid, frame, hflip, x2, y2, camX, camY)
|
||||
local offs = { [1] = 12, [2] = 28, [3] = 44 }
|
||||
local extra = (not (info and info.noReflectionLoad)) and offs[bridge] or 0
|
||||
local w, h = spr.width, spr.height
|
||||
local left = (obj.px or cx * CELL) + (16 - w) / 2 + (x2 or 0)
|
||||
local top = (obj.py or cy * CELL) + 16 - h + (h - 2) + extra - (y2 or 0)
|
||||
local left = (obj.px or (cx - ox) * CELL) + ox * CELL + (16 - w) / 2 + (x2 or 0)
|
||||
local top = (obj.py or (cy - oy) * CELL) + oy * CELL + 16 - h + (h - 2) + extra - (y2 or 0)
|
||||
local iw, ih = src.image:getDimensions()
|
||||
reflQuad = reflQuad or love.graphics.newQuad(0, 0, 1, 1, iw, ih)
|
||||
local fy = frame * h
|
||||
@@ -677,7 +687,7 @@ local function drawReflection(obj, gid, frame, hflip, x2, y2, camX, camY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for ty = c0y, c1y do
|
||||
for tx = c0x, c1x do
|
||||
local b = behaviorAt(tx, ty)
|
||||
local b = worldBehaviorAt(tx, ty)
|
||||
if b and B.reflective(b) then
|
||||
local ix0, ix1 = math.max(left, tx * CELL), math.min(left + w, tx * CELL + CELL)
|
||||
local iy0, iy1 = math.max(top, ty * CELL), math.min(top + h, ty * CELL + CELL)
|
||||
@@ -723,17 +733,45 @@ local function coverReflections(camX, camY)
|
||||
local mapDef = C and C._mapDef
|
||||
local layout = mapDef and mapDef.midLayout
|
||||
local NT = package.loaded["src.core.game3.tileset_native"]
|
||||
if not (layout and NT) then return end
|
||||
local ts = NT.get(mapDef.pair or layout.pair)
|
||||
local shader = ts and ts.midImage and getCoverShader()
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
if not (layout and NT and Map) then return end
|
||||
local shader = getCoverShader()
|
||||
if not shader then return end
|
||||
shader:send("mask", ts.midImage)
|
||||
love.graphics.setShader(shader)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
local loaded = NT._pairs or {}
|
||||
local np = 0
|
||||
for i = 1, n, 2 do
|
||||
local tx, ty = coverCells[i], coverCells[i + 1]
|
||||
local q = NT.quad(ts, NT.slotFor(ts, layout:midAt(tx, ty)))
|
||||
if q then love.graphics.draw(ts.image, q, tx * CELL - camX, ty * CELL - camY) end
|
||||
local mid, pair = Map.worldMidAt(tx, ty, mapDef)
|
||||
local ts = pair and loaded[pair]
|
||||
if ts and ts.midImage then
|
||||
local bucket = coverBuckets[pair]
|
||||
if not bucket then
|
||||
bucket = { n = 0 }
|
||||
coverBuckets[pair] = bucket
|
||||
end
|
||||
if bucket.n == 0 then
|
||||
np = np + 1
|
||||
coverPairs[np] = pair
|
||||
end
|
||||
local b = bucket.n
|
||||
bucket[b + 1], bucket[b + 2], bucket[b + 3] = tx, ty, mid
|
||||
bucket.n = b + 3
|
||||
end
|
||||
end
|
||||
for p = 1, np do
|
||||
local pair = coverPairs[p]
|
||||
local bucket = coverBuckets[pair]
|
||||
local ts = loaded[pair]
|
||||
shader:send("mask", ts.midImage)
|
||||
for i = 1, bucket.n, 3 do
|
||||
local tx, ty = bucket[i], bucket[i + 1]
|
||||
local q = NT.quad(ts, NT.slotFor(ts, bucket[i + 2]))
|
||||
if q then love.graphics.draw(ts.image, q, tx * CELL - camX, ty * CELL - camY) end
|
||||
end
|
||||
bucket.n = 0
|
||||
coverPairs[p] = nil
|
||||
end
|
||||
love.graphics.setShader()
|
||||
end
|
||||
@@ -755,6 +793,43 @@ local function drawReflections(camX, camY)
|
||||
end
|
||||
end
|
||||
end
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
local Ghosts = package.loaded["src.core.game3.ghosts"]
|
||||
local world = Map and Map.world
|
||||
if O and Ghosts and Ghosts.forDraw and type(world) == "table" then
|
||||
local C = Collision()
|
||||
local host = C and C._mapDef
|
||||
local FV = package.loaded["src.core.game3.field_view"]
|
||||
local Display = require("src.core.game3.display")
|
||||
local vw = FV and FV._viewW or Display.W
|
||||
local vh = FV and FV._viewH or Display.H
|
||||
local x0, y0 = camX - REFL_CULL, camY - REFL_CULL
|
||||
local x1, y1 = camX + vw + REFL_CULL, camY + vh + REFL_CULL
|
||||
for i = 1, #world do
|
||||
local entry = world[i]
|
||||
local ox, oy = entry.ox or 0, entry.oy or 0
|
||||
local L = entry.def and entry.def.midLayout
|
||||
local ex, ey = ox * CELL, oy * CELL
|
||||
if entry.def ~= host and L
|
||||
and ex + (L.width or 0) * CELL > x0 and ex < x1
|
||||
and ey + (L.height or 0) * CELL > y0 and ey < y1 then
|
||||
local live = Ghosts.forDraw(entry.id)
|
||||
if live then
|
||||
for j = 1, #live do
|
||||
local eo = live[j]
|
||||
if not eo.hideReflection and eo.graphicsId and not eo.virtualId then
|
||||
local spr = Ow.getDraw(eo.graphicsId)
|
||||
if spr then
|
||||
ghostPose.frame = eo.customFrame
|
||||
local frame, flip = Ow.pose(spr, eo.facing, O.walkPhase(eo), eo.stepFlip, ghostPose)
|
||||
drawReflection(eo, eo.graphicsId, frame, flip, eo.raiseX, eo.raiseY, camX, camY, ox, oy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local P = Player()
|
||||
if P and P.isVisible and P.isVisible() and not P.hideReflection then
|
||||
local rt = package.loaded["src.core.game3.runtime"]
|
||||
|
||||
+71
-23
@@ -149,26 +149,27 @@ function Map.refreshWorld(game, reachW, reachH, rootId)
|
||||
Map._loadedLayouts[entry.id] = true
|
||||
end
|
||||
local NativeTileset = package.loaded["src.core.game3.tileset_native"]
|
||||
if Map._warmPairs and NativeTileset and NativeTileset.get then
|
||||
if NativeTileset and NativeTileset.get then
|
||||
local sync = Map._warmPairs
|
||||
Map._warmPairs = nil
|
||||
Map._warmQueue = nil
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
local queue, seen, entries = {}, {}, {}
|
||||
for _, entry in ipairs(Map.world) do
|
||||
local pair = entry.def and (entry.def.pair or (entry.def.midLayout and entry.def.midLayout.pair))
|
||||
if pair and NativeTileset.ready(pair) then pcall(NativeTileset.get, pair) end
|
||||
end
|
||||
elseif NativeTileset and NativeTileset.get then
|
||||
-- Seamless crossing: queue the new world's unloaded pairs and load one per
|
||||
-- frame (Map.stepWarm) so a pair is ready before it scrolls into view.
|
||||
local queue, seen = {}, {}
|
||||
for _, entry in ipairs(Map.world) do
|
||||
local pair = entry.def and (entry.def.pair or (entry.def.midLayout and entry.def.midLayout.pair))
|
||||
if type(pair) == "string" and not seen[pair]
|
||||
and not (NativeTileset._pairs and NativeTileset._pairs[pair]) then
|
||||
seen[pair] = true
|
||||
queue[#queue + 1] = pair
|
||||
if sync and pair and Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
if NativeTileset.ready(pair) then pcall(NativeTileset.get, pair) end
|
||||
elseif type(pair) == "string" and not (NativeTileset._pairs and NativeTileset._pairs[pair]) then
|
||||
if not seen[pair] then
|
||||
seen[pair] = true
|
||||
queue[#queue + 1] = pair
|
||||
entries[pair] = {}
|
||||
end
|
||||
local list = entries[pair]
|
||||
list[#list + 1] = entry
|
||||
end
|
||||
end
|
||||
Map._warmQueue = queue[1] and queue or nil
|
||||
Map._warmEntries = queue[1] and entries or nil
|
||||
end
|
||||
Map._worldRoot = rootId
|
||||
Map._worldReachW = reachW
|
||||
@@ -178,21 +179,68 @@ function Map.refreshWorld(game, reachW, reachH, rootId)
|
||||
return Map.world
|
||||
end
|
||||
|
||||
--- Load at most one queued tileset pair (see refreshWorld).
|
||||
Map.WARM_MARGIN_SCREENS = 1
|
||||
Map.WARM_BUDGET_SEC = 0.030
|
||||
Map.WARM_MAX_DEFER = 3
|
||||
Map._warmDefer = 0
|
||||
|
||||
function Map.warmRect()
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local px, py = P and tonumber(P.cellX), P and tonumber(P.cellY)
|
||||
if not (px and py) then return nil end
|
||||
local FieldView = package.loaded["src.core.game3.field_view"]
|
||||
local Display = package.loaded["src.core.game3.display"]
|
||||
local vw = (FieldView and FieldView._viewW) or (Display and Display.W) or 240
|
||||
local vh = (FieldView and FieldView._viewH) or (Display and Display.H) or 160
|
||||
local cols, rows = math.ceil(vw / 16), math.ceil(vh / 16)
|
||||
local mx = math.ceil(cols / 2) + 1 + cols * Map.WARM_MARGIN_SCREENS
|
||||
local my = math.ceil(rows / 2) + 1 + rows * Map.WARM_MARGIN_SCREENS
|
||||
return px - mx, py - my, px + mx, py + my
|
||||
end
|
||||
|
||||
function Map.warmNear(entry, x0, y0, x1, y1)
|
||||
if not x0 then return true end
|
||||
local layout = entry.def and entry.def.midLayout
|
||||
local w, h = layout and layout.width, layout and layout.height
|
||||
if not (w and h) then return true end
|
||||
local ox, oy = entry.ox or 0, entry.oy or 0
|
||||
return ox + w > x0 and ox <= x1 and oy + h > y0 and oy <= y1
|
||||
end
|
||||
|
||||
function Map.stepWarm()
|
||||
local queue = Map._warmQueue
|
||||
if not queue then return false end
|
||||
local NativeTileset = package.loaded["src.core.game3.tileset_native"]
|
||||
while queue[1] do
|
||||
local pair = table.remove(queue, 1)
|
||||
if NativeTileset and not (NativeTileset._pairs and NativeTileset._pairs[pair])
|
||||
and NativeTileset.ready and NativeTileset.ready(pair) then
|
||||
pcall(NativeTileset.get, pair)
|
||||
if not queue[1] then Map._warmQueue = nil end
|
||||
return true
|
||||
local timer = love and love.timer
|
||||
if timer and timer.getDelta and timer.getDelta() > Map.WARM_BUDGET_SEC
|
||||
and Map._warmDefer < Map.WARM_MAX_DEFER then
|
||||
Map._warmDefer = Map._warmDefer + 1
|
||||
return false
|
||||
end
|
||||
Map._warmDefer = 0
|
||||
local entries = Map._warmEntries or {}
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
local i = 1
|
||||
while queue[i] do
|
||||
local pair = queue[i]
|
||||
if NativeTileset and (NativeTileset._pairs and NativeTileset._pairs[pair])
|
||||
or not (NativeTileset and NativeTileset.ready and NativeTileset.ready(pair)) then
|
||||
table.remove(queue, i)
|
||||
else
|
||||
local near = not entries[pair]
|
||||
for _, entry in ipairs(entries[pair] or {}) do
|
||||
if Map.warmNear(entry, x0, y0, x1, y1) then near = true break end
|
||||
end
|
||||
if near then
|
||||
table.remove(queue, i)
|
||||
pcall(NativeTileset.get, pair)
|
||||
if not queue[1] then Map._warmQueue = nil end
|
||||
return true
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
Map._warmQueue = nil
|
||||
if not queue[1] then Map._warmQueue = nil end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
@@ -21,6 +21,62 @@ function Palette.load(blob)
|
||||
return NativePack.palsToRgb8(bgr), bgr
|
||||
end
|
||||
|
||||
local bit = require("bit")
|
||||
local band, bor, bxor, bnot = bit.band, bit.bor, bit.bxor, bit.bnot
|
||||
local lshift, rshift, rol = bit.lshift, bit.rshift, bit.rol
|
||||
|
||||
local MD5_K, MD5_S = {}, {
|
||||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
||||
}
|
||||
for i = 0, 63 do
|
||||
MD5_K[i + 1] = math.floor(math.abs(math.sin(i + 1)) * 4294967296) % 4294967296
|
||||
end
|
||||
|
||||
local function md5hex(msg)
|
||||
local len = #msg
|
||||
local padLen = (55 - len) % 64
|
||||
msg = msg .. "\128" .. string.rep("\0", padLen)
|
||||
.. string.char(
|
||||
(len * 8) % 256, math.floor(len * 8 / 256) % 256,
|
||||
math.floor(len * 8 / 65536) % 256, math.floor(len * 8 / 16777216) % 256,
|
||||
0, 0, 0, 0)
|
||||
local a0, b0, c0, d0 = 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
|
||||
local M = {}
|
||||
for off = 1, #msg, 64 do
|
||||
for j = 0, 15 do
|
||||
local b1, b2, b3, b4 = msg:byte(off + j * 4, off + j * 4 + 3)
|
||||
M[j] = bor(b1, lshift(b2, 8), lshift(b3, 16), lshift(b4, 24))
|
||||
end
|
||||
local A, B, C, D = a0, b0, c0, d0
|
||||
for i = 0, 63 do
|
||||
local F, g
|
||||
if i < 16 then
|
||||
F = bor(band(B, C), band(bnot(B), D)); g = i
|
||||
elseif i < 32 then
|
||||
F = bor(band(D, B), band(bnot(D), C)); g = (5 * i + 1) % 16
|
||||
elseif i < 48 then
|
||||
F = bxor(B, C, D); g = (3 * i + 5) % 16
|
||||
else
|
||||
F = bxor(C, bor(B, bnot(D))); g = (7 * i) % 16
|
||||
end
|
||||
F = F + A + MD5_K[i + 1] + M[g]
|
||||
A = D; D = C; C = B
|
||||
B = B + rol(F, MD5_S[i + 1])
|
||||
end
|
||||
a0, b0, c0, d0 = a0 + A, b0 + B, c0 + C, d0 + D
|
||||
end
|
||||
local out = {}
|
||||
for _, v in ipairs({ a0, b0, c0, d0 }) do
|
||||
for k = 0, 3 do out[#out + 1] = string.format("%02x", band(rshift(v, k * 8), 255)) end
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
Palette._md5hex = md5hex
|
||||
|
||||
--- Stable hash of BGR555 pals (+ optional extraBlob) for RGBA disk cache keys.
|
||||
function Palette.hash(bgrPals, extraBlob)
|
||||
local parts = {}
|
||||
@@ -33,13 +89,11 @@ function Palette.hash(bgrPals, extraBlob)
|
||||
if extraBlob and type(extraBlob) == "string" then
|
||||
parts[#parts + 1] = extraBlob
|
||||
end
|
||||
-- FNV-1a 32-bit over hex stream (stable, no bit lib).
|
||||
local h = 2166136261
|
||||
local s = table.concat(parts)
|
||||
for i = 1, #s do
|
||||
h = (h * 16777619 + s:byte(i)) % 4294967296
|
||||
if love and love.data and love.data.hash then
|
||||
return (love.data.encode("string", "hex", love.data.hash("md5", s)):sub(1, 16))
|
||||
end
|
||||
return string.format("%08x", h)
|
||||
return md5hex(s):sub(1, 16)
|
||||
end
|
||||
|
||||
--- Identity tint hook (weather / time later).
|
||||
|
||||
@@ -28,6 +28,7 @@ function TilesetAnim.install(cache)
|
||||
end
|
||||
|
||||
function TilesetAnim.invalidate()
|
||||
TilesetAnim._manifests = nil
|
||||
TilesetAnim._pairs = {}
|
||||
TilesetAnim._visible = {}
|
||||
TilesetAnim.counter = 0
|
||||
@@ -42,9 +43,18 @@ local function native_root()
|
||||
end
|
||||
|
||||
local function read_manifest(cache, pair)
|
||||
local memo = TilesetAnim._manifests
|
||||
if not memo then
|
||||
memo = {}
|
||||
TilesetAnim._manifests = memo
|
||||
end
|
||||
local hit = memo[pair]
|
||||
if hit ~= nil then return hit or nil end
|
||||
local src = cache:read(native_root() .. pair .. "/anim_manifest.lua")
|
||||
local chunk = src and load(src, "@anim_manifest.lua", "t", {})
|
||||
return chunk and chunk() or nil
|
||||
local man = chunk and chunk() or nil
|
||||
memo[pair] = man or false
|
||||
return man
|
||||
end
|
||||
|
||||
local function rse_luts(cache, pair)
|
||||
@@ -295,10 +305,7 @@ function TilesetAnim.bindPair(pair, atlas)
|
||||
local entry = TilesetAnim._pairs[pair]
|
||||
if entry == false then return false end
|
||||
if not entry then
|
||||
local src = cache:read(
|
||||
(Extract.CACHE_ROOT or "data/generated/gba") .. "/native/" .. pair .. "/anim_manifest.lua")
|
||||
local chunk = src and load(src, "@anim_manifest.lua", "t", {})
|
||||
local man = chunk and chunk()
|
||||
local man = read_manifest(cache, pair)
|
||||
if not man then
|
||||
TilesetAnim._pairs[pair] = false
|
||||
return false
|
||||
|
||||
@@ -134,20 +134,26 @@ function NativePack.decodeIdx(blob)
|
||||
midIds[i] = read_u16(blob, off)
|
||||
off = off + 2
|
||||
end
|
||||
local pixels = {}
|
||||
local dataOff = off
|
||||
local n = midCount * 256
|
||||
for i = 1, n do
|
||||
pixels[i] = blob:byte(off + i - 1) or 0
|
||||
end
|
||||
return {
|
||||
return setmetatable({
|
||||
formatVersion = formatVersion,
|
||||
flags = flags,
|
||||
midCount = midCount,
|
||||
atlasCols = atlasCols,
|
||||
atlasRows = atlasRows,
|
||||
midIds = midIds,
|
||||
pixels = pixels,
|
||||
}
|
||||
}, {
|
||||
__index = function(self, k)
|
||||
if k ~= "pixels" then return nil end
|
||||
local pixels = {}
|
||||
for i = 1, n do
|
||||
pixels[i] = blob:byte(dataOff + i - 1) or 0
|
||||
end
|
||||
rawset(self, "pixels", pixels)
|
||||
return pixels
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function NativePack.encodePalettes(mapPals)
|
||||
|
||||
@@ -991,7 +991,7 @@ function Birch:draw()
|
||||
if self.showDialogue then
|
||||
local w = WIN_TEXT
|
||||
Kit.birchDialogueFrame(w.left, w.top, w.width, w.height)
|
||||
if self.printer then self.printer:draw(w.left * 8, w.top * 8 + 1, { colors = Kit.messageColors() }) end
|
||||
if self.printer then self.printer:draw(w.left * 8, w.top * 8 + 1, { colors = Kit.messageColors(), clip = { w.left * 8, w.top * 8, w.width * 8, w.height * 8 } }) end
|
||||
end
|
||||
if self.genderMenu then
|
||||
local w = WIN_GENDER
|
||||
|
||||
@@ -461,6 +461,12 @@ function Printer:draw(x, y, opts)
|
||||
opts = opts or {}
|
||||
local colors = opts.colors or Kit.messageColors()
|
||||
local cx, cy = x, y
|
||||
local clip = opts.clip
|
||||
local sx, sy, sw, sh
|
||||
if clip then
|
||||
sx, sy, sw, sh = love.graphics.getScissor()
|
||||
love.graphics.intersectScissor(clip[1], clip[2], clip[3], clip[4])
|
||||
end
|
||||
for i, line in ipairs(self.lines) do
|
||||
local ly = y + (i - 1) * self.pitch - self.scrollY
|
||||
if line ~= "" then
|
||||
@@ -471,6 +477,9 @@ function Printer:draw(x, y, opts)
|
||||
end
|
||||
cy = ly
|
||||
end
|
||||
if clip then
|
||||
if sx then love.graphics.setScissor(sx, sy, sw, sh) else love.graphics.setScissor() end
|
||||
end
|
||||
if self.arrowFrame then
|
||||
local img = Kit.rgbaImage("data/generated/gba/chrome/fonts/down_arrow.rgba", 8, 48)
|
||||
if img then
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "shots"
|
||||
|
||||
local function fail(msg)
|
||||
print("FAIL " .. msg)
|
||||
love.event.quit(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 Map = require("src.core.game3.map")
|
||||
local FxRse = require("src.core.game3.field_effects_rse")
|
||||
local Ghosts = require("src.core.game3.ghosts")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local r104 = game.data.maps["EM_ROUTE104"]
|
||||
if not r104 then return fail("EM_ROUTE104 missing") end
|
||||
|
||||
Map.load(nil, game, "EM_ROUTE104", { x = 28, y = 1, facing = "down" })
|
||||
U.wait(60)
|
||||
U.still(game, DIR .. "/em_control_player_on_own_map_reflects.png")
|
||||
if (FxRse.lastReflections or 0) < 1 then return fail("control player has no reflection") end
|
||||
|
||||
Map.load(nil, game, "EM_RUSTBORO_CITY", { x = 28, y = 57, facing = "down" })
|
||||
U.wait(60)
|
||||
U.still(game, DIR .. "/em_rustboro_view_route104_no_npc.png")
|
||||
|
||||
local defs = { { localId = 1, index = 1, x = 28, y = 1, graphicsId = 5, movementType = 8, elevation = 3, flag = 0 } }
|
||||
Ghosts._pools["EM_ROUTE104"] = Objects.spawnFromDefs(defs, r104, "EM_ROUTE104")
|
||||
U.wait(30)
|
||||
U.still(game, DIR .. "/em_rustboro_view_route104_ghost_npc_on_shore.png")
|
||||
local n = FxRse.lastReflections or 0
|
||||
print("GHOST_VIEW_REFL", n)
|
||||
if n < 1 then return fail("neighbor NPC has no reflection") end
|
||||
print("PASS")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -38,8 +38,8 @@ return function(game)
|
||||
U.wait(20)
|
||||
expect(require("src.ui.game3.stack").busy(), "h1_start_menu_open")
|
||||
key("1")
|
||||
expect(game.options.speedOverworld == 2 and game.options.speedMenu == 1,
|
||||
"h1_start_menu_1_bumps_overworld")
|
||||
expect(game.options.speedOverworld == 2 and game.options.speedMenu == 2
|
||||
and game.options.speedBattle == 2, "h1_start_menu_1_syncs_all_categories")
|
||||
game.speedOverride = 1
|
||||
U.still(game, ".bazinga/BSA/09-24-26-01-followup/shots/game3_speed_hotkey_h1/h1_start_menu_speed2.png")
|
||||
game.speedOverride = nil
|
||||
|
||||
@@ -33,7 +33,8 @@ do
|
||||
eq(g:speedCategory(), "overworld", "menu over the field inherits overworld")
|
||||
g:keypressed("1")
|
||||
eq(g.options.speedOverworld, 2, "1 with a field menu open bumps OVERWORLD SPEED")
|
||||
eq(g.options.speedMenu, 1, "1 with a field menu open leaves MENU SPEED alone")
|
||||
eq(g.options.speedMenu, 2, "1 with a field menu open syncs MENU SPEED")
|
||||
eq(g.options.speedBattle, 2, "and syncs BATTLE SPEED")
|
||||
stackBusy = false
|
||||
eq(g:logicSpeed(), 2, "walk speed sticks after the menu closes")
|
||||
battleActive = true
|
||||
@@ -42,7 +43,7 @@ do
|
||||
eq(g:logicSpeed(), 10, "a local gen3 battle runs at BATTLE SPEED")
|
||||
g:keypressed("1")
|
||||
check(g.options.speedBattle ~= 10, "1 in battle cycles BATTLE SPEED")
|
||||
eq(g.options.speedOverworld, 2, "and leaves OVERWORLD SPEED alone")
|
||||
eq(g.options.speedOverworld, g.options.speedBattle, "and syncs OVERWORLD SPEED")
|
||||
battleActive = false
|
||||
eq(newGame3("boot"):speedCategory(), "menu", "boot phase stays menu")
|
||||
eq(newGame3("quest_log"):speedCategory(), "menu", "quest log stays menu")
|
||||
|
||||
@@ -180,8 +180,8 @@ do
|
||||
function g:writeOptions() writeOptions.calls = writeOptions.calls + 1 end
|
||||
g:_cycleSpeed(1)
|
||||
eq(g.save.options.speedBattle, 2, "cycling during battle bumps speedBattle")
|
||||
eq(g.save.options.speedOverworld, 1, "...and leaves speedOverworld alone")
|
||||
eq(g.save.options.speedMenu, 1, "...and leaves speedMenu alone")
|
||||
eq(g.save.options.speedOverworld, 2, "...and syncs speedOverworld")
|
||||
eq(g.save.options.speedMenu, 2, "...and syncs speedMenu")
|
||||
eq(writeOptions.calls, 1, "a successful cycle persists the option")
|
||||
end
|
||||
do
|
||||
@@ -200,7 +200,8 @@ do
|
||||
function g:writeOptions() end
|
||||
g:_cycleSpeed(1)
|
||||
eq(g.save.options.speedOverworld, 2, "cycling on the overworld bumps speedOverworld")
|
||||
eq(g.save.options.speedBattle, 1, "...and leaves speedBattle alone")
|
||||
eq(g.save.options.speedBattle, 2, "...and syncs speedBattle")
|
||||
eq(g.save.options.speedMenu, 2, "...and syncs speedMenu")
|
||||
end
|
||||
do
|
||||
local g = gameWith({ overlay },
|
||||
@@ -208,6 +209,21 @@ do
|
||||
function g:writeOptions() end
|
||||
g:_cycleSpeed(1)
|
||||
eq(g.save.options.speedMenu, 2, "cycling in a menu bumps speedMenu")
|
||||
eq(g.save.options.speedOverworld, 2, "...and syncs speedOverworld")
|
||||
end
|
||||
do
|
||||
local g = gameWith({ overworld },
|
||||
{ speedOverworld = 3, speedBattle = 10, speedMenu = 1 })
|
||||
function g:writeOptions() end
|
||||
g:_cycleSpeed(-1)
|
||||
eq(g.save.options.speedOverworld, 2, "speed down steps the active category")
|
||||
eq(g.save.options.speedBattle, 2, "speed down syncs battle")
|
||||
eq(g.save.options.speedMenu, 2, "speed down syncs menu")
|
||||
g:_cycleSpeed(-1)
|
||||
g:_cycleSpeed(-1)
|
||||
eq(g.save.options.speedOverworld, GameSpeed.LEVELS[#GameSpeed.LEVELS],
|
||||
"wrapping down from NORMAL lands on the top level for every category")
|
||||
eq(g.save.options.speedMenu, GameSpeed.LEVELS[#GameSpeed.LEVELS], "including menu")
|
||||
end
|
||||
|
||||
-- A cart may narrow the ladder (CartManifest's `speeds`), and returning to
|
||||
|
||||
@@ -181,6 +181,7 @@ local GEN3_HOOKS = {
|
||||
"trainer.party", "catch.rate", "exp.gain", "evolution.check",
|
||||
"battle.damage", "battle.crit", "battle.accuracy", "battle.charge_required",
|
||||
"battle.run", "battle.turn_order", "battle.enemy_action",
|
||||
"battle.low_health_alarm",
|
||||
}
|
||||
|
||||
local function assertShared(name, sites, kind)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env luajit
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
require("tests.game3_cache").requireData("game3_low_hp_hook_test")
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local Audio = require("src.core.game3.audio")
|
||||
local SE = require("src.core.game3.se_ids")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
|
||||
local plays, stops = 0, 0
|
||||
Audio.playSe = function(id)
|
||||
if id == SE.SE_LOW_HEALTH then plays = plays + 1 end
|
||||
end
|
||||
Audio.stopSe = function(id)
|
||||
if id == SE.SE_LOW_HEALTH then stops = stops + 1 end
|
||||
end
|
||||
|
||||
local function reset()
|
||||
plays, stops = 0, 0
|
||||
Battle._lowHpSong = false
|
||||
Battle._st = { player = { mon = { hp = 1, maxHp = 100 } } }
|
||||
end
|
||||
|
||||
local function step(n)
|
||||
for _ = 1, n do Battle._updateLowHpMusic() end
|
||||
end
|
||||
|
||||
reset()
|
||||
step(3)
|
||||
check(plays == 1, "no hook: siren starts once")
|
||||
|
||||
reset()
|
||||
ModRuntime.hooks = {
|
||||
chains = { ["battle.low_health_alarm"] = {} },
|
||||
call = function(_, _, vanilla, ctx)
|
||||
ctx.on = false
|
||||
return vanilla(ctx)
|
||||
end,
|
||||
}
|
||||
step(3)
|
||||
check(plays == 0, "hook forcing on=false suppresses siren")
|
||||
|
||||
reset()
|
||||
local sawBattle = false
|
||||
ModRuntime.hooks = {
|
||||
chains = { ["battle.low_health_alarm"] = {} },
|
||||
call = function(_, _, vanilla, ctx)
|
||||
sawBattle = ctx.battle == Battle._st and ctx.on == true
|
||||
return vanilla(ctx)
|
||||
end,
|
||||
}
|
||||
step(2)
|
||||
check(plays == 1 and sawBattle, "pass-through hook sees on=true and battle, siren plays once")
|
||||
|
||||
if failed > 0 then
|
||||
print(failed .. " failed")
|
||||
os.exit(1)
|
||||
end
|
||||
print("all passed")
|
||||
os.exit(0)
|
||||
@@ -0,0 +1,79 @@
|
||||
package.path = package.path .. ";./?.lua"
|
||||
|
||||
local calls = {}
|
||||
local scissor = nil
|
||||
|
||||
love = love or {}
|
||||
love.graphics = love.graphics or {}
|
||||
love.graphics.getScissor = function()
|
||||
if scissor then return scissor[1], scissor[2], scissor[3], scissor[4] end
|
||||
return nil
|
||||
end
|
||||
love.graphics.setScissor = function(x, y, w, h)
|
||||
if x then scissor = { x, y, w, h } else scissor = nil end
|
||||
end
|
||||
love.graphics.intersectScissor = function(x, y, w, h)
|
||||
if not scissor then
|
||||
scissor = { x, y, w, h }
|
||||
return
|
||||
end
|
||||
local x1 = math.max(scissor[1], x)
|
||||
local y1 = math.max(scissor[2], y)
|
||||
local x2 = math.min(scissor[1] + scissor[3], x + w)
|
||||
local y2 = math.min(scissor[2] + scissor[4], y + h)
|
||||
scissor = { x1, y1, math.max(0, x2 - x1), math.max(0, y2 - y1) }
|
||||
end
|
||||
love.graphics.setColor = function() end
|
||||
love.graphics.draw = function() end
|
||||
|
||||
local okKit, Kit = pcall(require, "src.ui.game3.rse.scene_kit")
|
||||
if not okKit then
|
||||
print("[skip] scene_kit needs runtime: " .. tostring(Kit))
|
||||
os.exit(0)
|
||||
end
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
FrlgFont.draw = function(line, x, y)
|
||||
calls[#calls + 1] = { y = y, scissor = scissor and { scissor[1], scissor[2], scissor[3], scissor[4] } or nil }
|
||||
return x + 8, nil
|
||||
end
|
||||
FrlgFont.measure = function() return 8 end
|
||||
Kit.messageColors = function() return {} end
|
||||
|
||||
local okP, p = pcall(Kit.printer, {}, { speed = 0 })
|
||||
if not okP then
|
||||
print("[skip] printer needs cache: " .. tostring(p))
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print(" PASS: " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print(" FAIL: " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
p.lines = { "aaa", "bbb", "" }
|
||||
p.scrollY = 5
|
||||
p.arrowFrame = nil
|
||||
local clip = { 16, 120, 216, 32 }
|
||||
p:draw(16, 121, { clip = clip })
|
||||
check(#calls == 2, "lines drawn")
|
||||
local s = calls[1].scissor
|
||||
check(s and s[1] == 16 and s[2] == 120 and s[3] == 216 and s[4] == 32, "scissor equals clip while drawing")
|
||||
check(scissor == nil, "scissor restored to none")
|
||||
|
||||
scissor = { 0, 0, 240, 100 }
|
||||
calls = {}
|
||||
p:draw(16, 121, { clip = clip })
|
||||
check(calls[1].scissor[2] == 120 and calls[1].scissor[4] == 0 + 100 - 120 + 0 or calls[1].scissor[4] == 0, "clip intersects outer scissor")
|
||||
check(scissor and scissor[4] == 100, "outer scissor restored")
|
||||
|
||||
scissor = nil
|
||||
calls = {}
|
||||
p:draw(16, 121)
|
||||
check(calls[1].scissor == nil, "no clip leaves scissor untouched")
|
||||
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
@@ -0,0 +1,77 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed = 0
|
||||
local function check(ok, msg)
|
||||
if not ok then
|
||||
failed = failed + 1
|
||||
print("FAIL " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
love = love or {}
|
||||
local draws = 0
|
||||
love.graphics = love.graphics or {}
|
||||
love.graphics.setColor = function() end
|
||||
love.graphics.newQuad = function() return { setViewport = function() end } end
|
||||
love.graphics.draw = function() draws = draws + 1 end
|
||||
|
||||
local okMap, Map = pcall(require, "src.core.game3.map")
|
||||
if not okMap then
|
||||
print("[skip] map needs runtime: " .. tostring(Map))
|
||||
os.exit(0)
|
||||
end
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local Scripts = require("src.core.game3.scripting.interaction_scripts")
|
||||
local MB = require("src.core.game3.mb")
|
||||
local FxRse = require("src.core.game3.field_effects_rse")
|
||||
|
||||
local WATER = MB.id("POND_WATER")
|
||||
local function layout(pair, mid)
|
||||
return { width = 4, height = 4, pair = pair, midAt = function() return mid end }
|
||||
end
|
||||
|
||||
local host = { midLayout = layout("test_host", 1), pair = "test_host" }
|
||||
local south = { midLayout = layout("test_south", 7), pair = "test_south" }
|
||||
Scripts.behaviors.test_host = {}
|
||||
Scripts.behaviors.test_south = { [7] = WATER }
|
||||
|
||||
Collision._mapDef = host
|
||||
Map.neighborList = { { dir = "south", offset = 0, def = south } }
|
||||
Map.world = {
|
||||
{ id = "HOST", def = host, ox = 0, oy = 0 },
|
||||
{ id = "SOUTH", def = south, ox = 0, oy = 4 },
|
||||
}
|
||||
|
||||
check(Collision.behavior(1, 5) == nil, "gameplay behavior stays bounded to the current map")
|
||||
check(Collision.worldBehavior and Collision.worldBehavior(1, 5) == WATER,
|
||||
"worldBehavior resolves a neighbor cell across the seam")
|
||||
check(Collision.worldBehavior and Collision.worldBehavior(1, 1) == nil,
|
||||
"worldBehavior keeps current-map cells")
|
||||
|
||||
local spr = {
|
||||
width = 16, height = 32, frameCount = 1, quads = { [0] = true },
|
||||
image = { getDimensions = function() return 16, 32 end },
|
||||
}
|
||||
package.loaded["src.core.game3.ow_sprites"] = {
|
||||
getDraw = function() return spr end,
|
||||
pose = function() return 0, false end,
|
||||
}
|
||||
FxRse._fc = { gfx = {}, palTagNone = 0x11FF }
|
||||
|
||||
local ghost = { cellX = 1, cellY = 0, px = 16, py = 0, moving = false }
|
||||
draws = 0
|
||||
check(FxRse.drawReflection(ghost, 5, 0, false, 0, 0, 0, 0, 0, 4) == true,
|
||||
"ghost object on a neighbor map reflects off neighbor water")
|
||||
check(draws > 0, "ghost reflection draws quads")
|
||||
|
||||
local near = { cellX = 1, cellY = 2, px = 16, py = 32, moving = false }
|
||||
draws = 0
|
||||
check(FxRse.drawReflection(near, 5, 0, false, 0, 0, 0, 0) == true,
|
||||
"current-map object next to the seam reflects off neighbor water")
|
||||
|
||||
local dry = { cellX = 1, cellY = 0, px = 16, py = 0, moving = false }
|
||||
check(FxRse.drawReflection(dry, 5, 0, false, 0, 0, 0, 0) == false,
|
||||
"object with no water below does not reflect")
|
||||
|
||||
if failed > 0 then os.exit(1) end
|
||||
print("game3_reflection_neighbor_test ok")
|
||||
@@ -0,0 +1,121 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failures = 0
|
||||
local function check(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
end
|
||||
|
||||
local NativePack = require("src.import.gba.native_pack")
|
||||
local Palette = require("src.core.game3.palette")
|
||||
|
||||
do
|
||||
local pixels = {}
|
||||
for i = 1, 512 do pixels[i] = (i * 7) % 256 end
|
||||
local blob = NativePack.encodeIdx({
|
||||
midCount = 2, atlasCols = 16, atlasRows = 1, midIds = { 0, 5 }, pixels = pixels,
|
||||
})
|
||||
local t = NativePack.decodeIdx(blob)
|
||||
check(rawget(t, "pixels") == nil, "decodeIdx leaves pixels undecoded")
|
||||
check(t.midCount == 2 and t.midIds[2] == 5, "header and midIds decode eagerly")
|
||||
local px = t.pixels
|
||||
check(#px == 512 and px[1] == pixels[1] and px[512] == pixels[512], "pixels decode on first access")
|
||||
check(rawget(t, "pixels") == px and t.pixels == px, "pixels decode once")
|
||||
local rgb = {}
|
||||
for p = 0, 15 do
|
||||
rgb[p] = {}
|
||||
for c = 0, 15 do rgb[p][c] = { p * 10, c * 10, 3 } end
|
||||
end
|
||||
local rgba, w, h = NativePack.bakeRgba(NativePack.decodeIdx(blob), rgb, {})
|
||||
check(#rgba == w * h * 4, "bakeRgba works on a lazy table")
|
||||
end
|
||||
|
||||
do
|
||||
check(Palette._md5hex("") == "d41d8cd98f00b204e9800998ecf8427e", "md5 empty vector")
|
||||
check(Palette._md5hex("The quick brown fox jumps over the lazy dog") == "9e107d9d372bb6826bd81d3542a419d6",
|
||||
"md5 fox vector")
|
||||
local bgr = {}
|
||||
for p = 0, 15 do
|
||||
bgr[p] = {}
|
||||
for c = 0, 15 do bgr[p][c] = (p * 977 + c * 31) % 32768 end
|
||||
end
|
||||
local chunks = {}
|
||||
for i = 1, 4099 do chunks[i] = string.char((i * 13 + i % 7) % 256) end
|
||||
local extra = table.concat(chunks)
|
||||
local key = Palette.hash(bgr, extra)
|
||||
check(key:match("^%x+$") and #key == 16, "Palette.hash is 16 hex chars")
|
||||
check(Palette.hash(bgr, extra) == key, "Palette.hash is deterministic")
|
||||
local tweaked = extra:sub(1, 2000) .. "\255" .. extra:sub(2002)
|
||||
check(Palette.hash(bgr, tweaked) ~= key, "one byte of extra changes the key")
|
||||
bgr[3][4] = (bgr[3][4] + 1) % 32768
|
||||
check(Palette.hash(bgr, extra) ~= key, "one palette color changes the key")
|
||||
bgr[3][4] = (bgr[3][4] - 1) % 32768
|
||||
check(Palette.hash(bgr, extra) == key, "restoring the color restores the key")
|
||||
local savedLove = love
|
||||
love = { data = {
|
||||
hash = function(_, s) return (Palette._md5hex(s):gsub("%x%x", function(h) return string.char(tonumber(h, 16)) end)) end,
|
||||
encode = function(_, _, s) return (s:gsub(".", function(c) return string.format("%02x", c:byte()) end)) end,
|
||||
} }
|
||||
check(Palette.hash(bgr, extra) == key, "love.data path yields the same key as the fallback")
|
||||
love = savedLove
|
||||
end
|
||||
|
||||
do
|
||||
local loads = {}
|
||||
local Native = { _pairs = {} }
|
||||
function Native.ready() return true end
|
||||
function Native.get(pair) loads[#loads + 1] = pair; Native._pairs[pair] = {}; return {} end
|
||||
package.loaded["src.core.game3.tileset_native"] = Native
|
||||
package.loaded["src.core.game3.player"] = { cellX = 10, cellY = 10 }
|
||||
local Map = require("src.core.game3.map")
|
||||
local function def(pair, w, h) return { pair = pair, midLayout = { pair = pair, width = w, height = h } } end
|
||||
Map.computeWorld = function()
|
||||
return {
|
||||
{ id = "NEAR", def = def("pnear", 20, 20), ox = 20, oy = 0 },
|
||||
{ id = "FAR", def = def("pfar", 20, 20), ox = 400, oy = 400 },
|
||||
}
|
||||
end
|
||||
Map.ensureMidLayout = function() end
|
||||
Map._warmPairs = nil
|
||||
Map._worldRoot = nil
|
||||
Map.refreshWorld({ data = { maps = {} } }, 10, 10, "ROOT")
|
||||
check(#Map._warmQueue == 2, "both pairs queued")
|
||||
Map.stepWarm()
|
||||
check(#loads == 1 and loads[1] == "pnear", "near pair loads first")
|
||||
Map.stepWarm()
|
||||
check(#loads == 1 and Map._warmQueue and #Map._warmQueue == 1, "far pair is not loaded")
|
||||
package.loaded["src.core.game3.player"].cellX = 400
|
||||
package.loaded["src.core.game3.player"].cellY = 400
|
||||
Map.stepWarm()
|
||||
check(#loads == 2 and loads[2] == "pfar" and Map._warmQueue == nil, "far pair loads once in range")
|
||||
|
||||
Native._pairs = {}
|
||||
loads = {}
|
||||
package.loaded["src.core.game3.player"].cellX = 10
|
||||
package.loaded["src.core.game3.player"].cellY = 10
|
||||
Map._warmPairs = true
|
||||
Map._worldRoot = nil
|
||||
Map.refreshWorld({ data = { maps = {} } }, 10, 10, "ROOT")
|
||||
check(#loads == 1 and loads[1] == "pnear" and Map._warmQueue and #Map._warmQueue == 1,
|
||||
"warp preload only loads near pairs synchronously")
|
||||
|
||||
love = { timer = { getDelta = function() return 0.2 end } }
|
||||
Map._warmDefer = 0
|
||||
loads = {}
|
||||
Native._pairs = {}
|
||||
Map._warmQueue = { "pnear" }
|
||||
Map._warmEntries = {}
|
||||
Map.stepWarm()
|
||||
check(#loads == 0, "slow frame defers the load")
|
||||
Map.stepWarm()
|
||||
Map.stepWarm()
|
||||
Map.stepWarm()
|
||||
check(#loads == 1, "deferral is bounded")
|
||||
love = nil
|
||||
package.loaded["src.core.game3.tileset_native"] = nil
|
||||
package.loaded["src.core.game3.player"] = nil
|
||||
end
|
||||
|
||||
print(failures == 0 and "PASS game3_seam_warm_budget_test"
|
||||
or ("FAIL game3_seam_warm_budget_test failures=" .. failures))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
Reference in New Issue
Block a user