mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 11:50:50 +02:00
Compare commits
2 Commits
horde-mode
...
add-ledges
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e08f09c27 | |||
| c9d7e26858 |
+59
-1
@@ -1,6 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.0
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- **Ledges are cliffs now.** A ledge used to be taken at face value: a 6px
|
||||
speed bump extruded out of a flat world. But the drawing is the game
|
||||
telling you about terrain -- the side you hop FROM is higher ground --
|
||||
so the world now has real elevation: everything above a hop-down edge
|
||||
stands one ledge-height (6px) up, the lip sits flush with the plateau it
|
||||
rims, and its south face is the cliff drop wearing the same cropped lip
|
||||
art it always wore. Stack ledges and the tiers stack; Pallet Town is the
|
||||
sea-level datum at 0, and the terrain tops out 22px up.
|
||||
|
||||
The subtlety is that ledges do not enclose anything -- every one can be
|
||||
walked around through a gap, so a plateau flood fill would leak through
|
||||
it, meet itself across its own ledge line and conclude an area sits a
|
||||
tier above itself. So lib/Elevation.lua closes the lines before it
|
||||
fills anything. Ledge cells group into runs, and each run is extended
|
||||
along its own axis from either end -- across the gap the player detours
|
||||
through -- until it reaches something that closes it: unwalkable
|
||||
ground, another ledge, or the edge of the world. A run that finds
|
||||
nothing within reach is not a cliff anybody walks around and stays
|
||||
open. The cells that extension crosses become STEPS, and they are the
|
||||
one piece of sloped ground in the world: their tiles grade into 4px and
|
||||
2px treads, so walking a sealed gap climbs three crisp 2px risers where
|
||||
the cliff line crosses it.
|
||||
|
||||
With every line closed, an ordinary flood over the walkable ground cuts
|
||||
the world into areas -- and now they really are the areas enclosed by
|
||||
ledges. Those areas are then levelled against each other in whole
|
||||
tiers, by least squares over the area graph rather than by propagation:
|
||||
each lip votes "the area behind me is one tier over the area in front",
|
||||
areas meeting across ordinary trees or water cast a small
|
||||
same-ground vote, and a stray lip is outvoted by the run it disagrees
|
||||
with instead of tipping half a route. Because each area is a single
|
||||
variable the result is exactly FLAT -- these are plateaus, not a
|
||||
smoothed field. The solve is global and anchored on Pallet's own
|
||||
ground, so two connected maps never disagree about a seam; it runs once
|
||||
inside the build budget, in well under a second for all 43k cells of
|
||||
Kanto, and cuts them into 199 areas rising six tiers from Pallet at 0 to
|
||||
the Route 22/23 highlands at 42px.
|
||||
|
||||
Roughly three quarters of the ledges with standable ground on both
|
||||
sides come out as an exact one-tier drop. The rest -- and the ones
|
||||
whose high side is impassable mountain rock, where there is no plateau
|
||||
to stand on at all -- take the level of the ground around them, which
|
||||
reads as terrain rather than as a mistake, but they are the cases still
|
||||
worth an eye in-game.
|
||||
|
||||
Everything that stands on the ground rides it: characters and NPCs
|
||||
(including ghosts on neighbour maps), grass tufts, flowers, props,
|
||||
buildings (each on one flattened pad -- no terraced floorboards), tree
|
||||
hulls, battle arenas and their camera rig, cast shadows (the sun
|
||||
frustum grows by the tallest base), and the free-roam camera's focus,
|
||||
which eases after the player's ground height instead of pinning to the
|
||||
old flat plane. Cliff skirts fall out of the mesher's own
|
||||
neighbour-difference rule, banded in cell-local height so every crop
|
||||
the flat world drew is byte-identical there. Interiors and any map not
|
||||
connected to Pallet keep the classic flat reading, ledge bumps and all.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ return {
|
||||
ground = 0,
|
||||
water = -2,
|
||||
void = 0,
|
||||
-- doubles as the terrain TIER: on the connected overworld the ground
|
||||
-- above a hop-down edge stands this many pixels up (lib/Elevation.lua)
|
||||
-- and the lip's box sinks by the same amount to sit flush as its rim,
|
||||
-- so retuning it retunes the cliffs with the faces that clothe them
|
||||
ledge = 6,
|
||||
fence = 10,
|
||||
sign = 12,
|
||||
|
||||
+8
-2
@@ -242,13 +242,19 @@ end
|
||||
-- Whether both mons would be in plain view from the battle camera.
|
||||
function BattleArena.clearance(map, arena)
|
||||
local BattleCam = V.require("BattleCam")
|
||||
local ok, rig = pcall(BattleCam.rig, arena, 0)
|
||||
-- rig and rays at the arena's OWN floor height: on solved terrain a
|
||||
-- fight on a plateau stands (and is judged) that many pixels up, or
|
||||
-- the raised ground itself would read as an obstacle over every mon
|
||||
local gy = heightAt(map, arena.player[1], arena.player[2])
|
||||
local ok, rig = pcall(BattleCam.rig, arena, gy)
|
||||
if not (ok and rig and rig.eye) then return true end
|
||||
local eye = rig.eye
|
||||
local H = BattleArena.MON_H
|
||||
for _, mark in ipairs({ arena.player, arena.enemy }) do
|
||||
for _, hy in ipairs({ 1, H * 0.5, H }) do
|
||||
if not lineClear(map, eye, mark[1], hy, mark[2]) then return false end
|
||||
if not lineClear(map, eye, mark[1], gy + hy, mark[2]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
|
||||
+11
-4
@@ -701,12 +701,19 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
|
||||
vote(tx + bw, ty + r)
|
||||
end
|
||||
|
||||
-- One building, ONE base: the model is rigid, so it stands at the
|
||||
-- elevation under its door row and the pad beneath is flattened to
|
||||
-- match -- a house near a ramp must not have terraced floorboards.
|
||||
local my = S.base
|
||||
and S.base[keyOf(tx + math.floor(bw / 2), ty + bh - 1)] or 0
|
||||
|
||||
for r = 0, bh - 1 do
|
||||
for c = 0, bw - 1 do
|
||||
local k = keyOf(tx + c, ty + r)
|
||||
S.shapeAt[k] = shape
|
||||
S.skip[k] = true
|
||||
S.ground[k] = best or false
|
||||
if S.base then S.base[k] = my ~= 0 and my or nil end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -714,10 +721,10 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
|
||||
local out = S.objectQuads
|
||||
for _, q in ipairs(quads) do
|
||||
out[#out + 1] = {
|
||||
{ q[1][1] + mx, q[1][2], q[1][3] + mz },
|
||||
{ q[2][1] + mx, q[2][2], q[2][3] + mz },
|
||||
{ q[3][1] + mx, q[3][2], q[3][3] + mz },
|
||||
{ q[4][1] + mx, q[4][2], q[4][3] + mz },
|
||||
{ q[1][1] + mx, q[1][2] + my, q[1][3] + mz },
|
||||
{ q[2][1] + mx, q[2][2] + my, q[2][3] + mz },
|
||||
{ q[3][1] + mx, q[3][2] + my, q[3][3] + mz },
|
||||
{ q[4][1] + mx, q[4][2] + my, q[4][3] + mz },
|
||||
uv = q.uv, shade = q.shade,
|
||||
-- placements only ever scan the BODY, so a building is always this
|
||||
-- map's own structure: the mesher's edge keep-rules must not eat
|
||||
|
||||
+69
-33
@@ -239,13 +239,22 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
local atlasW = tileset.imageWidth or (perRow * 8)
|
||||
local atlasH = tileset.imageHeight or 48
|
||||
|
||||
-- The terrain's base elevation under a tile (0 on a map without a
|
||||
-- solved field -- every interior), and the ABSOLUTE height of what
|
||||
-- stands there: base + the shape's own extrusion. Side faces are
|
||||
-- derived from neighbour height differences, so once every height is
|
||||
-- measured from the same datum the cliff skirt under a raised cell
|
||||
-- falls out of the same band loop that has always clothed walls.
|
||||
local baseAt = S.base and function(k) return S.base[k] or 0 end
|
||||
or function() return 0 end
|
||||
|
||||
local function heightAt(tx, ty)
|
||||
local k = keyOf(tx, ty)
|
||||
if S.skip[k] then return 0 end
|
||||
if S.skip[k] then return baseAt(k) end
|
||||
local run = S.runs[k]
|
||||
if run then return run.h end
|
||||
if run then return baseAt(k) + run.h end
|
||||
local s = S.shapeAt[k]
|
||||
return s and s.h or 0
|
||||
return baseAt(k) + (s and s.h or 0)
|
||||
end
|
||||
|
||||
-- one atlas-rect UV, optionally cropped to art rows [vTop, vBot] of 8
|
||||
@@ -344,13 +353,17 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
-- blocks half the sky, so the closer a voxel sits to it the less ambient
|
||||
-- light reaches it -- which is what plants a prop on the floor instead
|
||||
-- of leaving it looking pasted over the top.
|
||||
-- `floor` is the terrain base the prop stands on: contact darkening
|
||||
-- measures height above the prop's OWN ground, not above the world
|
||||
-- datum, or every plant on a plateau would lose its feet.
|
||||
local aoProp = { 0, 0, 0, 0 }
|
||||
local function groundShades(c, shade)
|
||||
local function groundShades(c, shade, floor)
|
||||
if type(shade) == "table" then return shade end
|
||||
floor = floor or 0
|
||||
local y1, y2, y3, y4 = c[1][2], c[2][2], c[3][2], c[4][2]
|
||||
if math.min(y1, y2, y3, y4) >= AO_RISE then return shade end
|
||||
if math.min(y1, y2, y3, y4) - floor >= AO_RISE then return shade end
|
||||
for i = 1, 4 do
|
||||
local t = c[i][2] / AO_RISE
|
||||
local t = (c[i][2] - floor) / AO_RISE
|
||||
aoProp[i] = shade * (t >= 1 and 1 or (1 - AO_GROUND * (1 - t)))
|
||||
end
|
||||
return aoProp
|
||||
@@ -456,28 +469,32 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
-- prebuilt prism quads (appended below) carry the art
|
||||
local g = S.ground[k]
|
||||
if g then
|
||||
topQuad(tx * 8, ty * 8, 0, g, 1)
|
||||
-- the claimed tile is still ground at height 0, and water next
|
||||
local b = baseAt(k)
|
||||
topQuad(tx * 8, ty * 8, b, g, 1)
|
||||
-- the claimed tile is still ground at its base, and water next
|
||||
-- door still recesses below it: without the same below-ground
|
||||
-- side bands ordinary ground emits, the two-pixel shoreline
|
||||
-- face is a slit into the sky behind the mesh -- which is
|
||||
-- exactly what a building plot or a sign standing at the
|
||||
-- waterline showed. Same bands, cut from the synthesized
|
||||
-- ground's own art
|
||||
-- ground's own art. Bands run in CELL-LOCAL height (world
|
||||
-- minus base), so the crop is the one the flat world always
|
||||
-- drew, translated up with the terrain.
|
||||
for _, side in ipairs(SIDES) do
|
||||
local nh = heightAt(tx + side[1], ty + side[2])
|
||||
if nh < 0 then
|
||||
if nh < b then
|
||||
local d = side[3]
|
||||
local lat = LATERAL[d]
|
||||
local hl = lat and heightAt(tx + lat[1], ty + lat[2]) or 0
|
||||
local hr = lat and heightAt(tx + lat[3], ty + lat[4]) or 0
|
||||
for band = math.floor(nh / 8), -1 do
|
||||
local y0 = math.max(nh, band * 8)
|
||||
local y1 = math.min(0, band * 8 + 8)
|
||||
if y1 > y0 then
|
||||
sideQuad(d, tx * 8, ty * 8, y0, y1, g,
|
||||
(band * 8 + 8) - y1, (band * 8 + 8) - y0,
|
||||
sideShades(hl, hr, y0, y1, y0 <= nh,
|
||||
local nl = nh - b
|
||||
for band = math.floor(nl / 8), -1 do
|
||||
local ly0 = math.max(nl, band * 8)
|
||||
local ly1 = math.min(0, band * 8 + 8)
|
||||
if ly1 > ly0 then
|
||||
sideQuad(d, tx * 8, ty * 8, b + ly0, b + ly1, g,
|
||||
(band * 8 + 8) - ly1, (band * 8 + 8) - ly0,
|
||||
sideShades(hl, hr, b + ly0, b + ly1, ly0 <= nl,
|
||||
Voxel3D.FACE_SHADE[d]))
|
||||
end
|
||||
end
|
||||
@@ -486,7 +503,11 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
end
|
||||
elseif s then
|
||||
local run = S.runs[k]
|
||||
local h = run and run.h or s.h
|
||||
local b = baseAt(k)
|
||||
-- h is ABSOLUTE (base + extrusion), matching heightAt; hLocal is
|
||||
-- the extrusion alone, which is the space the art bands live in
|
||||
local hLocal = run and run.h or s.h
|
||||
local h = b + hLocal
|
||||
local x0, z0 = tx * 8, ty * 8
|
||||
|
||||
-- top face. A roofed volume gets a GABLE segment: the roof rises
|
||||
@@ -502,9 +523,10 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
-- everything else its own art.
|
||||
if run and run.rise > 0 then
|
||||
local mid = run.extent / 2
|
||||
local runTop = b + run.h -- the facade top, absolute
|
||||
local function gableH(d) -- d = rows north of the south eave
|
||||
local t = d <= mid and d / mid or (run.extent - d) / (run.extent - mid)
|
||||
return run.h + run.rise * math.max(0, math.min(1, t))
|
||||
return runTop + run.rise * math.max(0, math.min(1, t))
|
||||
end
|
||||
local d0 = run.front - ty -- rows from the south edge
|
||||
local hS = gableH(d0)
|
||||
@@ -515,13 +537,13 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
math.floor((1 - rel) * run.roofRows))
|
||||
local roofTile = map:tileAt(tx, run.north + idx)
|
||||
local swY, seY, neY, nwY = hS, hS, hN, hN
|
||||
if heightAt(tx - 1, ty) < run.h then -- west flank: hip
|
||||
swY = math.max(run.h, hS - 8)
|
||||
nwY = math.max(run.h, hN - 8)
|
||||
if heightAt(tx - 1, ty) < runTop then -- west flank: hip
|
||||
swY = math.max(runTop, hS - 8)
|
||||
nwY = math.max(runTop, hN - 8)
|
||||
end
|
||||
if heightAt(tx + 1, ty) < run.h then -- east flank: hip
|
||||
seY = math.max(run.h, hS - 8)
|
||||
neY = math.max(run.h, hN - 8)
|
||||
if heightAt(tx + 1, ty) < runTop then -- east flank: hip
|
||||
seY = math.max(runTop, hS - 8)
|
||||
neY = math.max(runTop, hN - 8)
|
||||
end
|
||||
local u0, u1, v0, v1 = uvRect(roofTile, 0, 8)
|
||||
push({ { x0, swY, z0 + 8 }, { x0 + 8, seY, z0 + 8 },
|
||||
@@ -558,7 +580,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
break
|
||||
end
|
||||
end
|
||||
local row = math.min(ty, front - math.floor(h / 8))
|
||||
local row = math.min(ty, front - math.floor(hLocal / 8))
|
||||
if row < north then
|
||||
-- the whole run folded onto the face: top with the drawn
|
||||
-- row just above it when that row is furniture too (a
|
||||
@@ -583,6 +605,14 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
-- sides: 8px bands wherever the neighbour is lower. Band k spans
|
||||
-- heights [8k, 8k+8) and shows one full tile of art; a partial
|
||||
-- band crops the art rows to match, so nothing ever stretches.
|
||||
-- Bands count in CELL-LOCAL height (world minus this cell's base):
|
||||
-- the fold starts at the cell's own feet wherever the terrain
|
||||
-- raised them, and the crop a 6px lip face has always worn stays
|
||||
-- byte-identical on a flat map. Negative bands are the SKIRT a
|
||||
-- raised cell shows a lower neighbour -- terrain that had no face
|
||||
-- at all before elevation -- and they wear the cell's own art
|
||||
-- from its top row down, the same convention the recessed-water
|
||||
-- shoreline bands established below zero.
|
||||
for _, side in ipairs(SIDES) do
|
||||
local nh = heightAt(tx + side[1], ty + side[2])
|
||||
if nh < h then
|
||||
@@ -593,9 +623,11 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
local lat = LATERAL[d]
|
||||
local hl = lat and heightAt(tx + lat[1], ty + lat[2]) or 0
|
||||
local hr = lat and heightAt(tx + lat[3], ty + lat[4]) or 0
|
||||
for band = math.floor(nh / 8), math.ceil(h / 8) - 1 do
|
||||
local y0 = math.max(nh, band * 8)
|
||||
local y1 = math.min(h, band * 8 + 8)
|
||||
local nl = nh - b
|
||||
for band = math.floor(nl / 8), math.ceil(hLocal / 8) - 1 do
|
||||
local ly0 = math.max(nl, band * 8)
|
||||
local ly1 = math.min(hLocal, band * 8 + 8)
|
||||
local y0, y1 = b + ly0, b + ly1
|
||||
if y1 > y0 then
|
||||
local src, shade = tile, Voxel3D.FACE_SHADE[d]
|
||||
if run then
|
||||
@@ -639,7 +671,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
end
|
||||
end
|
||||
sideQuad(d, x0, z0, y0, y1, src,
|
||||
(band * 8 + 8) - y1, (band * 8 + 8) - y0,
|
||||
(band * 8 + 8) - ly1, (band * 8 + 8) - ly0,
|
||||
sideShades(hl, hr, y0, y1, y0 <= nh, shade))
|
||||
end
|
||||
end
|
||||
@@ -716,7 +748,11 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
-- the neighbour will ever draw that geometry
|
||||
if q.own or outwardOnEdge(q, x0, z0, x1, z1)
|
||||
or keepQuad(x0, z0, x1, z1) then
|
||||
push({ q[1], q[2], q[3], q[4] }, quadUV(q), groundShades(q, q.shade))
|
||||
local fl = S.base
|
||||
and baseAt(keyOf(math.floor(q[1][1] / 8),
|
||||
math.floor(q[1][3] / 8))) or 0
|
||||
push({ q[1], q[2], q[3], q[4] }, quadUV(q),
|
||||
groundShades(q, q.shade, fl))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -760,7 +796,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
for i = 1, 4 do
|
||||
local c, s2 = q[i], sc[i]
|
||||
s2[1] = c[1] + mx
|
||||
s2[2] = c[2]
|
||||
s2[2] = c[2] + (st.my or 0)
|
||||
s2[3] = c[3] + mz
|
||||
end
|
||||
local ok = keepAll
|
||||
@@ -772,7 +808,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
|
||||
ok = keepQuad(x0, z0, x1, z1)
|
||||
end
|
||||
if ok then
|
||||
push(sc, quadUV(q), groundShades(sc, q.shade))
|
||||
push(sc, quadUV(q), groundShades(sc, q.shade, st.my))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+6
-2
@@ -29,6 +29,7 @@ local V = ...
|
||||
|
||||
local Mat4 = V.require("Mat4")
|
||||
local Voxel = V.require("VoxelState")
|
||||
local Elevation = V.require("Elevation")
|
||||
|
||||
local ShadowMap = {}
|
||||
|
||||
@@ -295,7 +296,10 @@ local function fit(cx, cy, vw, vh)
|
||||
local f = sunDir()
|
||||
local view = Mat4.lookAt({ 0, 0, 0 }, f, { 0, 0, -1 })
|
||||
|
||||
local reach = ShadowMap.HEIGHT
|
||||
-- the tallest thing that can cast: the fixed geometry ceiling plus the
|
||||
-- terrain base it may be standing on (0 wherever no elevation solved)
|
||||
local top = ShadowMap.HEIGHT + Elevation.maxBase()
|
||||
local reach = top
|
||||
* math.max(math.abs(ShadowMap.KX), math.abs(ShadowMap.KZ)) + 24
|
||||
local north = groundReach(vh)
|
||||
-- the view widens with distance, so the far ground spans more than the
|
||||
@@ -303,7 +307,7 @@ local function fit(cx, cy, vw, vh)
|
||||
-- frustum's true spread and costs a good deal less resolution
|
||||
local spread = north * 0.5
|
||||
local xs = { cx - vw / 2 - spread, cx + vw / 2 + spread + reach }
|
||||
local ys = { -32, ShadowMap.HEIGHT } -- -32 covers recessed water
|
||||
local ys = { -32, top } -- -32 covers recessed water
|
||||
local zs = { cy - north, cy + vh / 2 + reach }
|
||||
|
||||
local l, r, b, t, zn, zf
|
||||
|
||||
+76
-23
@@ -49,6 +49,7 @@ local Map = require("src.world.Map")
|
||||
local Buildings = V.require("Buildings")
|
||||
local TileShape = V.require("TileShape")
|
||||
local Budget = V.require("BuildBudget")
|
||||
local Elevation = V.require("Elevation")
|
||||
|
||||
local Structures = {}
|
||||
|
||||
@@ -208,6 +209,38 @@ function Structures.forMap(map)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- the terrain's base elevation under every tile ----
|
||||
--
|
||||
-- Elevation solves the ledge-bounded terrain once, globally, at CELL
|
||||
-- granularity (see lib/Elevation.lua); here it lands per TILE so the
|
||||
-- mesher and every quad emitter below read one table. Two wrinkles:
|
||||
--
|
||||
-- * a ledge tile's box KEEPS its authored height but sinks by it --
|
||||
-- base + h then puts the lip's top flush with the high plateau it
|
||||
-- is the rim of, and its exposed south face is exactly the tier
|
||||
-- drop wearing the same cropped lip art it always wore;
|
||||
-- * ring tiles read through baseAtTile's edge clamp, so the border
|
||||
-- apron continues the body's elevation instead of cliffing to 0.
|
||||
--
|
||||
-- Maps without a field (every interior) get no table at all, and every
|
||||
-- consumer's `S.base and ...` guard keeps the classic flat path.
|
||||
local base = nil
|
||||
if Elevation.fieldFor(map.id) then
|
||||
base = {}
|
||||
for ty = y0, y1 do
|
||||
for tx = x0, x1 do
|
||||
Budget.tick()
|
||||
local k = keyOf(tx, ty)
|
||||
if tileAt[k] then
|
||||
local b = Elevation.baseAtTile(map, tx, ty)
|
||||
local s = shapeAt[k]
|
||||
if s and s.class == "ledge" then b = b - (s.h or 0) end
|
||||
if b ~= 0 then base[k] = b end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- buildings: whole sprites voxelized band by band ----
|
||||
--
|
||||
-- Before anything else looks at this grid. A profiled building is a
|
||||
@@ -222,7 +255,7 @@ function Structures.forMap(map)
|
||||
-- still overdraws a walker's feet even though characters stamp over
|
||||
-- terrain.)
|
||||
S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def),
|
||||
hideBareRing = hullRingOnly or nil,
|
||||
hideBareRing = hullRingOnly or nil, base = base,
|
||||
runs = {}, skip = {}, ground = {}, doorFold = {}, objectQuads = {},
|
||||
grassQuads = {}, flowerQuads = {}, roundStamps = {}, figures = {} }
|
||||
Buildings.build(S, map, pixels(tileset), perRow)
|
||||
@@ -1002,7 +1035,7 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
|
||||
ground = tpl.bg or false
|
||||
S.roundStamps[#S.roundStamps + 1] =
|
||||
{ quads = tpl.quads, mx = cx * 16 + 16, mz = cy * 16 + 16,
|
||||
r = 16 }
|
||||
r = 16, my = S.base and S.base[keyOf(cx * 2, cy * 2)] or 0 }
|
||||
end
|
||||
for dy = 0, 3 do
|
||||
for dx = 0, 3 do
|
||||
@@ -1035,7 +1068,8 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
|
||||
end
|
||||
ground = tpl.bg or false
|
||||
S.roundStamps[#S.roundStamps + 1] =
|
||||
{ quads = tpl.quads, mx = cx * 16 + 8, mz = cy * 16 + 8 }
|
||||
{ quads = tpl.quads, mx = cx * 16 + 8, mz = cy * 16 + 8,
|
||||
my = S.base and S.base[keyOf(cx * 2, cy * 2)] or 0 }
|
||||
end
|
||||
-- headless (no pixels): no hull, but still claim the tiles so
|
||||
-- the volume path never boxes a pinned cell. Ground is the
|
||||
@@ -1135,6 +1169,8 @@ function Structures.buildRelief(S, map, region, data, perRow, h)
|
||||
|
||||
local quads = S.objectQuads
|
||||
local wx0, wz0 = region.minX * 8, region.minY * 8
|
||||
-- a relief lies ON the terrain, so the whole slab rides the region's base
|
||||
local by = S.base and S.base[keyOf(region.minX, region.minY)] or 0
|
||||
for py = 0, bh - 1 do
|
||||
for px = 0, bw - 1 do
|
||||
if on(px, py) then
|
||||
@@ -1142,26 +1178,27 @@ function Structures.buildRelief(S, map, region, data, perRow, h)
|
||||
local u = (srcU[i] + 0.5) / atlasW
|
||||
local v = (srcV[i] + 0.5) / atlasH
|
||||
local x, z = wx0 + px, wz0 + py
|
||||
local y0, y1 = by, by + h
|
||||
local function quad(c1, c2, c3, c4, shade)
|
||||
quads[#quads + 1] = { c1, c2, c3, c4, u = u, v = v, shade = shade }
|
||||
end
|
||||
quad({ x, h, z }, { x + 1, h, z }, { x + 1, h, z + 1 },
|
||||
{ x, h, z + 1 }, RELIEF_SHADE.top)
|
||||
quad({ x, y1, z }, { x + 1, y1, z }, { x + 1, y1, z + 1 },
|
||||
{ x, y1, z + 1 }, RELIEF_SHADE.top)
|
||||
if not on(px, py + 1) then
|
||||
quad({ x, 0, z + 1 }, { x + 1, 0, z + 1 }, { x + 1, h, z + 1 },
|
||||
{ x, h, z + 1 }, RELIEF_SHADE.south)
|
||||
quad({ x, y0, z + 1 }, { x + 1, y0, z + 1 }, { x + 1, y1, z + 1 },
|
||||
{ x, y1, z + 1 }, RELIEF_SHADE.south)
|
||||
end
|
||||
if not on(px, py - 1) then
|
||||
quad({ x + 1, 0, z }, { x, 0, z }, { x, h, z },
|
||||
{ x + 1, h, z }, RELIEF_SHADE.north)
|
||||
quad({ x + 1, y0, z }, { x, y0, z }, { x, y1, z },
|
||||
{ x + 1, y1, z }, RELIEF_SHADE.north)
|
||||
end
|
||||
if not on(px - 1, py) then
|
||||
quad({ x, 0, z }, { x, 0, z + 1 }, { x, h, z + 1 },
|
||||
{ x, h, z }, RELIEF_SHADE.side)
|
||||
quad({ x, y0, z }, { x, y0, z + 1 }, { x, y1, z + 1 },
|
||||
{ x, y1, z }, RELIEF_SHADE.side)
|
||||
end
|
||||
if not on(px + 1, py) then
|
||||
quad({ x + 1, 0, z + 1 }, { x + 1, 0, z }, { x + 1, h, z },
|
||||
{ x + 1, h, z + 1 }, RELIEF_SHADE.side)
|
||||
quad({ x + 1, y0, z + 1 }, { x + 1, y0, z }, { x + 1, y1, z },
|
||||
{ x + 1, y1, z + 1 }, RELIEF_SHADE.side)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1208,10 +1245,11 @@ local function bookcaseRank(S, map, tx, northTy, frontTy, capTile)
|
||||
return ns ~= nil and ns.art == "bookcase"
|
||||
end
|
||||
|
||||
local by = S.base and S.base[keyOf(tx, frontTy)] or 0
|
||||
for band = 0, bands - 1 do
|
||||
local tile = band < size and map:tileAt(tx, frontTy - band) or capTile
|
||||
local u0, u1, v0, v1 = uvRect(tile)
|
||||
local y0, y1 = band * 8, band * 8 + 8
|
||||
local y0, y1 = by + band * 8, by + band * 8 + 8
|
||||
quads[#quads + 1] = { { x0, y0, z1 }, { x1, y0, z1 },
|
||||
{ x1, y1, z1 }, { x0, y1, z1 },
|
||||
uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
|
||||
@@ -1330,6 +1368,7 @@ local function stairCell(S, map, data, cx, cy, s)
|
||||
local atlasW = map.tileset.imageWidth or 128
|
||||
local atlasH = map.tileset.imageHeight or 48
|
||||
local quads = S.objectQuads
|
||||
local q0 = #quads
|
||||
local down = s.class == "stair_down_e" or s.class == "stair_down_w"
|
||||
local east = s.class == "stair_e" or s.class == "stair_down_e"
|
||||
local mx, mz = cx * 16, cy * 16
|
||||
@@ -1466,6 +1505,16 @@ local function stairCell(S, map, data, cx, cy, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the whole flight was built from y=0; a raised base lifts it after
|
||||
-- the fact so the geometry above stays in the cell's own space
|
||||
local by = S.base and S.base[keyOf(cx * 2, cy * 2)] or 0
|
||||
if by ~= 0 then
|
||||
for i = q0 + 1, #quads do
|
||||
local q = quads[i]
|
||||
for c = 1, 4 do q[c][2] = q[c][2] + by end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Structures.buildStairs(S, map, x0, x1, y0, y1)
|
||||
@@ -2028,6 +2077,8 @@ function Structures.buildObject(S, map, region, cluster,
|
||||
baseY, support = bs.h, bs
|
||||
end
|
||||
end
|
||||
-- and whatever it stands on, it stands on it at the terrain's base
|
||||
baseY = baseY + (S.base and S.base[keyOf(cluster.minX, cluster.maxY)] or 0)
|
||||
local atlasW = map.tileset.imageWidth or 128
|
||||
local atlasH = map.tileset.imageHeight or 48
|
||||
local quads = S.objectQuads
|
||||
@@ -2291,7 +2342,7 @@ local function buildFigure(S, map, fig, tx, ty, perRow)
|
||||
quads = quads,
|
||||
wx = tx * 8 + minX,
|
||||
wz = ty * 8 + math.floor(lowY / 8) * 8 + 4,
|
||||
y = baseY,
|
||||
y = baseY + (S.base and S.base[keyOf(tx, ty + fig.h - 1)] or 0),
|
||||
}
|
||||
|
||||
-- What each covered tile wears now that he is off it. Only the ART
|
||||
@@ -2436,12 +2487,13 @@ function Structures.buildGrass(S, map, x0, x1, y0, y1, data)
|
||||
templates[tileId] = tpl
|
||||
end
|
||||
local wx, wz = tx * 8, ty * 8
|
||||
local wy = S.base and S.base[k] or 0
|
||||
for _, q in ipairs(tpl) do
|
||||
quads[#quads + 1] = {
|
||||
{ q[1][1] + wx, q[1][2], q[1][3] + wz },
|
||||
{ q[2][1] + wx, q[2][2], q[2][3] + wz },
|
||||
{ q[3][1] + wx, q[3][2], q[3][3] + wz },
|
||||
{ q[4][1] + wx, q[4][2], q[4][3] + wz },
|
||||
{ q[1][1] + wx, q[1][2] + wy, q[1][3] + wz },
|
||||
{ q[2][1] + wx, q[2][2] + wy, q[2][3] + wz },
|
||||
{ q[3][1] + wx, q[3][2] + wy, q[3][3] + wz },
|
||||
{ q[4][1] + wx, q[4][2] + wy, q[4][3] + wz },
|
||||
uv = q.uv, shade = q.shade,
|
||||
}
|
||||
end
|
||||
@@ -2635,12 +2687,13 @@ function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data)
|
||||
templates[tileId] = tpl
|
||||
end
|
||||
local wx, wz = tx * 8, ty * 8
|
||||
local wy = S.base and S.base[k] or 0
|
||||
for _, q in ipairs(tpl) do
|
||||
quads[#quads + 1] = {
|
||||
{ q[1][1] + wx, q[1][2], q[1][3] + wz },
|
||||
{ q[2][1] + wx, q[2][2], q[2][3] + wz },
|
||||
{ q[3][1] + wx, q[3][2], q[3][3] + wz },
|
||||
{ q[4][1] + wx, q[4][2], q[4][3] + wz },
|
||||
{ q[1][1] + wx, q[1][2] + wy, q[1][3] + wz },
|
||||
{ q[2][1] + wx, q[2][2] + wy, q[2][3] + wz },
|
||||
{ q[3][1] + wx, q[3][2] + wy, q[3][3] + wz },
|
||||
{ q[4][1] + wx, q[4][2] + wy, q[4][3] + wz },
|
||||
uv = q.uv, shade = q.shade,
|
||||
}
|
||||
end
|
||||
|
||||
+7
-2
@@ -509,8 +509,13 @@ function Voxel3D.viewProjection(cx, cy, vw, vh)
|
||||
local fov = 2 * math.atan(1 / (2 * focal))
|
||||
Voxel3D.fovY = fov
|
||||
|
||||
local focus = { cx, 0, cy }
|
||||
local eye = { cx, dist * math.cos(a), cy + dist * math.sin(a) }
|
||||
-- The height the orbit looks AT -- the smoothed ground under the
|
||||
-- player's feet (VoxelScene tracks it), so climbing a terrace does not
|
||||
-- slide the walker up the screen at a pitched camera. Zero on flat
|
||||
-- terrain, which is the framing this rig always had.
|
||||
local fy = Voxel3D.focusY or 0
|
||||
local focus = { cx, fy, cy }
|
||||
local eye = { cx, fy + dist * math.cos(a), cy + dist * math.sin(a) }
|
||||
-- exposed for camera-facing billboards (VoxelScene yaws sprites at it)
|
||||
Voxel3D.eye = eye
|
||||
Voxel3D.focus = focus
|
||||
|
||||
+32
-7
@@ -24,11 +24,15 @@ local Sky = V.require("Sky")
|
||||
local Water = V.require("Water")
|
||||
local VoxelGrid = V.require("VoxelGrid")
|
||||
local DayNight = V.require("DayNight")
|
||||
local Elevation = V.require("Elevation")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Map = require("src.world.Map")
|
||||
|
||||
local VoxelScene = {}
|
||||
|
||||
-- the camera's smoothed focus height (see render); nil until first framed
|
||||
local focusHeight = nil
|
||||
|
||||
-- What the active display mode actually paints with.
|
||||
--
|
||||
-- paletteFor hands back a map's RAW SGB zone palette, and that is not what
|
||||
@@ -166,24 +170,33 @@ local YAW = {
|
||||
-- top of it rather than sunk into it. Uses the same bottom-left collision
|
||||
-- tile the engine walks on (Map:cellTile).
|
||||
local function groundAt(map, cellX, cellY)
|
||||
-- The terrain's base under the cell -- 0 wherever no elevation field
|
||||
-- exists (every interior), so the flat world keeps its old answers.
|
||||
local b = Elevation.baseAt(map, cellX, cellY)
|
||||
-- Off the map, cellTile border-extends into the map's borderBlock --
|
||||
-- which on maps ringed with trees is a RAISED tile. The only entity
|
||||
-- ever standing off-map is the player mid seam-step (placed one cell
|
||||
-- before the connection entry), and the ground actually rendered
|
||||
-- there is the departed neighbour's flat walkway: height 0. Without
|
||||
-- this, crossing into such a map hoisted the walker tree-high for
|
||||
-- exactly one step -- the "hops like a ledge" seam bug.
|
||||
if not map:inBounds(cellX, cellY) then return 0 end
|
||||
-- there is the departed neighbour's flat walkway: the border apron's
|
||||
-- own base (baseAt clamps to the nearest body cell, which is how the
|
||||
-- apron is meshed). Without this, crossing into such a map hoisted
|
||||
-- the walker tree-high for exactly one step -- the "hops like a
|
||||
-- ledge" seam bug.
|
||||
if not map:inBounds(cellX, cellY) then return b end
|
||||
local shapes = TileShape.forMap(map)
|
||||
local s = shapes[map:cellTile(cellX, cellY)]
|
||||
if not s then return 0 end
|
||||
if not s then return b end
|
||||
-- a recessed class (water) still supports whatever stands on it; only
|
||||
-- raised ground lifts the model. Stairs never do: the class height is
|
||||
-- the flight's TALL end, but the player enters at floor level and the
|
||||
-- warp fires as they step in -- lifting them onto the geometry read as
|
||||
-- climbing an invisible block
|
||||
if s.art == "stair" then return 0 end
|
||||
return s.h > 0 and s.h or 0
|
||||
if s.art == "stair" then return b end
|
||||
-- on solved terrain a ledge is the high plateau's rim, its lip flush
|
||||
-- with the ground it belongs to: the solver's base IS its top. On a
|
||||
-- flat map it is still the classic 6px bump you stand on top of.
|
||||
if s.class == "ledge" and Elevation.fieldFor(map.id) then return b end
|
||||
return b + (s.h > 0 and s.h or 0)
|
||||
end
|
||||
|
||||
VoxelScene.YAW = YAW
|
||||
@@ -776,6 +789,18 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
|
||||
castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh, atlasFor,
|
||||
water, nbWater)
|
||||
|
||||
-- The camera's focus height chases the ground under the player's feet,
|
||||
-- eased so a 2px terrace tread is a glide rather than a pop; a WARP-
|
||||
-- sized jump (raised route -> interior at 0) snaps instead of swooping
|
||||
-- the whole frame through the floor.
|
||||
local targetY = me and me.gh or 0
|
||||
if focusHeight == nil or math.abs(targetY - focusHeight) > 24 then
|
||||
focusHeight = targetY
|
||||
else
|
||||
focusHeight = focusHeight + (targetY - focusHeight) * 0.12
|
||||
end
|
||||
Voxel3D.focusY = focusHeight
|
||||
|
||||
if not Voxel3D.beginScene(w, h, cx, cy, vw, vh, skyFor(state.map)) then
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -700,6 +700,23 @@ mod.events:on("map.reloaded", function(payload)
|
||||
if mapId then ChunkMesher.invalidate(mapId) end
|
||||
end)
|
||||
|
||||
-- ------- the terrain solver needs the whole map registry
|
||||
--
|
||||
-- lib/Elevation.lua cuts the connected overworld into plateaus, which
|
||||
-- takes every map's blocks and connections at once -- not just the one
|
||||
-- being walked. The engine keeps that registry in main.lua's `Game`,
|
||||
-- which is a LOCAL there and reachable from no mod, so it arrives here
|
||||
-- instead: `mods.loaded` carries the merged dataset, and it is the only
|
||||
-- moment the whole of it is handed over. Without this the solver found
|
||||
-- no data, answered "no field" for every map, and the world stayed as
|
||||
-- flat as it ever was -- silently, which is the part that cost a while.
|
||||
mod.events:on("mods.loaded", function(payload)
|
||||
local data = payload and payload.data
|
||||
if data and data.maps then
|
||||
V.require("Elevation").install(data)
|
||||
end
|
||||
end)
|
||||
|
||||
-- ------- rows come and go, so the menu has to notice
|
||||
--
|
||||
-- OptionsMenu builds its row list ONCE, when it is opened, and then reads
|
||||
|
||||
Reference in New Issue
Block a user