131 lines
5.0 KiB
TypeScript
131 lines
5.0 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 · ввод движения"
|
|
: stats.twoD
|
|
? "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;
|
|
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;
|
|
};
|
|
}
|
|
const release = () => {
|
|
stickId = null;
|
|
runtime.releaseInput();
|
|
knob.style.transform = "";
|
|
};
|
|
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);
|
|
});
|