Publish Forma Engine 0.3.0 source with documentation and CI
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { FormaRuntime } from "./runtime.ts";
|
||||
import { validateProject, type Project } from "./schema.ts";
|
||||
import "./player.css";
|
||||
|
||||
const canvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
const ui = document.getElementById("game-ui")!;
|
||||
ui.innerHTML = '<div class="loading">Загрузка проекта…</div>';
|
||||
|
||||
async function boot() {
|
||||
const response = await fetch("./project.forma.json");
|
||||
if (!response.ok) throw Error(`Project: HTTP ${response.status}`);
|
||||
const project: Project = await response.json();
|
||||
validateProject(project);
|
||||
ui.innerHTML = `<div class="hud"><div><strong id="title"></strong><small id="hint"></small></div><nav><button id="pause">Пауза</button><button id="restart">Перезапустить</button></nav></div><div class="touch"><div id="stick" aria-label="Джойстик"><i></i></div><div class="actions"><button id="jump" aria-label="Прыжок">↑</button><button id="action" aria-label="Действие">A</button></div></div><div id="error" hidden></div>`;
|
||||
document.getElementById("title")!.textContent = project.name;
|
||||
let firstPerson = false;
|
||||
const runtime = new FormaRuntime(canvas, {
|
||||
stats: (stats) => {
|
||||
firstPerson = stats.firstPerson;
|
||||
document.getElementById("hint")!.textContent = firstPerson
|
||||
? "Клик · захват мыши / Esc · отпустить / WASD · ввод движения"
|
||||
: "WASD и кнопки действий передаются скриптам проекта";
|
||||
},
|
||||
log: (level, message) => {
|
||||
if (level !== "error") return;
|
||||
const error = document.getElementById("error")!;
|
||||
error.hidden = false;
|
||||
error.textContent = message;
|
||||
},
|
||||
});
|
||||
const pause = document.getElementById("pause")!;
|
||||
const setPaused = (value: boolean) => {
|
||||
runtime.paused = value;
|
||||
runtime.releaseInput();
|
||||
pause.textContent = value ? "Продолжить" : "Пауза";
|
||||
if (value && document.pointerLockElement === canvas)
|
||||
document.exitPointerLock();
|
||||
};
|
||||
pause.onclick = () => setPaused(!runtime.paused);
|
||||
const restart = document.getElementById("restart") as HTMLButtonElement;
|
||||
restart.onclick = async () => {
|
||||
restart.disabled = true;
|
||||
try {
|
||||
await runtime.stop(project);
|
||||
await runtime.play(project);
|
||||
setPaused(false);
|
||||
document.getElementById("error")!.hidden = true;
|
||||
} catch (error) {
|
||||
const panel = document.getElementById("error")!;
|
||||
panel.hidden = false;
|
||||
panel.textContent = String(error);
|
||||
} finally {
|
||||
restart.disabled = false;
|
||||
}
|
||||
};
|
||||
const stick = document.getElementById("stick")!;
|
||||
const knob = stick.querySelector("i")!;
|
||||
let stickId: number | null = null;
|
||||
let look: { id: number; x: number; y: number } | null = null;
|
||||
const move = (event: PointerEvent) => {
|
||||
if (event.pointerId !== stickId || runtime.paused) return;
|
||||
const bounds = stick.getBoundingClientRect();
|
||||
const x = (event.clientX - bounds.left - bounds.width / 2) / 35;
|
||||
const y = (event.clientY - bounds.top - bounds.height / 2) / 35;
|
||||
const length = Math.max(1, Math.hypot(x, y));
|
||||
runtime.touch.x = x / length;
|
||||
runtime.touch.z = -y / length;
|
||||
knob.style.transform = `translate(${(x / length) * 28}px,${(y / length) * 28}px)`;
|
||||
};
|
||||
stick.onpointerdown = (event) => {
|
||||
stickId = event.pointerId;
|
||||
stick.setPointerCapture(event.pointerId);
|
||||
move(event);
|
||||
};
|
||||
stick.onpointermove = move;
|
||||
stick.onpointerup = stick.onpointercancel = () => {
|
||||
stickId = null;
|
||||
runtime.touch.x = runtime.touch.z = 0;
|
||||
knob.style.transform = "";
|
||||
};
|
||||
for (const [id, input] of [
|
||||
["jump", "jump"],
|
||||
["action", "attack"],
|
||||
] as const) {
|
||||
const button = document.getElementById(id)!;
|
||||
button.onpointerdown = (event) => {
|
||||
if (runtime.paused) return;
|
||||
button.setPointerCapture(event.pointerId);
|
||||
runtime.touch[input] = true;
|
||||
if (input === "jump") runtime.requestAction("jump");
|
||||
};
|
||||
button.onpointerup = button.onpointercancel = () => {
|
||||
runtime.touch[input] = false;
|
||||
};
|
||||
}
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (!firstPerson || runtime.paused) return;
|
||||
if (event.pointerType === "mouse") {
|
||||
const lock = canvas.requestPointerLock?.();
|
||||
if (lock && typeof lock.catch === "function") lock.catch(() => {});
|
||||
} else {
|
||||
look = { id: event.pointerId, x: event.clientX, y: event.clientY };
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
}
|
||||
});
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
if (!look || look.id !== event.pointerId || runtime.paused) return;
|
||||
runtime.lookBy(
|
||||
(event.clientX - look.x) * 0.004,
|
||||
(look.y - event.clientY) * 0.004,
|
||||
);
|
||||
look.x = event.clientX;
|
||||
look.y = event.clientY;
|
||||
});
|
||||
const release = () => {
|
||||
stickId = null;
|
||||
look = null;
|
||||
runtime.releaseInput();
|
||||
knob.style.transform = "";
|
||||
};
|
||||
canvas.addEventListener("pointerup", () => {
|
||||
look = null;
|
||||
});
|
||||
canvas.addEventListener("pointercancel", () => {
|
||||
look = null;
|
||||
});
|
||||
window.addEventListener("blur", release);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
release();
|
||||
if (document.hidden) setPaused(true);
|
||||
});
|
||||
window.addEventListener("pagehide", () => runtime.dispose(), { once: true });
|
||||
await runtime.play(project);
|
||||
}
|
||||
boot().catch((error) => {
|
||||
ui.textContent = "Не удалось открыть проект: " + String(error);
|
||||
});
|
||||
Reference in New Issue
Block a user