Files
atomic-engine/web/src/main.ts
T
nico f5d16ffe64 v0.6: Actor system (rigidbody) replaces pixel player
- New actors.rs: Rigidbody Actor with gravity, grid collision, powder displacement
- Actor rendered as colored rectangle on pixel buffer (zoom-scaled)
- WASM API: spawn_actor, move_actor, update_actor, actor_x/y
- WASD/Arrow keys control actor, camera follows with soft lerp
- displace_powders pushes sand/dirt out of actor area
- Collision: stops at solids, walks through powders
- Updated AGENTS.md with actor documentation
2026-07-10 16:09:58 +02:00

234 lines
8.5 KiB
TypeScript

import { createEngine, M, MATERIAL_NAMES, MATERIAL_COLORS } from "./engine";
import { Renderer } from "./renderer";
import { Input } from "./input";
import { Camera } from "./camera";
const GRID_WIDTH = 4096;
const GRID_HEIGHT = 3072;
const PAINT_MATERIALS = [M.SAND, M.WATER, M.LAVA, M.WOOD, M.FIRE, M.ICE, M.OIL, M.ACID, M.IRON, M.STONE, M.DIRT, M.GLASS];
async function main() {
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const renderer = new Renderer(canvas);
const input = new Input();
const camera = new Camera();
camera.x = GRID_WIDTH / 2;
camera.y = GRID_HEIGHT / 2;
camera.zoom = 3;
const engine = await createEngine(GRID_WIDTH, GRID_HEIGHT);
// === EPIC WORLD: tiled 16 copies of the scene ===
function buildScene(ox: number, oy: number) {
engine.fillRect(ox, oy + 740, 1024, 28, M.STONE);
engine.fillRect(ox + 20, oy + 680, 80, 60, M.STONE);
engine.fillRect(ox + 30, oy + 640, 60, 40, M.STONE);
engine.fillRect(ox + 40, oy + 610, 40, 30, M.STONE);
engine.fillCircle(ox + 60, oy + 600, 15, M.DIRT);
engine.fillRect(ox + 50, oy + 660, 20, 20, M.DIRT);
engine.fillRect(ox, oy + 720, 120, 20, M.STONE);
engine.fillRect(ox + 85, oy + 705, 35, 15, M.STONE);
engine.fillRect(ox + 5, oy + 705, 60, 15, M.STONE);
engine.fillCircle(ox + 60, oy + 712, 12, M.LAVA);
engine.fillCircle(ox + 70, oy + 708, 6, M.LAVA);
engine.fillRect(ox + 60, oy + 695, 20, 10, M.DIRT);
engine.fillRect(ox + 350, oy + 680, 100, 60, M.STONE);
engine.fillRect(ox + 365, oy + 640, 70, 40, M.STONE);
engine.fillRect(ox + 380, oy + 610, 40, 30, M.STONE);
engine.fillRect(ox + 390, oy + 660, 20, 20, M.GLASS);
engine.fillRect(ox + 370, oy + 700, 60, 40, M.WOOD);
engine.fillCircle(ox + 400, oy + 690, 8, M.FIRE);
engine.fillRect(ox + 395, oy + 675, 10, 5, M.FIRE);
engine.fillRect(ox + 750, oy + 640, 50, 100, M.STONE);
engine.fillRect(ox + 760, oy + 610, 30, 30, M.STONE);
engine.fillRect(ox + 770, oy + 590, 10, 20, M.STONE);
engine.fillCircle(ox + 775, oy + 580, 8, M.FIRE);
engine.fillRect(ox + 760, oy + 650, 30, 40, M.WOOD);
engine.fillRect(ox + 180, oy + 420, 100, 20, M.STONE);
engine.fillRect(ox + 190, oy + 400, 80, 20, M.DIRT);
engine.fillCircle(ox + 230, oy + 390, 15, M.SAND);
engine.fillRect(ox + 200, oy + 380, 30, 20, M.WOOD);
engine.fillCircle(ox + 215, oy + 385, 6, M.FIRE);
engine.fillRect(ox + 240, oy + 410, 10, 10, M.GLASS);
engine.fillRect(ox + 620, oy + 350, 80, 18, M.STONE);
engine.fillRect(ox + 630, oy + 335, 60, 15, M.DIRT);
engine.fillCircle(ox + 660, oy + 325, 12, M.SAND);
engine.fillCircle(ox + 640, oy + 340, 8, M.ICE);
engine.fillRect(ox, oy + 700, 100, 40, M.WATER);
engine.fillRect(ox + 10, oy + 680, 80, 20, M.WATER);
engine.fillRect(ox + 20, oy + 660, 60, 20, M.WATER);
engine.fillRect(ox + 880, oy + 700, 80, 40, M.LAVA);
engine.fillRect(ox + 890, oy + 680, 60, 20, M.LAVA);
engine.fillRect(ox + 900, oy + 660, 40, 20, M.LAVA);
engine.fillCircle(ox + 420, oy + 280, 30, M.ICE);
engine.fillCircle(ox + 450, oy + 270, 20, M.ICE);
engine.fillCircle(ox + 390, oy + 290, 18, M.ICE);
engine.fillRect(ox + 420, oy + 300, 20, 15, M.ICE);
engine.fillRect(ox + 100, oy + 720, 180, 20, M.SAND);
engine.fillCircle(ox + 120, oy + 710, 25, M.SAND);
engine.fillCircle(ox + 180, oy + 708, 30, M.SAND);
engine.fillCircle(ox + 160, oy + 700, 18, M.SAND);
engine.fillCircle(ox + 220, oy + 715, 20, M.SAND);
engine.fillCircle(ox + 850, oy + 580, 22, M.OIL);
engine.fillCircle(ox + 870, oy + 590, 14, M.OIL);
engine.fillRect(ox + 840, oy + 600, 40, 20, M.OIL);
engine.fillRect(ox + 470, oy + 660, 280, 10, M.WOOD);
engine.fillRect(ox + 490, oy + 650, 40, 10, M.WOOD);
engine.fillRect(ox + 710, oy + 650, 40, 10, M.WOOD);
engine.fillRect(ox + 520, oy + 720, 180, 20, M.STONE);
engine.fillRect(ox + 540, oy + 710, 140, 10, M.STONE);
engine.fillCircle(ox + 600, oy + 718, 15, M.LAVA);
engine.fillCircle(ox + 620, oy + 716, 10, M.LAVA);
engine.fillRect(ox + 100, oy + 730, 40, 10, M.ACID);
engine.fillCircle(ox + 120, oy + 732, 8, M.ACID);
engine.fillRect(ox + 280, oy + 640, 40, 40, M.GLASS);
engine.fillRect(ox + 290, oy + 650, 20, 30, M.STONE);
engine.fillCircle(ox + 300, oy + 670, 5, M.FIRE);
engine.fillCircle(ox + 550, oy + 520, 20, M.DIRT);
engine.fillCircle(ox + 570, oy + 530, 14, M.DIRT);
engine.fillCircle(ox + 200, oy + 520, 30, M.STONE);
engine.fillCircle(ox + 220, oy + 500, 20, M.STONE);
engine.fillRect(ox + 300, oy + 580, 12, 20, M.WATER);
engine.fillRect(ox + 700, oy + 500, 10, 30, M.WATER);
}
for (let ty = 0; ty < 4; ty++) {
for (let tx = 0; tx < 4; tx++) {
buildScene(tx * 1024, ty * 768);
}
}
engine.spawnActor(400, 550);
camera.x = 400;
camera.y = 550;
const rw = engine.renderWidth();
const rh = engine.renderHeight();
function resize() {
const dpr = window.devicePixelRatio || 1;
renderer.resize(window.innerWidth, window.innerHeight, dpr);
}
window.addEventListener("resize", resize);
resize();
let paintMat: number = M.SAND;
function screenToGrid(sx: number, sy: number) {
const mx = (sx / window.innerWidth - 0.5) * rw / camera.zoom + camera.x;
const my = (sy / window.innerHeight - 0.5) * rh / camera.zoom + camera.y;
return { x: Math.floor(mx), y: Math.floor(my) };
}
function mouseWorld() {
return {
x: (input.mouseX / window.innerWidth - 0.5) * rw / camera.zoom + camera.x,
y: (input.mouseY / window.innerHeight - 0.5) * rh / camera.zoom + camera.y,
};
}
const panel = document.getElementById("panel")!;
function buildPanel() {
panel.innerHTML = "";
PAINT_MATERIALS.forEach((mat, idx) => {
const div = document.createElement("div");
div.className = "mat" + (mat === paintMat ? " active" : "");
div.dataset.mat = String(mat);
div.innerHTML = `<div class="swatch" style="background:${MATERIAL_COLORS[mat]}"></div><div class="label"><span class="key">${idx}</span> ${MATERIAL_NAMES[mat]}</div>`;
div.addEventListener("click", () => selectMat(mat));
panel.appendChild(div);
});
}
function selectMat(mat: number) {
paintMat = mat;
document.querySelectorAll(".mat").forEach((el) => {
el.classList.toggle("active", (el as HTMLElement).dataset.mat === String(mat));
});
}
buildPanel();
window.addEventListener("keydown", (e) => {
const idx = parseInt(e.key);
if (idx >= 0 && idx < PAINT_MATERIALS.length) {
selectMat(PAINT_MATERIALS[idx]);
}
});
let lastTime = performance.now();
let simAccum = 0;
const simStep = 1 / 60;
let fpsFrames = 0;
let fpsLast = performance.now();
const fpsEl = document.getElementById("fps")!;
function loop(now: number) {
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
fpsFrames++;
if (now - fpsLast >= 500) {
const fps = Math.round(fpsFrames / ((now - fpsLast) / 1000));
fpsEl.textContent = `${fps} FPS`;
fpsFrames = 0;
fpsLast = now;
}
simAccum += dt;
while (simAccum >= simStep) {
simAccum -= simStep;
engine.simulate(
Math.round(camera.x),
Math.round(camera.y),
camera.zoom,
);
}
let mx = 0;
let my = 0;
if (input.isDown("KeyA") || input.isDown("ArrowLeft")) mx = -1;
if (input.isDown("KeyD") || input.isDown("ArrowRight")) mx = 1;
if (input.isDown("KeyW") || input.isDown("ArrowUp")) my = -1;
engine.moveActor(mx, my);
engine.updateActor(dt);
const ax = engine.actorX();
const ay = engine.actorY();
if (ax > 0 || ay > 0) {
camera.x += (ax - camera.x) * 10 * dt;
camera.y += (ay - 60 - camera.y) * 10 * dt;
}
if (input.isDown("Equal")) camera.setZoom(camera.zoom + 2 * dt);
if (input.isDown("Minus")) camera.setZoom(camera.zoom - 2 * dt);
if (input.wheel !== 0) {
camera.setZoom(camera.zoom + input.wheel * 0.2);
input.wheel = 0;
}
if (input.mouseDown) {
const g = screenToGrid(input.mouseX, input.mouseY);
engine.fillCircle(g.x, g.y, 6, paintMat);
}
camera.update(dt);
engine.updateRender(
Math.round(camera.x),
Math.round(camera.y),
camera.zoom,
);
const ptr = engine.renderBufferPtr();
const len = engine.renderBufferLen();
const buf = new Uint8Array(engine.memory.buffer, ptr, len).slice(0);
renderer.uploadTexture(buf, rw, rh);
renderer.render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
main();