meat/app/level.ts

141 lines
4.6 KiB
TypeScript
Raw Normal View History

2026-08-04 02:09:54 +02:00
import { Color } from "../engine/render/Color"
import type { SkyConfig } from "../engine/render/Sky"
import type { Mesh, Vertex } from "../engine/scene/Mesh"
type Corner = [number, number, number]
/** Axis-aligned solid. Blocks the player horizontally while their feet are
* below `top`; if `standable`, its `top` also counts as ground to land on. */
export type Aabb = {
minX: number
maxX: number
minZ: number
maxZ: number
top: number
standable: boolean
}
/** The playground: geometry split by texture, its collision solids, where the
* NPC stands, and the sky to draw behind it. */
export type Level = {
floor: Mesh
walls: Mesh
crate: Mesh
colliders: Aabb[]
npcPosition: { x: number; y: number; z: number }
sky: SkyConfig
}
const ARENA = 12
const WALL_HEIGHT = 5
const CRATE = { x: -2, z: -2, half: 1, top: 1 }
export function buildLevel(): Level {
const floor = mesh()
quadGrid(floor, [-ARENA, 0, -ARENA], [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [-ARENA, 0, ARENA], 12, 12, 16)
const walls = mesh()
const h = WALL_HEIGHT
// Inward-facing perimeter, no ceiling so the sky shows above.
quadGrid(walls, [-ARENA, 0, -ARENA], [ARENA, 0, -ARENA], [ARENA, h, -ARENA], [-ARENA, h, -ARENA], 12, 2.5, 12)
quadGrid(walls, [ARENA, 0, ARENA], [-ARENA, 0, ARENA], [-ARENA, h, ARENA], [ARENA, h, ARENA], 12, 2.5, 12)
quadGrid(walls, [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [ARENA, h, ARENA], [ARENA, h, -ARENA], 12, 2.5, 12)
quadGrid(walls, [-ARENA, 0, ARENA], [-ARENA, 0, -ARENA], [-ARENA, h, -ARENA], [-ARENA, h, ARENA], 12, 2.5, 12)
const crate = mesh()
box(crate, CRATE.x, CRATE.z, CRATE.half, CRATE.top)
const colliders: Aabb[] = [
wall(-ARENA, ARENA, -ARENA, -ARENA + 1),
wall(-ARENA, ARENA, ARENA - 1, ARENA),
wall(ARENA - 1, ARENA, -ARENA, ARENA),
wall(-ARENA, -ARENA + 1, -ARENA, ARENA),
{
minX: CRATE.x - CRATE.half,
maxX: CRATE.x + CRATE.half,
minZ: CRATE.z - CRATE.half,
maxZ: CRATE.z + CRATE.half,
top: CRATE.top,
standable: true,
},
]
const sky: SkyConfig = {
zenith: Color.rgb(58, 108, 196),
horizon: Color.rgb(178, 198, 226),
sun: Color.rgb(255, 246, 214),
sunDir: { x: 0.3, y: 0.5, z: -0.8 },
sunSize: 0.04,
}
return { floor, walls, crate, colliders, npcPosition: { x: 2, y: 0, z: -1 }, sky }
}
function mesh(): Mesh {
return { vertices: [], indices: [] }
}
function wall(minX: number, maxX: number, minZ: number, maxZ: number): Aabb {
return { minX, maxX, minZ, maxZ, top: WALL_HEIGHT, standable: false }
}
/** A quad tessellated into an n*n grid so affine texture warp stays per-tile.
* Corners run a (uv 0,0) -> b (us,0) -> c (us,vs) -> d (0,vs). */
function quadGrid(m: Mesh, a: Corner, b: Corner, c: Corner, d: Corner, us: number, vs: number, n: number): void {
const base = m.vertices.length
const row = n + 1
for (let i = 0; i <= n; i++) {
const t = i / n
for (let j = 0; j <= n; j++) {
const s = j / n
const wa = (1 - s) * (1 - t)
const wb = s * (1 - t)
const wc = s * t
const wd = (1 - s) * t
m.vertices.push({
pos: {
x: a[0] * wa + b[0] * wb + c[0] * wc + d[0] * wd,
y: a[1] * wa + b[1] * wb + c[1] * wc + d[1] * wd,
z: a[2] * wa + b[2] * wb + c[2] * wc + d[2] * wd,
},
uv: { x: us * s, y: vs * t },
})
}
}
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
const p = base + i * row + j
m.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
}
}
}
/** A box centered at (cx, cz) on the floor: top face plus four sides, one uv
* tile per face. No bottom (never seen). */
function box(m: Mesh, cx: number, cz: number, half: number, top: number): void {
const x0 = cx - half
const x1 = cx + half
const z0 = cz - half
const z1 = cz + half
quad(m, [x0, top, z0], [x1, top, z0], [x1, top, z1], [x0, top, z1])
quad(m, [x0, 0, z0], [x1, 0, z0], [x1, top, z0], [x0, top, z0])
quad(m, [x1, 0, z1], [x0, 0, z1], [x0, top, z1], [x1, top, z1])
quad(m, [x1, 0, z0], [x1, 0, z1], [x1, top, z1], [x1, top, z0])
quad(m, [x0, 0, z1], [x0, 0, z0], [x0, top, z0], [x0, top, z1])
}
function quad(m: Mesh, a: Corner, b: Corner, c: Corner, d: Corner): void {
const base = m.vertices.length
const corners: [Corner, [number, number]][] = [
[a, [0, 0]],
[b, [1, 0]],
[c, [1, 1]],
[d, [0, 1]],
]
for (const [pos, uv] of corners) {
const vertex: Vertex = { pos: { x: pos[0], y: pos[1], z: pos[2] }, uv: { x: uv[0], y: uv[1] } }
m.vertices.push(vertex)
}
m.indices.push(base, base + 1, base + 2, base, base + 2, base + 3)
}