import { Color } from "../engine/render/Color" import type { CloudLayer, SkyConfig } from "../engine/render/Sky" import type { Mesh } from "../engine/scene/Mesh" import { Terrain } from "../engine/scene/Terrain" 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: a flat-floored room dropped into the center of a big open * landscape. Geometry is split by texture, plus the collision solids, where the * NPC stands, the heightfield the outdoor ground + player share, and the sky. */ export type Level = { floor: Mesh walls: Mesh crate: Mesh ground: Mesh colliders: Aabb[] npcPosition: { x: number; y: number; z: number } terrain: Terrain sky: SkyConfig } const ARENA = 12 const WALL_HEIGHT = 4 /** How deep the perimeter walls are. Thick enough to read as solid walls (and to * give the doorway real jambs); their outer faces sit flush with the room edge, * so they eat into the interior, not the terrain. */ const WALL_THICKNESS = 1.5 const CRATE = { x: -2, z: -2, half: 1, height: 1 } /** Z-bias lifting the stone floor above the terrain skirt that laps under the * room edge (see `buildLevel`). Big enough to beat depth precision, too small * to see. */ const FLOOR_LIFT = 0.02 /** The world around the room: a flat clearing the size of the room (`inner`), * rolling hills beyond, ramping into very high peaks at the `outer` rim ~20x * the room across. Tune freely -- crank `peakHeight` for taller mountains, * `outer` for a bigger world. */ const TERRAIN: Terrain = { inner: ARENA, outer: ARENA * 20, blend: 12, amplitude: 5, frequency: 0.14, peakHeight: 90, peakFrequency: 0.05, peakStart: 0.45, } /** Outdoor ground mesh resolution. A fixed grid over the whole world, so cell * size (and cost) is set here, not by the world's size: bigger `outer` gives * chunkier terrain, not more triangles. `GROUND_UV` sets texture tiles/unit. */ const GROUND_DIVISIONS = 56 const GROUND_UV = 0.25 /** The two cloud styles; swap which one the sky uses in `buildLevel`. * `basicCumulus` is cheap flat puffs; `fancyCumulus` is the pricier * heightfield-shaded, domain-warped version with faked volume. */ export const basicCumulus: CloudLayer = { kind: "basic", color: Color.rgb(248, 250, 255), coverage: 0.5, scale: 0.9, speed: 0.5, edge: 0.005, } export const fancyCumulus: CloudLayer = { kind: "fancy", color: Color.rgb(250, 251, 255), coverage: 0.5, scale: 0.6, speed: 0.5, edge: 0.02, warp: 0.4, relief: 7, } export function buildLevel(): Level { // Flat room floor, lifted a hair above the terrain's clearing (y 0). The // outdoor grid's cells straddle the room boundary and lap under the floor's // edges; this small z-bias keeps the flat stone floor winning the depth test // there instead of z-fighting the grass. The step is invisible at the doorway. const floor = mesh() const fy = FLOOR_LIFT quad(floor, [-ARENA, fy, -ARENA], [ARENA, fy, -ARENA], [ARENA, fy, ARENA], [-ARENA, fy, ARENA], 12, 12) // The big surrounding landscape, with a hole where the room sits. const ground = Terrain.ground(TERRAIN, GROUND_DIVISIONS, GROUND_UV) const walls = mesh() const h = WALL_HEIGHT const t = WALL_THICKNESS // Three thick perimeter walls, outer faces flush with the room edge; the north // (-Z) side is left open onto the world. No ceiling, so the sky shows above. slab(walls, -ARENA, ARENA, ARENA - t, ARENA, 0, h, 0.5) // south (+Z) slab(walls, ARENA - t, ARENA, -ARENA, ARENA - t, 0, h, 0.5) // east (+X) slab(walls, -ARENA, -ARENA + t, -ARENA, ARENA - t, 0, h, 0.5) // west (-X) // Crate on the flat room floor. const crate = mesh() box(crate, CRATE.x, CRATE.z, CRATE.half, 0, CRATE.height) const colliders: Aabb[] = [ wall(-ARENA, ARENA, ARENA - t, ARENA), wall(ARENA - t, ARENA, -ARENA, ARENA - t), wall(-ARENA, -ARENA + t, -ARENA, ARENA - t), { minX: CRATE.x - CRATE.half, maxX: CRATE.x + CRATE.half, minZ: CRATE.z - CRATE.half, maxZ: CRATE.z + CRATE.half, top: CRATE.height, 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, clouds: basicCumulus, } const npcPosition = { x: 2, y: 0, z: -1 } return { floor, walls, crate, ground, colliders, npcPosition, terrain: TERRAIN, sky } } function mesh(): Mesh { return { vertices: [], indices: [] } } /** A perimeter wall collider: blocks from the sides, and `standable` so you can * jump up and land on its top (given enough JUMP_SPEED to clear WALL_HEIGHT). */ function wall(minX: number, maxX: number, minZ: number, maxZ: number): Aabb { return { minX, maxX, minZ, maxZ, top: WALL_HEIGHT, standable: true } } /** One flat quad (two tris). Corners run a (uv 0,0) -> b (us,0) -> c (us,vs) -> * d (0,vs); `us`/`vs` set how many texture tiles span it. No subdivision is * needed -- texturing is perspective-correct, so a single quad looks right at * any size. */ function quad(m: Mesh, a: Corner, b: Corner, c: Corner, d: Corner, us: number, vs: number): void { const base = m.vertices.length m.vertices.push( { pos: { x: a[0], y: a[1], z: a[2] }, uv: { x: 0, y: 0 } }, { pos: { x: b[0], y: b[1], z: b[2] }, uv: { x: us, y: 0 } }, { pos: { x: c[0], y: c[1], z: c[2] }, uv: { x: us, y: vs } }, { pos: { x: d[0], y: d[1], z: d[2] }, uv: { x: 0, y: vs } }, ) m.indices.push(base, base + 1, base + 2, base, base + 2, base + 3) } /** An axis-aligned box from (x0,z0)-(x1,z1), y0..y1: four sides + top, no bottom * (never seen from below). `tpu` = texture tiles per world unit, so every face * tiles at the same density whatever its size. Used for the thick walls. */ function slab(m: Mesh, x0: number, x1: number, z0: number, z1: number, y0: number, y1: number, tpu: number): void { const dx = (x1 - x0) * tpu const dz = (z1 - z0) * tpu const dy = (y1 - y0) * tpu quad(m, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], dx, dz) quad(m, [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], dx, dy) quad(m, [x1, y0, z1], [x0, y0, z1], [x0, y1, z1], [x1, y1, z1], dx, dy) quad(m, [x0, y0, z1], [x0, y0, z0], [x0, y1, z0], [x0, y1, z1], dz, dy) quad(m, [x1, y0, z0], [x1, y0, z1], [x1, y1, z1], [x1, y1, z0], dz, dy) } /** A box centered at (cx, cz), rising `height` units from `base`: top face plus * four sides, one uv tile per face. No bottom (never seen). */ function box(m: Mesh, cx: number, cz: number, half: number, base: number, height: number): void { const x0 = cx - half const x1 = cx + half const z0 = cz - half const z1 = cz + half const y0 = base const y1 = base + height quad(m, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], 1, 1) quad(m, [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], 1, 1) quad(m, [x1, y0, z1], [x0, y0, z1], [x0, y1, z1], [x1, y1, z1], 1, 1) quad(m, [x1, y0, z0], [x1, y0, z1], [x1, y1, z1], [x1, y1, z0], 1, 1) quad(m, [x0, y0, z1], [x0, y0, z0], [x0, y1, z0], [x0, y1, z1], 1, 1) }