meat/app/main.ts
2026-08-04 14:02:12 +02:00

131 lines
4.5 KiB
TypeScript

import { Framebuffer } from "../engine/render/Framebuffer"
import { Rasterizer } from "../engine/render/Rasterizer"
import { RenderConfig } from "../engine/render/RenderConfig"
import { Sky } from "../engine/render/Sky"
import { Camera } from "../engine/scene/Camera"
import { Sprite } from "../engine/scene/Sprite"
import { loadTextures } from "./assets"
import { buildLevel } from "./level"
import { EYE_HEIGHT, Player } from "./player"
const FOV = Math.PI / 3
const screen = document.querySelector<HTMLCanvasElement>("#screen")!
const ctx = screen.getContext("2d")!
const back = document.createElement("canvas")
const backCtx = back.getContext("2d")!
const fpsEl = document.querySelector<HTMLDivElement>("#fps")!
let config: RenderConfig = RenderConfig.standard
let fb = Framebuffer.create(1, 1)
let image = new ImageData(1, 1)
function useConfig(next: RenderConfig): void {
config = next
fb = Framebuffer.create(config.internalWidth, config.internalHeight)
back.width = fb.width
back.height = fb.height
image = new ImageData(new Uint8ClampedArray(fb.color.buffer as ArrayBuffer), fb.width, fb.height)
}
function resize(): void {
screen.width = globalThis.innerWidth
screen.height = globalThis.innerHeight
}
function present(): void {
backCtx.putImageData(image, 0, 0)
const scale = Math.max(1, Math.floor(Math.min(screen.width / fb.width, screen.height / fb.height)))
const w = fb.width * scale
const h = fb.height * scale
const x = (screen.width - w) >> 1
const y = (screen.height - h) >> 1
ctx.imageSmoothingEnabled = config.upscaleFilter === "linear"
ctx.clearRect(0, 0, screen.width, screen.height)
ctx.drawImage(back, x, y, w, h)
}
async function main(): Promise<void> {
const textures = await loadTextures()
const level = buildLevel()
const npc: Sprite = { position: level.npcPosition, size: { x: 1.1, y: 1.5 }, texture: textures.npc }
const player: Player = { position: { x: 0, y: 0, z: 8 }, yaw: 0, pitch: 0, velocityY: 0, onGround: true }
const keys = new Set<string>()
globalThis.addEventListener("keydown", (e) => {
keys.add(e.code)
if (e.code === "Digit1") {
useConfig(RenderConfig.standard)
}
if (e.code === "Digit2") {
useConfig(RenderConfig.soft)
}
if (e.code === "Digit3") {
useConfig(RenderConfig.clean)
}
})
globalThis.addEventListener("keyup", (e) => {
keys.delete(e.code)
})
// Losing focus (alt-tab, pointer-lock exit) drops keyup events, so clear held
// keys or movement sticks on.
globalThis.addEventListener("blur", () => {
keys.clear()
})
screen.addEventListener("click", () => {
screen.requestPointerLock()
})
globalThis.addEventListener("mousemove", (e) => {
if (document.pointerLockElement !== screen) {
return
}
player.yaw += e.movementX * 0.0025
player.pitch = Math.max(-1.4, Math.min(1.4, player.pitch - e.movementY * 0.0025))
})
useConfig(config)
globalThis.addEventListener("resize", resize)
resize()
let last = performance.now()
let fpsLast = last
let fpsFrames = 0
function frame(now: number): void {
const dt = Math.min(0.05, (now - last) / 1000)
last = now
fpsFrames++
if (now - fpsLast >= 250) {
fpsEl.textContent = `${Math.round((fpsFrames * 1000) / (now - fpsLast))} fps`
fpsLast = now
fpsFrames = 0
}
Player.update(player, keys, dt, level)
const camera: Camera = {
position: { x: player.position.x, y: player.position.y + EYE_HEIGHT, z: player.position.z },
yaw: player.yaw,
pitch: player.pitch,
fov: FOV,
}
const viewProj = Camera.viewProjection(camera, fb.width / fb.height)
Sky.render(fb, camera, level.sky, now / 1000)
Rasterizer.draw(fb, level.ground, textures.grass, viewProj, config)
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.boulders, textures.rock, 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()
requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}
main().catch((error) => {
console.error(error)
})