feat: boulders
This commit is contained in:
parent
9aa0ec6a3f
commit
680e08aadc
7 changed files with 169 additions and 11 deletions
25
AGENTS.md
25
AGENTS.md
|
|
@ -58,7 +58,9 @@ rules live in `.agents/rules/*.md`.
|
||||||
edge peaks. `Terrain.ground` builds the outdoor mesh with a hole for the room;
|
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), `Tree`
|
`Terrain.height` is the shared ground-height sampler for the player), `Tree`
|
||||||
(procedural low-poly oak/spruce geometry, sapling..full via a `growth` knob;
|
(procedural low-poly oak/spruce geometry, sapling..full via a `growth` knob;
|
||||||
`Tree.build` appends into shared trunk + foliage meshes).
|
`Tree.build` appends into shared trunk + foliage meshes), `Boulder`
|
||||||
|
(procedural low-poly rock: a squashed, jittered, part-buried sphere;
|
||||||
|
`Boulder.build` appends into a shared mesh).
|
||||||
- `app/` — browser glue.
|
- `app/` — browser glue.
|
||||||
- `main.ts` — game loop, input, preset switching, canvas blit, FPS meter.
|
- `main.ts` — game loop, input, preset switching, canvas blit, FPS meter.
|
||||||
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes
|
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes
|
||||||
|
|
@ -66,10 +68,11 @@ rules live in `.agents/rules/*.md`.
|
||||||
- `level.ts` — builds the playground: a flat stone-floored room (three thick
|
- `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`
|
walls via `slab`, north side open) in the center of a big grassy `Terrain`
|
||||||
world (~20x across).
|
world (~20x across).
|
||||||
Per-texture meshes incl. the outdoor grass `ground` and a scattered forest
|
Per-texture meshes incl. the outdoor grass `ground`, a scattered forest
|
||||||
(`scatterTrees` → `trunks`/`oakFoliage`/`spruceFoliage`, `TREE_COUNT`/`_SEED`/
|
(`scatterTrees` → `trunks`/`oakFoliage`/`spruceFoliage`, `TREE_COUNT`/`_SEED`/
|
||||||
`_REACH`), `Aabb` colliders (incl. grown-tree trunks), NPC position, `Terrain`
|
`_REACH`) and `boulders` (`scatterBoulders`, `BOULDER_COUNT`/`_SEED`/`_REACH`),
|
||||||
config + `GROUND_DIVISIONS`/`GROUND_UV`, sky/cloud config.
|
`Aabb` colliders (incl. grown-tree trunks + big boulders), 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
|
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
|
terrain skirt that laps under the room edges. Room surfaces are single flat
|
||||||
quads -- no subdivision needed since texturing is perspective-correct; ground
|
quads -- no subdivision needed since texturing is perspective-correct; ground
|
||||||
|
|
@ -83,9 +86,9 @@ rules live in `.agents/rules/*.md`.
|
||||||
`#fps` meter div (styled inline).
|
`#fps` meter div (styled inline).
|
||||||
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
|
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
|
||||||
writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`.
|
writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`.
|
||||||
- `assets/` — generated `floor/grass/bark/leaf/needle/wall/crate/npc` PNGs
|
- `assets/` — generated `floor/grass/bark/leaf/needle/rock/wall/crate/npc` PNGs
|
||||||
(`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` = tree
|
(`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` = tree
|
||||||
trunk/oak/spruce). Swap for real art anytime;
|
trunk/oak/spruce, `rock` = boulders). Swap for real art anytime;
|
||||||
filenames are the contract.
|
filenames are the contract.
|
||||||
- `server/` — Bun server stub. `shared/` — isomorphic slot.
|
- `server/` — Bun server stub. `shared/` — isomorphic slot.
|
||||||
|
|
||||||
|
|
@ -93,8 +96,8 @@ rules live in `.agents/rules/*.md`.
|
||||||
|
|
||||||
`Player.update` → build `Camera` → `Camera.viewProjection` →
|
`Player.update` → build `Camera` → `Camera.viewProjection` →
|
||||||
`Sky.render` (fills color + resets depth, replaces a clear) →
|
`Sky.render` (fills color + resets depth, replaces a clear) →
|
||||||
`Rasterizer.draw` ground, floor, walls, crate, tree trunks, oak foliage, spruce
|
`Rasterizer.draw` ground, floor, walls, crate, boulders, tree trunks, oak
|
||||||
foliage (one call per texture) →
|
foliage, spruce foliage (one call per texture) →
|
||||||
`Sprite.billboard(npc)` drawn via `Rasterizer.draw` →
|
`Sprite.billboard(npc)` drawn via `Rasterizer.draw` →
|
||||||
`Framebuffer.quantize` → `present` (integer-scale, letterboxed blit;
|
`Framebuffer.quantize` → `present` (integer-scale, letterboxed blit;
|
||||||
`imageSmoothingEnabled` follows `upscaleFilter`).
|
`imageSmoothingEnabled` follows `upscaleFilter`).
|
||||||
|
|
@ -152,6 +155,12 @@ canopy blobs (oak) / tiers (spruce); `seed` gives each tree its own wobble.
|
||||||
(one bark trunk mesh, oak-leaf and spruce-needle foliage meshes). `app/level.ts`
|
(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.
|
`scatterTrees` seeds the forest; add a species by extending the union + a builder.
|
||||||
|
|
||||||
|
**Boulders** (`engine/scene/Boulder.ts`) work the same way: `Boulder.build`
|
||||||
|
appends a squashed, per-vertex-jittered low-poly sphere (seam/pole-safe so it
|
||||||
|
never cracks) into one shared rock mesh, sunk partway into the ground.
|
||||||
|
`scatterBoulders` sizes them small→big (biased small) and drops colliders on the
|
||||||
|
big ones.
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse
|
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import grassUrl from "../assets/grass.png"
|
||||||
import leafUrl from "../assets/leaf.png"
|
import leafUrl from "../assets/leaf.png"
|
||||||
import needleUrl from "../assets/needle.png"
|
import needleUrl from "../assets/needle.png"
|
||||||
import npcUrl from "../assets/npc.png"
|
import npcUrl from "../assets/npc.png"
|
||||||
|
import rockUrl from "../assets/rock.png"
|
||||||
import wallUrl from "../assets/wall.png"
|
import wallUrl from "../assets/wall.png"
|
||||||
|
|
||||||
export type Textures = {
|
export type Textures = {
|
||||||
|
|
@ -14,6 +15,7 @@ export type Textures = {
|
||||||
bark: Texture
|
bark: Texture
|
||||||
leaf: Texture
|
leaf: Texture
|
||||||
needle: Texture
|
needle: Texture
|
||||||
|
rock: Texture
|
||||||
wall: Texture
|
wall: Texture
|
||||||
crate: Texture
|
crate: Texture
|
||||||
npc: Texture
|
npc: Texture
|
||||||
|
|
@ -21,17 +23,18 @@ export type Textures = {
|
||||||
|
|
||||||
/** Load every game texture up front. Call once before starting the loop. */
|
/** Load every game texture up front. Call once before starting the loop. */
|
||||||
export async function loadTextures(): Promise<Textures> {
|
export async function loadTextures(): Promise<Textures> {
|
||||||
const [floor, grass, bark, leaf, needle, wall, crate, npc] = await Promise.all([
|
const [floor, grass, bark, leaf, needle, rock, wall, crate, npc] = await Promise.all([
|
||||||
loadTexture(floorUrl),
|
loadTexture(floorUrl),
|
||||||
loadTexture(grassUrl),
|
loadTexture(grassUrl),
|
||||||
loadTexture(barkUrl),
|
loadTexture(barkUrl),
|
||||||
loadTexture(leafUrl),
|
loadTexture(leafUrl),
|
||||||
loadTexture(needleUrl),
|
loadTexture(needleUrl),
|
||||||
|
loadTexture(rockUrl),
|
||||||
loadTexture(wallUrl),
|
loadTexture(wallUrl),
|
||||||
loadTexture(crateUrl),
|
loadTexture(crateUrl),
|
||||||
loadTexture(npcUrl),
|
loadTexture(npcUrl),
|
||||||
])
|
])
|
||||||
return { floor, grass, bark, leaf, needle, wall, crate, npc }
|
return { floor, grass, bark, leaf, needle, rock, wall, crate, npc }
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTexture(url: string): Promise<Texture> {
|
function loadTexture(url: string): Promise<Texture> {
|
||||||
|
|
|
||||||
41
app/level.ts
41
app/level.ts
|
|
@ -1,6 +1,7 @@
|
||||||
import { Color } from "../engine/render/Color"
|
import { Color } from "../engine/render/Color"
|
||||||
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
||||||
import type { Mesh } from "../engine/scene/Mesh"
|
import type { Mesh } from "../engine/scene/Mesh"
|
||||||
|
import { Boulder } from "../engine/scene/Boulder"
|
||||||
import { Terrain } from "../engine/scene/Terrain"
|
import { Terrain } from "../engine/scene/Terrain"
|
||||||
import { Tree } from "../engine/scene/Tree"
|
import { Tree } from "../engine/scene/Tree"
|
||||||
|
|
||||||
|
|
@ -31,6 +32,8 @@ export type Level = {
|
||||||
* each takes its own texture in one draw call. */
|
* each takes its own texture in one draw call. */
|
||||||
oakFoliage: Mesh
|
oakFoliage: Mesh
|
||||||
spruceFoliage: Mesh
|
spruceFoliage: Mesh
|
||||||
|
/** Scattered boulders (rock texture). */
|
||||||
|
boulders: Mesh
|
||||||
colliders: Aabb[]
|
colliders: Aabb[]
|
||||||
npcPosition: { x: number; y: number; z: number }
|
npcPosition: { x: number; y: number; z: number }
|
||||||
terrain: Terrain
|
terrain: Terrain
|
||||||
|
|
@ -71,6 +74,12 @@ const TREE_COUNT = 500
|
||||||
const TREE_SEED = 0x5EED
|
const TREE_SEED = 0x5EED
|
||||||
const TREE_REACH = 0.6
|
const TREE_REACH = 0.6
|
||||||
|
|
||||||
|
/** Boulders: how many to scatter, their seed, and how far out they reach
|
||||||
|
* (fraction of the world). Sizes range small pebble .. big boulder. */
|
||||||
|
const BOULDER_COUNT = 70
|
||||||
|
const BOULDER_SEED = 0xB0142
|
||||||
|
const BOULDER_REACH = 0.7
|
||||||
|
|
||||||
/** Outdoor ground mesh resolution. A fixed grid over the whole world, so cell
|
/** 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
|
* 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. */
|
* chunkier terrain, not more triangles. `GROUND_UV` sets texture tiles/unit. */
|
||||||
|
|
@ -156,7 +165,11 @@ export function buildLevel(): Level {
|
||||||
const spruceFoliage = mesh()
|
const spruceFoliage = mesh()
|
||||||
scatterTrees(trunks, oakFoliage, spruceFoliage, colliders)
|
scatterTrees(trunks, oakFoliage, spruceFoliage, colliders)
|
||||||
|
|
||||||
return { floor, walls, crate, ground, trunks, oakFoliage, spruceFoliage, colliders, npcPosition, terrain: TERRAIN, sky }
|
// Scatter boulders across the terrain; big ones get colliders.
|
||||||
|
const boulders = mesh()
|
||||||
|
scatterBoulders(boulders, colliders)
|
||||||
|
|
||||||
|
return { floor, walls, crate, ground, trunks, oakFoliage, spruceFoliage, boulders, colliders, npcPosition, terrain: TERRAIN, sky }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Place `TREE_COUNT` trees around the room on walkable grass: each sits on the
|
/** Place `TREE_COUNT` trees around the room on walkable grass: each sits on the
|
||||||
|
|
@ -188,6 +201,32 @@ function scatterTrees(trunks: Mesh, oakFoliage: Mesh, spruceFoliage: Mesh, colli
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Scatter `BOULDER_COUNT` boulders across the terrain, sizes biased toward
|
||||||
|
* small. Each sits on the ground; big ones drop a blocking collider so you
|
||||||
|
* can't walk through them (little rocks stay passable). */
|
||||||
|
function scatterBoulders(boulders: Mesh, colliders: Aabb[]): void {
|
||||||
|
const rand = mulberry(BOULDER_SEED)
|
||||||
|
const maxDist = TERRAIN.outer * BOULDER_REACH
|
||||||
|
let placed = 0
|
||||||
|
for (let guard = 0; placed < BOULDER_COUNT && guard < BOULDER_COUNT * 20; guard++) {
|
||||||
|
const angle = rand() * Math.PI * 2
|
||||||
|
const dist = ARENA + 4 + rand() * (maxDist - ARENA - 4)
|
||||||
|
const x = Math.cos(angle) * dist
|
||||||
|
const z = Math.sin(angle) * dist
|
||||||
|
if (Math.max(Math.abs(x), Math.abs(z)) < TERRAIN.inner + 2) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Square the roll so most rocks are small, a few are big.
|
||||||
|
const radius = 0.35 + rand() * rand() * 2.2
|
||||||
|
const position = { x, y: Terrain.height(TERRAIN, x, z), z }
|
||||||
|
Boulder.build({ position, radius, seed: (rand() * 0xFFFFFFFF) | 0 }, boulders)
|
||||||
|
if (radius > 0.7) {
|
||||||
|
colliders.push({ minX: x - radius, maxX: x + radius, minZ: z - radius, maxZ: z + radius, top: position.y + radius * 0.7, standable: false })
|
||||||
|
}
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Deterministic 0..1 generator (mulberry32) for tree placement. */
|
/** Deterministic 0..1 generator (mulberry32) for tree placement. */
|
||||||
function mulberry(seed: number): () => number {
|
function mulberry(seed: number): () => number {
|
||||||
let a = seed >>> 0
|
let a = seed >>> 0
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ async function main(): Promise<void> {
|
||||||
Rasterizer.draw(fb, level.floor, textures.floor, viewProj, config)
|
Rasterizer.draw(fb, level.floor, textures.floor, viewProj, config)
|
||||||
Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config)
|
Rasterizer.draw(fb, level.walls, textures.wall, viewProj, config)
|
||||||
Rasterizer.draw(fb, level.crate, textures.crate, viewProj, config)
|
Rasterizer.draw(fb, level.crate, textures.crate, viewProj, config)
|
||||||
|
Rasterizer.draw(fb, level.boulders, textures.rock, viewProj, config)
|
||||||
Rasterizer.draw(fb, level.trunks, textures.bark, viewProj, config)
|
Rasterizer.draw(fb, level.trunks, textures.bark, viewProj, config)
|
||||||
Rasterizer.draw(fb, level.oakFoliage, textures.leaf, viewProj, config)
|
Rasterizer.draw(fb, level.oakFoliage, textures.leaf, viewProj, config)
|
||||||
Rasterizer.draw(fb, level.spruceFoliage, textures.needle, viewProj, config)
|
Rasterizer.draw(fb, level.spruceFoliage, textures.needle, viewProj, config)
|
||||||
|
|
|
||||||
BIN
assets/rock.png
Normal file
BIN
assets/rock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
96
engine/scene/Boulder.ts
Normal file
96
engine/scene/Boulder.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import type { Vec3 } from "../math/Vec3"
|
||||||
|
import type { Mesh } from "./Mesh"
|
||||||
|
|
||||||
|
const TAU = Math.PI * 2
|
||||||
|
|
||||||
|
/** One procedural boulder. `radius` is the overall size; `seed` drives the
|
||||||
|
* per-rock lumpiness and squash so no two look alike. It sits partly sunk into
|
||||||
|
* the ground at `position`, like a real rock. */
|
||||||
|
export type Boulder = {
|
||||||
|
/** Resting point on the ground (the rock is centered a bit above and buried). */
|
||||||
|
position: Vec3
|
||||||
|
radius: number
|
||||||
|
seed: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low-poly boulder geometry in the same faceted flat-shaded style as the rest of
|
||||||
|
* the world. A squashed, per-vertex-jittered sphere reads as an angular chunk of
|
||||||
|
* rock once flat shading gives each face its own tone. Radial jitter is kept
|
||||||
|
* seam- and pole-safe (the longitude wrap and both poles reuse one value) so the
|
||||||
|
* rock never cracks open. `build` appends into a caller-owned mesh, so a whole
|
||||||
|
* field of boulders batches into a single draw call.
|
||||||
|
*/
|
||||||
|
export namespace Boulder {
|
||||||
|
export function build(boulder: Boulder, mesh: Mesh): void {
|
||||||
|
const rand = rng(boulder.seed)
|
||||||
|
const seg = 5
|
||||||
|
const rings = 4
|
||||||
|
const r = boulder.radius
|
||||||
|
// Squat and slightly oval, so it reads as a rock, not a ball.
|
||||||
|
const sx = r * (0.8 + rand() * 0.5)
|
||||||
|
const sy = r * (0.55 + rand() * 0.3)
|
||||||
|
const sz = r * (0.8 + rand() * 0.5)
|
||||||
|
const cx = boulder.position.x
|
||||||
|
const cz = boulder.position.z
|
||||||
|
// Center lifted less than the half-height, so the base sinks into the ground.
|
||||||
|
const cy = boulder.position.y + sy * 0.55
|
||||||
|
|
||||||
|
const jitter = jitterGrid(seg, rings, rand)
|
||||||
|
const start = mesh.vertices.length
|
||||||
|
for (let ir = 0; ir <= rings; ir++) {
|
||||||
|
const phi = (ir / rings) * Math.PI
|
||||||
|
const cyv = Math.cos(phi)
|
||||||
|
const crv = Math.sin(phi)
|
||||||
|
for (let is = 0; is <= seg; is++) {
|
||||||
|
const theta = (is / seg) * TAU
|
||||||
|
const j = jitter[ir][is]
|
||||||
|
mesh.vertices.push({
|
||||||
|
pos: {
|
||||||
|
x: cx + crv * Math.cos(theta) * sx * j,
|
||||||
|
y: cy + cyv * sy * j,
|
||||||
|
z: cz + crv * Math.sin(theta) * sz * j,
|
||||||
|
},
|
||||||
|
uv: { x: (is / seg) * 1.5, y: (ir / rings) * 1.5 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const row = seg + 1
|
||||||
|
for (let ir = 0; ir < rings; ir++) {
|
||||||
|
for (let is = 0; is < seg; is++) {
|
||||||
|
const p = start + ir * row + is
|
||||||
|
mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-vertex radial scale in ~0.72..1.14 for a chunky, angular surface. The
|
||||||
|
* longitude seam (last column == first) and each pole row (one shared value)
|
||||||
|
* match so the mesh stays closed. */
|
||||||
|
function jitterGrid(seg: number, rings: number, rand: () => number): number[][] {
|
||||||
|
const grid: number[][] = []
|
||||||
|
for (let ir = 0; ir <= rings; ir++) {
|
||||||
|
const pole = ir === 0 || ir === rings
|
||||||
|
grid[ir] = []
|
||||||
|
for (let is = 0; is <= seg; is++) {
|
||||||
|
if (is === seg || (pole && is > 0)) {
|
||||||
|
grid[ir][is] = grid[ir][0]
|
||||||
|
} else {
|
||||||
|
grid[ir][is] = 0.72 + rand() * 0.42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deterministic 0..1 generator (mulberry32) seeded per boulder. */
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -142,6 +142,15 @@ const needle: Shade = (x, y) => {
|
||||||
return [40 + n * 0.4 + clump * 0.3, 84 + n + streak + clump, 58 + n * 0.5 + clump * 0.4, 255]
|
return [40 + n * 0.4 + clump * 0.3, 84 + n + streak + clump, 58 + n * 0.5 + clump * 0.4, 255]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Boulder rock: cool grey, mottled patches, the odd dark crack.
|
||||||
|
const rock: Shade = (x, y) => {
|
||||||
|
const n = noise(x, y) * 20
|
||||||
|
const patch = noise(Math.floor(x / 7), Math.floor(y / 7)) * 16
|
||||||
|
const crack = noise(Math.floor(x / 5) + 3, Math.floor(y / 9)) < -0.5 ? -28 : 0
|
||||||
|
const g = 118 + n + patch + crack
|
||||||
|
return [g, g + 3, g + 8, 255]
|
||||||
|
}
|
||||||
|
|
||||||
const wall: Shade = (x, y) => {
|
const wall: Shade = (x, y) => {
|
||||||
const row = Math.floor(y / 16)
|
const row = Math.floor(y / 16)
|
||||||
const bx = (x + (row % 2) * 16) % 32
|
const bx = (x + (row % 2) * 16) % 32
|
||||||
|
|
@ -199,6 +208,7 @@ const assets: Array<[string, number, number, Shade]> = [
|
||||||
["bark", 64, 64, bark],
|
["bark", 64, 64, bark],
|
||||||
["leaf", 64, 64, leaf],
|
["leaf", 64, 64, leaf],
|
||||||
["needle", 64, 64, needle],
|
["needle", 64, 64, needle],
|
||||||
|
["rock", 64, 64, rock],
|
||||||
["wall", 64, 64, wall],
|
["wall", 64, 64, wall],
|
||||||
["crate", 64, 64, crate],
|
["crate", 64, 64, crate],
|
||||||
["npc", 48, 64, npc],
|
["npc", 48, 64, npc],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue