import * as agv from "@akashic/akashic-gameview"; import * as agvWeb from "@akashic/akashic-gameview-web"; import { ObjectChipSelector } from "./objectChipSelector"; export type FloorData = string[][]; export interface ObjectData { no: number; x: number; y: number; image: string; event?: string; wall?: string; vars?: string; } export interface Messages { [key: string]: string; } export interface Flag { value: boolean; event?: string; vars?: string; } export interface EditorState { floors: FloorData[]; objects: ObjectDataMap; message?: Messages; flags?: Flag[]; map: { floors: FloorData[]; objects: ObjectDataMap; message?: Messages; flags?: Flag[]; }; } export namespace Const { export const MovingCodeMap = { left: 1, up: 2, right: 4, down: 8, }; } export type EditMode = "editFloor" | "editObject" | "scroll" | "raiseEvent" | "preview"; export type ChangeEditModeEvent = { type: "changeEditMode"; mode: EditMode; floor?: number; }; export type ObjectDataMap = { [floor: string]: ObjectData[] }; export type DirectionType = "left" | "up" | "right" | "down"; export const minWidth = 480; export const minHeight = 480; export class GameController extends EventTarget { content: agvWeb.GameContent; view: agvWeb.AkashicGameView; playerId: string; alive: boolean; editMode: EditMode = "scroll"; objectChipSelector?: ObjectChipSelector; inputHandlerElement: HTMLElement | null = null; keydownHandler: (event: KeyboardEvent) => void; keyupHandler: (event: KeyboardEvent) => void; moveState: { [id: number]: number }; latestMoveStateId: number; constructor(playerId: string, content: agvWeb.GameContent, view: agvWeb.AkashicGameView) { super(); this.playerId = playerId; this.content = content; this.view = view; this.alive = false; this.moveState = {}; // キーボードだけは絶対いるものとして最初に作っておく this.moveState[-1] = 0; this.latestMoveStateId = -1; this.addContentLoadEvent(() => { this.handleLoad(); this.alive = true; this.inputHandlerElement = this.content._element?._innerHtmlElement?.querySelector(".input-handler"); this.inputHandlerElement?.addEventListener("keydown", this.keydownHandler); this.inputHandlerElement?.addEventListener("keyup", this.keyupHandler); }); this.keydownHandler = (e) => { if (this.alive === false) return; switch (e.code) { case "ArrowLeft": e.preventDefault(); this.startMove(-1, "left"); break; case "ArrowUp": e.preventDefault(); this.startMove(-1, "up"); break; case "ArrowRight": e.preventDefault(); this.startMove(-1, "right"); break; case "ArrowDown": e.preventDefault(); this.startMove(-1, "down"); break; case "Digit1": this.doChangeCharacter(0); break; case "Digit2": this.doChangeCharacter(1); break; case "Digit3": this.doChangeCharacter(2); break; case "Digit4": this.doChangeCharacter(3); break; case "KeyZ": case "Enter": this.doAction(); e.preventDefault(); break; } }; this.keyupHandler = (e) => { if (this.alive === false) return; switch (e.code) { case "ArrowLeft": e.preventDefault(); this.stopMove(-1, "left"); break; case "ArrowUp": e.preventDefault(); this.stopMove(-1, "up"); break; case "ArrowRight": e.preventDefault(); this.stopMove(-1, "right"); break; case "ArrowDown": e.preventDefault(); this.stopMove(-1, "down"); break; } }; } handleLoad() { const gameDriver = this.content.getGameDriver(); if (gameDriver == null || gameDriver._platform == null) { console.error("invalid gameDriver", gameDriver); return; } gameDriver._platform.sendToExternal = (playId: string, data: any) => { this.handleSendToExternal(data); }; const orgTick = gameDriver._game.tick; gameDriver._game.tick = (advanceAge: any, omittedTickCount: any, events: any) => { try { orgTick.call(gameDriver._game, advanceAge, omittedTickCount, events); } catch (error) { if (error instanceof Error) { const event = new CustomEvent("error", { detail: { message: error.message, name: error.name, stack: error.stack, }, }); this.dispatchEvent(event); console.error(error); } } }; } handleSendToExternal(data: any) { console.log("external event", data); if (data.type === "edited") { const event = new CustomEvent("edited"); this.dispatchEvent(event); } else if (data.type === "posObject") { const event = new CustomEvent("posObject", { detail: data }); this.dispatchEvent(event); } else if (data.type === "media") { const event = new CustomEvent("media", { detail: data }); this.dispatchEvent(event); } else if (data.type === "gameStartedAge") { const event = new CustomEvent("gameStartedAge", { detail: data }); this.dispatchEvent(event); } } setObjectChipSelector(objectChipSelector: ObjectChipSelector) { this.objectChipSelector = objectChipSelector; } addContentLoadEvent(event: Function) { this.content.addContentLoadListener({ onLoad: () => { event(); }, }); } sendMessageEvent(data: object) { this.content.sendEvents([[32, null, this.playerId, data]]); } sendLocalMessageEvent(data: object) { const gameDriver = this.content.getGameDriver(); if (gameDriver == null || gameDriver._eventBuffer == null) { console.error("invalid gameDriver", gameDriver); return; } gameDriver._eventBuffer.onEvent([32, null, this.playerId, data, true]); } changeEditMode(editMode: EditMode, floor?: number) { this.editMode = editMode; const event: ChangeEditModeEvent = { type: "changeEditMode", mode: editMode, }; if (floor != null) { event.floor = floor; } this.sendMessageEvent(event); } setGameSize(width: number, height: number, gameWidth: number, gameHeight: number) { const setGameWidth = gameWidth < minWidth ? minWidth : gameWidth; const setGameHeight = gameHeight < minHeight ? minHeight : gameHeight; //ゲームサイズ設定が先でないと拡大表示が出来ない this.content.setGameContentSize({ width: setGameWidth, height: setGameHeight }); this.content.setContentArea({ x: 0, y: 0, width: width, height: height }); this.view.setViewSize(width, height); } getEditorState(): Promise { return new Promise((resolve, reject) => { let attempts = 0; const maxAttempts = 5; const attemptToGetState = () => { this.content.getGameVars("editorState", async (editorState: EditorState) => { if (editorState !== undefined) { resolve(editorState); } else if (attempts < maxAttempts) { attempts++; setTimeout(attemptToGetState, 500); } else { console.error("エラー: editorStateを取得できませんでした。最大試行回数に到達しました。"); reject(new Error("エラー: editorStateを取得できませんでした。最大試行回数に到達しました。")); } }); }; attemptToGetState(); }); } focusInputHandler() { if (this.inputHandlerElement != null) { this.inputHandlerElement.focus(); return true; } return false; } doChangeCharacter(newIndex: number) { this.sendMessageEvent({ type: "changeCharacter", index: newIndex, }); } startMove(id: number, type: DirectionType) { switch (type) { case "up": if (this.trySetMove(id, type)) { this.latestMoveStateId = id; this.resetMove(id, Const.MovingCodeMap.up); this.moveUp(); } this.moveUp(); break; case "down": if (this.trySetMove(id, type)) { this.latestMoveStateId = id; this.resetMove(id, Const.MovingCodeMap.down); this.moveDown(); } break; case "left": if (this.trySetMove(id, type)) { this.latestMoveStateId = id; this.resetMove(id, Const.MovingCodeMap.left); this.moveLeft(); } break; case "right": if (this.trySetMove(id, type)) { this.latestMoveStateId = id; this.resetMove(id, Const.MovingCodeMap.right); this.moveRight(); } break; } } stopMove(id: number, type: DirectionType) { switch (type) { case "up": case "down": case "left": case "right": if ((this.moveState[id] & Const.MovingCodeMap[type]) === Const.MovingCodeMap[type]) { this.moveState[id] ^= Const.MovingCodeMap[type]; if (this.moveState[id] === 0) { this.moveStop(); } } break; default: console.warn("Controller#action detected unknown action type", type); } } resetMove(id: number, without: number, dontSend?: boolean) { if (this.moveState[id] == null) this.moveState[id] = 0; const canSend = this.moveState[id] !== 0 && dontSend !== true; let types: DirectionType[] = []; (["left", "right", "down", "up"] as DirectionType[]).forEach((code) => { const movingCode = Const.MovingCodeMap[code]; if ((without & movingCode) === movingCode) return; if ((this.moveState[id] & movingCode) === movingCode) { this.moveState[id] ^= movingCode; types.push(code); } }); if (canSend && this.moveState[id] === 0) { this.moveStop(); } } trySetMove(id: number, type: DirectionType) { this.latestMoveStateId = id; const movingCode = Const.MovingCodeMap[type]; if ((this.moveState[id] & movingCode) !== movingCode) { this.moveState[id] |= movingCode; return true; } return false; } moveUp() { this.sendMessageEvent({ type: "move", angle: "up", }); } moveDown() { this.sendMessageEvent({ type: "move", angle: "down", }); } moveLeft() { this.sendMessageEvent({ type: "move", angle: "left", }); } moveRight() { this.sendMessageEvent({ type: "move", angle: "right", }); } moveStop() { this.sendMessageEvent({ type: "stop", }); } doReset(floor?: number) { this.sendMessageEvent({ type: "reset", floor, }); } doAction() { this.sendLocalMessageEvent({ type: "action", }); } } export function handleGame(targetGameUrl: string) { const playerId = "user1"; const gameView = new agvWeb.AkashicGameView({ container: document.getElementById("gameContainer")!, width: 480, height: 480, untrustedFrameUrl: "dummy", trustedChildOrigin: /.*/, }); const gameContent = new agvWeb.GameContent({ contentUrl: targetGameUrl, player: { id: playerId, name: "user1" }, playConfig: { playId: "dummy_play_id", executionMode: agv.ExecutionMode.Active }, argument: { bgmVolume: 0, seVolume: 0 }, }); // エラーハンドラを設定。 gameContent.addErrorListener({ onError: function (e) { console.error(e); }, }); // GameView にコンテンツを配置。 gameView.addContent(gameContent); const controller = new GameController(playerId, gameContent, gameView); return controller; }