feat: better walls
This commit is contained in:
parent
81474b6bca
commit
61389f091c
3 changed files with 68 additions and 21 deletions
18
AGENTS.md
18
AGENTS.md
|
|
@ -61,8 +61,9 @@ rules live in `.agents/rules/*.md`.
|
|||
- `main.ts` — game loop, input, preset switching, canvas blit, FPS meter.
|
||||
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes
|
||||
are already the `Color` layout).
|
||||
- `level.ts` — builds the playground: a flat stone-floored room (three walls,
|
||||
north side open) in the center of a big grassy `Terrain` world (~20x across).
|
||||
- `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.
|
||||
The stone floor is lifted by `FLOOR_LIFT` (a z-bias) so it stays clean over the
|
||||
|
|
@ -70,8 +71,10 @@ rules live in `.agents/rules/*.md`.
|
|||
quads -- no subdivision needed since texturing is perspective-correct; ground
|
||||
triangle count is `GROUND_DIVISIONS` (fixed grid, so world size sets cell
|
||||
chunkiness, not tri count).
|
||||
- `player.ts` — feet-cylinder player: gravity/jump + circle-vs-AABB/-circle
|
||||
collision; ground height comes from `Terrain.height` (plus standable AABBs).
|
||||
- `player.ts` — feet-cylinder player: gravity/jump + Shift-run
|
||||
(`RUN_MULTIPLIER`) + circle-vs-AABB/-circle collision, substepped so fast
|
||||
running can't tunnel walls; ground height from `Terrain.height` (plus
|
||||
standable AABBs).
|
||||
- `index.html` — Vite entry at repo root; holds the `#screen` canvas and the
|
||||
`#fps` meter div (styled inline).
|
||||
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
|
||||
|
|
@ -130,9 +133,10 @@ branching in the cloud shader. Cost scales with sky resolution — fine at
|
|||
|
||||
## Controls
|
||||
|
||||
WASD move · mouse look (click canvas to pointer-lock) · **Space** jump ·
|
||||
**1/2/3** switch look presets. FPS shown bottom-right. The room's north wall is
|
||||
open — walk out onto the terrain and toward the peaks.
|
||||
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse
|
||||
look (click canvas to pointer-lock) · **Space** jump · **1/2/3** switch look
|
||||
presets. FPS shown bottom-right. The room's north wall is open — walk out onto
|
||||
the terrain and toward the peaks.
|
||||
|
||||
## Code style (oxlint-enforced — match it)
|
||||
|
||||
|
|
|
|||
39
app/level.ts
39
app/level.ts
|
|
@ -32,6 +32,10 @@ export type Level = {
|
|||
|
||||
const ARENA = 12
|
||||
const WALL_HEIGHT = 4
|
||||
/** How deep the perimeter walls are. Thick enough to read as solid walls (and to
|
||||
* give the doorway real jambs); their outer faces sit flush with the room edge,
|
||||
* so they eat into the interior, not the terrain. */
|
||||
const WALL_THICKNESS = 1.5
|
||||
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
|
||||
|
|
@ -96,20 +100,21 @@ export function buildLevel(): Level {
|
|||
|
||||
const walls = mesh()
|
||||
const h = WALL_HEIGHT
|
||||
// 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)
|
||||
const t = WALL_THICKNESS
|
||||
// Three thick perimeter walls, outer faces flush with the room edge; the north
|
||||
// (-Z) side is left open onto the world. No ceiling, so the sky shows above.
|
||||
slab(walls, -ARENA, ARENA, ARENA - t, ARENA, 0, h, 0.5) // south (+Z)
|
||||
slab(walls, ARENA - t, ARENA, -ARENA, ARENA - t, 0, h, 0.5) // east (+X)
|
||||
slab(walls, -ARENA, -ARENA + t, -ARENA, ARENA - t, 0, h, 0.5) // west (-X)
|
||||
|
||||
// Crate on the flat room floor.
|
||||
const crate = mesh()
|
||||
box(crate, CRATE.x, CRATE.z, CRATE.half, 0, CRATE.height)
|
||||
|
||||
const colliders: Aabb[] = [
|
||||
wall(-ARENA, ARENA, ARENA - 1, ARENA),
|
||||
wall(ARENA - 1, ARENA, -ARENA, ARENA),
|
||||
wall(-ARENA, -ARENA + 1, -ARENA, ARENA),
|
||||
wall(-ARENA, ARENA, ARENA - t, ARENA),
|
||||
wall(ARENA - t, ARENA, -ARENA, ARENA - t),
|
||||
wall(-ARENA, -ARENA + t, -ARENA, ARENA - t),
|
||||
{
|
||||
minX: CRATE.x - CRATE.half,
|
||||
maxX: CRATE.x + CRATE.half,
|
||||
|
|
@ -138,8 +143,10 @@ function mesh(): Mesh {
|
|||
return { vertices: [], indices: [] }
|
||||
}
|
||||
|
||||
/** A perimeter wall collider: blocks from the sides, and `standable` so you can
|
||||
* jump up and land on its top (given enough JUMP_SPEED to clear WALL_HEIGHT). */
|
||||
function wall(minX: number, maxX: number, minZ: number, maxZ: number): Aabb {
|
||||
return { minX, maxX, minZ, maxZ, top: WALL_HEIGHT, standable: false }
|
||||
return { minX, maxX, minZ, maxZ, top: WALL_HEIGHT, standable: true }
|
||||
}
|
||||
|
||||
/** One flat quad (two tris). Corners run a (uv 0,0) -> b (us,0) -> c (us,vs) ->
|
||||
|
|
@ -157,6 +164,20 @@ function quad(m: Mesh, a: Corner, b: Corner, c: Corner, d: Corner, us: number, v
|
|||
m.indices.push(base, base + 1, base + 2, base, base + 2, base + 3)
|
||||
}
|
||||
|
||||
/** An axis-aligned box from (x0,z0)-(x1,z1), y0..y1: four sides + top, no bottom
|
||||
* (never seen from below). `tpu` = texture tiles per world unit, so every face
|
||||
* tiles at the same density whatever its size. Used for the thick walls. */
|
||||
function slab(m: Mesh, x0: number, x1: number, z0: number, z1: number, y0: number, y1: number, tpu: number): void {
|
||||
const dx = (x1 - x0) * tpu
|
||||
const dz = (z1 - z0) * tpu
|
||||
const dy = (y1 - y0) * tpu
|
||||
quad(m, [x0, y1, z0], [x1, y1, z0], [x1, y1, z1], [x0, y1, z1], dx, dz)
|
||||
quad(m, [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], dx, dy)
|
||||
quad(m, [x1, y0, z1], [x0, y0, z1], [x0, y1, z1], [x1, y1, z1], dx, dy)
|
||||
quad(m, [x0, y0, z1], [x0, y0, z0], [x0, y1, z0], [x0, y1, z1], dz, dy)
|
||||
quad(m, [x1, y0, z0], [x1, y0, z1], [x1, y1, z1], [x1, y1, z0], dz, dy)
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ export type Player = {
|
|||
|
||||
export const EYE_HEIGHT = 1.6
|
||||
const RADIUS = 0.35
|
||||
const SPEED = 4
|
||||
const SPEED = 6
|
||||
/** Speed multiplier while a Run key (Shift) is held. Tweak to taste; set high to
|
||||
* blast across the big terrain -- move+collision is substepped, so walls stay
|
||||
* solid even at big multipliers. */
|
||||
const RUN_MULTIPLIER = 2
|
||||
const GRAVITY = 22
|
||||
const JUMP_SPEED = 8
|
||||
const JUMP_SPEED = 20
|
||||
const NPC_RADIUS = 0.5
|
||||
|
||||
export namespace Player {
|
||||
|
|
@ -26,13 +30,31 @@ export namespace Player {
|
|||
player.velocityY = JUMP_SPEED
|
||||
player.onGround = false
|
||||
}
|
||||
moveHorizontal(player, keys, dt)
|
||||
collide(player, level)
|
||||
// Move + collide in small substeps: collision is discrete (move, then push
|
||||
// out), so a single big running step could otherwise skip clean through a
|
||||
// wall. Substepping keeps each advance short enough to always hit it.
|
||||
const steps = moveSubsteps(keys, dt)
|
||||
for (let i = 0; i < steps; i++) {
|
||||
moveHorizontal(player, keys, dt / steps)
|
||||
collide(player, level)
|
||||
}
|
||||
fall(player, dt, level)
|
||||
}
|
||||
|
||||
/** Run-speed factor for the frame: RUN_MULTIPLIER while Shift is held, else 1. */
|
||||
function runFactor(keys: Set<string>): number {
|
||||
return keys.has("ShiftLeft") || keys.has("ShiftRight") ? RUN_MULTIPLIER : 1
|
||||
}
|
||||
|
||||
/** Number of move+collide substeps so each advances at most ~RADIUS, keeping
|
||||
* the player from tunneling walls however fast they run. */
|
||||
function moveSubsteps(keys: Set<string>, dt: number): number {
|
||||
const perFrame = SPEED * runFactor(keys) * dt * Math.SQRT2
|
||||
return Math.max(1, Math.ceil(perFrame / RADIUS))
|
||||
}
|
||||
|
||||
function moveHorizontal(player: Player, keys: Set<string>, dt: number): void {
|
||||
const speed = SPEED * dt
|
||||
const speed = SPEED * runFactor(keys) * dt
|
||||
const fx = Math.sin(player.yaw)
|
||||
const fz = -Math.cos(player.yaw)
|
||||
const rx = Math.cos(player.yaw)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue