Files
forma-engine/engine/player-entry.ts
T

156 lines
5.8 KiB
TypeScript

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);
if (project.settings.presentation) {
ui.innerHTML = "";
const runtime = new FormaRuntime(canvas, {
log: (level, message) => {
if (level === "error") {
const error = document.createElement("div");
error.id = "error";
error.textContent = message;
ui.replaceChildren(error);
}
},
});
window.addEventListener("pagehide", () => runtime.dispose(), {
once: true,
});
await runtime.play(project);
return;
}
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);
});