import { Color } from "../engine/render/Color" import type { Material } from "../engine/render/Material" import type { CloudLayer, SkyConfig } from "../engine/render/Sky" import { ChunkBuilder } from "../engine/render/ChunkBuilder" import { Actor, type ActorDefinition } from "../engine/scene/Actor" import { Mesh } from "../engine/scene/Mesh" import { MeshBuilder } from "../engine/scene/MeshBuilder" import { Prefab, type PlacedPrefab, type Prefab as PrefabDefinition, } from "../engine/scene/Prefab" import type { BoxCollider, Collider } from "../engine/world/Collider" import { Level, type Level as RuntimeLevel } from "../engine/world/Level" import { Terrain, type RollingTerrainConfig, type Terrain as Ground, } from "../engine/world/Terrain" import { Boulder } from "./actors/Boulder" import { Bush } from "./actors/Bush" import { Flower, type FlowerStyle } from "./actors/Flower" import type { Mob, MobState } from "./actors/Mob" import type { Tree } from "./actors/Tree" import { Bee } from "./actors/mobs/Bee" import { Frog } from "./actors/mobs/Frog" import { Robin } from "./actors/mobs/Robin" import { Birch } from "./actors/trees/Birch" import { Oak } from "./actors/trees/Oak" import { Spruce } from "./actors/trees/Spruce" import type { Textures } from "./textures" type Materials = { floor: Material grass: Material bark: Material birch: Material leaf: Material needle: Material rock: Material flower: Material wall: Material crate: Material npc: Material frog: Material bee: Material robin: Material } type WeightedTree = { definition: PrefabDefinition weight: number } type MobSpawn = { definition: ActorDefinition count: number minScale: number maxScale: number grounded: boolean phase: (random: () => number) => number } const ARENA = 12 const WALL_HEIGHT = 4 const WALL_THICKNESS = 1.5 const CRATE = { x: -2, z: -2, half: 1, height: 1 } const FLOOR_LIFT = 0.02 const TERRAIN_CONFIG: RollingTerrainConfig = { inner: ARENA, outer: ARENA * 10, blend: 12, amplitude: 5, frequency: 0.14, peakHeight: 0, peakFrequency: 0.05, peakStart: 0.45, } const TERRAIN = Terrain.rolling(TERRAIN_CONFIG) const TREE_COUNT = 50 const TREE_SEED = 0x5EED const TREE_REACH = 1 const BOULDER_COUNT = 50 const BOULDER_SEED = 0xB0142 const BOULDER_REACH = 1 const BUSH_COUNT = 50 const BUSH_SEED = 0xB554 const BUSH_REACH = 1 const FLOWER_COUNT = 50 const FLOWER_SEED = 0xF10E const FLOWER_REACH = 0.3 const FLOWER_STYLES: FlowerStyle[] = [Flower.white, Flower.red, Flower.yellow] const FROG_COUNT = 20 const BEE_COUNT = 20 const ROBIN_COUNT = 20 const MOB_SEED = 0x30B const MOB_REACH = 1 const CHUNK_GRID = 12 const TERRAIN_SUBDIV = 5 const GROUND_UV = 0.25 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, } /** Concrete playground data assembled through engine-owned level mechanisms. */ export function buildLevel(textures: Textures): RuntimeLevel { const materials = createMaterials(textures) const floor = Mesh.create() MeshBuilder.quad( floor, [-ARENA, FLOOR_LIFT, -ARENA], [ARENA, FLOOR_LIFT, -ARENA], [ARENA, FLOOR_LIFT, ARENA], [-ARENA, FLOOR_LIFT, ARENA], 12, 12, ) const walls = Mesh.create() const thickness = WALL_THICKNESS MeshBuilder.slab( walls, -ARENA, ARENA, ARENA - thickness, ARENA, 0, WALL_HEIGHT, 0.5, ) MeshBuilder.slab( walls, ARENA - thickness, ARENA, -ARENA, ARENA - thickness, 0, WALL_HEIGHT, 0.5, ) MeshBuilder.slab( walls, -ARENA, -ARENA + thickness, -ARENA, ARENA - thickness, 0, WALL_HEIGHT, 0.5, ) const crate = Mesh.create() MeshBuilder.box(crate, CRATE.x, CRATE.z, CRATE.half, 0, CRATE.height) const npcPosition = { x: 2, y: 0, z: -1 } const staticColliders: Collider[] = [ wall(-ARENA, ARENA, ARENA - thickness, ARENA), wall(ARENA - thickness, ARENA, -ARENA, ARENA - thickness), wall(-ARENA, -ARENA + thickness, -ARENA, ARENA - thickness), { shape: "box", 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, }, { shape: "circle", x: npcPosition.x, z: npcPosition.z, radius: 0.5, top: Infinity, standable: false, }, ] const treeDefinitions: WeightedTree[] = [ { definition: Oak.create(materials.bark, materials.leaf), weight: 0.4 }, { definition: Spruce.create(materials.bark, materials.needle), weight: 0.32, }, { definition: Birch.create(materials.birch, materials.leaf), weight: 0.28 }, ] const props = [ ...placeTrees(treeDefinitions), ...placeBoulders(Boulder.create(materials.rock)), ...placeBushes(Bush.create(materials.leaf)), ...placeFlowers(Flower.create(materials.flower)), ] for (const prop of props) { if (prop.collider !== null) { staticColliders.push(prop.collider) } } const chunks = ChunkBuilder.build( { minX: TERRAIN.minX, minZ: TERRAIN.minZ, maxX: TERRAIN.maxX, maxZ: TERRAIN.maxZ, columns: CHUNK_GRID, rows: CHUNK_GRID, bakeCell(near, far, cell) { const ground = near.mesh(materials.grass) far.use(materials.grass, ground) Terrain.patch( TERRAIN, ground, cell.x0, cell.z0, cell.x1, cell.z1, TERRAIN_SUBDIV, TERRAIN_SUBDIV, GROUND_UV, (x, z) => Math.max(Math.abs(x), Math.abs(z)) >= ARENA, ) }, }, props, ) const frog = Frog.create(materials.frog) const bee = Bee.create(materials.bee) const robin = Robin.create(materials.robin) const actors = placeMobs([ { definition: frog, count: FROG_COUNT, minScale: 0.5, maxScale: 0.85, grounded: true, phase: () => 0, }, { definition: bee, count: BEE_COUNT, minScale: 0.5, maxScale: 0.8, grounded: false, phase: (random) => random() * 10, }, { definition: robin, count: ROBIN_COUNT, minScale: 0.4, maxScale: 0.65, grounded: true, phase: () => 0, }, ]) 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: fancyCumulus, skybox: { texture: textures.skybox }, } return Level.create({ terrain: TERRAIN, actorWorld: TERRAIN, actors, staticColliders, staticGroups: [ { mesh: floor, material: materials.floor }, { mesh: walls, material: materials.wall }, { mesh: crate, material: materials.crate }, ], chunks, billboards: [ { position: npcPosition, size: { x: 1.1, y: 1.5 }, material: materials.npc, }, ], sky, }) } function createMaterials(textures: Textures): Materials { return { floor: { texture: textures.floor, cull: false }, grass: { texture: textures.grass, cull: true }, bark: { texture: textures.bark, cull: true }, birch: { texture: textures.birch, cull: true }, leaf: { texture: textures.leaf, cull: true }, needle: { texture: textures.needle, cull: true }, rock: { texture: textures.rock, cull: true }, flower: { texture: textures.flower, cull: false }, wall: { texture: textures.wall, cull: false }, crate: { texture: textures.crate, cull: false }, npc: { texture: textures.npc, cull: false }, frog: { texture: textures.frog, cull: false }, bee: { texture: textures.bee, cull: false }, robin: { texture: textures.robin, cull: false }, } } function placeTrees(definitions: WeightedTree[]): PlacedPrefab[] { const random = mulberry(TREE_SEED) const maxDistance = TERRAIN_CONFIG.outer * TREE_REACH const trees: PlacedPrefab[] = [] for ( let guard = 0; trees.length < TREE_COUNT && guard < TREE_COUNT * 20; guard++ ) { const point = scatterPoint(random, maxDistance, 5, 3) if (point === null) { continue } const definition = weightedTree(definitions, random()) const tree: Tree = { position: { x: point.x, y: TERRAIN.heightAt(point.x, point.z), z: point.z, }, growth: 0.08 + random() * 0.92, seed: (random() * 0xFFFFFFFF) | 0, } trees.push(Prefab.place(definition, tree)) } return trees } function placeBoulders(definition: PrefabDefinition): PlacedPrefab[] { const random = mulberry(BOULDER_SEED) const maxDistance = TERRAIN_CONFIG.outer * BOULDER_REACH const boulders: PlacedPrefab[] = [] for ( let guard = 0; boulders.length < BOULDER_COUNT && guard < BOULDER_COUNT * 20; guard++ ) { const point = scatterPoint(random, maxDistance, 4, 2) if (point === null) { continue } boulders.push( Prefab.place(definition, { position: { x: point.x, y: TERRAIN.heightAt(point.x, point.z), z: point.z, }, radius: 0.35 + random() * random() * 2.2, seed: (random() * 0xFFFFFFFF) | 0, }), ) } return boulders } function placeBushes(definition: PrefabDefinition): PlacedPrefab[] { const random = mulberry(BUSH_SEED) const maxDistance = TERRAIN_CONFIG.outer * BUSH_REACH const bushes: PlacedPrefab[] = [] for ( let guard = 0; bushes.length < BUSH_COUNT && guard < BUSH_COUNT * 20; guard++ ) { const point = scatterPoint(random, maxDistance, 3, 2) if (point === null) { continue } bushes.push( Prefab.place(definition, { position: { x: point.x, y: TERRAIN.heightAt(point.x, point.z), z: point.z, }, size: 0.8 + random(), seed: (random() * 0xFFFFFFFF) | 0, }), ) } return bushes } function placeFlowers(definition: PrefabDefinition): PlacedPrefab[] { const random = mulberry(FLOWER_SEED) const maxDistance = TERRAIN_CONFIG.outer * FLOWER_REACH const flowers: PlacedPrefab[] = [] for ( let guard = 0; flowers.length < FLOWER_COUNT && guard < FLOWER_COUNT * 20; guard++ ) { const point = scatterPoint(random, maxDistance, 2, 1) if (point === null) { continue } flowers.push( Prefab.place(definition, { position: { x: point.x, y: TERRAIN.heightAt(point.x, point.z), z: point.z, }, style: FLOWER_STYLES[(random() * FLOWER_STYLES.length) | 0], size: 0.28 + random() * 0.22, seed: (random() * 0xFFFFFFFF) | 0, }), ) } return flowers } function placeMobs(spawns: MobSpawn[]): Mob[] { const random = mulberry(MOB_SEED) const maxDistance = TERRAIN_CONFIG.outer * MOB_REACH const mobs: Mob[] = [] for (const spawn of spawns) { let placed = 0 for ( let guard = 0; placed < spawn.count && guard < spawn.count * 20; guard++ ) { const point = scatterPoint(random, maxDistance, 3, 2) if (point === null) { continue } const y = TERRAIN.heightAt(point.x, point.z) const scale = spawn.minScale + random() * (spawn.maxScale - spawn.minScale) const heading = random() * Math.PI * 2 const state: MobState = { home: { x: point.x, y, z: point.z }, position: { x: point.x, y, z: point.z }, heading, scale, seed: (random() * 0xFFFFFFFF) | 0, vx: 0, vz: 0, vy: 0, timer: random() * 1.5, phase: spawn.phase(random), grounded: spawn.grounded, } mobs.push(Actor.create(spawn.definition, state)) placed++ } } return mobs } function scatterPoint( random: () => number, maxDistance: number, clearance: number, flatMargin: number, ): { x: number; z: number } | null { const angle = random() * Math.PI * 2 const distance = ARENA + clearance + random() * (maxDistance - ARENA - clearance) const x = Math.cos(angle) * distance const z = Math.sin(angle) * distance return Math.max(Math.abs(x), Math.abs(z)) < TERRAIN_CONFIG.inner + flatMargin ? null : { x, z } } function weightedTree( definitions: WeightedTree[], roll: number, ): PrefabDefinition { let cumulative = 0 for (const entry of definitions) { cumulative += entry.weight if (roll < cumulative) { return entry.definition } } return definitions[definitions.length - 1].definition } function wall( minX: number, maxX: number, minZ: number, maxZ: number, ): BoxCollider { return { shape: "box", minX, maxX, minZ, maxZ, top: WALL_HEIGHT, standable: true, } } function mulberry(seed: number): () => number { let state = seed >>> 0 return () => { state = (state + 0x6D2B79F5) | 0 let value = Math.imul(state ^ (state >>> 15), 1 | state) value ^= value + Math.imul(value ^ (value >>> 7), 61 | value) return ((value ^ (value >>> 14)) >>> 0) / 4294967296 } }