2026-08-04 02:09:54 +02:00
|
|
|
import { Color } from "../engine/render/Color"
|
2026-08-04 03:00:58 +02:00
|
|
|
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
2026-08-04 02:39:20 +02:00
|
|
|
import type { Mesh } from "../engine/scene/Mesh"
|
2026-08-04 12:51:07 +02:00
|
|
|
import { Terrain } from "../engine/scene/Terrain"
|
2026-08-04 02:09:54 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 12:51:07 +02:00
|
|
|
/** 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. */
|
2026-08-04 02:09:54 +02:00
|
|
|
export type Level = {
|
|
|
|
|
floor: Mesh
|
|
|
|
|
walls: Mesh
|
|
|
|
|
crate: Mesh
|
2026-08-04 12:51:07 +02:00
|
|
|
ground: Mesh
|
2026-08-04 02:09:54 +02:00
|
|
|
colliders: Aabb[]
|
|
|
|
|
npcPosition: { x: number; y: number; z: number }
|
2026-08-04 12:51:07 +02:00
|
|
|
terrain: Terrain
|
2026-08-04 02:09:54 +02:00
|
|
|
sky: SkyConfig
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ARENA = 12
|
2026-08-04 02:39:20 +02:00
|
|
|
const WALL_HEIGHT = 4
|
2026-08-04 12:51:07 +02:00
|
|
|
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
|
2026-08-04 02:39:20 +02:00
|
|
|
|
2026-08-04 03:00:58 +02:00
|
|
|
/** 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,
|
2026-08-04 12:51:07 +02:00
|
|
|
edge: 0.005,
|
2026-08-04 03:00:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 02:09:54 +02:00
|
|
|
export function buildLevel(): Level {
|
2026-08-04 12:51:07 +02:00
|
|
|
// 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.
|
2026-08-04 02:09:54 +02:00
|
|
|
const floor = mesh()
|
2026-08-04 12:51:07 +02:00
|
|
|
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)
|
2026-08-04 02:09:54 +02:00
|
|
|
|
|
|
|
|
const walls = mesh()
|
|
|
|
|
const h = WALL_HEIGHT
|
2026-08-04 12:51:07 +02:00
|
|
|
// Three inward-facing walls; the north (-Z) side is left open onto the world.
|
|
|
|
|
// No ceiling, so the sky shows above.
|
|
|
|
|
quad(walls, [ARENA, 0, ARENA], [-ARENA, 0, ARENA], [-ARENA, h, ARENA], [ARENA, h, ARENA], 12, 2.5)
|
|
|
|
|
quad(walls, [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [ARENA, h, ARENA], [ARENA, h, -ARENA], 12, 2.5)
|
|
|
|
|
quad(walls, [-ARENA, 0, ARENA], [-ARENA, 0, -ARENA], [-ARENA, h, -ARENA], [-ARENA, h, ARENA], 12, 2.5)
|
2026-08-04 02:09:54 +02:00
|
|
|
|
2026-08-04 12:51:07 +02:00
|
|
|
// Crate on the flat room floor.
|
2026-08-04 02:09:54 +02:00
|
|
|
const crate = mesh()
|
2026-08-04 12:51:07 +02:00
|
|
|
box(crate, CRATE.x, CRATE.z, CRATE.half, 0, CRATE.height)
|
2026-08-04 02:09:54 +02:00
|
|
|
|
|
|
|
|
const colliders: Aabb[] = [
|
|
|
|
|
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,
|
2026-08-04 12:51:07 +02:00
|
|
|
top: CRATE.height,
|
2026-08-04 02:09:54 +02:00
|
|
|
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,
|
2026-08-04 11:39:39 +02:00
|
|
|
clouds: basicCumulus,
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 12:51:07 +02:00
|
|
|
const npcPosition = { x: 2, y: 0, z: -1 }
|
|
|
|
|
|
|
|
|
|
return { floor, walls, crate, ground, colliders, npcPosition, terrain: TERRAIN, sky }
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 12:51:07 +02:00
|
|
|
/** 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 {
|
2026-08-04 02:09:54 +02:00
|
|
|
const base = m.vertices.length
|
2026-08-04 12:51:07 +02:00
|
|
|
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)
|
2026-08-04 02:39:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 12:51:07 +02:00
|
|
|
/** 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 {
|
2026-08-04 02:09:54 +02:00
|
|
|
const x0 = cx - half
|
|
|
|
|
const x1 = cx + half
|
|
|
|
|
const z0 = cz - half
|
|
|
|
|
const z1 = cz + half
|
2026-08-04 12:51:07 +02:00
|
|
|
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)
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|