mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:10:49 +02:00
Added overworld battles
This commit is contained in:
+392
-10
@@ -10,7 +10,12 @@ local T = require("tests.modkit")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
|
||||
local Data = T.fixtures.load()
|
||||
local run = T.sdk.loadMod("mods/DRAMATIC_SHAPE", { data = Data })
|
||||
-- DS_MOD_PATH lets the suite run against a copy of the mod. The game holds
|
||||
-- an open handle on the live directory while it is running, and on Windows
|
||||
-- that is enough to make the headless loader's directory probe fail, so a
|
||||
-- run alongside a live session points at a copy instead.
|
||||
local MOD_PATH = os.getenv("DS_MOD_PATH") or "mods/DramaticShapeVoxelMod"
|
||||
local run = T.sdk.loadMod(MOD_PATH, { data = Data })
|
||||
|
||||
T.eq(#run.errors, 0,
|
||||
"DRAMATIC_SHAPE loads clean: " .. table.concat(run.errors, "; "))
|
||||
@@ -100,19 +105,25 @@ T.eq(byLabel.VOXEL.value(), "15", "the row renders the current rung's label")
|
||||
|
||||
-- ------- this mod's own settings
|
||||
--
|
||||
-- Neither is a pipeline (they parameterise the voxel pass rather than
|
||||
-- owning one), so they reach the same menu through the ui.options.rows
|
||||
-- hook, and store themselves where the mod manager's settings page looks.
|
||||
-- None of them is a pipeline -- two parameterise the voxel pass rather than
|
||||
-- owning one, and the third decides what a BATTLE is drawn over, which is
|
||||
-- not a stage the registry has -- so they reach the same menu through the
|
||||
-- ui.options.rows hook, and store themselves where the mod manager's
|
||||
-- settings page looks.
|
||||
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data }, { { id = "text_speed" } })
|
||||
T.eq(#hookedRows, 3, "the options hook added a row per setting")
|
||||
local grid, curve = hookedRows[2], hookedRows[3]
|
||||
T.eq(#hookedRows, 4, "the options hook added a row per setting")
|
||||
local grid, curve, battles = hookedRows[2], hookedRows[3], hookedRows[4]
|
||||
T.eq(grid.label, "V-GRID", "the grid row carries its label")
|
||||
T.eq(grid.value(), "OFF", "the grid starts off")
|
||||
T.eq(curve.label, "V-CURVE", "the curve row carries its label")
|
||||
T.eq(curve.value(), "OFF", "the curve starts off")
|
||||
T.eq(battles.label, "3D-BTL", "the overworld-battle row carries its label")
|
||||
T.eq(battles.value(), "ON",
|
||||
"overworld battles are on by default -- the mode's headline is the world "
|
||||
.. "in 3D, and a battle is where the player spends half the game")
|
||||
|
||||
-- stepping writes through to the one place both rows read
|
||||
local settingGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||
@@ -694,13 +705,14 @@ T.eq(modeColors(nil), nil, "and a pipeline given no paletteFor at all is safe")
|
||||
-- 5 V-GRID toggle the wireframe
|
||||
-- 6 T-SHIFT cycle the blur ladder
|
||||
-- 7 V-CURVE cycle the horizon bend
|
||||
-- 8 3D-BTL toggle overworld battles
|
||||
--
|
||||
-- Only 6 reaches the pipeline registry the documented way. Game:keypressed
|
||||
-- answers the engine's own display keys first and returns -- 3 is TILT and
|
||||
-- 5 is GBC FX -- expressly so a pipeline cannot shadow one, and 7 belongs
|
||||
-- to settings that own no pass and so have no registry to claim from. The
|
||||
-- mod therefore wraps Game:keypressed, and these pin what that wrapper is
|
||||
-- allowed to take.
|
||||
-- 5 is GBC FX -- expressly so a pipeline cannot shadow one, and 7 and 8
|
||||
-- belong to settings that own no pass and so have no registry to claim
|
||||
-- from. The mod therefore wraps Game:keypressed, and these pin what that
|
||||
-- wrapper is allowed to take.
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
Pipelines.reset()
|
||||
@@ -739,6 +751,13 @@ local curveBefore = Curve.setting:get()
|
||||
Game.keypressed(keyGame, "7")
|
||||
T.neq(Curve.setting:get(), curveBefore, "7 cycles V-CURVE")
|
||||
|
||||
local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
|
||||
T.eq(Battles.setting:get(), true, "3D-BTL starts on")
|
||||
Game.keypressed(keyGame, "8")
|
||||
T.eq(Battles.setting:get(), false, "8 toggles overworld battles off")
|
||||
Game.keypressed(keyGame, "8")
|
||||
T.eq(Battles.setting:get(), true, "and back on")
|
||||
|
||||
-- 3 also clears the two engine modes it displaced. Without this a player
|
||||
-- who left TILT or GBC FX on before enabling the mod has no key left to
|
||||
-- turn them off with, and both fight the diorama -- TILT is the flat fake
|
||||
@@ -890,6 +909,369 @@ T.check(skyRGB("gbc_inv")[3] ~= blue[3],
|
||||
|
||||
Voxel.angle = 0
|
||||
|
||||
-- ------- overworld battles: where the fight is staged
|
||||
--
|
||||
-- The arena search is pure map arithmetic, so it is driven here against
|
||||
-- hand-drawn maps rather than a fixture: the shape it looks for, the order
|
||||
-- it relaxes in, and what it refuses are all things a picture can state
|
||||
-- exactly.
|
||||
--
|
||||
-- The stub answers the small surface BattleArena asks a Map for. `rows` are
|
||||
-- strings, one per cell row, "." open and anything else solid; "w" is water
|
||||
-- (open to a surfer only) and "d" a warp tile.
|
||||
|
||||
local BattleArena = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattleArena")
|
||||
|
||||
local function stubMap(rows)
|
||||
local at = function(cx, cy)
|
||||
local row = rows[cy + 1]
|
||||
return row and row:sub(cx + 1, cx + 1) or "#"
|
||||
end
|
||||
return {
|
||||
widthCells = #rows[1],
|
||||
heightCells = #rows,
|
||||
inBounds = function(_, cx, cy)
|
||||
return cx >= 0 and cy >= 0 and cx < #rows[1] and cy < #rows
|
||||
end,
|
||||
warpAtCell = function() return nil end,
|
||||
isWarpTileCell = function(_, cx, cy) return at(cx, cy) == "d" end,
|
||||
isWalkableCell = function(_, cx, cy) return at(cx, cy) == "." end,
|
||||
isWaterCell = function(_, cx, cy) return at(cx, cy) == "w" end,
|
||||
isGrassCell = function(_, cx, cy) return at(cx, cy) == "g" end,
|
||||
}
|
||||
end
|
||||
|
||||
-- a field with room for the wide arena on its right-hand side only
|
||||
local field = stubMap({
|
||||
"##########",
|
||||
"#....#####",
|
||||
"#....#####",
|
||||
"#....#####",
|
||||
"#....#####",
|
||||
"#....#####",
|
||||
"#....#####",
|
||||
"##########",
|
||||
})
|
||||
|
||||
local arena = BattleArena.find(field, 2, 3, false)
|
||||
T.check(arena ~= nil, "a field with a 3x6 clearing has an arena")
|
||||
T.eq(arena.shape, "wide", "and it is the wide shape, not the fallback")
|
||||
T.eq(arena.w, 3, "the wide arena is three cells across")
|
||||
T.eq(arena.h, 6, "and six deep")
|
||||
|
||||
-- the two mons stand three cells apart down the middle column, which is
|
||||
-- what the picture in BattleArena says and what the camera is built around
|
||||
T.eq(arena.enemyCell[1], arena.playerCell[1],
|
||||
"both mons stand in the arena's middle column")
|
||||
T.eq(arena.playerCell[2] - arena.enemyCell[2], 3,
|
||||
"with three cells of ground between them")
|
||||
T.check(arena.enemyCell[2] < arena.playerCell[2],
|
||||
"the enemy is the NORTH one -- the far end from a camera parked south")
|
||||
|
||||
-- every cell of the shape is open, apron included: that one-cell margin is
|
||||
-- the difference between a staged shot and a fight in a doorway
|
||||
for cy = arena.y, arena.y + arena.h - 1 do
|
||||
for cx = arena.x, arena.x + arena.w - 1 do
|
||||
T.check(field:isWalkableCell(cx, cy),
|
||||
("arena cell (%d,%d) is open ground"):format(cx, cy))
|
||||
end
|
||||
end
|
||||
|
||||
-- world-pixel centres, which is the unit everything downstream works in
|
||||
T.eq(arena.player[1], arena.playerCell[1] * 16 + 8,
|
||||
"the player's mark is the centre of its cell in world pixels")
|
||||
T.eq(arena.mid[2], (arena.enemy[2] + arena.player[2]) / 2,
|
||||
"and the midpoint is halfway between the two")
|
||||
|
||||
-- nearest, not first found: two clearings, and the one under the player wins.
|
||||
-- Wide enough that the battle camera -- which stands a few cells east and
|
||||
-- south of whatever it is aimed at -- is over real ground for BOTH of them,
|
||||
-- so this measures proximity rather than the clearance preference below.
|
||||
local twin = stubMap({
|
||||
"########################",
|
||||
"#.##########.###########",
|
||||
"#.##########.###########",
|
||||
"#.##########.###########",
|
||||
"#.##########.###########",
|
||||
"########################",
|
||||
"########################",
|
||||
"########################",
|
||||
"########################",
|
||||
"########################",
|
||||
})
|
||||
local near = BattleArena.find(twin, 12, 3, false)
|
||||
T.eq(near.shape, "narrow", "no 3x6 anywhere, so the search relaxes")
|
||||
T.eq(near.x, 12, "and takes the corridor the player is standing in")
|
||||
T.eq(BattleArena.find(twin, 1, 3, false).x, 1,
|
||||
"the same map from the other side picks the other one")
|
||||
|
||||
-- the wide shape wins even when a narrow one is closer: it is the shot this
|
||||
-- mode is framed for, so proximity does not get to overrule it
|
||||
local both = stubMap({
|
||||
"#.#########",
|
||||
"#.####...##",
|
||||
"#.####...##",
|
||||
"#.####...##",
|
||||
"######...##",
|
||||
"######...##",
|
||||
"######...##",
|
||||
})
|
||||
T.eq(BattleArena.find(both, 1, 1, false).shape, "wide",
|
||||
"a wide arena across the map beats a narrow one underfoot")
|
||||
|
||||
-- water is ground for a surfer and nothing at all for anyone else
|
||||
local sea = stubMap({
|
||||
"wwwwww",
|
||||
"wwwwww",
|
||||
"wwwwww",
|
||||
"wwwwww",
|
||||
"wwwwww",
|
||||
"wwwwww",
|
||||
})
|
||||
T.eq(BattleArena.find(sea, 2, 2, false), nil,
|
||||
"open water is not open ground to someone walking")
|
||||
T.check(BattleArena.find(sea, 2, 2, true) ~= nil,
|
||||
"but it is to a surfer, who is standing on it")
|
||||
|
||||
-- tall grass is walkable and is still not a stage: it is knee-high geometry
|
||||
-- standing between a nearly-level camera and the mon behind it
|
||||
local meadow = stubMap({
|
||||
"########",
|
||||
"#ggg..g#",
|
||||
"#ggg..g#",
|
||||
"#ggg..g#",
|
||||
"#ggg..g#",
|
||||
"#ggg..g#",
|
||||
"#ggg..g#",
|
||||
"########",
|
||||
})
|
||||
local mown = BattleArena.find(meadow, 2, 3, false)
|
||||
T.check(mown ~= nil, "a meadow with a bare strip still has an arena")
|
||||
for cy = mown.y, mown.y + mown.h - 1 do
|
||||
for cx = mown.x, mown.x + mown.w - 1 do
|
||||
T.check(not meadow:isGrassCell(cx, cy),
|
||||
("no cell of the arena is tall grass (%d,%d)"):format(cx, cy))
|
||||
end
|
||||
end
|
||||
T.eq(BattleArena.find(stubMap({
|
||||
"#####", "#ggg#", "#ggg#", "#ggg#", "#ggg#", "#ggg#", "#ggg#", "#####",
|
||||
}), 2, 3, false), nil,
|
||||
"a map that is nothing but grass has nowhere to stand a fight")
|
||||
|
||||
-- a doormat is walkable and is still not a stage
|
||||
local hall = stubMap({
|
||||
"######",
|
||||
"#..d.#",
|
||||
"#....#",
|
||||
"#....#",
|
||||
"#....#",
|
||||
"######",
|
||||
})
|
||||
local halled = BattleArena.find(hall, 2, 3, false)
|
||||
T.check(halled == nil or halled.shape == "narrow",
|
||||
"a warp tile is excluded, so the room's only 3x6 does not qualify")
|
||||
|
||||
T.eq(BattleArena.find(stubMap({ "###", "###" }), 1, 1, false), nil,
|
||||
"a map with no room at all yields no arena, and the battle draws plainly")
|
||||
|
||||
-- an authored refusal is honoured over the search: a map looked at and found
|
||||
-- to have nowhere a fight reads has to be able to say so, or the fallback
|
||||
-- goes and finds one of the spots that were already rejected by eye
|
||||
local roomy = stubMap({
|
||||
"#####", "#...#", "#...#", "#...#", "#...#", "#...#", "#...#", "#####",
|
||||
})
|
||||
roomy.id = "TEST_REFUSED"
|
||||
T.check(BattleArena.find(roomy, 2, 3, false) ~= nil,
|
||||
"a roomy map finds an arena by search when nothing is authored")
|
||||
BattleArena.setOverride("TEST_REFUSED", false)
|
||||
T.eq(BattleArena.find(roomy, 2, 3, false), nil,
|
||||
"an authored false refuses the map outright, search and all")
|
||||
BattleArena.setOverride("TEST_REFUSED", nil)
|
||||
T.check(BattleArena.find(roomy, 2, 3, false) ~= nil,
|
||||
"and dropping the refusal restores the search")
|
||||
|
||||
-- ------- overworld battles: the over-the-shoulder framing
|
||||
--
|
||||
-- The claim the whole shot rests on. The two pics are PINNED to their cells,
|
||||
-- so the rig has to put those two patches of ground exactly where the GB's
|
||||
-- own battle screen puts its two pics -- the player's low and left at
|
||||
-- (40, 96), the enemy's high and right at (124, 56). Reprojected through the
|
||||
-- real camera rather than asserted about the constants, so the day someone
|
||||
-- retunes the rig this either still lands or says so.
|
||||
|
||||
local BattleCam = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattleCam")
|
||||
local BattleScene = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattleScene")
|
||||
local Voxel3Dcam = run.loader.exports.DRAMATIC_SHAPE.lib.require("Voxel3D")
|
||||
|
||||
-- where a world point lands in the 160x144 frame, or nil behind the camera
|
||||
local function project(cam, point, w, h, fov)
|
||||
local saved = cam.fov
|
||||
if fov then cam.fov = fov end
|
||||
Voxel3Dcam.camera = cam
|
||||
local m = Voxel3Dcam.viewProjection(0, 0, w or 160, h or 144)
|
||||
Voxel3Dcam.camera = nil
|
||||
cam.fov = saved
|
||||
local x = m[1] * point[1] + m[2] * 0 + m[3] * point[2] + m[4]
|
||||
local y = m[5] * point[1] + m[6] * 0 + m[7] * point[2] + m[8]
|
||||
local cw = m[13] * point[1] + m[14] * 0 + m[15] * point[2] + m[16]
|
||||
if cw <= 1e-6 then return nil end
|
||||
return (x / cw * 0.5 + 0.5) * (w or 160), (y / cw * 0.5 + 0.5) * (h or 144)
|
||||
end
|
||||
|
||||
BattleCam.reset()
|
||||
local shot = BattleArena.find(field, 2, 3, false)
|
||||
local rig, pitch = BattleCam.rig(shot, 0)
|
||||
|
||||
local px, py = project(rig, shot.player)
|
||||
local ex, ey = project(rig, shot.enemy)
|
||||
T.check(px ~= nil and ex ~= nil, "both marks are in front of the camera")
|
||||
T.check(math.abs(px - 26) < 1 and math.abs(py - 96) < 1,
|
||||
("the player's cell projects onto its pic's feet at (26, 96): got "
|
||||
.. "(%.2f, %.2f)"):format(px, py))
|
||||
T.check(math.abs(ex - 124) < 1 and math.abs(ey - 56) < 1,
|
||||
("the enemy's cell projects onto its pic's feet at (124, 56): got "
|
||||
.. "(%.2f, %.2f)"):format(ex, ey))
|
||||
T.check(px < ex, "which puts the player's mon LEFT of the enemy's")
|
||||
T.check(py > ey, "and lower in the frame -- nearer the camera")
|
||||
|
||||
-- low and long: the eye is near the floor looking almost along it, which is
|
||||
-- what a 56-pixel sprite standing on a 16-pixel tile costs
|
||||
T.check(pitch > math.rad(60) and pitch < math.rad(85),
|
||||
"the rig watches the arena from near ground level")
|
||||
|
||||
-- ------- a mon covers its own square
|
||||
--
|
||||
-- The pics are drawn at INTEGER scales -- 56 pixels for a front pic at 1x, 64
|
||||
-- for a back pic at 2x -- so the only way a mon can stand in one overworld
|
||||
-- square is for the camera to make that square that big. This is the pair of
|
||||
-- equations the default rig was solved against alongside the two anchors, and
|
||||
-- it is the one that sets how far away the camera has to be.
|
||||
local function span(cam, point)
|
||||
local a = project(cam, { point[1] - 8, point[2] })
|
||||
local b = project(cam, { point[1] + 8, point[2] })
|
||||
return math.abs(b - a)
|
||||
end
|
||||
|
||||
T.check(math.abs(span(rig, shot.player) - 64) < 4,
|
||||
("the player's square is a back pic wide (64px at 2x): got %.2f")
|
||||
:format(span(rig, shot.player)))
|
||||
T.check(math.abs(span(rig, shot.enemy) - 56) < 4,
|
||||
("the enemy's square is a front pic wide (56px at 1x): got %.2f")
|
||||
:format(span(rig, shot.enemy)))
|
||||
|
||||
-- ------- the close rig, for rooms the default cannot stand back from
|
||||
--
|
||||
-- Five blocks is further than a gym is wide, so on one the default eye lands
|
||||
-- outside the map and the border ring crosses the near mon. An arena asks for
|
||||
-- the short rig by name, and the two things that have to hold are that it
|
||||
-- really is close enough to sit in a room, and that it frames the SAME shot
|
||||
-- -- both marks still on their anchors -- so swapping rigs changes the lens
|
||||
-- and nothing about the composition.
|
||||
local function eyeDistance(cam)
|
||||
local dx = cam.eye[1] - cam.focus[1]
|
||||
local dy = cam.eye[2] - cam.focus[2]
|
||||
local dz = cam.eye[3] - cam.focus[3]
|
||||
return math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
end
|
||||
|
||||
T.check(eyeDistance(rig) > 120,
|
||||
"the default long lens stands well back -- that is what sizes the mons")
|
||||
|
||||
BattleCam.reset()
|
||||
local snug = { mid = shot.mid, cam = "wide" }
|
||||
local closeRig = BattleCam.rig(snug, 0)
|
||||
local cd = eyeDistance(closeRig)
|
||||
T.check(cd < 80,
|
||||
("the wide lens is within five cells, so it fits inside a gym: got %.1f")
|
||||
:format(cd))
|
||||
T.check(cd < eyeDistance(rig), "and is nearer than the default")
|
||||
|
||||
local cpx, cpy = project(closeRig, shot.player)
|
||||
local cex, cey = project(closeRig, shot.enemy)
|
||||
T.check(math.abs(cpx - 26) < 1 and math.abs(cpy - 96) < 1,
|
||||
("the wide lens lands the player's mark on the same anchor: (%.2f, %.2f)")
|
||||
:format(cpx, cpy))
|
||||
T.check(math.abs(cex - 124) < 1 and math.abs(cey - 56) < 1,
|
||||
("and the enemy's too: (%.2f, %.2f)"):format(cex, cey))
|
||||
T.check(span(closeRig, shot.player) < span(rig, shot.player),
|
||||
"the mons render smaller on it, which is what it trades for fitting")
|
||||
|
||||
-- an arena picks its rig by name, and anything unnamed gets the default
|
||||
T.eq(BattleCam.rigFor({ cam = "wide" }), BattleCam.RIGS.wide,
|
||||
"an arena that asks for the wide lens gets it")
|
||||
T.eq(BattleCam.rigFor({}), BattleCam.RIGS.tele, "and one that asks for nothing")
|
||||
T.eq(BattleCam.rigFor({ cam = "nonsense" }), BattleCam.RIGS.tele,
|
||||
"as does one that asks for a rig that does not exist")
|
||||
|
||||
-- ------- the pins survive the window
|
||||
--
|
||||
-- The scene renders at the WINDOW's resolution, not the GB's, so the rig's
|
||||
-- field of view is widened by the ratio the window bears to the letterbox.
|
||||
-- What that has to buy is exactness: the letterbox sub-rectangle of the
|
||||
-- widened render must be the framing the rig asked for, or the pics come
|
||||
-- unpinned from the ground by however much it is out.
|
||||
|
||||
for _, win in ipairs({ { 1920, 1080, 7 }, { 640, 576, 4 }, { 1280, 1024, 7 },
|
||||
{ 800, 720, 5 } }) do
|
||||
local pw, ph, s = win[1], win[2], win[3]
|
||||
local lx = math.floor((pw - 160 * s) / 2)
|
||||
local ly = math.floor((ph - 144 * s) / 2)
|
||||
local wide = BattleScene.letterboxFov(rig.fov, ph, s)
|
||||
T.check(wide >= rig.fov - 1e-9,
|
||||
"a window taller than the letterbox needs a wider lens, never a tighter one")
|
||||
local wx, wy = project(rig, shot.player, pw, ph, wide)
|
||||
local gx, gy = (wx - lx) / s, (wy - ly) / s
|
||||
T.check(math.abs(gx - px) < 0.01 and math.abs(gy - py) < 0.01,
|
||||
("%dx%d at scale %d reproduces the GB framing exactly: (%.3f, %.3f) vs "
|
||||
.. "(%.3f, %.3f)"):format(pw, ph, s, gx, gy, px, py))
|
||||
end
|
||||
|
||||
-- ------- the drift
|
||||
--
|
||||
-- With the pics pinned, the drift is not decoration on a backdrop -- it
|
||||
-- moves the mons themselves, and the whole reason it reads as depth is that
|
||||
-- it moves the near one and the far one by DIFFERENT amounts. A backdrop
|
||||
-- that merely slid would move them by the same one.
|
||||
BattleCam.update(BattleCam.PAN_PERIOD / 4)
|
||||
local rig2 = BattleCam.rig(shot, 0)
|
||||
local px2 = project(rig2, shot.player)
|
||||
local ex2 = project(rig2, shot.enemy)
|
||||
T.check(math.abs(px2 - px) > 0.5, "the drift moves the near mark")
|
||||
T.check((px2 - px) * (ex2 - ex) < 0,
|
||||
"and the far one the OTHER WAY -- parallax about a point between them")
|
||||
T.check(math.abs(px2 - px) < 8 and math.abs(ex2 - ex) < 8,
|
||||
"neither is flung across the frame: the mons drift, they do not travel")
|
||||
|
||||
-- very slow: a quarter of the cycle is several seconds, and what it moves in
|
||||
-- one FRAME has to be imperceptible
|
||||
BattleCam.reset()
|
||||
BattleCam.update(1 / 60)
|
||||
local slow = BattleCam.rig(shot, 0)
|
||||
local sx = project(slow, shot.player)
|
||||
T.check(math.abs(sx - px) < 0.2,
|
||||
"one frame of drift moves a mon by a fifth of a pixel")
|
||||
|
||||
-- a placed camera declines the world curve outright: the bend exists to
|
||||
-- drop the horizon away from a walking player, and here it would tip the
|
||||
-- arena floor out from under the two mons pinned to it
|
||||
T.eq(rig.curve, 0, "the battle camera switches the world curve off")
|
||||
|
||||
-- ------- the depth of field is measured off the two marks
|
||||
--
|
||||
-- The slab held sharp is the one the mons are standing in, so the band has
|
||||
-- to be derived from where they landed rather than from a constant -- and it
|
||||
-- has to hold BOTH, which a band narrower than the gap between them would
|
||||
-- not.
|
||||
local BattleDOF = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattleDOF")
|
||||
local focusY, band, range = BattleDOF.bandFor(96, 56, 144)
|
||||
T.check(math.abs(focusY - 76 / 144) < 1e-9,
|
||||
"the band centres between the two marks")
|
||||
T.check(focusY - band < 56 / 144 and focusY + band > 96 / 144,
|
||||
"and is wide enough that both of them are inside it")
|
||||
T.check(range > 0, "with a ramp out of it, so the band edge has no seam")
|
||||
local wide = select(2, BattleDOF.bandFor(120, 30, 144))
|
||||
T.check(wide > band, "marks further apart hold a deeper slab in focus")
|
||||
|
||||
Pipelines.reset()
|
||||
run.release()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user