2026-08-04 02:09:54 +02:00
|
|
|
import { Color } from "../engine/render/Color"
|
2026-08-24 15:17:53 +02:00
|
|
|
import type { Material } from "../engine/render/Material"
|
2026-08-04 03:00:58 +02:00
|
|
|
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
2026-08-24 15:17:53 +02:00
|
|
|
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"
|
2026-08-08 14:15:32 +02:00
|
|
|
import { Boulder } from "./actors/Boulder"
|
|
|
|
|
import { Bush } from "./actors/Bush"
|
2026-08-24 15:17:53 +02:00
|
|
|
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"
|
2026-08-08 14:15:32 +02:00
|
|
|
import type { Textures } from "./textures"
|
2026-08-04 02:09:54 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
type Materials = {
|
|
|
|
|
floor: Material
|
2026-08-07 16:58:24 +02:00
|
|
|
grass: Material
|
|
|
|
|
bark: Material
|
|
|
|
|
birch: Material
|
|
|
|
|
leaf: Material
|
|
|
|
|
needle: Material
|
|
|
|
|
rock: Material
|
|
|
|
|
flower: Material
|
2026-08-24 15:17:53 +02:00
|
|
|
wall: Material
|
|
|
|
|
crate: Material
|
|
|
|
|
npc: Material
|
|
|
|
|
frog: Material
|
|
|
|
|
bee: Material
|
|
|
|
|
robin: Material
|
2026-08-04 15:05:02 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
type WeightedTree = {
|
|
|
|
|
definition: PrefabDefinition<Tree>
|
|
|
|
|
weight: number
|
|
|
|
|
}
|
2026-08-07 19:11:43 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
type MobSpawn = {
|
|
|
|
|
definition: ActorDefinition<MobState, Ground>
|
|
|
|
|
count: number
|
|
|
|
|
minScale: number
|
|
|
|
|
maxScale: number
|
|
|
|
|
grounded: boolean
|
|
|
|
|
phase: (random: () => number) => number
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ARENA = 12
|
2026-08-04 02:39:20 +02:00
|
|
|
const WALL_HEIGHT = 4
|
2026-08-04 13:09:26 +02:00
|
|
|
const WALL_THICKNESS = 1.5
|
2026-08-04 12:51:07 +02:00
|
|
|
const CRATE = { x: -2, z: -2, half: 1, height: 1 }
|
|
|
|
|
const FLOOR_LIFT = 0.02
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
const TERRAIN_CONFIG: RollingTerrainConfig = {
|
2026-08-04 12:51:07 +02:00
|
|
|
inner: ARENA,
|
2026-08-08 14:15:32 +02:00
|
|
|
outer: ARENA * 10,
|
2026-08-04 12:51:07 +02:00
|
|
|
blend: 12,
|
|
|
|
|
amplitude: 5,
|
|
|
|
|
frequency: 0.14,
|
2026-08-08 14:15:32 +02:00
|
|
|
peakHeight: 0,
|
2026-08-04 12:51:07 +02:00
|
|
|
peakFrequency: 0.05,
|
|
|
|
|
peakStart: 0.45,
|
|
|
|
|
}
|
2026-08-24 15:17:53 +02:00
|
|
|
const TERRAIN = Terrain.rolling(TERRAIN_CONFIG)
|
2026-08-04 12:51:07 +02:00
|
|
|
|
2026-08-08 14:15:32 +02:00
|
|
|
const TREE_COUNT = 50
|
2026-08-04 13:53:59 +02:00
|
|
|
const TREE_SEED = 0x5EED
|
2026-08-08 14:15:32 +02:00
|
|
|
const TREE_REACH = 1
|
|
|
|
|
const BOULDER_COUNT = 50
|
2026-08-04 14:02:12 +02:00
|
|
|
const BOULDER_SEED = 0xB0142
|
2026-08-08 14:15:32 +02:00
|
|
|
const BOULDER_REACH = 1
|
|
|
|
|
const BUSH_COUNT = 50
|
2026-08-04 23:03:55 +02:00
|
|
|
const BUSH_SEED = 0xB554
|
2026-08-08 14:15:32 +02:00
|
|
|
const BUSH_REACH = 1
|
|
|
|
|
const FLOWER_COUNT = 50
|
2026-08-04 23:03:55 +02:00
|
|
|
const FLOWER_SEED = 0xF10E
|
|
|
|
|
const FLOWER_REACH = 0.3
|
2026-08-24 15:17:53 +02:00
|
|
|
const FLOWER_STYLES: FlowerStyle[] = [Flower.white, Flower.red, Flower.yellow]
|
2026-08-08 14:15:32 +02:00
|
|
|
const FROG_COUNT = 20
|
|
|
|
|
const BEE_COUNT = 20
|
|
|
|
|
const ROBIN_COUNT = 20
|
2026-08-07 12:49:13 +02:00
|
|
|
const MOB_SEED = 0x30B
|
2026-08-08 14:15:32 +02:00
|
|
|
const MOB_REACH = 1
|
2026-08-04 15:05:02 +02:00
|
|
|
const CHUNK_GRID = 12
|
|
|
|
|
const TERRAIN_SUBDIV = 5
|
2026-08-04 12:51:07 +02:00
|
|
|
const GROUND_UV = 0.25
|
2026-08-04 02:39:20 +02:00
|
|
|
|
2026-08-04 03:00:58 +02:00
|
|
|
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-24 15:17:53 +02:00
|
|
|
/** Concrete playground data assembled through engine-owned level mechanisms. */
|
|
|
|
|
export function buildLevel(textures: Textures): RuntimeLevel<Ground> {
|
|
|
|
|
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)
|
2026-08-04 02:09:54 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
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),
|
2026-08-04 02:09:54 +02:00
|
|
|
{
|
2026-08-24 15:17:53 +02:00
|
|
|
shape: "box",
|
2026-08-04 02:09:54 +02:00
|
|
|
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,
|
|
|
|
|
},
|
2026-08-24 15:17:53 +02:00
|
|
|
{
|
|
|
|
|
shape: "circle",
|
|
|
|
|
x: npcPosition.x,
|
|
|
|
|
z: npcPosition.z,
|
|
|
|
|
radius: 0.5,
|
|
|
|
|
top: Infinity,
|
|
|
|
|
standable: false,
|
|
|
|
|
},
|
2026-08-04 02:09:54 +02:00
|
|
|
]
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
|
2026-08-04 02:09:54 +02:00
|
|
|
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-08 14:15:32 +02:00
|
|
|
clouds: fancyCumulus,
|
|
|
|
|
skybox: { texture: textures.skybox },
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-08-04 12:51:07 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function createMaterials(textures: Textures): Materials {
|
|
|
|
|
return {
|
|
|
|
|
floor: { texture: textures.floor, cull: false },
|
2026-08-07 16:58:24 +02:00
|
|
|
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 },
|
2026-08-24 15:17:53 +02:00
|
|
|
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 },
|
2026-08-07 16:58:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
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) {
|
2026-08-04 13:53:59 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-08-24 15:17:53 +02:00
|
|
|
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,
|
2026-08-04 13:53:59 +02:00
|
|
|
}
|
2026-08-24 15:17:53 +02:00
|
|
|
trees.push(Prefab.place(definition, tree))
|
2026-08-04 13:53:59 +02:00
|
|
|
}
|
2026-08-04 15:05:02 +02:00
|
|
|
return trees
|
2026-08-04 13:53:59 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function placeBoulders(definition: PrefabDefinition<Boulder>): 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) {
|
2026-08-04 14:02:12 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-08-24 15:17:53 +02:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-08-04 14:02:12 +02:00
|
|
|
}
|
2026-08-04 15:05:02 +02:00
|
|
|
return boulders
|
2026-08-04 14:02:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function placeBushes(definition: PrefabDefinition<Bush>): 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) {
|
2026-08-04 23:03:55 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-08-24 15:17:53 +02:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-08-04 23:03:55 +02:00
|
|
|
}
|
|
|
|
|
return bushes
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function placeFlowers(definition: PrefabDefinition<Flower>): 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) {
|
2026-08-04 23:03:55 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-08-24 15:17:53 +02:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-08-04 23:03:55 +02:00
|
|
|
}
|
|
|
|
|
return flowers
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function placeMobs(spawns: MobSpawn[]): Mob[] {
|
|
|
|
|
const random = mulberry(MOB_SEED)
|
|
|
|
|
const maxDistance = TERRAIN_CONFIG.outer * MOB_REACH
|
2026-08-07 12:49:13 +02:00
|
|
|
const mobs: Mob[] = []
|
2026-08-24 15:17:53 +02:00
|
|
|
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++
|
2026-08-07 12:49:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return mobs
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
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 }
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function weightedTree(
|
|
|
|
|
definitions: WeightedTree[],
|
|
|
|
|
roll: number,
|
|
|
|
|
): PrefabDefinition<Tree> {
|
|
|
|
|
let cumulative = 0
|
|
|
|
|
for (const entry of definitions) {
|
|
|
|
|
cumulative += entry.weight
|
|
|
|
|
if (roll < cumulative) {
|
|
|
|
|
return entry.definition
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return definitions[definitions.length - 1].definition
|
2026-08-04 02:39:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
function wall(
|
|
|
|
|
minX: number,
|
|
|
|
|
maxX: number,
|
|
|
|
|
minZ: number,
|
|
|
|
|
maxZ: number,
|
|
|
|
|
): BoxCollider {
|
|
|
|
|
return {
|
|
|
|
|
shape: "box",
|
|
|
|
|
minX,
|
|
|
|
|
maxX,
|
|
|
|
|
minZ,
|
|
|
|
|
maxZ,
|
|
|
|
|
top: WALL_HEIGHT,
|
|
|
|
|
standable: true,
|
|
|
|
|
}
|
2026-08-04 13:09:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|