feat: trees
This commit is contained in:
parent
61389f091c
commit
9aa0ec6a3f
10 changed files with 311 additions and 10 deletions
33
AGENTS.md
33
AGENTS.md
|
|
@ -56,7 +56,9 @@ rules live in `.agents/rules/*.md`.
|
|||
`Mesh` (indexed tris), `Sprite` (Y-axis billboard), `Terrain` (procedural
|
||||
heightfield around the room: flat clearing in the center, rolling hills, tall
|
||||
edge peaks. `Terrain.ground` builds the outdoor mesh with a hole for the room;
|
||||
`Terrain.height` is the shared ground-height sampler for the player).
|
||||
`Terrain.height` is the shared ground-height sampler for the player), `Tree`
|
||||
(procedural low-poly oak/spruce geometry, sapling..full via a `growth` knob;
|
||||
`Tree.build` appends into shared trunk + foliage meshes).
|
||||
- `app/` — browser glue.
|
||||
- `main.ts` — game loop, input, preset switching, canvas blit, FPS meter.
|
||||
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes
|
||||
|
|
@ -64,8 +66,10 @@ rules live in `.agents/rules/*.md`.
|
|||
- `level.ts` — builds the playground: a flat stone-floored room (three thick
|
||||
walls via `slab`, north side open) in the center of a big grassy `Terrain`
|
||||
world (~20x across).
|
||||
Per-texture meshes incl. the outdoor grass `ground`, `Aabb` colliders, NPC
|
||||
position, `Terrain` config + `GROUND_DIVISIONS`/`GROUND_UV`, sky/cloud config.
|
||||
Per-texture meshes incl. the outdoor grass `ground` and a scattered forest
|
||||
(`scatterTrees` → `trunks`/`oakFoliage`/`spruceFoliage`, `TREE_COUNT`/`_SEED`/
|
||||
`_REACH`), `Aabb` colliders (incl. grown-tree trunks), NPC position, `Terrain`
|
||||
config + `GROUND_DIVISIONS`/`GROUND_UV`, sky/cloud config.
|
||||
The stone floor is lifted by `FLOOR_LIFT` (a z-bias) so it stays clean over the
|
||||
terrain skirt that laps under the room edges. Room surfaces are single flat
|
||||
quads -- no subdivision needed since texturing is perspective-correct; ground
|
||||
|
|
@ -79,8 +83,9 @@ rules live in `.agents/rules/*.md`.
|
|||
`#fps` meter div (styled inline).
|
||||
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
|
||||
writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`.
|
||||
- `assets/` — generated `floor/grass/wall/crate/npc` PNGs (`floor` = room stone,
|
||||
`grass` = outdoor ground). Swap for real art anytime;
|
||||
- `assets/` — generated `floor/grass/bark/leaf/needle/wall/crate/npc` PNGs
|
||||
(`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` = tree
|
||||
trunk/oak/spruce). Swap for real art anytime;
|
||||
filenames are the contract.
|
||||
- `server/` — Bun server stub. `shared/` — isomorphic slot.
|
||||
|
||||
|
|
@ -88,7 +93,8 @@ rules live in `.agents/rules/*.md`.
|
|||
|
||||
`Player.update` → build `Camera` → `Camera.viewProjection` →
|
||||
`Sky.render` (fills color + resets depth, replaces a clear) →
|
||||
`Rasterizer.draw` ground, floor, walls, crate (one call per texture) →
|
||||
`Rasterizer.draw` ground, floor, walls, crate, tree trunks, oak foliage, spruce
|
||||
foliage (one call per texture) →
|
||||
`Sprite.billboard(npc)` drawn via `Rasterizer.draw` →
|
||||
`Framebuffer.quantize` → `present` (integer-scale, letterboxed blit;
|
||||
`imageSmoothingEnabled` follows `upscaleFilter`).
|
||||
|
|
@ -131,6 +137,21 @@ Both are exported presets in `app/level.ts`; the active one is set in
|
|||
branching in the cloud shader. Cost scales with sky resolution — fine at
|
||||
`standard`, heavy at `clean` (mitigate: fewer fbm octaves or half-res sky).
|
||||
|
||||
## Trees (`engine/scene/Tree.ts`)
|
||||
|
||||
Procedural low-poly geometry, faceted flat-shaded like everything else. Two
|
||||
`kind`s carry the species read purely by silhouette:
|
||||
- **`oak`** — short tapered trunk, a couple of branches, a broad cluster of
|
||||
lumpy canopy `blob`s (wider than tall, bushy).
|
||||
- **`spruce`** — tall thin trunk under stacked narrowing `cone` tiers pointing
|
||||
to a tip (taller than wide, conical).
|
||||
|
||||
`growth` (0..1) runs **sapling → full grown**: it scales height/girth and adds
|
||||
canopy blobs (oak) / tiers (spruce); `seed` gives each tree its own wobble.
|
||||
`Tree.build` appends into caller meshes so a forest batches into 3 draw calls
|
||||
(one bark trunk mesh, oak-leaf and spruce-needle foliage meshes). `app/level.ts`
|
||||
`scatterTrees` seeds the forest; add a species by extending the union + a builder.
|
||||
|
||||
## Controls
|
||||
|
||||
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import type { Texture } from "../engine/render/Texture"
|
||||
import barkUrl from "../assets/bark.png"
|
||||
import crateUrl from "../assets/crate.png"
|
||||
import floorUrl from "../assets/floor.png"
|
||||
import grassUrl from "../assets/grass.png"
|
||||
import leafUrl from "../assets/leaf.png"
|
||||
import needleUrl from "../assets/needle.png"
|
||||
import npcUrl from "../assets/npc.png"
|
||||
import wallUrl from "../assets/wall.png"
|
||||
|
||||
export type Textures = {
|
||||
floor: Texture
|
||||
grass: Texture
|
||||
bark: Texture
|
||||
leaf: Texture
|
||||
needle: Texture
|
||||
wall: Texture
|
||||
crate: Texture
|
||||
npc: Texture
|
||||
|
|
@ -15,14 +21,17 @@ export type Textures = {
|
|||
|
||||
/** Load every game texture up front. Call once before starting the loop. */
|
||||
export async function loadTextures(): Promise<Textures> {
|
||||
const [floor, grass, wall, crate, npc] = await Promise.all([
|
||||
const [floor, grass, bark, leaf, needle, wall, crate, npc] = await Promise.all([
|
||||
loadTexture(floorUrl),
|
||||
loadTexture(grassUrl),
|
||||
loadTexture(barkUrl),
|
||||
loadTexture(leafUrl),
|
||||
loadTexture(needleUrl),
|
||||
loadTexture(wallUrl),
|
||||
loadTexture(crateUrl),
|
||||
loadTexture(npcUrl),
|
||||
])
|
||||
return { floor, grass, wall, crate, npc }
|
||||
return { floor, grass, bark, leaf, needle, wall, crate, npc }
|
||||
}
|
||||
|
||||
function loadTexture(url: string): Promise<Texture> {
|
||||
|
|
|
|||
62
app/level.ts
62
app/level.ts
|
|
@ -2,6 +2,7 @@ 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"
|
||||
import { Tree } from "../engine/scene/Tree"
|
||||
|
||||
type Corner = [number, number, number]
|
||||
|
||||
|
|
@ -24,6 +25,12 @@ export type Level = {
|
|||
walls: Mesh
|
||||
crate: Mesh
|
||||
ground: Mesh
|
||||
/** All tree trunks + branches (bark texture). */
|
||||
trunks: Mesh
|
||||
/** Oak canopies (leaf texture) and spruce foliage (needle texture), split so
|
||||
* each takes its own texture in one draw call. */
|
||||
oakFoliage: Mesh
|
||||
spruceFoliage: Mesh
|
||||
colliders: Aabb[]
|
||||
npcPosition: { x: number; y: number; z: number }
|
||||
terrain: Terrain
|
||||
|
|
@ -57,6 +64,13 @@ const TERRAIN: Terrain = {
|
|||
peakStart: 0.45,
|
||||
}
|
||||
|
||||
/** Forest: how many trees to scatter on the grass, and the seed for their
|
||||
* placement/kind/growth. Trees ring the room out to `TREE_REACH` of the world;
|
||||
* each rolls oak-or-spruce and a growth 0..1 (sapling .. full grown). */
|
||||
const TREE_COUNT = 500
|
||||
const TREE_SEED = 0x5EED
|
||||
const TREE_REACH = 0.6
|
||||
|
||||
/** 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. */
|
||||
|
|
@ -136,7 +150,53 @@ export function buildLevel(): Level {
|
|||
|
||||
const npcPosition = { x: 2, y: 0, z: -1 }
|
||||
|
||||
return { floor, walls, crate, ground, colliders, npcPosition, terrain: TERRAIN, sky }
|
||||
// Scatter a forest on the grass; appends into the tree meshes + trunk colliders.
|
||||
const trunks = mesh()
|
||||
const oakFoliage = mesh()
|
||||
const spruceFoliage = mesh()
|
||||
scatterTrees(trunks, oakFoliage, spruceFoliage, colliders)
|
||||
|
||||
return { floor, walls, crate, ground, trunks, oakFoliage, spruceFoliage, colliders, npcPosition, terrain: TERRAIN, sky }
|
||||
}
|
||||
|
||||
/** Place `TREE_COUNT` trees around the room on walkable grass: each sits on the
|
||||
* terrain, rolls oak/spruce and a growth stage, and (once past sapling size)
|
||||
* drops a trunk collider so you can't walk through it. */
|
||||
function scatterTrees(trunks: Mesh, oakFoliage: Mesh, spruceFoliage: Mesh, colliders: Aabb[]): void {
|
||||
const rand = mulberry(TREE_SEED)
|
||||
const maxDist = TERRAIN.outer * TREE_REACH
|
||||
let placed = 0
|
||||
for (let guard = 0; placed < TREE_COUNT && guard < TREE_COUNT * 20; guard++) {
|
||||
const angle = rand() * Math.PI * 2
|
||||
const dist = ARENA + 5 + rand() * (maxDist - ARENA - 5)
|
||||
const x = Math.cos(angle) * dist
|
||||
const z = Math.sin(angle) * dist
|
||||
// Stay out of the room clearing and its flat rim.
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 3) {
|
||||
continue
|
||||
}
|
||||
const kind = rand() < 0.5 ? "oak" : "spruce"
|
||||
const growth = 0.08 + rand() * 0.92
|
||||
const position = { x, y: Terrain.height(TERRAIN, x, z), z }
|
||||
Tree.build({ kind, position, growth, seed: (rand() * 0xFFFFFFFF) | 0 }, trunks, kind === "oak" ? oakFoliage : spruceFoliage)
|
||||
// Saplings are passable; grown trunks block. Square footprint, non-standable.
|
||||
if (growth > 0.35) {
|
||||
const r = growth * (kind === "oak" ? 0.3 : 0.2) + 0.15
|
||||
colliders.push({ minX: x - r, maxX: x + r, minZ: z - r, maxZ: z + r, top: position.y + 3, standable: false })
|
||||
}
|
||||
placed++
|
||||
}
|
||||
}
|
||||
|
||||
/** Deterministic 0..1 generator (mulberry32) for tree placement. */
|
||||
function mulberry(seed: number): () => number {
|
||||
let a = seed >>> 0
|
||||
return () => {
|
||||
a = (a + 0x6D2B79F5) | 0
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
function mesh(): Mesh {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,9 @@ async function main(): Promise<void> {
|
|||
Rasterizer.draw(fb, level.floor, textures.floor, viewProj, config)
|
||||
Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config)
|
||||
Rasterizer.draw(fb, level.crate, textures.crate, viewProj, config)
|
||||
Rasterizer.draw(fb, level.trunks, textures.bark, viewProj, config)
|
||||
Rasterizer.draw(fb, level.oakFoliage, textures.leaf, viewProj, config)
|
||||
Rasterizer.draw(fb, level.spruceFoliage, textures.needle, viewProj, config)
|
||||
Rasterizer.draw(fb, Sprite.billboard(npc, camera), npc.texture, viewProj, config)
|
||||
Framebuffer.quantize(fb, config)
|
||||
present()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const SPEED = 6
|
|||
* solid even at big multipliers. */
|
||||
const RUN_MULTIPLIER = 2
|
||||
const GRAVITY = 22
|
||||
const JUMP_SPEED = 20
|
||||
const JUMP_SPEED = 14
|
||||
const NPC_RADIUS = 0.5
|
||||
|
||||
export namespace Player {
|
||||
|
|
|
|||
BIN
assets/bark.png
Normal file
BIN
assets/bark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/leaf.png
Normal file
BIN
assets/leaf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/needle.png
Normal file
BIN
assets/needle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
181
engine/scene/Tree.ts
Normal file
181
engine/scene/Tree.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { Vec3 } from "../math/Vec3"
|
||||
import type { Mesh } from "./Mesh"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
/** One procedural tree instance. `growth` 0..1 runs sapling -> full grown: it
|
||||
* scales height and girth and adds canopy blobs (oak) / tiers (spruce). `seed`
|
||||
* drives the per-tree random wobble so a forest doesn't look cloned. */
|
||||
export type Tree = {
|
||||
kind: "oak" | "spruce"
|
||||
/** Trunk base, sitting on the ground. */
|
||||
position: Vec3
|
||||
growth: number
|
||||
seed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-poly tree geometry, in the same faceted flat-shaded style as the rest of
|
||||
* the world. Two silhouettes carry the species read:
|
||||
* oak -- short tapered trunk, a couple of branches, a broad cluster of
|
||||
* rounded canopy blobs (bushy, wider than tall).
|
||||
* spruce -- tall thin trunk under stacked cones that narrow to a point
|
||||
* (tiered, taller than wide).
|
||||
* `build` appends into caller-owned meshes so a whole forest batches into a few
|
||||
* draw calls: all trunks share one bark mesh, foliage splits oak vs spruce so
|
||||
* each can carry its own leaf/needle texture.
|
||||
*/
|
||||
export namespace Tree {
|
||||
/** Append one tree into the shared `trunk` (bark) mesh and the `foliage` mesh
|
||||
* for its kind (oak leaf vs spruce needle). */
|
||||
export function build(tree: Tree, trunk: Mesh, foliage: Mesh): void {
|
||||
const rand = rng(tree.seed)
|
||||
if (tree.kind === "oak") {
|
||||
oak(tree.position, tree.growth, rand, trunk, foliage)
|
||||
} else {
|
||||
spruce(tree.position, tree.growth, rand, trunk, foliage)
|
||||
}
|
||||
}
|
||||
|
||||
function oak(base: Vec3, g: number, rand: () => number, trunk: Mesh, leaves: Mesh): void {
|
||||
const h = lerp(0.8, 7, g)
|
||||
const rTrunk = lerp(0.04, 0.32, g)
|
||||
const forkY = base.y + h * 0.5
|
||||
limb(trunk, base, { x: base.x, y: forkY, z: base.z }, rTrunk, rTrunk * 0.6, 5)
|
||||
|
||||
const blobR = h * 0.3
|
||||
const spread = h * 0.32
|
||||
const canopyY = base.y + h * 0.72
|
||||
// Central blob plus, as it grows, a couple offset ones -> broad bushy crown.
|
||||
const blobs = 1 + Math.round(g * 2)
|
||||
for (let i = 0; i < blobs; i++) {
|
||||
const angle = rand() * TAU
|
||||
const rad = i === 0 ? 0 : spread * (0.5 + rand() * 0.5)
|
||||
const center = {
|
||||
x: base.x + Math.cos(angle) * rad,
|
||||
y: canopyY + (rand() - 0.4) * spread,
|
||||
z: base.z + Math.sin(angle) * rad,
|
||||
}
|
||||
blob(leaves, center, blobR * (0.7 + rand() * 0.4), rand)
|
||||
}
|
||||
// Grown oaks throw out a few branches, each tipped with a leaf tuft.
|
||||
if (g > 0.55) {
|
||||
const branches = 2 + Math.round(rand())
|
||||
for (let i = 0; i < branches; i++) {
|
||||
const angle = rand() * TAU
|
||||
const dir = Vec3.normalize({ x: Math.cos(angle), y: 1.2, z: Math.sin(angle) })
|
||||
const start = { x: base.x, y: base.y + h * 0.42, z: base.z }
|
||||
const end = Vec3.add(start, Vec3.scale(dir, h * 0.3))
|
||||
limb(trunk, start, end, rTrunk * 0.4, rTrunk * 0.2, 4)
|
||||
blob(leaves, end, blobR * 0.6, rand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function spruce(base: Vec3, g: number, rand: () => number, trunk: Mesh, needles: Mesh): void {
|
||||
const h = lerp(0.6, 9, g)
|
||||
const rTrunk = lerp(0.03, 0.2, g)
|
||||
limb(trunk, base, { x: base.x, y: base.y + h, z: base.z }, rTrunk, rTrunk * 0.25, 5)
|
||||
|
||||
// Stacked cones: widest low, shrinking to a point up top -> conical tiers.
|
||||
const tiers = 2 + Math.round(g * 3)
|
||||
const bottom = base.y + h * 0.1
|
||||
const span = h * 0.9
|
||||
for (let i = 0; i < tiers; i++) {
|
||||
const t = i / tiers
|
||||
const y = bottom + t * span * 0.82
|
||||
const radius = lerp(h * 0.3, h * 0.05, t) * (0.9 + rand() * 0.2)
|
||||
const coneH = (span / tiers) * 1.9
|
||||
cone(needles, { x: base.x, y, z: base.z }, coneH, radius, 6)
|
||||
}
|
||||
}
|
||||
|
||||
/** A tapered tube between two points (trunk or branch), `sides`-gonal. */
|
||||
function limb(mesh: Mesh, a: Vec3, b: Vec3, ra: number, rb: number, sides: number): void {
|
||||
const axis = Vec3.normalize(Vec3.sub(b, a))
|
||||
const [u, v] = basis(axis)
|
||||
const len = Vec3.length(Vec3.sub(b, a))
|
||||
const start = mesh.vertices.length
|
||||
for (let i = 0; i <= sides; i++) {
|
||||
const angle = (i / sides) * TAU
|
||||
const dir = Vec3.add(Vec3.scale(u, Math.cos(angle)), Vec3.scale(v, Math.sin(angle)))
|
||||
const s = i / sides
|
||||
mesh.vertices.push({ pos: Vec3.add(a, Vec3.scale(dir, ra)), uv: { x: s * 1.5, y: 0 } })
|
||||
mesh.vertices.push({ pos: Vec3.add(b, Vec3.scale(dir, rb)), uv: { x: s * 1.5, y: len * 0.5 } })
|
||||
}
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const p = start + i * 2
|
||||
mesh.indices.push(p, p + 2, p + 3, p, p + 3, p + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** A cone standing on a base ring, apex `height` above it (one spruce tier). */
|
||||
function cone(mesh: Mesh, base: Vec3, height: number, radius: number, sides: number): void {
|
||||
const start = mesh.vertices.length
|
||||
mesh.vertices.push({ pos: { x: base.x, y: base.y + height, z: base.z }, uv: { x: 0.5, y: 0 } })
|
||||
for (let i = 0; i <= sides; i++) {
|
||||
const angle = (i / sides) * TAU
|
||||
mesh.vertices.push({
|
||||
pos: { x: base.x + Math.cos(angle) * radius, y: base.y, z: base.z + Math.sin(angle) * radius },
|
||||
uv: { x: (i / sides) * 2, y: 1 },
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < sides; i++) {
|
||||
mesh.indices.push(start, start + 1 + i, start + 2 + i)
|
||||
}
|
||||
}
|
||||
|
||||
/** A lumpy low-poly sphere (one oak canopy blob). Per-ring radius wobble keeps
|
||||
* it organic without cracking the longitude seam. */
|
||||
function blob(mesh: Mesh, center: Vec3, radius: number, rand: () => number): void {
|
||||
const seg = 5
|
||||
const rings = 3
|
||||
const start = mesh.vertices.length
|
||||
for (let r = 0; r <= rings; r++) {
|
||||
const phi = (r / rings) * Math.PI
|
||||
const cy = Math.cos(phi)
|
||||
const cr = Math.sin(phi)
|
||||
const scale = radius * (0.85 + rand() * 0.3)
|
||||
for (let s = 0; s <= seg; s++) {
|
||||
const theta = (s / seg) * TAU
|
||||
mesh.vertices.push({
|
||||
pos: {
|
||||
x: center.x + cr * Math.cos(theta) * scale,
|
||||
y: center.y + cy * scale,
|
||||
z: center.z + cr * Math.sin(theta) * scale,
|
||||
},
|
||||
uv: { x: (s / seg) * 2, y: (r / rings) * 2 },
|
||||
})
|
||||
}
|
||||
}
|
||||
const row = seg + 1
|
||||
for (let r = 0; r < rings; r++) {
|
||||
for (let s = 0; s < seg; s++) {
|
||||
const p = start + r * row + s
|
||||
mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Two unit vectors spanning the plane perpendicular to `axis`. */
|
||||
function basis(axis: Vec3): [Vec3, Vec3] {
|
||||
const ref = Math.abs(axis.y) < 0.99 ? { x: 0, y: 1, z: 0 } : { x: 1, y: 0, z: 0 }
|
||||
const u = Vec3.normalize(Vec3.cross(ref, axis))
|
||||
return [u, Vec3.cross(axis, u)]
|
||||
}
|
||||
|
||||
function lerp(a: number, b: number, t: number): number {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
/** Deterministic 0..1 generator (mulberry32) seeded per tree. */
|
||||
function rng(seed: number): () => number {
|
||||
let a = seed >>> 0
|
||||
return () => {
|
||||
a = (a + 0x6D2B79F5) | 0
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +118,30 @@ const grass: Shade = (x, y) => {
|
|||
return [56 + n * 0.7 + patch * 0.5, 116 + n + blade + patch, 50 + n * 0.5 + patch * 0.4, 255]
|
||||
}
|
||||
|
||||
// Tree bark: brown with vertical grain and the odd dark crack.
|
||||
const bark: Shade = (x, y) => {
|
||||
const grain = Math.sin(x * 0.6 + noise(0, y) * 2) * 10
|
||||
const n = noise(x, y) * 14
|
||||
const crack = noise(Math.floor(x / 6), y) < -0.45 ? -24 : 0
|
||||
return [92 + n + grain + crack, 66 + n * 0.8 + grain * 0.6 + crack, 44 + n * 0.6 + crack, 255]
|
||||
}
|
||||
|
||||
// Oak leaves: mid green, blotchy clumps with brighter highlights.
|
||||
const leaf: Shade = (x, y) => {
|
||||
const n = noise(x, y) * 26
|
||||
const clump = noise(Math.floor(x / 5), Math.floor(y / 5)) * 22
|
||||
const hi = noise(x, y) > 0.6 ? 22 : 0
|
||||
return [58 + n * 0.5 + clump * 0.4, 120 + n + clump + hi, 42 + n * 0.4 + clump * 0.3, 255]
|
||||
}
|
||||
|
||||
// Spruce needles: darker, cooler blue-green, finer streaky grain.
|
||||
const needle: Shade = (x, y) => {
|
||||
const n = noise(x, y) * 18
|
||||
const streak = noise(x, y * 2) * 10
|
||||
const clump = noise(Math.floor(x / 6), Math.floor(y / 6)) * 12
|
||||
return [40 + n * 0.4 + clump * 0.3, 84 + n + streak + clump, 58 + n * 0.5 + clump * 0.4, 255]
|
||||
}
|
||||
|
||||
const wall: Shade = (x, y) => {
|
||||
const row = Math.floor(y / 16)
|
||||
const bx = (x + (row % 2) * 16) % 32
|
||||
|
|
@ -172,6 +196,9 @@ const npc: Shade = (x, y) => {
|
|||
const assets: Array<[string, number, number, Shade]> = [
|
||||
["floor", 64, 64, floor],
|
||||
["grass", 64, 64, grass],
|
||||
["bark", 64, 64, bark],
|
||||
["leaf", 64, 64, leaf],
|
||||
["needle", 64, 64, needle],
|
||||
["wall", 64, 64, wall],
|
||||
["crate", 64, 64, crate],
|
||||
["npc", 48, 64, npc],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue