mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:10:49 +02:00
force 3d battle ui, initial victory road updates
This commit is contained in:
@@ -43,6 +43,32 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- **The two HP boxes snap to the window's edges during a staged battle.** The
|
||||
battle screen is 160x144 in the middle of the window and the world is the
|
||||
whole of it, which left both HUD blocks huddled together in the middle of the
|
||||
frame with map showing on either side of them -- a Game Boy screenshot pasted
|
||||
over a diorama rather than the diorama's own furniture. The foe's block now
|
||||
sits against the left edge and the player's against the right, on the same
|
||||
frosted glass, with the same tiles at the same size on the same rows. The
|
||||
pokeball rows and the safari ball count travel with the block whose rows they
|
||||
share. On a window shaped like the GB screen there is nowhere to go and
|
||||
nothing moves.
|
||||
|
||||
The engine draws them into the 160x144 canvas, which clips at its own edges,
|
||||
so the layer is rendered to a texture and composited into the world image --
|
||||
the one surface here that covers the whole window. A driver that cannot do
|
||||
that falls back to the HUD in the frame rather than to no HUD.
|
||||
|
||||
- **`BATTLE LAYOUT` is pinned to `OG` while battles are staged on the map**, and
|
||||
the row comes off the OPTIONS menu with the rows `FULL` owns. The staged shot
|
||||
is composed in the GB's own frame -- the arena camera is solved to put a cell
|
||||
under each pic's feet, and the HUD rects and the intercepted background fill
|
||||
are measured there too -- and `WIDE` re-lays that screen out on a 304x144
|
||||
surface, moving every one of them. Set rather than worked around, on every
|
||||
route in: the options row, hotkey `8`, the mod manager's page, `FULL`'s
|
||||
preset, and a save that arrived with `WIDE` already on. Switching `3D-BTL`
|
||||
off hands the row back with `WIDE` selectable again.
|
||||
|
||||
- **Hotkey `3` walks the angle rungs only and steps over `FULL`.** The key is
|
||||
a display-mode cycler -- it should change the camera and nothing else --
|
||||
and `FULL` reaches in and rewrites four other settings. Landing on it
|
||||
|
||||
@@ -38,6 +38,21 @@ menu.
|
||||
**3D-BTL** is on by default and is independent of **VOXEL**: battles draw
|
||||
on the world whether or not the free-roam camera is pitched over.
|
||||
|
||||
While a battle can be staged on the map — **3D-BTL** on, or **VOXEL** on
|
||||
`FULL`, which owns that row — the engine's **BATTLE LAYOUT** row is set to
|
||||
`OG` and taken off the OPTIONS menu. The staged shot is composed in the Game
|
||||
Boy's own 160x144 frame: the arena camera is solved to put a cell under each
|
||||
of the two pics' feet, and `WIDE` re-lays that screen out on a 304x144
|
||||
surface, which moves every anchor it is solved against. Switching **3D-BTL**
|
||||
off hands the row straight back with `WIDE` selectable again.
|
||||
|
||||
The two HP boxes snap to the window's own edges while a battle is staged —
|
||||
the foe's to the left, yours to the right — instead of huddling in the middle
|
||||
of the frame with map showing either side of them. Same tiles, same size,
|
||||
same rows, on the same frosted glass; only the pair's position changes. On a
|
||||
window shaped like the GB screen there is nowhere to snap to and nothing
|
||||
moves.
|
||||
|
||||
## Where a battle is staged
|
||||
|
||||
The mod looks for the nearest clearing shaped like this, where every `x` is
|
||||
|
||||
+162
-141
@@ -102,6 +102,9 @@ return {
|
||||
fence = 10,
|
||||
sign = 12,
|
||||
wall = 16,
|
||||
-- masonry drawn two courses tall (the Indigo Plateau's rim, the
|
||||
-- badge-check gates): as tall as a statue on its plinth
|
||||
cliff = 32,
|
||||
tree = 16,
|
||||
roof = 28,
|
||||
bed = 7,
|
||||
@@ -2258,6 +2261,165 @@ return {
|
||||
-- south-east). Neither map places them; they belong with the
|
||||
-- wall band above if one ever does.
|
||||
},
|
||||
|
||||
-- The approach to the Pokemon League: two maps, INDIGO_PLATEAU (the
|
||||
-- forecourt the League itself stands on) and ROUTE_23 (the long
|
||||
-- climb up to it, with its badge-check gates). Everything this
|
||||
-- blockset draws is one piece of architecture -- striated rock
|
||||
-- walls, white pillars, and the bird STATUES that line both the
|
||||
-- avenue and the plaza. 29 of its 73 blocks are never placed, so
|
||||
-- every tile named below really is one of these two maps'.
|
||||
--
|
||||
-- A statue is built exactly like the badge gyms' (see GYM above):
|
||||
-- one cell of FIGURE ($10/$12 over $28/$29) standing on one cell of
|
||||
-- PLINTH ($15/$16 the cap over $30/$31 the plaque). 47 of them --
|
||||
-- 12 on INDIGO_PLATEAU, six a side down the avenue, and 35 more
|
||||
-- across ROUTE_23's plaza (blocks $42/$43; the $25/$26 twins that
|
||||
-- stand the same statue on grass are never placed).
|
||||
--
|
||||
-- What the detector made of them is the bug this entry exists for.
|
||||
-- On the avenue the statues stack with NO gap: the plinth's plaque
|
||||
-- row is drawn directly above the next figure's head, so the
|
||||
-- flood-fill joined all six of a column into ONE region 24 tile rows
|
||||
-- tall, and the volume builder's repeat scan read 32px down one half
|
||||
-- of the drawing and 24px down the other. Each row came out as a
|
||||
-- continuous stepped RIDGE of boxes wearing the statue art folded
|
||||
-- onto its south face -- probed INDIGO_PLATEAU tiles (16,12)-(17,35)
|
||||
-- at 32/24 and (22,12)-(23,35) mirrored. ROUTE_23's plaza statues
|
||||
-- stand alone and fared no better: 24px boxes with the figure's top
|
||||
-- row skipped outright.
|
||||
--
|
||||
-- Pinned the gyms' way the ridge becomes statues. The plinth is a
|
||||
-- SOLID 16px `wall` block; the figure is a per-pixel cutout 5 voxels
|
||||
-- deep (the thin `prop` pool) that rides the plinth's top face
|
||||
-- through the authored-box support rule and collapses to the
|
||||
-- plinth's SINGLE cell of footprint -- Structures' wall-support case,
|
||||
-- so the base never marches backwards.
|
||||
--
|
||||
-- The one thing the gyms did not have to deal with: $28/$29 is
|
||||
-- SHARED. The same bird is drawn again at the foot of every white
|
||||
-- pillar, framed there by the pillar's black edge ($25/$26 over that
|
||||
-- same $28/$29, blocks $18/$1B) -- 80 of them, 76 down ROUTE_23 and
|
||||
-- four on INDIGO_PLATEAU (its two outer corners and the pair
|
||||
-- flanking the League's recess). So $25/$26 joins the same pool:
|
||||
-- pinning half a cell would have stood a half-height bird under a
|
||||
-- wall. That in turn is why the pillar itself is pinned -- see the
|
||||
-- last paragraph of `wall`.
|
||||
PLATEAU = {
|
||||
-- ONE 16px course for every piece of masonry here.
|
||||
--
|
||||
-- $03 is the striated rock face, 2436 placements and the bulk of
|
||||
-- both maps. It already read 16 nearly everywhere, but in the
|
||||
-- columns of INDIGO_PLATEAU's rim that stand over a corner pillar
|
||||
-- the repeat scan came out 24 -- a stagger in the plateau's
|
||||
-- skyline (probed `03w24` at tiles (8,0)-(9,2), (12,0)-(13,2) and
|
||||
-- their two mirrors). Authored, the rim is one course.
|
||||
--
|
||||
-- $0D/$0F/$0E are the League's outer wall -- top band, face and
|
||||
-- base. The same three tiles draw the Pokemon League's own
|
||||
-- facade, the long walls flanking the avenue, and every
|
||||
-- badge-check gate down ROUTE_23.
|
||||
--
|
||||
-- $15/$16 + $05/$06 + $30/$31 are the pilaster: cap, shaft, and
|
||||
-- the plaque base. $15/$16 over $30/$31 IS the statue's plinth --
|
||||
-- the artist drew the same stone twice -- which is why one pin
|
||||
-- serves the gate corners and the statues alike.
|
||||
--
|
||||
-- $2E/$2F the white pillar shaft and $20/$21 its cap change no
|
||||
-- HEIGHT: they derive 16 already. What the pin buys is
|
||||
-- `authored`, which is exactly what the prop support rule tests.
|
||||
-- Without it a pillar-foot bird finds no support, drops to ground
|
||||
-- level, and leaves a hole punched clean through the pillar
|
||||
-- (probed, shot, then fixed).
|
||||
-- TWO courses (32px) for the masonry that ENCLOSES both maps -- the
|
||||
-- plateau's rim and every wall around the terraces. It is drawn two
|
||||
-- cells tall, which is exactly the height of a statue on its plinth
|
||||
-- (a 16px plinth under a 16px standee), and that is the read: you are
|
||||
-- walking in a walled compound whose wall matches the statues lining
|
||||
-- it, not a room with a 16px skirting. At one course the rim was a
|
||||
-- kerb you appeared to look over.
|
||||
--
|
||||
-- $03 the striated rock face, $0D/$0F/$0E the League's outer wall
|
||||
-- (top band, face, base -- the same three tiles draw the League's
|
||||
-- facade, the walls flanking the avenue and every badge-check gate
|
||||
-- down ROUTE_23), and $2E/$2F the white pillar shaft with $20/$21 its
|
||||
-- cap. These four groups are the enclosure.
|
||||
cliff = { 3,
|
||||
13, 14, 15,
|
||||
32, 33, 46, 47 },
|
||||
-- ONE course, and it must stay one: $15/$16 + $05/$06 + $30/$31 is
|
||||
-- the pilaster -- cap, shaft, plaque base -- and $15/$16 over $30/$31
|
||||
-- IS the statue's plinth, the artist having drawn the same stone
|
||||
-- twice. Raising it would carry every statue standee up to 48px and
|
||||
-- break the very match the cliff height was chosen for.
|
||||
wall = { 21, 22, 48, 49, 5, 6 },
|
||||
-- The statues, and the same bird at the pillar feet. Black-outline
|
||||
-- segmented: the outline and everything it encloses stay, the sky
|
||||
-- and the paving around them flood away.
|
||||
prop = { 16, 18, 40, 41, 37, 38 },
|
||||
-- Round drawings, one voxel ball per 16x16 cell -- the treatment
|
||||
-- the overworld's canopies and Celadon's hedge take.
|
||||
--
|
||||
-- $07/$08 over $17/$18: the seven canopies planted on pillar tops
|
||||
-- along ROUTE_23 (blocks $44/$45; the $0F block that tiles four of
|
||||
-- them together is never placed). Boxed, the canopy art smeared
|
||||
-- down the whole pillar column beneath it.
|
||||
--
|
||||
-- $2A/$2B over $22/$1D: the boulders strewn across ROUTE_23's
|
||||
-- middle terrace (blocks $02/$13/$16), one per cell and NOT
|
||||
-- walkable. As boxes they sat flat enough to look painted onto
|
||||
-- the path; as balls they read as the obstacles they are.
|
||||
cylinder = { 7, 8, 23, 24,
|
||||
29, 34, 42, 43 },
|
||||
-- The Route 23 sign, one cell, block $48's only placement.
|
||||
-- Unpinned it probed `09w00 0Aw00 / 19w08 1Aw08` -- an 8px stub
|
||||
-- with its board skipped. Same thin plate on a stick every other
|
||||
-- outdoor sign gets.
|
||||
signpost = { 9, 10, 25, 26 },
|
||||
-- The Route 22 gate's roof, drawn from ABOVE and filling
|
||||
-- ROUTE_23's last two block rows ($3D the tiling, $3E/$44 its
|
||||
-- edges, $40/$41 the corners). Art on the TOP face -- the way
|
||||
-- SHIP_PORT's hull is pinned -- rather than folded up a 16px kerb.
|
||||
-- It stands past the map's last walkable row (you warp to
|
||||
-- ROUTE_22_GATE before you reach it), so this is a tidy-up rather
|
||||
-- than a fix.
|
||||
roof = { 61, 62, 64, 65, 68 },
|
||||
-- The ground painted where a pinned figure's cell used to be:
|
||||
-- $23, the pale paving both maps are floored with. It is also the
|
||||
-- white the pillars are drawn in, so the cell a pillar-foot bird
|
||||
-- vacates reads as more pillar -- where the neighbour vote left a
|
||||
-- BLACK hole, having nothing to elect (all four neighbours of that
|
||||
-- cell are wall). On the avenue and the plaza the statues' own
|
||||
-- cells simply keep the paving they stand on.
|
||||
prop_ground = { [16] = 35, [18] = 35, [40] = 35, [41] = 35,
|
||||
[37] = 35, [38] = 35 },
|
||||
-- Deliberately NOT pinned:
|
||||
--
|
||||
-- $14, the water -- 1446 placements, the pond on ROUTE_23's middle
|
||||
-- terrace -- and its bank shading $32/$33/$1F. $14 and $32 are
|
||||
-- two of the three stale-cache water ids the trap is named for,
|
||||
-- but here they are honestly water: TILEANIM_WATER animates $14,
|
||||
-- the pond is real, and $32/$33/$1F are drawn in the TOP half of
|
||||
-- water cells as the waterline itself, so the cell rule dropping
|
||||
-- them to -2 is what the art means. Left to fall through to the
|
||||
-- engine's water set. ($48, the third trap id, is not in this
|
||||
-- atlas at all -- it stops at $45.)
|
||||
--
|
||||
-- $0B/$0C over $1B/$1C, the barred doors: the Pokemon League's two
|
||||
-- and Victory Road's two. They are the tileset's own doorTiles,
|
||||
-- so the door fold already stands them upright in the facade;
|
||||
-- probed 16px identically before and after this entry.
|
||||
--
|
||||
-- $23/$2C/$2D are in the walkable list outright, and $45 is the
|
||||
-- tileset's grassTile, which derives its own standing-tuft pin.
|
||||
--
|
||||
-- The Victory Road entrance is a `buildings` template
|
||||
-- (victory_road_gate, at the bottom of this file): 36x6 tiles at
|
||||
-- ROUTE_23 (0,58), and its ends are built out of $25/$26/$28/$29
|
||||
-- and $15/$16/$05/$06. Buildings claim their tiles before any of
|
||||
-- the above can reach them -- probed `b` class over all 216 of
|
||||
-- them, unchanged by this entry.
|
||||
},
|
||||
},
|
||||
|
||||
-- Buildings whose whole sprite is voxelized band by band (lib/Buildings.lua,
|
||||
@@ -3003,146 +3165,5 @@ return {
|
||||
slab = 4, frontEave = 4, ledge = nil,
|
||||
},
|
||||
},
|
||||
|
||||
-- The approach to the Pokemon League: two maps, INDIGO_PLATEAU (the
|
||||
-- forecourt the League itself stands on) and ROUTE_23 (the long
|
||||
-- climb up to it, with its badge-check gates). Everything this
|
||||
-- blockset draws is one piece of architecture -- striated rock
|
||||
-- walls, white pillars, and the bird STATUES that line both the
|
||||
-- avenue and the plaza. 29 of its 73 blocks are never placed, so
|
||||
-- every tile named below really is one of these two maps'.
|
||||
--
|
||||
-- A statue is built exactly like the badge gyms' (see GYM above):
|
||||
-- one cell of FIGURE ($10/$12 over $28/$29) standing on one cell of
|
||||
-- PLINTH ($15/$16 the cap over $30/$31 the plaque). 47 of them --
|
||||
-- 12 on INDIGO_PLATEAU, six a side down the avenue, and 35 more
|
||||
-- across ROUTE_23's plaza (blocks $42/$43; the $25/$26 twins that
|
||||
-- stand the same statue on grass are never placed).
|
||||
--
|
||||
-- What the detector made of them is the bug this entry exists for.
|
||||
-- On the avenue the statues stack with NO gap: the plinth's plaque
|
||||
-- row is drawn directly above the next figure's head, so the
|
||||
-- flood-fill joined all six of a column into ONE region 24 tile rows
|
||||
-- tall, and the volume builder's repeat scan read 32px down one half
|
||||
-- of the drawing and 24px down the other. Each row came out as a
|
||||
-- continuous stepped RIDGE of boxes wearing the statue art folded
|
||||
-- onto its south face -- probed INDIGO_PLATEAU tiles (16,12)-(17,35)
|
||||
-- at 32/24 and (22,12)-(23,35) mirrored. ROUTE_23's plaza statues
|
||||
-- stand alone and fared no better: 24px boxes with the figure's top
|
||||
-- row skipped outright.
|
||||
--
|
||||
-- Pinned the gyms' way the ridge becomes statues. The plinth is a
|
||||
-- SOLID 16px `wall` block; the figure is a per-pixel cutout 5 voxels
|
||||
-- deep (the thin `prop` pool) that rides the plinth's top face
|
||||
-- through the authored-box support rule and collapses to the
|
||||
-- plinth's SINGLE cell of footprint -- Structures' wall-support case,
|
||||
-- so the base never marches backwards.
|
||||
--
|
||||
-- The one thing the gyms did not have to deal with: $28/$29 is
|
||||
-- SHARED. The same bird is drawn again at the foot of every white
|
||||
-- pillar, framed there by the pillar's black edge ($25/$26 over that
|
||||
-- same $28/$29, blocks $18/$1B) -- 80 of them, 76 down ROUTE_23 and
|
||||
-- four on INDIGO_PLATEAU (its two outer corners and the pair
|
||||
-- flanking the League's recess). So $25/$26 joins the same pool:
|
||||
-- pinning half a cell would have stood a half-height bird under a
|
||||
-- wall. That in turn is why the pillar itself is pinned -- see the
|
||||
-- last paragraph of `wall`.
|
||||
PLATEAU = {
|
||||
-- ONE 16px course for every piece of masonry here.
|
||||
--
|
||||
-- $03 is the striated rock face, 2436 placements and the bulk of
|
||||
-- both maps. It already read 16 nearly everywhere, but in the
|
||||
-- columns of INDIGO_PLATEAU's rim that stand over a corner pillar
|
||||
-- the repeat scan came out 24 -- a stagger in the plateau's
|
||||
-- skyline (probed `03w24` at tiles (8,0)-(9,2), (12,0)-(13,2) and
|
||||
-- their two mirrors). Authored, the rim is one course.
|
||||
--
|
||||
-- $0D/$0F/$0E are the League's outer wall -- top band, face and
|
||||
-- base. The same three tiles draw the Pokemon League's own
|
||||
-- facade, the long walls flanking the avenue, and every
|
||||
-- badge-check gate down ROUTE_23.
|
||||
--
|
||||
-- $15/$16 + $05/$06 + $30/$31 are the pilaster: cap, shaft, and
|
||||
-- the plaque base. $15/$16 over $30/$31 IS the statue's plinth --
|
||||
-- the artist drew the same stone twice -- which is why one pin
|
||||
-- serves the gate corners and the statues alike.
|
||||
--
|
||||
-- $2E/$2F the white pillar shaft and $20/$21 its cap change no
|
||||
-- HEIGHT: they derive 16 already. What the pin buys is
|
||||
-- `authored`, which is exactly what the prop support rule tests.
|
||||
-- Without it a pillar-foot bird finds no support, drops to ground
|
||||
-- level, and leaves a hole punched clean through the pillar
|
||||
-- (probed, shot, then fixed).
|
||||
wall = { 3,
|
||||
13, 14, 15,
|
||||
21, 22, 48, 49, 5, 6,
|
||||
32, 33, 46, 47 },
|
||||
-- The statues, and the same bird at the pillar feet. Black-outline
|
||||
-- segmented: the outline and everything it encloses stay, the sky
|
||||
-- and the paving around them flood away.
|
||||
prop = { 16, 18, 40, 41, 37, 38 },
|
||||
-- Round drawings, one voxel ball per 16x16 cell -- the treatment
|
||||
-- the overworld's canopies and Celadon's hedge take.
|
||||
--
|
||||
-- $07/$08 over $17/$18: the seven canopies planted on pillar tops
|
||||
-- along ROUTE_23 (blocks $44/$45; the $0F block that tiles four of
|
||||
-- them together is never placed). Boxed, the canopy art smeared
|
||||
-- down the whole pillar column beneath it.
|
||||
--
|
||||
-- $2A/$2B over $22/$1D: the boulders strewn across ROUTE_23's
|
||||
-- middle terrace (blocks $02/$13/$16), one per cell and NOT
|
||||
-- walkable. As boxes they sat flat enough to look painted onto
|
||||
-- the path; as balls they read as the obstacles they are.
|
||||
cylinder = { 7, 8, 23, 24,
|
||||
29, 34, 42, 43 },
|
||||
-- The Route 23 sign, one cell, block $48's only placement.
|
||||
-- Unpinned it probed `09w00 0Aw00 / 19w08 1Aw08` -- an 8px stub
|
||||
-- with its board skipped. Same thin plate on a stick every other
|
||||
-- outdoor sign gets.
|
||||
signpost = { 9, 10, 25, 26 },
|
||||
-- The Route 22 gate's roof, drawn from ABOVE and filling
|
||||
-- ROUTE_23's last two block rows ($3D the tiling, $3E/$44 its
|
||||
-- edges, $40/$41 the corners). Art on the TOP face -- the way
|
||||
-- SHIP_PORT's hull is pinned -- rather than folded up a 16px kerb.
|
||||
-- It stands past the map's last walkable row (you warp to
|
||||
-- ROUTE_22_GATE before you reach it), so this is a tidy-up rather
|
||||
-- than a fix.
|
||||
roof = { 61, 62, 64, 65, 68 },
|
||||
-- The ground painted where a pinned figure's cell used to be:
|
||||
-- $23, the pale paving both maps are floored with. It is also the
|
||||
-- white the pillars are drawn in, so the cell a pillar-foot bird
|
||||
-- vacates reads as more pillar -- where the neighbour vote left a
|
||||
-- BLACK hole, having nothing to elect (all four neighbours of that
|
||||
-- cell are wall). On the avenue and the plaza the statues' own
|
||||
-- cells simply keep the paving they stand on.
|
||||
prop_ground = { [16] = 35, [18] = 35, [40] = 35, [41] = 35,
|
||||
[37] = 35, [38] = 35 },
|
||||
-- Deliberately NOT pinned:
|
||||
--
|
||||
-- $14, the water -- 1446 placements, the pond on ROUTE_23's middle
|
||||
-- terrace -- and its bank shading $32/$33/$1F. $14 and $32 are
|
||||
-- two of the three stale-cache water ids the trap is named for,
|
||||
-- but here they are honestly water: TILEANIM_WATER animates $14,
|
||||
-- the pond is real, and $32/$33/$1F are drawn in the TOP half of
|
||||
-- water cells as the waterline itself, so the cell rule dropping
|
||||
-- them to -2 is what the art means. Left to fall through to the
|
||||
-- engine's water set. ($48, the third trap id, is not in this
|
||||
-- atlas at all -- it stops at $45.)
|
||||
--
|
||||
-- $0B/$0C over $1B/$1C, the barred doors: the Pokemon League's two
|
||||
-- and Victory Road's two. They are the tileset's own doorTiles,
|
||||
-- so the door fold already stands them upright in the facade;
|
||||
-- probed 16px identically before and after this entry.
|
||||
--
|
||||
-- $23/$2C/$2D are in the walkable list outright, and $45 is the
|
||||
-- tileset's grassTile, which derives its own standing-tuft pin.
|
||||
--
|
||||
-- The Victory Road entrance is a `buildings` template
|
||||
-- (victory_road_gate, at the bottom of this file): 36x6 tiles at
|
||||
-- ROUTE_23 (0,58), and its ends are built out of $25/$26/$28/$29
|
||||
-- and $15/$16/$05/$06. Buildings claim their tiles before any of
|
||||
-- the above can reach them -- probed `b` class over all 216 of
|
||||
-- them, unchanged by this entry.
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+64
-5
@@ -201,6 +201,23 @@ local function frostRect(rect, box)
|
||||
return fx, fy, math.max(1, fw), math.max(1, fh)
|
||||
end
|
||||
|
||||
-- The same map for a rect that is ALREADY in world-canvas pixels. A HUD
|
||||
-- snapped out to the window's edge has left the GB frame, so it has no GB
|
||||
-- coordinates to be placed from -- see OverworldBattle.snapRects.
|
||||
local function frostRectWorld(rect, box)
|
||||
local kx = frostW / box.pw
|
||||
local ky = frostH / box.ph
|
||||
return rect[1] * kx, rect[2] * ky,
|
||||
math.max(1, rect[3] * kx), math.max(1, rect[4] * ky)
|
||||
end
|
||||
|
||||
-- Which of the two the caller's rects are in. One frost buffer, one panel
|
||||
-- draw, two coordinate spaces: the GB frame (rects land in the 160x144 UI
|
||||
-- canvas) or world pixels (rects land in the window-resolution world image).
|
||||
local function mapper(world)
|
||||
return world and frostRectWorld or frostRect
|
||||
end
|
||||
|
||||
-- ------- the verdict
|
||||
--
|
||||
-- ONE answer for the whole frame, not one per panel. Both HUDs draw in a
|
||||
@@ -211,11 +228,12 @@ end
|
||||
-- the tint below then commits both panels to that reading.
|
||||
local wasDark = false
|
||||
|
||||
function BattleHud.verdict(rects, box)
|
||||
function BattleHud.verdict(rects, box, world)
|
||||
if not (frost and box and box.scale and box.scale > 0) then return false end
|
||||
local toFrost = mapper(world)
|
||||
local darkest = nil
|
||||
for key, rect in pairs(rects) do
|
||||
local fx, fy, fw, fh = frostRect(rect, box)
|
||||
local fx, fy, fw, fh = toFrost(rect, box)
|
||||
local v = sampleLuma(key, fx, fy, fw, fh)
|
||||
if v and (not darkest or v < darkest) then darkest = v end
|
||||
end
|
||||
@@ -230,14 +248,16 @@ function BattleHud.verdict(rects, box)
|
||||
return wasDark
|
||||
end
|
||||
|
||||
-- Draw one HUD panel into the current target, in GB coordinates.
|
||||
-- Draw one HUD panel into the current target, in that target's own
|
||||
-- coordinates: GB ones for the 160x144 UI canvas, world pixels (world = true)
|
||||
-- for a panel laid straight onto the world image.
|
||||
--
|
||||
-- The tint always pushes AWAY from the glyph colour that is about to be
|
||||
-- used, so the contrast is guaranteed rather than hoped for: a dark panel
|
||||
-- gets darker under white text, a bright one brighter under black text.
|
||||
function BattleHud.panel(rect, box, dark)
|
||||
function BattleHud.panel(rect, box, dark, world)
|
||||
if not (frost and box and box.scale and box.scale > 0) then return false end
|
||||
local fx, fy, fw, fh = frostRect(rect, box)
|
||||
local fx, fy, fw, fh = mapper(world)(rect, box)
|
||||
local ok = pcall(function()
|
||||
local quad = love.graphics.newQuad(fx, fy, fw, fh, frostW, frostH)
|
||||
love.graphics.setColor(1, 1, 1, BattleHud.FROST)
|
||||
@@ -330,6 +350,44 @@ function BattleHud.flipGlyphs(w, h, fn)
|
||||
love.graphics.setShader()
|
||||
end
|
||||
|
||||
-- ------- the whole HUD layer as a texture
|
||||
--
|
||||
-- The two blocks do not sit in the same place any more: each is snapped to its
|
||||
-- own side of the WINDOW, which is outside the 160x144 canvas the engine draws
|
||||
-- them in (see OverworldBattle.snapRects). A draw cannot be aimed at two
|
||||
-- places at once, so the layer is rendered ONCE into a GB-sized canvas and
|
||||
-- each block is then blitted out of it as a quad.
|
||||
--
|
||||
-- `dark` runs the ink through the same flip the in-frame HUD uses, here baked
|
||||
-- into the texture rather than composited into the caller's target -- the world
|
||||
-- image the quads land on is a colour canvas, and a flip pass over it would
|
||||
-- whiten the terrain behind the glyphs along with them.
|
||||
local hudLayer = nil
|
||||
|
||||
function BattleHud.layerTexture(w, h, dark, fn)
|
||||
if not hudLayer or hudLayer:getWidth() ~= w or hudLayer:getHeight() ~= h then
|
||||
hudLayer = canvasOf(w, h, "nearest")
|
||||
if not hudLayer then return nil end
|
||||
end
|
||||
local g = love.graphics
|
||||
local prevCanvas = g.getCanvas()
|
||||
local prevBlend, prevAlpha = g.getBlendMode()
|
||||
local ok, err = pcall(function()
|
||||
g.setCanvas(hudLayer)
|
||||
g.clear(0, 0, 0, 0)
|
||||
g.setBlendMode("alpha")
|
||||
g.setColor(1, 1, 1, 1)
|
||||
-- flipGlyphs renders fn into its own scratch layer and composites the
|
||||
-- whitened result into whatever is bound, which is this canvas
|
||||
if dark then BattleHud.flipGlyphs(w, h, fn) else fn() end
|
||||
end)
|
||||
if prevCanvas then g.setCanvas(prevCanvas) else g.setCanvas() end
|
||||
g.setBlendMode(prevBlend or "alpha", prevAlpha)
|
||||
g.setColor(1, 1, 1, 1)
|
||||
if not ok then error(err, 0) end
|
||||
return hudLayer
|
||||
end
|
||||
|
||||
-- The last luminance measured, for the shot driver's log.
|
||||
function BattleHud.lastLuma()
|
||||
local best = nil
|
||||
@@ -344,6 +402,7 @@ function BattleHud.invalidate()
|
||||
frostW, frostH = 0, 0
|
||||
luma = {}
|
||||
wasDark = false
|
||||
layer, hudLayer = nil, nil
|
||||
end
|
||||
|
||||
return BattleHud
|
||||
|
||||
+191
-2
@@ -140,6 +140,54 @@ OverworldBattle.HUD_RECT = {
|
||||
player = { 72, 56, 88, 40 },
|
||||
}
|
||||
|
||||
-- ------- the HUDs, out at the window's own edges
|
||||
--
|
||||
-- The battle screen is 160x144 in the MIDDLE of the window and the world is the
|
||||
-- whole of it. That left both HUD blocks huddled together in the middle of the
|
||||
-- frame with map showing on either side of them, which reads as a Game Boy
|
||||
-- screenshot pasted over a diorama rather than as the diorama's own furniture.
|
||||
--
|
||||
-- So each block is snapped to its own side: the foe's to the left edge of the
|
||||
-- window, the player's to the right. Nothing about either block changes -- same
|
||||
-- tiles, same size, same rows, drawn by the engine's own DrawEnemyHUDAndHPBar
|
||||
-- and DrawPlayerHUDAndHPBar -- only where the pair sits. On a window the shape
|
||||
-- of the GB screen there is nowhere to go and the snap is a no-op.
|
||||
--
|
||||
-- They cannot simply be MOVED there: the engine draws them into the 160x144 UI
|
||||
-- canvas and everything outside it is clipped away. So the layer is rendered to
|
||||
-- a texture and composited into the WORLD image instead, which is the one
|
||||
-- surface in this mode that covers the whole window.
|
||||
|
||||
-- The rows each block is cut out of, full width. Generous on purpose:
|
||||
-- AnimationShakeEnemyHUD nudges the foe's block sideways, a long name reaches
|
||||
-- further than the panel does, and the pokeball rows and the safari ball count
|
||||
-- belong to the block whose rows they sit in. Nothing drawHUDs draws lies
|
||||
-- outside rows 0-96, and the two bands split that between them.
|
||||
OverworldBattle.HUD_BAND = {
|
||||
enemy = { 0, 0, 160, 48 },
|
||||
player = { 0, 48, 160, 48 },
|
||||
}
|
||||
|
||||
-- Where each block lands, in WORLD-canvas pixels: the panel rect the frosted
|
||||
-- glass is cut to, plus the x its band is blitted at.
|
||||
--
|
||||
-- The foe's panel starts at the window's left edge and the player's ends at the
|
||||
-- right one. The vertical is untouched, so both stay on the rows the GB put
|
||||
-- them on. A band's own origin sits outside the window by the panel's inset --
|
||||
-- the couple of pixels a HUD shake can push past the edge are clipped there,
|
||||
-- which is the whole cost of the snap and is invisible.
|
||||
function OverworldBattle.snapRects(shot)
|
||||
local s = shot.scale
|
||||
local e, p = OverworldBattle.HUD_RECT.enemy, OverworldBattle.HUD_RECT.player
|
||||
local ex = -e[1] * s -- foe: panel's left edge to 0
|
||||
local px = shot.pw - (p[1] + p[3]) * s -- player: right edge to the far side
|
||||
local rects = {
|
||||
enemy = { ex + e[1] * s, shot.ly + e[2] * s, e[3] * s, e[4] * s },
|
||||
player = { px + p[1] * s, shot.ly + p[2] * s, p[3] * s, p[4] * s },
|
||||
}
|
||||
return rects, { enemy = ex, player = px }
|
||||
end
|
||||
|
||||
-- ------- the live battle
|
||||
--
|
||||
-- nil when no overworld battle is running. Never more than one: battles do
|
||||
@@ -150,6 +198,13 @@ local function game()
|
||||
return require("src.core.Game")
|
||||
end
|
||||
|
||||
-- Whether this frame's HUDs went out to the window's edges instead of being
|
||||
-- drawn in the GB frame. False whenever the composite could not be made, which
|
||||
-- is what leaves the in-frame HUD as the fallback rather than no HUD at all.
|
||||
local function snapped()
|
||||
return (session and session.snapped) and true or false
|
||||
end
|
||||
|
||||
-- Put the map's cast back. Both lists are handed back by identity, so
|
||||
-- anything that captured one before the battle still sees the same table.
|
||||
local function restoreCast()
|
||||
@@ -174,6 +229,36 @@ local function cullCast(state)
|
||||
state.ghosts = {}
|
||||
end
|
||||
|
||||
-- ------- one right battle layout
|
||||
--
|
||||
-- Everything this file composes is measured in the GB's own 160x144 frame: the
|
||||
-- two ANCHORs the arena camera is solved to put a cell under, the HUD_RECTs
|
||||
-- the frosted panels are cut to, and the full-frame white intercepted to let
|
||||
-- the world through. BATTLE LAYOUT's WIDE lays the same battle out on a
|
||||
-- 304x144 surface (src/battle/WideBattle.lua), which moves every one of those
|
||||
-- -- the mons would stand where no camera was solved for them, and the panels
|
||||
-- would land beside the HUDs they are supposed to be under.
|
||||
--
|
||||
-- So while a fight can be staged on the map there is one right answer, and it
|
||||
-- is SET rather than worked around. The engine reads the option live
|
||||
-- (BattleState:isWideBattleLayout is asked per frame, and Renderer asks the
|
||||
-- top state for its surface the same way), so writing it here lands on the
|
||||
-- battle being pushed as well as every one after it.
|
||||
--
|
||||
-- This is the last line rather than the first: the OPTIONS menu takes the row
|
||||
-- off the list and pins the value while 3D-BTL is on (see main.lua), so a
|
||||
-- player is never offered a switch that gets reverted under them. What reaches
|
||||
-- here is a value that arrived some other way -- a save written before the mod
|
||||
-- was installed, the mod manager's own page, another mod.
|
||||
function OverworldBattle.forceOG(g)
|
||||
g = g or game()
|
||||
local opts = g and g.save and g.save.options
|
||||
if not opts or opts.battleLayout ~= "wide" then return false end
|
||||
opts.battleLayout = "og"
|
||||
if g.writeOptions then pcall(g.writeOptions, g) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Stage a battle triggered from `state`, if this mode can. Returns true when
|
||||
-- a session started -- which is also the only case where anything visible
|
||||
-- changes, so a map with no room for an arena plays exactly the vanilla
|
||||
@@ -189,6 +274,10 @@ function OverworldBattle.begin(state, battle)
|
||||
state.player.surfing)
|
||||
if not (ok and arena) then return false end
|
||||
|
||||
-- the fight is staged from here on, so the layout it is composed for is not
|
||||
-- optional any more (see forceOG)
|
||||
OverworldBattle.forceOG()
|
||||
|
||||
session = { state = state, arena = arena, battle = battle, shot = nil,
|
||||
armed = false, token = 0 }
|
||||
cullCast(state)
|
||||
@@ -276,11 +365,13 @@ function OverworldBattle.update(dt)
|
||||
-- tries again. Rethrowing would hand the whole voxel mode to Pipelines'
|
||||
-- guard, which retires a pipeline for the session.
|
||||
session.shot = nil
|
||||
session.snapped = false
|
||||
session.broken = true
|
||||
V.mod.log:warn("overworld battle scene failed: %s -- this battle draws "
|
||||
.. "on the plain battle background", tostring(shot))
|
||||
return
|
||||
end
|
||||
session.snapped = false
|
||||
if shot and shot.canvas then
|
||||
-- the depth of field is measured off the two marks: the slab in focus is
|
||||
-- the one the mons are standing in, at whatever the drift has done to
|
||||
@@ -295,6 +386,20 @@ function OverworldBattle.update(dt)
|
||||
-- so a panel over a blurred far field is frosted from what is actually
|
||||
-- behind it
|
||||
pcall(BattleHud.build, shot.canvas)
|
||||
-- and then the HUDs go ON that backdrop, snapped out to the window's own
|
||||
-- edges (snapHUDs). Here rather than in the battle's draw for the same
|
||||
-- reason the scene is: it binds a canvas of its own. After the frost, so
|
||||
-- the glass is frosted from the world alone and never from the glyphs
|
||||
-- about to sit on it.
|
||||
local okHud, up = pcall(OverworldBattle.snapHUDs, session.battle, shot)
|
||||
session.snapped = (okHud and up) and true or false
|
||||
-- once per battle, not once per frame: a driver that cannot do this cannot
|
||||
-- do it sixty times a second either, and the fallback is silent and fine
|
||||
if not okHud and not session.hudWarned then
|
||||
session.hudWarned = true
|
||||
V.mod.log:warn("overworld battle HUD snap failed: %s -- the HUDs draw "
|
||||
.. "in the battle frame this battle", tostring(up))
|
||||
end
|
||||
end
|
||||
session.shot = shot
|
||||
end
|
||||
@@ -394,6 +499,7 @@ local texturing = nil
|
||||
|
||||
local texCanvas = {}
|
||||
local innerPics = nil -- captured by install()
|
||||
local innerHUDs = nil -- likewise, for the snapped HUD layer
|
||||
|
||||
local function texCanvasFor(side)
|
||||
local c = texCanvas[side]
|
||||
@@ -695,8 +801,12 @@ function OverworldBattle.install()
|
||||
--
|
||||
-- The HP bar is untouched: it is drawn in its own greens and reds, and
|
||||
-- only an exactly-black set is remapped.
|
||||
local innerHUDs = BattleState.drawHUDs
|
||||
innerHUDs = BattleState.drawHUDs
|
||||
function BattleState:drawHUDs(slide)
|
||||
-- Normally the HUDs have already been drawn this frame, snapped out to the
|
||||
-- window's edges and composited into the world image (snapHUDs). Drawing
|
||||
-- them here as well would show each block twice, once in each place.
|
||||
if self.dramaticShapeShot and snapped() then return end
|
||||
if not (self.dramaticShapeShot and self.dramaticShapeDark) then
|
||||
return innerHUDs(self, slide)
|
||||
end
|
||||
@@ -726,12 +836,91 @@ function OverworldBattle.hudLive(battle, slide)
|
||||
return enemy and true or false, player and true or false
|
||||
end
|
||||
|
||||
-- ------- the snapped composite
|
||||
--
|
||||
-- The engine's own HUD layer, rendered into a texture.
|
||||
--
|
||||
-- One thing is falsified for the render, and it is falsified because this layer
|
||||
-- never reaches the battle's zone pass -- it is composited into the world image,
|
||||
-- outside the frame that pass covers. In the colorized pipeline drawHUDs leaves
|
||||
-- the HP bar's fill as DMG gray for the zone pass to colour by region (#229);
|
||||
-- answered false, it tints its own greens and reds instead, exactly as it does
|
||||
-- on the flat path. The glyphs are pure black either way, which is what the
|
||||
-- flip in BattleHud.layerTexture is measured against.
|
||||
--
|
||||
-- Shadowed on the instance for this call only, the way drawZonePass shadows
|
||||
-- activeBgp: putting the field back to whatever it was (normally nil) lets the
|
||||
-- class method be found again.
|
||||
function OverworldBattle.hudTexture(battle, slide, dark)
|
||||
if not (innerHUDs and battle) then return nil end
|
||||
local had = rawget(battle, "colorMode")
|
||||
battle.colorMode = function() return false end
|
||||
local ok, layer = pcall(BattleHud.layerTexture,
|
||||
BattleScene.GB_W, BattleScene.GB_H, dark,
|
||||
function() innerHUDs(battle, slide) end)
|
||||
battle.colorMode = had
|
||||
return ok and layer or nil
|
||||
end
|
||||
|
||||
-- Draw both HUD blocks into the world image at the window's edges, each on its
|
||||
-- own frosted panel. Returns true when the frame's HUDs are up there and the
|
||||
-- in-frame draw must be skipped; false leaves the battle screen's own HUD
|
||||
-- exactly as it was before any of this existed.
|
||||
--
|
||||
-- Both bands are blitted whether or not that side's HUD is LIVE, because a band
|
||||
-- carries more than the HUD: the pokeball rows of the intro and of an enemy
|
||||
-- faint, and the safari ball count, all draw in these rows and belong at the
|
||||
-- same edge as the block they share it with. The panels are the ones that
|
||||
-- follow hudLive -- frosted glass under nothing is a slab floating in the arena.
|
||||
function OverworldBattle.snapHUDs(battle, shot)
|
||||
if not (battle and shot and shot.canvas and (shot.scale or 0) > 0) then
|
||||
return false
|
||||
end
|
||||
local slide = (battle.introSlide or 0) * 4
|
||||
local rects, bandX = OverworldBattle.snapRects(shot)
|
||||
local enemy, player = OverworldBattle.hudLive(battle, slide)
|
||||
local live = {}
|
||||
if enemy then live.enemy = rects.enemy end
|
||||
if player then live.player = rects.player end
|
||||
-- measured under the SNAPPED rects: the panels are over whatever the world
|
||||
-- shows at the window's edges now, which is not what was behind them in the
|
||||
-- middle of the frame
|
||||
local dark = BattleHud.verdict(live, shot, true)
|
||||
local layer = OverworldBattle.hudTexture(battle, slide, dark)
|
||||
if not layer then return false end
|
||||
|
||||
local g = love.graphics
|
||||
local prevCanvas = g.getCanvas()
|
||||
local prevBlend, prevAlpha = g.getBlendMode()
|
||||
local ok, err = pcall(function()
|
||||
g.setCanvas(shot.canvas)
|
||||
g.setBlendMode("alpha")
|
||||
for _, rect in pairs(live) do BattleHud.panel(rect, shot, dark, true) end
|
||||
g.setColor(1, 1, 1, 1)
|
||||
for side, band in pairs(OverworldBattle.HUD_BAND) do
|
||||
local quad = g.newQuad(band[1], band[2], band[3], band[4],
|
||||
BattleScene.GB_W, BattleScene.GB_H)
|
||||
g.draw(layer, quad, bandX[side] + band[1] * shot.scale,
|
||||
shot.ly + band[2] * shot.scale, 0, shot.scale, shot.scale)
|
||||
end
|
||||
end)
|
||||
if prevCanvas then g.setCanvas(prevCanvas) else g.setCanvas() end
|
||||
g.setBlendMode(prevBlend or "alpha", prevAlpha)
|
||||
g.setColor(1, 1, 1, 1)
|
||||
if not ok then error(err, 0) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Lay the frosted glass down under whichever HUD is about to draw, and
|
||||
-- record which way the glyphs have to flip.
|
||||
--
|
||||
-- The fallback path only: with the HUDs snapped out to the window's edges their
|
||||
-- panels went with them, and there is nothing left inside the GB frame to lay
|
||||
-- glass under.
|
||||
function OverworldBattle.drawHudPanels(battle)
|
||||
local shot = battle.dramaticShapeShot
|
||||
battle.dramaticShapeDark = nil
|
||||
if not shot then return end
|
||||
if not shot or snapped() then return end
|
||||
local slide = (battle.introSlide or 0) * 4
|
||||
local enemy, player = OverworldBattle.hudLive(battle, slide)
|
||||
if not (enemy or player) then return end
|
||||
|
||||
@@ -54,6 +54,12 @@ local FALLBACK_HEIGHTS = {
|
||||
sign = 12,
|
||||
wall = 16,
|
||||
tree = 16,
|
||||
-- masonry drawn TWO courses tall: the Indigo Plateau's rim and the
|
||||
-- badge-check gates down Route 23 are drawn 32px, the same height as a
|
||||
-- statue on its plinth, and read as a step in the terrain rather than a
|
||||
-- room's wall. Same fold as `wall`, twice the height -- and its own
|
||||
-- class because `wall` is 16px for every interior in the game.
|
||||
cliff = 32,
|
||||
roof = 28,
|
||||
cylinder = 16,
|
||||
-- big round scenery: a 2x2-CELL drawing carved as ONE 32px voxel hull
|
||||
@@ -110,6 +116,7 @@ local ART = {
|
||||
ledge = "top",
|
||||
roof = "top",
|
||||
wall = "upright",
|
||||
cliff = "upright",
|
||||
tree = "upright",
|
||||
fence = "upright",
|
||||
sign = "upright",
|
||||
|
||||
@@ -281,9 +281,26 @@ applyFull = function(level)
|
||||
-- half of it is spent. Set rather than forced -- the row is gone from the
|
||||
-- menu while FULL is on, but a save that already had it off gets it on.
|
||||
OverworldBattle.setting:setIndex(1, Game)
|
||||
-- and the battle screen the staged fight is composed for. WIDE re-lays that
|
||||
-- screen out on a 304x144 surface, which moves every anchor the arena camera
|
||||
-- is solved against (OverworldBattle.forceOG); FULL has just switched staged
|
||||
-- fights on, so the layout follows them.
|
||||
OverworldBattle.forceOG(Game)
|
||||
if Game.writeOptions then pcall(Game.writeOptions, Game) end
|
||||
end
|
||||
|
||||
-- Whether a fight can be staged on the map, as far as the OPTIONS menu is
|
||||
-- concerned: 3D-BTL is on, or FULL is selected -- which owns that row and
|
||||
-- switches it on. Deliberately NOT gated on Voxel3D.available(): the engine
|
||||
-- offers a pipeline's row whether or not the hardware can run it
|
||||
-- (Pipelines.rows), so this mode's rows say ON on a machine without a depth
|
||||
-- buffer too, and a menu that claims 3D battles are on must not also offer the
|
||||
-- layout they cannot be drawn in.
|
||||
local function stagedBattles()
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
return OverworldBattle.enabled() or Voxel.isFull(Pipelines.level("voxel"))
|
||||
end
|
||||
|
||||
local SETTINGS = {
|
||||
{ VoxelGrid.setting, "One-pixel wireframe along every voxel edge." },
|
||||
{ WorldCurve.setting,
|
||||
@@ -393,6 +410,11 @@ do
|
||||
-- when the fight starts, so flipping it from inside one would be a
|
||||
-- switch that appeared to do nothing.
|
||||
claim:cycle(self)
|
||||
-- 8 is one of the two ways staged battles get switched on, and they
|
||||
-- pin BATTLE LAYOUT to OG (see the rows hook). The other two keys
|
||||
-- parameterise the pass and leave the layout alone; the guard answers
|
||||
-- for all three, so nothing here has to know which key it was.
|
||||
if stagedBattles() then OverworldBattle.forceOG(self) end
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -442,6 +464,17 @@ mod.hooks:wrap("ui.options.rows", function(next, game, rows)
|
||||
local out = next(game, rows)
|
||||
if type(out) ~= "table" then return out end
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
-- BATTLE LAYOUT is the ENGINE's row, and this is the one place the mod takes
|
||||
-- one away. While a fight can be staged on the map, OG is the only layout it
|
||||
-- can be composed in (OverworldBattle.forceOG), so the value is pinned there
|
||||
-- and the row comes off the list on the same reasoning as the rows FULL owns:
|
||||
-- a row that no longer decides anything is worse than no row. Nothing is
|
||||
-- lost by switching 3D-BTL off -- the row is back, WIDE and all, on the same
|
||||
-- keypress.
|
||||
if stagedBattles() then
|
||||
OverworldBattle.forceOG(game)
|
||||
dropRow(out, "battleLayout")
|
||||
end
|
||||
if Voxel.isFull(Pipelines.level("voxel")) then
|
||||
return dropRow(out, "pipeline:tiltshift")
|
||||
end
|
||||
@@ -457,6 +490,10 @@ mod.events:on("mod.options_changed", function(payload)
|
||||
for _, entry in ipairs(SETTINGS) do
|
||||
if payload.key == entry[1].key then entry[1]:sync(payload.value) end
|
||||
end
|
||||
-- 3D-BTL switched on from the manager's page pins BATTLE LAYOUT exactly as
|
||||
-- the OPTIONS row does. The manager persists its own value; this is the one
|
||||
-- that has to follow it.
|
||||
if stagedBattles() then OverworldBattle.forceOG() end
|
||||
end)
|
||||
|
||||
-- ------- keeping the geometry in step with the world
|
||||
@@ -539,7 +576,7 @@ mod.events:on("map.reloaded", function(payload)
|
||||
if mapId then ChunkMesher.invalidate(mapId) end
|
||||
end)
|
||||
|
||||
-- ------- FULL takes rows off the menu, so the menu has to notice
|
||||
-- ------- rows come and go, so the menu has to notice
|
||||
--
|
||||
-- OptionsMenu builds its row list ONCE, when it is opened, and then reads
|
||||
-- that list every frame. So stepping the VOXEL row onto or off FULL changed
|
||||
@@ -547,25 +584,40 @@ end)
|
||||
-- settings FULL owns stayed visible until the menu was closed and reopened,
|
||||
-- and a player who stepped off FULL could not see the rows come back.
|
||||
--
|
||||
-- Rebuilt in place, and only on a step that crosses FULL: every other rung
|
||||
-- returns the same list, and rebuilding on all of them would rerun every
|
||||
-- mod's ui.options.rows hook once per keypress. The cursor is clamped rather
|
||||
-- than reset, so it stays on the VOXEL row it was just used on instead of
|
||||
-- jumping to the top when the list below it shortens.
|
||||
-- Rebuilt in place, and only on a step that changes the LIST: crossing FULL,
|
||||
-- or toggling 3D-BTL, which is the other row that owns one (BATTLE LAYOUT).
|
||||
-- Every other rung returns the same list, and rebuilding on all of them would
|
||||
-- rerun every mod's ui.options.rows hook once per keypress. The cursor is
|
||||
-- clamped rather than reset, so it stays on the row it was just used on
|
||||
-- instead of jumping to the top when the list below it shortens.
|
||||
do
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
if not OptionsMenu.dramaticShapeFullHook then
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local inner = OptionsMenu.update
|
||||
|
||||
local function idAt(menu, index)
|
||||
local row = menu.rows and menu.rows[index or 1]
|
||||
return type(row) == "table" and row.id or nil
|
||||
end
|
||||
|
||||
function OptionsMenu:update(dt)
|
||||
local before = Pipelines.level("voxel")
|
||||
local hadBattles = OverworldBattle.enabled()
|
||||
local wasOn = idAt(self, self.index)
|
||||
inner(self, dt)
|
||||
local after = Pipelines.level("voxel")
|
||||
if after ~= before
|
||||
and (Voxel.isFull(before) or Voxel.isFull(after)) then
|
||||
local crossedFull = after ~= before
|
||||
and (Voxel.isFull(before) or Voxel.isFull(after))
|
||||
if crossedFull or OverworldBattle.enabled() ~= hadBattles then
|
||||
local rebuilt = OptionsMenu.new(self.game)
|
||||
self.rows = rebuilt.rows
|
||||
-- Follow the row the cursor was ON rather than the slot it was in:
|
||||
-- 3D-BTL takes BATTLE LAYOUT off the list ABOVE itself, which would
|
||||
-- otherwise slide the cursor onto the row under the one just used.
|
||||
for i = 1, #self.rows do
|
||||
if wasOn and idAt(self, i) == wasOn then self.index = i; break end
|
||||
end
|
||||
local cancel = #self.rows + 1
|
||||
if (self.index or 1) > cancel then self.index = cancel end
|
||||
end
|
||||
|
||||
@@ -135,6 +135,62 @@ T.check(not fullIds["DRAMATIC_SHAPE:grid"], "and V-GRID")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:curve"], "and V-CURVE")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:battles"], "and 3D-BTL")
|
||||
|
||||
-- ------- BATTLE LAYOUT is pinned to OG while a fight can be staged on the map
|
||||
--
|
||||
-- The staged battle is composed in the GB's own 160x144 frame: the anchors the
|
||||
-- arena camera is solved to put a cell under, the HUD rects the frosted panels
|
||||
-- are cut to, the full-frame white intercepted to let the world through. WIDE
|
||||
-- lays the same battle out on a 304x144 surface and moves every one of them. So
|
||||
-- the value is set and the ENGINE's row comes off the menu -- the one row this
|
||||
-- mod takes away that is not its own.
|
||||
local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
|
||||
T.eq(Battles.enabled(), true, "3D-BTL is on by default, which is what pins it")
|
||||
|
||||
-- off FULL first: the row on its own has to be enough, and FULL is checked
|
||||
-- separately below
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local layoutGame = {
|
||||
data = Data,
|
||||
save = { options = { battleLayout = "wide", pipelines = {}, modOptions = {} } },
|
||||
mods = { modOptions = {} },
|
||||
writeOptions = function() end,
|
||||
}
|
||||
local pinned = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
layoutGame,
|
||||
{ { id = "battleLayout" }, { id = "tilt" },
|
||||
{ id = "pipeline:voxel" } })
|
||||
local pinnedIds = {}
|
||||
for _, row in ipairs(pinned) do pinnedIds[row.id] = true end
|
||||
T.check(not pinnedIds["battleLayout"],
|
||||
"with staged battles on, BATTLE LAYOUT is off the menu")
|
||||
T.eq(layoutGame.save.options.battleLayout, "og",
|
||||
"and a save that had WIDE is set to OG -- the only layout the shot composes in")
|
||||
|
||||
-- switching 3D-BTL off hands the row straight back, WIDE and all
|
||||
Battles.setting:setIndex(2, layoutGame)
|
||||
T.eq(Battles.enabled(), false, "3D-BTL off")
|
||||
local handedBack = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
layoutGame,
|
||||
{ { id = "battleLayout" }, { id = "tilt" },
|
||||
{ id = "pipeline:voxel" } })
|
||||
local backIds = {}
|
||||
for _, row in ipairs(handedBack) do backIds[row.id] = true end
|
||||
T.check(backIds["battleLayout"], "the engine's row is back on the menu")
|
||||
layoutGame.save.options.battleLayout = "wide"
|
||||
Runtime.call("ui.options.rows", function(_, r) return r end, layoutGame,
|
||||
{ { id = "battleLayout" } })
|
||||
T.eq(layoutGame.save.options.battleLayout, "wide",
|
||||
"and WIDE is left alone once no battle can be staged on the map")
|
||||
|
||||
-- FULL owns the 3D-BTL row, so it pins the layout even with that row switched
|
||||
-- off underneath it
|
||||
Pipelines.setLevel("voxel", VoxelState.FULL_LEVEL)
|
||||
Runtime.call("ui.options.rows", function(_, r) return r end, layoutGame,
|
||||
{ { id = "battleLayout" } })
|
||||
T.eq(layoutGame.save.options.battleLayout, "og",
|
||||
"FULL pins the layout on its own, because it owns the row that would")
|
||||
Battles.setting:setIndex(1, layoutGame)
|
||||
|
||||
-- ------- and off FULL, the rows come back, grouped with the mode
|
||||
--
|
||||
-- The engine splices a pipeline row in beside TILT and lands a mod's own
|
||||
@@ -200,6 +256,29 @@ T.check(rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"and the rows came straight back without reopening the menu")
|
||||
T.check(rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT too")
|
||||
|
||||
-- ------- 3D-BTL owns BATTLE LAYOUT, and takes it off the OPEN menu too
|
||||
--
|
||||
-- The row this one takes away sits ABOVE it in the list, so the cursor has to
|
||||
-- follow the row it was ON rather than the slot it was in -- otherwise the very
|
||||
-- press that switched staged battles on would leave the cursor a row further
|
||||
-- down than the player left it.
|
||||
Battles.setting:setIndex(2, menuGame) -- staged battles off
|
||||
menuGame.save.options.battleLayout = "wide"
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local layoutMenu = OptionsMenu.new(menuGame)
|
||||
T.check(rowIndex(layoutMenu, "battleLayout"),
|
||||
"with staged battles off, the engine's BATTLE LAYOUT row is on the menu")
|
||||
layoutMenu.index = rowIndex(layoutMenu, "DRAMATIC_SHAPE:battles")
|
||||
pressed = { right = true }
|
||||
layoutMenu:update(0)
|
||||
pressed = {}
|
||||
T.eq(Battles.setting:get(), true, "the step switched staged battles on")
|
||||
T.check(not rowIndex(layoutMenu, "battleLayout"),
|
||||
"and BATTLE LAYOUT left the open menu with the same keypress")
|
||||
T.eq(menuGame.save.options.battleLayout, "og", "pinned to OG on the way out")
|
||||
T.eq(layoutMenu.index, rowIndex(layoutMenu, "DRAMATIC_SHAPE:battles"),
|
||||
"with the cursor still on the row the player just used")
|
||||
|
||||
-- level 2 is the "15" rung: any rung that is not FULL, so the settings the
|
||||
-- preset owns are back on the menu
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
@@ -1428,6 +1507,66 @@ 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")
|
||||
|
||||
-- ------- the HUDs are snapped to the window's own edges
|
||||
--
|
||||
-- The battle screen is 160x144 in the middle of the window and the world is the
|
||||
-- whole of it, which left both HUD blocks huddled in the middle of the frame
|
||||
-- with map on either side of them. Each is snapped to its own side instead: the
|
||||
-- foe's to the left edge, the player's to the right. Measured in world-canvas
|
||||
-- pixels, because that is the surface they are composited into -- the GB canvas
|
||||
-- they are drawn in cannot reach past its own 160 columns.
|
||||
local hudShot = { lx = 100, ly = 12, scale = 3, pw = 1000, ph = 500 }
|
||||
local hudRects, bandX = Battles.snapRects(hudShot)
|
||||
local hudRect = Battles.HUD_RECT
|
||||
|
||||
T.eq(hudRects.enemy[1], 0, "the foe's panel starts at the window's left edge")
|
||||
T.eq(hudRects.player[1] + hudRects.player[3], hudShot.pw,
|
||||
"and the player's ends at the right one")
|
||||
T.check(hudRects.enemy[1] < hudShot.lx,
|
||||
"so the foe's block has left the letterbox it used to sit in")
|
||||
T.check(hudRects.player[1] > hudShot.lx + hudRect.player[1] * hudShot.scale,
|
||||
"and the player's has gone the other way")
|
||||
|
||||
-- the vertical is untouched: both blocks stay on the rows the GB put them on
|
||||
T.eq(hudRects.enemy[2], hudShot.ly + hudRect.enemy[2] * hudShot.scale,
|
||||
"the foe's block keeps its own rows")
|
||||
T.eq(hudRects.player[2], hudShot.ly + hudRect.player[2] * hudShot.scale,
|
||||
"and so does the player's")
|
||||
|
||||
-- and their size: a block is the same GB tiles at the same scale as the rest of
|
||||
-- the art, moved and not stretched
|
||||
T.eq(hudRects.enemy[3], hudRect.enemy[3] * hudShot.scale,
|
||||
"a block is its own width at the frame's scale")
|
||||
T.eq(hudRects.player[4], hudRect.player[4] * hudShot.scale,
|
||||
"and its own height")
|
||||
|
||||
-- the band each block is cut out of is placed so the block lands on the rect
|
||||
-- above; that is what the panel and the glyphs agreeing depends on
|
||||
T.eq(bandX.enemy + hudRect.enemy[1] * hudShot.scale, hudRects.enemy[1],
|
||||
"the foe's band is offset so its block lands on its panel")
|
||||
T.eq(bandX.player + hudRect.player[1] * hudShot.scale, hudRects.player[1],
|
||||
"and the player's likewise")
|
||||
|
||||
-- a window the shape of the GB screen has nowhere to snap TO, and the player's
|
||||
-- block already ends at column 160, so it must not move at all
|
||||
local snug = { lx = 0, ly = 0, scale = 4, pw = 160 * 4, ph = 144 * 4 }
|
||||
local snugRects = Battles.snapRects(snug)
|
||||
T.eq(snugRects.player[1], hudRect.player[1] * snug.scale,
|
||||
"on a GB-shaped window the player's block stays exactly where it was")
|
||||
T.eq(snugRects.enemy[1], 0, "and the foe's is flush with a left edge it already met")
|
||||
|
||||
-- the bands together cover every row drawHUDs draws into (0-96: the two HUDs,
|
||||
-- the pokeball rows and the safari ball count) and never overlap, so nothing it
|
||||
-- draws is dropped or shown twice
|
||||
local e, p = Battles.HUD_BAND.enemy, Battles.HUD_BAND.player
|
||||
T.eq(e[2], 0, "the foe's band starts at the top of the frame")
|
||||
T.eq(e[2] + e[4], p[2], "the player's picks up exactly where it ends")
|
||||
T.check(p[2] + p[4] >= 96, "and together they reach the bottom of the HUD rows")
|
||||
T.check(e[2] + e[4] <= hudRect.player[2],
|
||||
"the split falls between the two blocks, so neither is cut in half")
|
||||
T.eq(e[1], 0, "the bands are full width")
|
||||
T.eq(e[3], 160, "so a shaken HUD or a long name is carried out with its block")
|
||||
|
||||
Pipelines.reset()
|
||||
run.release()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user