diff --git a/assets/roomba.png b/assets/roomba.png new file mode 100644 index 0000000..0bb9a88 Binary files /dev/null and b/assets/roomba.png differ diff --git a/assets/title.png b/assets/title.png index 0b8c787..1854e9f 100644 Binary files a/assets/title.png and b/assets/title.png differ diff --git a/package.json b/package.json index ba06ea6..5a57e64 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "devDependencies": { "@babel/preset-typescript": "^7.23.3", "@types/jest": "^29.5.11", + "eventemitter3": "^5.0.1", "jest": "^29.7.0", "typescript": "^5.2.2", "vite": "^5.0.5" diff --git a/src/board/board.ts b/src/board/board.ts index c4b74a3..dd7a842 100644 --- a/src/board/board.ts +++ b/src/board/board.ts @@ -9,14 +9,6 @@ interface BoardPlayer { position: number, } -export interface BoardPacket extends PlayerPacket { - type: PacketType.BOARD, - data: { - function: string, - args: any[], - } -} - const PLAYER_SIZE = 16; const BOARD_SIDE_LENGTH = 12; export default class GameBoard extends Phaser.GameObjects.Container { @@ -27,15 +19,12 @@ export default class GameBoard extends Phaser.GameObjects.Container { private TILE_SIZE: number; - private aznopoly: AzNopolyGame; - private players: Map; private size: number; constructor(aznopoly: AzNopolyGame, scene: Scene, x: number, y: number, size: number) { super(scene, x, y); this.size = size; - this.aznopoly = aznopoly; this.players = new Map(); this.TILE_SIZE = size / BOARD_SIDE_LENGTH; @@ -46,36 +35,9 @@ export default class GameBoard extends Phaser.GameObjects.Container { const targetScale = size / background.width; background.setScale(targetScale); background.setOrigin(0, 0); - - this.aznopoly.client.addEventListener(PacketType.BOARD, this.onBoardPacket.bind(this) as EventListener); - } - - private onBoardPacket(event: CustomEvent) { - const packet = event.detail; - if (this.aznopoly.isHost) return; - if (packet.target && packet.target !== this.aznopoly.client.id) return; - - switch (packet.data.function) { - case "addPlayer": - this.addPlayer(packet.data.args[0], packet.data.args[1]); - break; - case "movePlayer": - this.movePlayer(packet.data.args[0], packet.data.args[1]); - break; - } } addPlayer(uuid: string, startPos: number = 0) { - if (this.aznopoly.isHost) { - this.aznopoly.client.sendPacket({ - type: PacketType.BOARD, - data: { - function: "addPlayer", - args: [uuid, startPos], - }, - }) - } - if (this.players.has(uuid)) { throw new Error(`Player with UUID ${uuid} already exists!`); } @@ -88,23 +50,13 @@ export default class GameBoard extends Phaser.GameObjects.Container { }; this.add(player.gameObject); this.players.set(uuid, player) - this.movePlayer(uuid, 0); + this.movePlayerToPosition(uuid, 0); return player; } - movePlayer(uuid: string, distance: number) { - if (this.aznopoly.isHost) { - this.aznopoly.client.sendPacket({ - type: PacketType.BOARD, - data: { - function: "movePlayer", - args: [uuid, distance], - }, - }) - } - - if (!Number.isInteger(distance)) { - throw new Error(`Illegal parameter distance: Not an integer (${distance})`); + movePlayerToPosition(uuid: string, position: number) { + if (!Number.isInteger(position)) { + throw new Error(`Illegal parameter position: Not an integer (${position})`); } let player = this.players.get(uuid); @@ -112,13 +64,30 @@ export default class GameBoard extends Phaser.GameObjects.Container { throw new Error(`Player with UUID ${uuid} does not exist!`); } - player.position += distance; + player.position = position; const coords = GameBoard.getCoordForPos(player.position); player.gameObject.setPosition(coords.x * this.TILE_SIZE, coords.y * this.TILE_SIZE) this.checkPlayerColisions(); } + // movePlayerForward(uuid: string, distance: number) { + // if (!Number.isInteger(distance)) { + // throw new Error(`Illegal parameter distance: Not an integer (${distance})`); + // } + + // let player = this.players.get(uuid); + // if (!player) { + // throw new Error(`Player with UUID ${uuid} does not exist!`); + // } + + // player.position += distance; + // const coords = GameBoard.getCoordForPos(player.position); + + // player.gameObject.setPosition(coords.x * this.TILE_SIZE, coords.y * this.TILE_SIZE) + // this.checkPlayerColisions(); + // } + private checkPlayerColisions() { const positions: { [key: number]: string[] } = {}; this.players.forEach((player, uuid) => { diff --git a/src/debug-util.ts b/src/debug-util.ts index 61e611e..971db8b 100644 --- a/src/debug-util.ts +++ b/src/debug-util.ts @@ -13,6 +13,7 @@ export function mock(aznopoly: AzNopolyGame) { game._room = { connectedPlayerIds: ["1111-2222-3333-4444"], host: "1111-2222-3333-4444", + getPlayerName: () => "mockius maximus", }; game._name = "mockius maximus"; diff --git a/src/game.ts b/src/game.ts index 0d4962d..123a835 100644 --- a/src/game.ts +++ b/src/game.ts @@ -26,6 +26,10 @@ export default class AzNopolyGame { name: this._name, } } + + public get uuid(): string { + return this.client.id; + } public get client(): AzNopolyClient { return this._client; @@ -35,7 +39,7 @@ export default class AzNopolyGame { return this._room; } - public get players(): string[] { + public get connectedUuids(): string[] { return this.room.connectedPlayerIds; } diff --git a/src/main.ts b/src/main.ts index 167fc3f..a13b1bc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,16 @@ import Phaser from 'phaser'; import TitleScene from './scene/title-scene'; -import BoardScene from './scene/game-scene'; +import BoardScene from './scene/board-scene'; import LobbyScene from './scene/lobby-scene'; -import { SimonSaysScene } from './scene/minigame/simon-says-scene'; +// import { SimonSaysScene } from './scene/minigame/simon-says-scene'; import AzNopolyGame from './game'; import { RoombaScene } from './scene/minigame/roomba-scene'; +// import { mock } from './debug-util'; export const WIDTH = 1280; export const HEIGHT = 720; -window.onload = () => { +window.onload = async () => { let game = new Phaser.Game({ type: Phaser.AUTO, width: WIDTH, @@ -40,12 +41,15 @@ window.onload = () => { game.scene.add('game', new BoardScene(aznopoly)); //Minigames - game.scene.add('minigame-simon-says', new SimonSaysScene(aznopoly)) + //game.scene.add('minigame-simon-says', new SimonSaysScene(aznopoly)) game.scene.add('minigame-roomba', new RoombaScene(aznopoly)) //game.scene.start('minigame-roomba'); - game.scene.start('title'); + //mock(aznopoly); + //game.scene.start('game'); + game.scene.start('title'); + Object.assign(window, { game }); }; diff --git a/src/minigame/roomba.ts b/src/minigame/roomba.ts index 3aca268..f75860c 100644 --- a/src/minigame/roomba.ts +++ b/src/minigame/roomba.ts @@ -16,7 +16,7 @@ export interface RoombaConfig { export class Roomba extends Phaser.GameObjects.Container { static SIZE = SIZE; - private graphics: Phaser.GameObjects.Graphics; + private graphics: Phaser.GameObjects.Image; private arrow: Phaser.GameObjects.Graphics; private lastPaintPosition: Phaser.Math.Vector2; @@ -25,8 +25,14 @@ export class Roomba extends Phaser.GameObjects.Container { private paintColor: number; private speed: number; + private stopped = false; + public readonly id: string; + static preload(scene: Phaser.Scene) { + scene.load.image('roomba', 'assets/roomba.png'); + } + constructor(scene: Phaser.Scene, { id, x, y, angle, color, paintColor, speed} : RoombaConfig) { super(scene, x, y); @@ -35,14 +41,12 @@ export class Roomba extends Phaser.GameObjects.Container { this.paintColor = paintColor; this.speed = speed; - this.graphics = new Phaser.GameObjects.Graphics(scene); + this.graphics = new Phaser.GameObjects.Image(scene, 0, 0, 'roomba'); + this.graphics.setScale(SIZE / this.graphics.width); + this.arrow = new Phaser.GameObjects.Graphics(scene); this.lastPaintPosition = new Phaser.Math.Vector2(x, y); - this.graphics.fillStyle(this.color); - this.graphics.fillCircle(0, 0, SIZE / 2); - this.graphics.fillCircle(SIZE/5 * 2, 0, SIZE / 4); - scene.physics.world.enable(this); const body = this.body! as Phaser.Physics.Arcade.Body; body.setCollideWorldBounds(true) @@ -57,24 +61,37 @@ export class Roomba extends Phaser.GameObjects.Container { this.initDragEvents(); } + public stop() { + const body = this.body! as Phaser.Physics.Arcade.Body; + body.setVelocity(0, 0); + this.arrow.clear(); + this.arrow.setVisible(false); + } + private initDragEvents() { - this.graphics.setInteractive({ + this.setInteractive({ hitArea: new Phaser.Geom.Circle(0, 0, SIZE / 2), hitAreaCallback: Phaser.Geom.Circle.Contains, useHandCursor: false, draggable: true }, Phaser.Geom.Circle.Contains); - this.graphics.on('dragstart', () => { + this.on('dragstart', () => { + if (this.stopped) return; + this.arrow.visible = true; }); - this.graphics.on('drag', (event: any) => { + this.on('drag', (event: any) => { + if (this.stopped) return; + const dragOffset = new Phaser.Math.Vector2(event.x - this.x, event.y - this.y); this.drawArrow(Phaser.Math.Vector2.ZERO, dragOffset); }); - this.graphics.on('dragend', (event: any) => { + this.on('dragend', (event: any) => { + if (this.stopped) return; + const dragOffset = new Phaser.Math.Vector2(event.x - this.x, event.y - this.y); this.scene.events.emit('roomba-dragged', { id: this.id, offset: dragOffset}); this.drawArrow(new Phaser.Math.Vector2(0, 0), new Phaser.Math.Vector2(0, 0)); diff --git a/src/scene-switcher.ts b/src/scene-switcher.ts index c8838a6..87d7efb 100644 --- a/src/scene-switcher.ts +++ b/src/scene-switcher.ts @@ -4,15 +4,12 @@ import { PacketType, SceneChangePacket, SceneReadyPacket } from "./types/client" let switcher: any[] = [] export const SceneSwitcher = { - waitForPlayers: (aznopoly: AzNopolyGame, sceneKey: string, launchMethod: string) => { - sendSceneChangePacket(aznopoly, sceneKey, launchMethod); - return new Promise((resolve) => { - switcher.push(new SceneReadyListener(aznopoly, sceneKey, () => { - resolve(); - })); - }); + waitForPlayers: (aznopoly: AzNopolyGame, sceneKey: string, launchMethod: "launch" | "start" | "wake", callback: () => void) => { + switcher.push(new SceneReadyListener(aznopoly, sceneKey, callback)); + sendSceneChangePacket(aznopoly, sceneKey.split("_")[0], launchMethod); }, - updateScene: sendSceneReadyPacket + broadcastSceneReady: sendSceneReadyPacket, + listen: listenForSceneSwitch, } /** @@ -27,6 +24,8 @@ class SceneReadyListener { private listener?: EventListener; private readyList: Set = new Set(); + private id = Math.random(); + constructor(aznopoly: AzNopolyGame, sceneName: string, callback: () => void) { this.aznopoly = aznopoly; this.sceneName = sceneName; @@ -40,6 +39,7 @@ class SceneReadyListener { this.readyList.add(packet.detail.sender); if (this.readyList.size == this.aznopoly.room.connectedPlayerIds.length) { this.callback(); + console.log("Removing listener", this.id) this.aznopoly.client.removeEventListener(PacketType.SCEEN_READY, this.listener!); switcher = switcher.filter((listener) => listener != this); } @@ -47,6 +47,7 @@ class SceneReadyListener { } private startListening() { + console.log("Starting listener", this.id) this.listener = this.sceneReadyListener.bind(this) as EventListener; this.aznopoly.client.addEventListener(PacketType.SCEEN_READY, this.listener); } @@ -62,14 +63,16 @@ function sendSceneReadyPacket(aznopoly: AzNopolyGame, sceneName: string) { } } + console.log("Sending scene ready packet", packet); if (aznopoly.isHost) { - switcher.find(l => l.sceneName == sceneName)?.listener({detail: packet}) + const ss = switcher.find(l => l.sceneName == sceneName); + ss?.listener({detail: packet}) } else { aznopoly.client.sendPacket(packet); } } -function sendSceneChangePacket(aznopoly: AzNopolyGame, sceneName: string, launchMethod: string) { +function sendSceneChangePacket(aznopoly: AzNopolyGame, sceneName: string, launchMethod: "start" | "launch" | "wake") { const packet: SceneChangePacket = { type: PacketType.SCENE_CHANGE, sender: aznopoly.client.id, @@ -78,5 +81,43 @@ function sendSceneChangePacket(aznopoly: AzNopolyGame, sceneName: string, launch launchMethod, } } + console.log("Sending scene change packet", packet); aznopoly.client.sendPacket(packet); +} + +function listenForSceneSwitch(scene: Phaser.Scene, aznopoly: AzNopolyGame) { + const id = Math.random(); + + console.log("Listening for scene switch", id, scene.scene.key) + const packetListener = aznopoly.addPacketListener(PacketType.SCENE_CHANGE, ((event: CustomEvent) => { + if (!scene.scene.isActive(scene.scene.key ) || scene.scene.isSleeping(scene.scene.key)) { + console.warn("Received scene change packet for inactive scene", id, scene.scene.key, scene.scene.isActive(scene.scene.key ), scene.scene.isSleeping(scene.scene.key )) + return; + } + + const packet = event.detail + if (!aznopoly.isPlayerHost(packet.sender)) { + console.warn("Received scene change packet from non-host player"); + return; + } + + if (packet.data.launchMethod == "launch") { + console.log("Sleeping scene", id, scene.scene.key) + scene.scene.sleep(); + scene.scene.launch(packet.data.scene, { returnScene: scene.scene.key}); + } else if (packet.data.launchMethod == "wake") { + console.trace("Waking scene", id, scene, packet.data.scene) + scene.scene.stop(); + scene.scene.wake(packet.data.scene); + } else if (packet.data.launchMethod == "start") { + console.log("Starting scene", id, packet.data.scene) + scene.scene.start(packet.data.scene); + } else { + console.error("Unknown launch method", packet.data.launchMethod); + } + }) as EventListener); + scene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + console.log("Removing scene switch listener", id) + aznopoly.removePacketListener(PacketType.SCENE_CHANGE, packetListener) + }); } \ No newline at end of file diff --git a/src/scene/base-scene.ts b/src/scene/base-scene.ts deleted file mode 100644 index b2645d2..0000000 --- a/src/scene/base-scene.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Scene } from "phaser"; -import AzNopolyGame from "../game"; -import { SceneSwitcher } from "../scene-switcher"; -import { PacketType, PlayerPacket, SceneChangePacket } from "../types/client"; - - -export abstract class BaseScene extends Phaser.Scene { - - protected aznopoly: AzNopolyGame; - private packetListener: [string, EventListener][] = []; - private sync: boolean; - - constructor(aznopoly: AzNopolyGame, synced: boolean = true) { - super(); - this.aznopoly = aznopoly; - this.sync = synced; - } - - init(data: any) { - if(!this.aznopoly.client) return; - const launchMethod = (data || {}).launchMethod; - - if (this.sync) { - if (this.aznopoly.isHost) { - SceneSwitcher.waitForPlayers(this.aznopoly, this.scene.key, launchMethod).then(() => { - this.hostOnAllPlayerReady(); - }); - } - } - this.addPacketListener(PacketType.SCENE_CHANGE, this.onChangeScene.bind(this)); - this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { - this.cleanPacketListeners(); - }); - } - - create() { - SceneSwitcher.updateScene(this.aznopoly, this.scene.key); - } - - protected abstract hostOnAllPlayerReady(): void; - - protected addPacketListener(type: string, callback: (packet: T) => void) { - const listener: EventListener = (event: Event) => { - const packet = (event as CustomEvent).detail; - callback(packet); - }; - this.packetListener.push([type, listener]); - this.aznopoly.client.addEventListener(type, listener); - } - - private cleanPacketListeners() { - this.packetListener.forEach(([type, listener]) => { - this.aznopoly.client.removeEventListener(type, listener); - }); - } - - private onChangeScene(packet: SceneChangePacket) { - if (!this.aznopoly.isPlayerHost(packet.sender)) { - console.warn("Received scene change packet from non-host player"); - return; - } - - if (packet.data.launchMethod == "launch") { - this.scene.sleep(); - this.scene.launch(packet.data.scene, { previousScene: this.scene.key}); - } else { - this.scene.start(packet.data.scene, { previousScene: this.scene.key}); - } - } - -} \ No newline at end of file diff --git a/src/scene/base-scene-controller.ts b/src/scene/base/base-scene-controller.ts similarity index 52% rename from src/scene/base-scene-controller.ts rename to src/scene/base/base-scene-controller.ts index 9246c07..14ae565 100644 --- a/src/scene/base-scene-controller.ts +++ b/src/scene/base/base-scene-controller.ts @@ -1,59 +1,71 @@ -import AzNopolyGame from "../game"; +import AzNopolyGame from "../../game"; +import { DynamicPacket } from "../../types/client"; -export default class BaseSceneController { +/** + * A base class for scene controllers that use networking. + * Should be only created with a already initialized & booted scene. + * Would be best to create this class in the scene's init method. + */ +export default abstract class NetworkSceneController { protected aznopoly: AzNopolyGame; - - private _scene: Phaser.Scene; + protected scene: Phaser.Scene; private packetType: string; - private registeredMethods: string[] = []; + private registeredMethods: {method: string, hostOnly: boolean}[] = []; /** * A proxy object that allows for calling methods on the controller. * Functions called on the proxy will be sent to all clients and executed asynchonously * Functions must be registered with registerSyncedMethod */ - protected syncProxy = new Proxy(this, { + public syncProxy = new Proxy(this, { get: (_, prop) => { return this.onProxyCall(prop); }, }); constructor(scene: Phaser.Scene, aznopoly: AzNopolyGame) { - this._scene = scene; + this.scene = scene; this.aznopoly = aznopoly; - this.packetType = "CLIENT_MINIGAME_" + this.constructor.name.toUpperCase(); + this.packetType = "CLIENT_" + this.constructor.name.toUpperCase(); + + scene.events.once(Phaser.Scenes.Events.CREATE, () => { + this.registerPacketListener(); + this.onSceneCreate(); + }); } /** - * Should be called when the scene has initialized all of it's state - * and is ready to start receiving packets + * Will be called after the scene was created and all ui elements are ready */ - public onSceneReady() { + abstract onSceneCreate(): void; + + public registerPacketListener() { const listener = this.aznopoly.addPacketListener(this.packetType, this.onPacket.bind(this) as EventListener); - this._scene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.aznopoly.removePacketListener(this.packetType, listener)); + this.scene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.aznopoly.removePacketListener(this.packetType, listener)); } /** * Registers a method to be allowed to be called with syncProxy * @param method The method to be registered + * @param hostOnly Wether or not the method should only be called by the host */ - public registerSyncedMethod(method: Function) { + public registerSyncedMethod(method: Function, hostOnly: boolean) { const name = method.name; - if (this.registeredMethods.includes(name)) { + if (this.registeredMethods.find(m => m.method === name)) { throw new Error(`Packet executor with name ${name} already exists`); } - this.registeredMethods.push(name); + this.registeredMethods.push({method: name, hostOnly}); } - private isMethodAllowed(method: string) { - return this.registeredMethods.includes(method); + private isMethodAllowed(method: string, isCallerHost: boolean) { + return this.registeredMethods.find(m => m.method === method && (!m.hostOnly || isCallerHost)); } private onProxyCall(prop: symbol | string) { - if (!this.isMethodAllowed(String(prop))) { + if (!this.isMethodAllowed(String(prop), this.aznopoly.isHost)) { throw new Error(`This method was not registered for sync: ${String(prop)}`); } @@ -63,7 +75,7 @@ export default class BaseSceneController { } } - private onPacket(event: CustomEvent<{type: string, data: any}>) { + private onPacket(event: CustomEvent>) { const packet = event.detail; this.executePacket(packet); } @@ -71,6 +83,7 @@ export default class BaseSceneController { private sendExecPacket(method: string, ...args: any[]) { const packet = { type: this.packetType, + sender: this.aznopoly.uuid, data: { method, arguments: args @@ -81,13 +94,13 @@ export default class BaseSceneController { return packet; } - private executePacket(packet: {type: string, data: {method: string, arguments: any[]}}) { + private executePacket(packet: DynamicPacket<{method: string, arguments: any[]}>) { if (packet.type !== this.packetType) { console.error(`Packet type ${packet.type} does not match expected type ${this.packetType}`); return; } - if (!this.isMethodAllowed(packet.data.method)) { + if (!this.isMethodAllowed(packet.data.method, this.aznopoly.isPlayerHost(packet.sender))) { console.error(`Method ${packet.data.method} is not allowed`); return; } @@ -97,7 +110,7 @@ export default class BaseSceneController { console.error(`Method ${packet.data.method} is not a function`); return; } - (method).apply(this, packet.data.arguments); + (method).apply(this, [...packet.data.arguments, packet.sender]); } } \ No newline at end of file diff --git a/src/scene/base/base-scene.ts b/src/scene/base/base-scene.ts new file mode 100644 index 0000000..71618a4 --- /dev/null +++ b/src/scene/base/base-scene.ts @@ -0,0 +1,14 @@ +import AzNopolyGame from "../../game"; +import NetworkSceneController from "../base/base-scene-controller"; + +export abstract class BaseScene extends Phaser.Scene { + + protected aznopoly: AzNopolyGame; + protected controller!: T; + + constructor(aznopoly: AzNopolyGame) { + super(); + this.aznopoly = aznopoly; + } + +} \ No newline at end of file diff --git a/src/scene/base/minigame-scene-controller.ts b/src/scene/base/minigame-scene-controller.ts new file mode 100644 index 0000000..12d24e9 --- /dev/null +++ b/src/scene/base/minigame-scene-controller.ts @@ -0,0 +1,54 @@ +import AzNopolyGame from "../../game"; +import { SceneSwitcher } from "../../scene-switcher"; +import MinigameScene from "./minigame-scene"; +import SyncedSceneController from "./synced-scene-controller"; + +const RESULT_DISPLAY_TIME = 2000; +export default abstract class MinigameSceneController extends SyncedSceneController { + + declare protected scene: MinigameScene; + private previousScene: string; + + constructor(scene: Phaser.Scene, aznopoly: AzNopolyGame, /*previousScene: string */) { + super(scene, aznopoly, "launch"); + this.previousScene = "game"; + + this.registerSyncedMethod(this.showReady, true); + this.registerSyncedMethod(this.showStart, true); + this.registerSyncedMethod(this.endGame, true); + } + + onAllPlayersReady(): void { + console.log("onAllPlayersReady") + if (!this.aznopoly.isHost) { + return; + } + + this.syncProxy.showReady(); + setTimeout(() => this.syncProxy.showStart(), 1500); + } + + private showReady() { + this.scene.showReadyOverlay(); + } + + private showStart() { + this.scene.showStartOverlay(); + } + + protected endGame(playerWon: string[], sorted: boolean) { + this.scene.showResultOverlay(playerWon); + setTimeout(() => this.onGameOver(), RESULT_DISPLAY_TIME); + } + + abstract onMiniGameStart() : void; + + private onGameOver() { + if (!this.aznopoly.isHost) { + return; + } + this.scene.scene.stop(); + this.scene.scene.wake(this.previousScene); + } + +} \ No newline at end of file diff --git a/src/scene/base/minigame-scene.ts b/src/scene/base/minigame-scene.ts new file mode 100644 index 0000000..a6e0dfe --- /dev/null +++ b/src/scene/base/minigame-scene.ts @@ -0,0 +1,91 @@ +import { HEIGHT, WIDTH } from "../../main"; +import { FONT_STYLE_HEADLINE } from "../../style"; +import { BaseScene } from "./base-scene"; +import MinigameSceneController from "./minigame-scene-controller"; + +const START_TIME = 500; +export default abstract class MinigameScene extends BaseScene { + + private overlay!: Phaser.GameObjects.Image; + private centerText!: Phaser.GameObjects.Text; + + private startTimer = 0; + + preload() { + this.load.image('minigame_won', 'assets/crown.png'); + this.load.image('minigame_lost', 'assets/crown.png'); + this.load.image('minigame_start', 'assets/start.png'); + this.load.image('minigame_ready', 'assets/ready.png'); + } + + create() { + this.overlay = this.add.image(WIDTH/2, HEIGHT/2, 'minigame_ready').setOrigin(0, 0); + this.overlay.setOrigin(0.5, 0.5); + this.overlay.setDepth(1000); + this.overlay.setVisible(false); + + this.centerText = this.add.text(WIDTH/2, HEIGHT/2, 'Waiting for players...', FONT_STYLE_HEADLINE); + this.centerText.setOrigin(0.5, 0.5); + this.centerText.setDepth(1000); + } + + update(time: number, delta: number) { + if (this.startTimer > 0) { + this.startTimer -= delta; + + this.overlay.alpha = Math.max(0, this.startTimer / START_TIME); + this.overlay.scale = Math.max(1, 1 + ((START_TIME - this.startTimer) / START_TIME) * 0.25) + if (this.startTimer <= 0) { + this.hideOverlay(); + this.controller.onMiniGameStart(); + } + } + } + + public showReadyOverlay() { + this.overlay.setTexture('minigame_ready'); + this.overlay.setVisible(true); + this.centerText.setVisible(false); + } + + public showStartOverlay() { + this.overlay.setTexture('minigame_start'); + this.overlay.setVisible(true); + + this.startTimer = START_TIME; + } + + public hideOverlay() { + this.overlay.setVisible(false); + } + + public showResultOverlay(playerWon: string[]) { + const won = playerWon.includes(this.aznopoly.uuid); + this.overlay.setVisible(false); + this.overlay.alpha = 1; + this.overlay.scale = 1; + this.overlay.y -= 100; + + const names = playerWon.map(uuid => this.aznopoly.room.getPlayerName(uuid) + " won"); + this.centerText.setText(names.join("\n")) + this.centerText.setVisible(true); + + if (won) { + this.showGameWon(); + } else { + this.showGameLost(); + } + } + + private showGameWon() { + this.overlay.setTexture('minigame_won'); + this.overlay.setVisible(true); + this.overlay.alpha = 1; + } + + private showGameLost() { + this.overlay.setTexture('minigame_lost'); + this.overlay.setVisible(true); + } + +} \ No newline at end of file diff --git a/src/scene/base/synced-scene-controller.ts b/src/scene/base/synced-scene-controller.ts new file mode 100644 index 0000000..df712bd --- /dev/null +++ b/src/scene/base/synced-scene-controller.ts @@ -0,0 +1,56 @@ +import AzNopolyGame from "../../game"; +import { SceneSwitcher } from "../../scene-switcher"; +import NetworkSceneController from "./base-scene-controller"; + + +export default abstract class SyncedSceneController extends NetworkSceneController { + + private launchMethod: "launch" | "start" | "wake"; + + constructor(scene: Phaser.Scene, aznopoly: AzNopolyGame, launchMethod: "launch" | "start" | "wake") { + super(scene, aznopoly); + this.launchMethod = launchMethod; + + this.registerSyncedMethod(this.onAllPlayersReady, true); + this.registerSyncedMethod(this.onAllPlayersWake, true); + SceneSwitcher.listen(scene, aznopoly) + + const listener = this.onSceneWake.bind(this); + scene.events.on(Phaser.Scenes.Events.WAKE, listener); + scene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => scene.events.off(Phaser.Scenes.Events.WAKE, listener)); + } + + onSceneCreate(): void { + if (this.aznopoly.isHost) { + SceneSwitcher.waitForPlayers(this.aznopoly, this.scene.scene.key + "_CREATE", this.launchMethod, this.onAllPlayersJoined.bind(this)); + } + SceneSwitcher.broadcastSceneReady(this.aznopoly, this.scene.scene.key + "_CREATE") + } + + onSceneWake(): void { + if (this.aznopoly.isHost) { + SceneSwitcher.waitForPlayers(this.aznopoly, this.scene.scene.key + "_WAKE", "wake", this.onAllPlayersRejoined.bind(this)); + } + SceneSwitcher.broadcastSceneReady(this.aznopoly, this.scene.scene.key + "_WAKE") + } + + /** + * Will be called after all the host as acknowledged that that all players have joined the scene + */ + abstract onAllPlayersReady(): void; + /** + * Will be called after all the host as acknowledged that that all players have waked the scene and is ready + */ + onAllPlayersWake(): void { + + } + + private onAllPlayersJoined() { + this.syncProxy.onAllPlayersReady(); + } + + private onAllPlayersRejoined() { + this.syncProxy.onAllPlayersWake(); + } + +} \ No newline at end of file diff --git a/src/scene/base/synced-scene.ts b/src/scene/base/synced-scene.ts new file mode 100644 index 0000000..800aca0 --- /dev/null +++ b/src/scene/base/synced-scene.ts @@ -0,0 +1,9 @@ +import AzNopolyGame from "../../game"; +import { SceneSwitcher } from "../../scene-switcher"; +import { BaseScene } from "./base-scene"; +import SyncedSceneController from "./synced-scene-controller"; + + +export default abstract class SyncedScene extends BaseScene { + +} \ No newline at end of file diff --git a/src/scene/board-scene-controller.ts b/src/scene/board-scene-controller.ts new file mode 100644 index 0000000..4aa2ecb --- /dev/null +++ b/src/scene/board-scene-controller.ts @@ -0,0 +1,114 @@ +import AzNopolyGame from "../game"; +import { SceneSwitcher } from "../scene-switcher"; +import SyncedSceneController from "./base/synced-scene-controller"; +import BoardScene from "./board-scene"; + +interface Player { + uuid: string; + name: string; + money: number; + position: number; +} + +export default class BoardSceneController extends SyncedSceneController { + + declare protected scene: BoardScene; + + private currentPlayerUuid: string = ""; + private players!: Player[]; + + constructor(scene: BoardScene, aznopoly: AzNopolyGame) { + super(scene, aznopoly, "start"); + + this.registerSyncedMethod(this.addPlayersToBoard, true); + this.registerSyncedMethod(this.updatePlayerPosition, true); + this.registerSyncedMethod(this.startTurn, true); + this.registerSyncedMethod(this.startMinigame, true); + + this.registerSyncedMethod(this.doDiceRoll, false); + } + + onAllPlayersReady(): void { + if (this.aznopoly.isHost) { + const players = this.aznopoly.connectedUuids.map(uuid => ({ + uuid, + name: this.aznopoly.room.getPlayerName(uuid), + money: 1500, + position: 0, + })); + this.syncProxy.addPlayersToBoard(players); + this.syncProxy.startTurn(players[0].uuid); + } + } + + private addPlayersToBoard(players: Player[]) { + this.players = players; + this.scene.addPlayers(players.map(p => p.uuid)); + } + + private startTurn(uuid: string) { + this.currentPlayerUuid = uuid; + + if(uuid == this.aznopoly.uuid) { + this.scene.enableRollButton(); + } + } + + public onRollClick() { + this.scene.disableRollButton(); + this.syncProxy.doDiceRoll(); + } + + private doDiceRoll() { + if (!this.aznopoly.isHost) { + return; + } + + const sender = arguments[arguments.length - 1]; // Black Magic to get the player uuid that started the roll + if (this.currentPlayerUuid != sender) { + console.warn("Received roll from non-current player"); + return; + } + + const roll = Math.floor(Math.random() * 6) + 1; + const player = this.players.find(p => p.uuid == sender); + if (!player) { + console.error("Player not found"); + return; + } + + player.position += roll; + this.syncProxy.updatePlayerPosition(sender, player.position); + this.startNextTurn(); + } + + private startNextTurn() { + const currentIndex = this.players.findIndex(p => p.uuid == this.currentPlayerUuid); + const nextIndex = (currentIndex + 1) % this.players.length; + const nextPlayer = this.players[nextIndex]; + + this.syncProxy.startTurn(nextPlayer.uuid); + + if (nextIndex == 0) { + this.syncProxy.startMinigame(); + } + } + + private startMinigame() { + this.scene.showMinigameSelect("Roomba Outrage").then(() => { + setTimeout(() => { + this.scene.hideMinigameSelect(); + + if (this.aznopoly.isHost) { + this.scene.scene.sleep(); + this.scene.scene.launch("minigame-roomba"); + } + }, 500) + }); + } + + private updatePlayerPosition(uuid: string, position: number) { + this.scene.updatePlayerPosition(uuid, position); + } + +} \ No newline at end of file diff --git a/src/scene/board-scene.ts b/src/scene/board-scene.ts new file mode 100644 index 0000000..e2e0e31 --- /dev/null +++ b/src/scene/board-scene.ts @@ -0,0 +1,69 @@ +import GameBoard from "../board/board"; +import { HEIGHT, WIDTH } from "../main"; +import { FONT_STYLE_BODY } from "../style"; +import { AzNopolyButton } from "../ui/button"; +import PlayerList from "../ui/player-list"; +import RandomSelectionWheel from "../ui/random-selection-wheel"; +import { BaseScene } from "./base/base-scene"; +import BoardSceneController from "./board-scene-controller"; + + +export default class BoardScene extends BaseScene { + + private board!: GameBoard; + private rollButton!: AzNopolyButton; + private choiceWheel!: RandomSelectionWheel; + + preload() { + GameBoard.preload(this); + AzNopolyButton.preload(this); + } + + init() { + console.log("GameScene init"); + this.controller = new BoardSceneController(this, this.aznopoly); + } + + create() { + const boardSize = HEIGHT * 0.8; + this.board = this.add.existing(new GameBoard(this.aznopoly, this, (WIDTH - boardSize) * 0.5 - 200, (HEIGHT - boardSize) * 0.5, boardSize)); + + const playerList = this.add.existing(new PlayerList(this, false, WIDTH - 300, 0, 250)); + playerList.updatePlayerList(this.aznopoly.room.connectedPlayerIds.map(e => ({uuid: e, name: this.aznopoly.room.getPlayerName(e), host: false}))) + playerList.updateTitle(""); + + this.rollButton = this.add.existing(new AzNopolyButton(this, "Roll Dice", WIDTH - 150, HEIGHT - 100, this.controller.onRollClick.bind(this.controller))); + this.rollButton.disable(); + + this.choiceWheel = this.add.existing(new RandomSelectionWheel(this, WIDTH /2, HEIGHT / 2, {width: 300, height: 40})); + this.choiceWheel.setVisible(false); + + this.add.text(WIDTH - 300, 300, "Current Turn:", FONT_STYLE_BODY); + } + + public addPlayers(players: string[]) { + players.forEach(player => this.board.addPlayer(player)); + } + + public updatePlayerPosition(uuid: string, position: number) { + this.board.movePlayerToPosition(uuid, position); + } + + public enableRollButton() { + this.rollButton.enable(); + } + + public disableRollButton() { + this.rollButton.disable(); + } + + public showMinigameSelect(name: string) : Promise { + this.choiceWheel.setVisible(true); + return this.choiceWheel.startSpin(["Giga Chad says", "Poop up", "GRAND SURPRISE"], name); + } + + public hideMinigameSelect() { + this.choiceWheel.setVisible(false); + } + +} \ No newline at end of file diff --git a/src/scene/game-scene.ts b/src/scene/game-scene.ts deleted file mode 100644 index 8184c4d..0000000 --- a/src/scene/game-scene.ts +++ /dev/null @@ -1,129 +0,0 @@ -import GameBoard from "../board/board"; -import { HEIGHT, WIDTH } from "../main"; -import { FONT_STYLE_BODY } from "../style"; -import { GameTurnRollPacket, GameTurnStartPacket, PacketType } from "../types/client"; -import { AzNopolyButton } from "../ui/button"; -import PlayerList from "../ui/player-list"; -import { BaseScene } from "./base-scene"; - - -export default class GameScene extends BaseScene { - - private board!: GameBoard; - private rollButton!: AzNopolyButton; - - private currentPlayerIndex: number = 0; - private currentTurnValue!: Phaser.GameObjects.Text; - - private turnNumber: number = 0; - - preload() { - GameBoard.preload(this); - } - - hostOnAllPlayerReady() { - this.aznopoly.room.connectedPlayerIds.forEach(uuid => { - this.board.addPlayer(uuid); - }); - - this.startTurn(); - } - - create() { - console.log("GameScene created") - const boardSize = HEIGHT * 0.8; - this.board = this.add.existing(new GameBoard(this.aznopoly, this, (WIDTH - boardSize) * 0.5 - 200, (HEIGHT - boardSize) * 0.5, boardSize)); - - const playerList = this.add.existing(new PlayerList(this, false, WIDTH - 300, 0, 250)); - playerList.updatePlayerList(this.aznopoly.room.connectedPlayerIds.map(e => ({uuid: e, name: this.aznopoly.room.getPlayerName(e), host: false}))) - playerList.updateTitle(""); - - this.rollButton = this.add.existing(new AzNopolyButton(this, "Roll Dice", WIDTH - 150, HEIGHT - 100, this.onRollClick.bind(this))); - this.rollButton.disable(); - - this.add.text(WIDTH - 300, 300, "Current Turn:", FONT_STYLE_BODY); - this.currentTurnValue = this.add.text(WIDTH - 300, 350, "", FONT_STYLE_BODY); - - this.networkInit(); - super.create(); - } - - private networkInit() { - if (this.aznopoly.isHost) { - this.aznopoly.client.addEventListener(PacketType.GAME_TURN_ROLL, this.onTurnRoll.bind(this) as EventListener); - } - - this.aznopoly.client.addEventListener(PacketType.GAME_TURN_START, this.onTurnStart.bind(this) as EventListener); - } - - private onRollClick() { - this.rollButton.disable(); - - const packet: GameTurnRollPacket = { - type: PacketType.GAME_TURN_ROLL, - sender: this.aznopoly.player.uuid, - data: {} - } - - if (this.aznopoly.isHost) { - this.onTurnRoll(new CustomEvent(PacketType.GAME_TURN_ROLL, { detail: packet })); - } else { - this.aznopoly.client.sendPacket(packet); - } - } - - private onTurnStart(event: CustomEvent) { - const packet = event.detail; - if (packet.data.player == this.aznopoly.player.uuid) { - this.rollButton.enable(); - } - - this.currentTurnValue.setText(this.aznopoly.room.getPlayerName(packet.data.player)); - } - - private onTurnRoll(event: CustomEvent) { - if (!this.aznopoly.isHost) return; - - const packet = event.detail; - const currentPlayer = this.aznopoly.room.connectedPlayerIds[this.currentPlayerIndex]; - if (packet.sender == currentPlayer) { - this.rollDice(); - } - } - - private rollDice() { - const roll = Math.floor(Math.random() * 6) + 1; - - const nextPlayer = this.aznopoly.room.connectedPlayerIds[this.currentPlayerIndex]; - this.board.movePlayer(nextPlayer, roll); - - this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.aznopoly.room.connectedPlayerIds.length; - this.startTurn(); - } - - private startMiniGame(name: string) { - this.scene.sleep(); - this.scene.launch(name, { launchMethod: "launch", previousScene: this.scene.key }); - } - - private startTurn() { - this.turnNumber++; - if (this.turnNumber > 1 && this.currentPlayerIndex == 0) { - this.startMiniGame("minigame-roomba"); - } - - - const currentPlayer = this.aznopoly.room.connectedPlayerIds[this.currentPlayerIndex]; - - const packet: GameTurnStartPacket = { - type: PacketType.GAME_TURN_START, - sender: this.aznopoly.player.uuid, - data: { - player: currentPlayer, - } - } - this.aznopoly.client.sendPacket(packet); - this.onTurnStart(new CustomEvent(PacketType.GAME_TURN_START, { detail: packet })); - } - -} \ No newline at end of file diff --git a/src/scene/lobby-scene-controller.ts b/src/scene/lobby-scene-controller.ts new file mode 100644 index 0000000..6f32077 --- /dev/null +++ b/src/scene/lobby-scene-controller.ts @@ -0,0 +1,43 @@ +import AzNopolyGame from "../game"; +import { RoomEvent } from "../room"; +import { SceneSwitcher } from "../scene-switcher"; +import NetworkSceneController from "./base/base-scene-controller"; +import LobbyScene from "./lobby-scene"; + + +export default class LobbySceneController extends NetworkSceneController { + + declare protected scene: LobbyScene; + + constructor(scene: LobbyScene, aznopoly: AzNopolyGame) { + super(scene, aznopoly); + SceneSwitcher.listen(scene, aznopoly) + } + + onSceneCreate(): void { + this.aznopoly.room.addEventListener(RoomEvent.JOIN, this.onRoomUpdated.bind(this)); + this.aznopoly.room.addEventListener(RoomEvent.LEAVE, this.onRoomUpdated.bind(this)); + this.aznopoly.room.addEventListener(RoomEvent.READY, this.onRoomUpdated.bind(this)); + this.aznopoly.room.addEventListener(RoomEvent.UPDATE, this.onRoomUpdated.bind(this)); + + this.updatePlayerList(); + } + + private onRoomUpdated() { + this.updatePlayerList(); + } + + private updatePlayerList() { + const players = this.aznopoly.connectedUuids.map(uuid => ({ + uuid: uuid, + name: this.aznopoly.room.getPlayerName(uuid), + host: this.aznopoly.isPlayerHost(uuid) + })) + this.scene.updatePlayerList(players); + } + + public onStartClick() { + this.scene.scene.start('game'); + } + +} \ No newline at end of file diff --git a/src/scene/lobby-scene.ts b/src/scene/lobby-scene.ts index 0e178f2..4452e6f 100644 --- a/src/scene/lobby-scene.ts +++ b/src/scene/lobby-scene.ts @@ -1,14 +1,13 @@ import { FONT_STYLE_HEADLINE } from "../style"; -import { RoomEvent } from "../room"; import TilingBackground from "../ui/tiling-background"; import { AzNopolyButton } from "../ui/button"; import PlayerList from "../ui/player-list"; -import { SceneChangePacket, PacketType } from "../types/client"; import { HEIGHT, WIDTH } from "../main"; -import { BaseScene } from "./base-scene"; +import { BaseScene } from "./base/base-scene"; +import LobbySceneController from "./lobby-scene-controller"; -export default class LobbyScene extends BaseScene { +export default class LobbyScene extends BaseScene { private playerList!: PlayerList; @@ -18,64 +17,27 @@ export default class LobbyScene extends BaseScene { this.load.image('lobby_bg', 'assets/lobby_background.png'); } + init() { + this.controller = new LobbySceneController(this, this.aznopoly); + } + create() { this.add.existing(new TilingBackground(this, 'lobby_bg', new Phaser.Math.Vector2(2, 1), 35, 1.75)); this.add.text(0, 0, `Lobby ( ${this.aznopoly.room.id} )`, FONT_STYLE_HEADLINE); this.playerList = this.add.existing(new PlayerList(this, this.aznopoly.isHost, 100, 200, 450)); - this.updatePlayerList(); this.initButton(); - - this.addRoomEventListener(); } private initButton() { - // this.add.existing(new AzNopolyButton(this, "Exit", WIDTH - 400, HEIGHT - 120, () => { - // this.scene.stop('lobby'); - // })); if (!this.aznopoly.isHost) return; - this.add.existing(new AzNopolyButton(this, "Start Game", WIDTH - 200, HEIGHT - 120, () => { - this.startGameScene(); - })); - } - - private addRoomEventListener() { - const listener: [string, EventListener][] = [ - [RoomEvent.JOIN, this.onPlayerJoin.bind(this) as EventListener], - [RoomEvent.LEAVE, this.onPlayerLeave.bind(this) as EventListener], - [RoomEvent.UPDATE, this.onPlayerUpdate.bind(this) as EventListener] - ] - listener.forEach(([event, listener]) => { - this.aznopoly.room.addEventListener(event, listener); - }); - this.events.once('shutdown', () => { - listener.forEach(([event, listener]) => { - this.aznopoly.room.removeEventListener(event, listener); - }); - }); - } - - private startGameScene() { - this.scene.start('game', { aznopoly: this.aznopoly }); + const button = new AzNopolyButton(this, "Start Game", WIDTH - 200, HEIGHT - 120, this.controller.onStartClick.bind(this.controller)); + this.add.existing(button); } - private updatePlayerList() { - const arr = this.aznopoly.room.connectedPlayerIds; - const connectedNames = arr.map(uuid => ({ uuid, name: this.aznopoly.room.getPlayerName(uuid), host: this.aznopoly.isPlayerHost(uuid) })) - this.playerList.updatePlayerList(connectedNames); + public updatePlayerList(player: { uuid: string, name: string, host: boolean }[]) { + this.playerList.updatePlayerList(player); } - - private onPlayerJoin(event: CustomEvent) { - this.updatePlayerList(); - } - - private onPlayerLeave(event: CustomEvent) { - this.updatePlayerList(); - } - - private onPlayerUpdate() { - this.updatePlayerList(); - } - + } \ No newline at end of file diff --git a/src/scene/minigame-scene.ts b/src/scene/minigame-scene.ts deleted file mode 100644 index 7c623cf..0000000 --- a/src/scene/minigame-scene.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { HEIGHT, WIDTH } from "../main"; -import { PacketType, PlayerPacket } from "../types/client"; -import { BaseScene } from "./base-scene"; - - -export interface MinigameResultPacket extends PlayerPacket { - type: PacketType.MINIGAME_RESULT, - data: { - playerWon: string[], - sorted: boolean, - } -} - -export interface MinigameReadyPacket extends PlayerPacket { - type: PacketType.MINIGAME_READY, - data: { } -} - -export default abstract class MinigameScene extends BaseScene { - - private previousScene!: string; - - private overlay!: Phaser.GameObjects.Image; - - preload() { - this.load.image('minigame_won', 'assets/crown.png'); - this.load.image('minigame_lost', 'assets/crown.png'); - this.load.image('minigame_start', 'assets/start.png'); - this.load.image('minigame_ready', 'assets/ready.png'); - } - - init(data: any) { - super.init(data); - this.addPacketListener(PacketType.MINIGAME_READY, this.onMiniGameReady.bind(this)); - this.addPacketListener(PacketType.MINIGAME_RESULT, this.onResultPacket.bind(this)); - - this.previousScene = data?.previousScene; - } - - create() { - this.overlay = this.add.image(WIDTH/2, HEIGHT/2, 'minigame_ready').setOrigin(0, 0); - this.overlay.setOrigin(0.5, 0.5); - this.overlay.setDepth(1000); - - super.create(); - } - - protected hostOnAllPlayerReady(): void { - this.startMiniGame(); - } - - private startMiniGame() { - const packet: MinigameReadyPacket = { - type: PacketType.MINIGAME_READY, - sender: this.aznopoly.client.id, - data: {} - }; - - this.aznopoly.client.sendPacket(packet); - setTimeout(() => { - this.onMiniGameReady(packet); - }, 10) // simulate network delay - } - - abstract onMiniGameStart() : void; - - private onMiniGameReady(_: MinigameReadyPacket) { - setTimeout(() => { - this.overlay.setVisible(false); - this.onMiniGameStart(); - }, 1000); - } - - private onResultPacket(packet: MinigameResultPacket) { - if (packet.data.playerWon.includes(this.aznopoly.client.id)) { - this.showGameWon(); - } else { - this.showGameLost(); - } - - setTimeout(() => { - this.onGameOver(); - }, 1000) - } - - private showGameWon() { - this.overlay.setTexture('minigame_won'); - this.overlay.setVisible(true); - } - - private showGameLost() { - this.overlay.setTexture('minigame_lost'); - this.overlay.setVisible(true); - } - - private onGameOver() { - this.scene.stop(); - this.scene.wake(this.previousScene); - } - - protected endGame(playerWon: string[], sorted: boolean) { - if (!this.aznopoly.isHost) return; - const packet: MinigameResultPacket = { - type: PacketType.MINIGAME_RESULT, - sender: this.aznopoly.client.id, - data: { - playerWon, - sorted, - } - }; - - this.aznopoly.client.sendPacket(packet); - this.onResultPacket(packet); - } - -} \ No newline at end of file diff --git a/src/scene/minigame/roomba-scene-controller.ts b/src/scene/minigame/roomba-scene-controller.ts index e7163ee..ebe464f 100644 --- a/src/scene/minigame/roomba-scene-controller.ts +++ b/src/scene/minigame/roomba-scene-controller.ts @@ -2,14 +2,19 @@ import AzNopolyGame from "../../game"; import { HEIGHT as GAME_HEIGHT, WIDTH as GAME_WIDTH } from "../../main"; import { Roomba, RoombaConfig } from "../../minigame/roomba"; import { getColorFromUUID } from "../../util"; -import BaseSceneController from "../base-scene-controller"; +import MinigameSceneController from "../base/minigame-scene-controller"; import { RoombaScene } from "./roomba-scene"; import convert from 'color-convert'; -export default class RoombaSceneController extends BaseSceneController { +const MAX_GAME_TIME = 3000; +export default class RoombaSceneController extends MinigameSceneController { - private scene: RoombaScene; + declare protected scene: RoombaScene; + + private locked = false; + + private colorUuuidMap = new Map(); constructor(scene: RoombaScene, aznopoly: AzNopolyGame) { super(scene, aznopoly); @@ -17,28 +22,44 @@ export default class RoombaSceneController extends BaseSceneController { this.scene = scene; this.aznopoly = aznopoly; - this.registerSyncedMethod(this.initRoombas); - this.registerSyncedMethod(this.updateRoombaDirection); + this.registerSyncedMethod(this.initRoombas, true); + this.registerSyncedMethod(this.lockAllGameplay, true); + + this.registerSyncedMethod(this.updateRoombaDirection, false); } - public hostInit() { - const roombaConfigs = []; + onMiniGameStart(): void { + this.scene.events.on("roomba-dragged", this.onRoombaDragged.bind(this)); - for (let j = 0; j < this.aznopoly.players.length; j++) { - for (let i = 0; i < 5; i++) { - roombaConfigs.push(this.generateRandomRoombaConfig(this.aznopoly.players[j])); - } + if (!this.aznopoly.isHost) { + return; } - + + const roombaConfigs = this.generateRoombaConfigs(); this.syncProxy.initRoombas(roombaConfigs); + + setTimeout(() => { + this.syncProxy.lockAllGameplay(); + + const won = this.getPlayersWon(); + this.syncProxy.endGame(won, false); + }, MAX_GAME_TIME) } - public onSceneReady() { - super.onSceneReady(); - - this.scene.events.on("roomba-dragged", ({id, offset} : {id: string, offset: Phaser.Math.Vector2}) => { - this.syncProxy.updateRoombaDirection(id, offset); - }); + private getPlayersWon() { + const paintMap = this.scene.getPaintMap(); + const paintedColors = Object.keys(paintMap).filter(e => e != "000000"); + + const won = paintedColors.sort() + .map((key) => this.colorUuuidMap.get(key)!) + .slice(0, 1); + + return won; + } + + private onRoombaDragged({ id, offset}: {id: string, offset: Phaser.Math.Vector2}) { + if (this.locked) return; + this.syncProxy.updateRoombaDirection(id, offset); } private updateRoombaDirection(id: string, direction: Phaser.Math.Vector2) { @@ -49,6 +70,23 @@ export default class RoombaSceneController extends BaseSceneController { this.scene.initRoombas(configs); } + private lockAllGameplay() { + this.locked = true; + this.scene.stopRoombas(); + } + + private generateRoombaConfigs() { + const roombaConfigs = []; + for (let j = 0; j < this.aznopoly.connectedUuids.length; j++) { + const uuid = this.aznopoly.connectedUuids[j] ; + for (let i = 0; i < 5; i++) { + roombaConfigs.push(this.generateRandomRoombaConfig(uuid)); + } + this.colorUuuidMap.set(roombaConfigs[roombaConfigs.length-1].paintColor.toString(16).toUpperCase(), uuid); + } + return roombaConfigs; + } + private generateRandomRoombaConfig(playerid: string) { const x = Math.random() * (GAME_WIDTH - Roomba.SIZE * 2) + Roomba.SIZE; const y = Math.random() * (GAME_HEIGHT - Roomba.SIZE * 2) + Roomba.SIZE; @@ -64,7 +102,7 @@ export default class RoombaSceneController extends BaseSceneController { angle: Math.random() * Math.PI * 2, color: color, paintColor: paintColorHex, - speed: 50 + speed: 150 } } diff --git a/src/scene/minigame/roomba-scene.ts b/src/scene/minigame/roomba-scene.ts index a12d676..ed24be7 100644 --- a/src/scene/minigame/roomba-scene.ts +++ b/src/scene/minigame/roomba-scene.ts @@ -1,4 +1,4 @@ -import MinigameScene, { MinigameReadyPacket } from "../minigame-scene"; +import MinigameScene from "../base/minigame-scene"; import { Roomba, RoombaConfig } from "../../minigame/roomba"; import { HEIGHT, WIDTH } from "../../main"; import RoombaSceneController from "./roomba-scene-controller"; @@ -8,9 +8,7 @@ import convert from 'color-convert'; const GRAPHICS_SWAP_TIME = 1; const PAINT_REFRESH_TIME = 0.5; -export class RoombaScene extends MinigameScene { - - private controller: RoombaSceneController; +export class RoombaScene extends MinigameScene { private roombas: Roomba[] = []; @@ -23,21 +21,33 @@ export class RoombaScene extends MinigameScene { private colorProgressBar!: ColorProgressBar; constructor(aznopoly: AzNopolyGame) { - super(aznopoly, true); - - this.controller = new RoombaSceneController(this, aznopoly); + super(aznopoly); + } + + init() { + console.log("RoombaScene init", this.roombas); + this.controller = new RoombaSceneController(this, this.aznopoly); + this.roombas = [] + this.timeSinceLastPaint = 0; + this.timeSinceGraphicsSwap = 0; } preload() { super.preload(); + Roomba.preload(this); } create() { super.create(); this.physics.world.setBounds(0, 0, WIDTH, HEIGHT); + this.physics.world.fixedStep = true; this.paint = this.add.graphics(); this.paintTexture = this.textures.addDynamicTexture("roomba-paint", WIDTH, HEIGHT)!; + this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + this.paint.destroy(); + this.paintTexture.destroy(); + }); this.colorProgressBar = new ColorProgressBar(this, WIDTH / 2 - 200, 25, 400, 40); @@ -46,16 +56,10 @@ export class RoombaScene extends MinigameScene { } update(_: number, delta: number) { + super.update(_, delta); this.paintRoombaUpdate(delta); this.graphicSwapUpdate(delta); } - - onMiniGameStart() { - this.controller.onSceneReady(); - if (this.aznopoly.isHost) { - this.controller.hostInit(); - } - } public initRoombas(configs: RoombaConfig[]) { this.roombas = configs.map(config => new Roomba(this, config)); @@ -66,6 +70,12 @@ export class RoombaScene extends MinigameScene { this.physics.add.collider(this.roombas, this.roombas); } + public stopRoombas() { + this.roombas.forEach(roomba => { + roomba.stop(); + }); + } + public updateRoombaDirection(roombaId: String, direction: Phaser.Math.Vector2) { const roomba = this.roombas.find(roomba => roomba.id === roombaId); if (roomba) { @@ -79,6 +89,8 @@ export class RoombaScene extends MinigameScene { this.timeSinceLastPaint += delta / 1000; if (this.timeSinceLastPaint > PAINT_REFRESH_TIME) { this.timeSinceLastPaint = 0; + + console.log("Painting", this.roombas.length); this.roombas.forEach(roomba => { roomba.paintPath(this.paint); }); @@ -98,6 +110,11 @@ export class RoombaScene extends MinigameScene { } private calculatePaintPercentage() { + const result = this.getPaintMap(); + this.updateProgressBar(result); + } + + public getPaintMap() { if (this.paintTexture.renderTarget) { const renderer = this.paintTexture.renderer as Phaser.Renderer.WebGL.WebGLRenderer; @@ -110,7 +127,7 @@ export class RoombaScene extends MinigameScene { renderer.setFramebuffer(prevFramebuffer); const result = this.readPixelArray(pixels); - this.updateProgressBar(result); + return result; } else { var copyCanvas = Phaser.Display.Canvas.CanvasPool.createWebGL(this, WIDTH, HEIGHT) @@ -119,11 +136,11 @@ export class RoombaScene extends MinigameScene { const pixels = ctx.getImageData(0, 0, WIDTH, HEIGHT).data; const result = this.readPixelArray(pixels); - this.updateProgressBar(result); - Phaser.Display.Canvas.CanvasPool.remove(copyCanvas); + return result; } } + private updateProgressBar(colors: {[key: string]: number}) { const colorMap = new Map(); diff --git a/src/scene/minigame/simon-says-scene.ts b/src/scene/minigame/simon-says-scene.ts index ffcd35b..b6d9e56 100644 --- a/src/scene/minigame/simon-says-scene.ts +++ b/src/scene/minigame/simon-says-scene.ts @@ -1,8 +1,9 @@ +/* import { HEIGHT, WIDTH } from "../../main"; import { SimonSaysBoard } from "../../minigame/simon-says-board"; import { PacketType, PlayerPacket } from "../../types/client"; import { TimeBar } from "../../ui/time-bar"; -import MinigameScene from "../minigame-scene"; +import MinigameScene from "../base/minigame-scene"; import { Audio } from "../../types"; interface SimonSaysPacket extends PlayerPacket { @@ -192,8 +193,7 @@ export class SimonSaysScene extends MinigameScene { } // HOST ONLY - - async onMinigameStart() { + async onMiniGameStart() { this.activePlayer = this.aznopoly.room.connectedPlayerIds.slice(); await this.startRound(this.generateRandomSequence(4)); @@ -252,4 +252,5 @@ export class SimonSaysScene extends MinigameScene { return sequence; } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/scene/title-scene-controller.ts b/src/scene/title-scene-controller.ts new file mode 100644 index 0000000..0891391 --- /dev/null +++ b/src/scene/title-scene-controller.ts @@ -0,0 +1,64 @@ +import AzNopolyGame from "../game"; +import { RoomEvent } from "../room"; +import TitleScene from "./title-scene"; + + +export default class TitleSceneController { + + private musicOn: boolean = true; + + private scene: TitleScene; + private aznopoly: AzNopolyGame; + + constructor(scene: TitleScene, aznopoly: AzNopolyGame) { + this.scene = scene; + this.aznopoly = aznopoly; + } + + public onMusicButtonClicked() { + if (this.musicOn) { + this.scene.stopMusic(); + this.musicOn = false; + } else { + this.scene.startMusic(); + this.musicOn = true; + } + } + + public onJoinRoomClick() { + const code = this.scene.getInputtedLobbyCode(); + if (!code || code.length !== 6) { + return + } + + this.joinRoom(code); + } + + public onCreateRoom() { + const room = this.generateRoomName(); + this.joinRoom(room); + } + + private joinRoom(room: string) { + this.scene.playStartSound(); + setTimeout(() => { + this.scene.stopMusic(); + this.aznopoly.init(room); + this.aznopoly.room.addEventListener(RoomEvent.READY, () => { + this.scene.scene.start('lobby'); + }, { once: true }); + }, 500) + } + + private generateRoomName(length: number = 6) : string { + let roomName = ""; + let characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i++) { + roomName += characters.charAt(Math.floor(Math.random() * characters.length)); + } + + return roomName; + } + +} \ No newline at end of file diff --git a/src/scene/title-scene.ts b/src/scene/title-scene.ts index c3dd113..118395b 100644 --- a/src/scene/title-scene.ts +++ b/src/scene/title-scene.ts @@ -1,12 +1,12 @@ import AzNopolyGame from "../game"; import { WIDTH } from "../main"; -import { RoomEvent } from "../room"; import { AzNopolyButton } from "../ui/button"; -import { BaseScene } from "./base-scene"; +import { BaseScene } from "./base/base-scene"; +import TitleSceneController from "./title-scene-controller"; type Audio = Phaser.Sound.WebAudioSound | Phaser.Sound.NoAudioSound | Phaser.Sound.HTML5AudioSound -export default class TitleScene extends BaseScene { +export default class TitleScene extends BaseScene { private bgm!: Audio; private audioStart!: Audio; @@ -15,6 +15,10 @@ export default class TitleScene extends BaseScene { private domContainer!: Phaser.GameObjects.DOMElement; private domNameInput!: HTMLInputElement; private domLobbyCodeInput!: HTMLInputElement; + + constructor(aznopoly: AzNopolyGame) { + super(aznopoly); + } preload() { this.load.image('abstracto', 'assets/title.png'); @@ -28,6 +32,11 @@ export default class TitleScene extends BaseScene { AzNopolyButton.preload(this); } + init() { + console.log("TitleScene init", this.aznopoly) + this.controller = new TitleSceneController(this, this.aznopoly); + } + create() { const background = this.add.image(0, 0, 'abstracto'); const targetScale = WIDTH / background.width; @@ -46,67 +55,37 @@ export default class TitleScene extends BaseScene { this.audioStart = this.game.sound.add('game-start'); this.initButtons(); - - //setTimeout(() => { this.joinRoom("debugg", "Michael" + ("" + Math.random()).substring(2,4)) }, 1000) } private initButtons() { const centerX = WIDTH / 2; - this.add.existing(new AzNopolyButton(this, 'Join Lobby', centerX - 250, 600, this.onJoinRoomClick.bind(this))); - this.add.existing(new AzNopolyButton(this, 'Create Lobby', centerX + 250, 600, this.onCreateRoom.bind(this))); + this.add.existing(new AzNopolyButton(this, 'Join Lobby', centerX - 250, 600, this.controller.onJoinRoomClick.bind(this.controller))); + this.add.existing(new AzNopolyButton(this, 'Create Lobby', centerX + 250, 600, this.controller.onCreateRoom.bind(this.controller))); const graphics = this.add.graphics(); graphics.lineStyle(2, 0x000000, 1); graphics.strokeCircle(WIDTH - 50, 50, 20); this.btnMusic = this.add.image(WIDTH - 50, 50, 'music-on'); this.btnMusic.setInteractive(); - this.btnMusic.on('pointerdown', this.onMusicClick.bind(this)); + this.btnMusic.on('pointerdown', this.controller.onMusicButtonClicked.bind(this.controller)); } - - private generateRoomName(length: number = 6) : string { - let roomName = ""; - let characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i = 0; i < length; i++) { - roomName += characters.charAt(Math.floor(Math.random() * characters.length)); - } - - return roomName; + + public stopMusic() { + this.bgm.pause(); + this.btnMusic.setTexture('music-off'); } - private onMusicClick() { - const texture = this.bgm.isPlaying ? 'music-off' : 'music-on'; - this.btnMusic.setTexture(texture); - if (this.bgm.isPlaying) { - this.bgm.pause(); - } else { - this.bgm.resume(); - } + public startMusic() { + this.bgm.resume(); + this.btnMusic.setTexture('music-on'); } - private onJoinRoomClick() { - const name = this.domNameInput.value; - const room = this.domLobbyCodeInput.value; - this.joinRoom(room, name); + public playStartSound() { + this.audioStart.play(); } - private onCreateRoom() { - const name = this.domNameInput.value; - const room = this.generateRoomName(); - this.joinRoom(room, name); + public getInputtedLobbyCode(): string { + return this.domLobbyCodeInput.value; } - private joinRoom(room: string, name: string) { - setTimeout(() => { - this.audioStart.play(); - }, 100) - - setTimeout(() => { - this.aznopoly.init(room); - this.aznopoly.room.addEventListener(RoomEvent.READY, () => { - this.scene.start('lobby'); - }, { once: true }); - this.bgm.stop(); - }, 500) - } } \ No newline at end of file diff --git a/src/style.ts b/src/style.ts index 99f4ab0..024ddce 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,4 +1,5 @@ -export const COLOR_PRIMARY = 0x73c8e4; +export const COLOR_PRIMARY = 0x494949; +export const COLOR_PRIMARY_2 = 0x9f9f9f; export const COLOR_CONTRAST = 0xffffff; type TextStyle = Phaser.Types.GameObjects.Text.TextStyle; diff --git a/src/types/client.ts b/src/types/client.ts index 1dbcf91..9bd60b0 100644 --- a/src/types/client.ts +++ b/src/types/client.ts @@ -43,7 +43,11 @@ export interface BasePacket { export interface PlayerPacket extends BasePacket { sender: string; - target?: string; + data: any; +} + +export interface DynamicPacket extends PlayerPacket { + data: T; } export interface RoomInitPacket extends BasePacket { @@ -90,7 +94,7 @@ export interface SceneChangePacket extends PlayerPacket { type: PacketType.SCENE_CHANGE; data: { scene: string, - launchMethod: string, + launchMethod: "start" | "launch" | "wake", }; } diff --git a/src/ui/button.ts b/src/ui/button.ts index 57875d6..681f3a8 100644 --- a/src/ui/button.ts +++ b/src/ui/button.ts @@ -1,4 +1,3 @@ -import { Scene } from "phaser"; import { COLOR_CONTRAST, COLOR_PRIMARY } from "../style"; import { easeOutElastic } from "../util"; @@ -9,7 +8,7 @@ export const FONT_STYLE_BUTTON_DOWN: Phaser.Types.GameObjects.Text.TextStyle = { const MAX_HOVER_TIMER = 4; export class AzNopolyButton extends Phaser.GameObjects.Container { - public static preload(scene: Scene) { + public static preload(scene: Phaser.Scene) { scene.load.audio('button-over', 'assets/button_over.mp3'); scene.load.audio('button-out', 'assets/button_out.mp3'); scene.load.audio('button-down', 'assets/button_down.mp3'); @@ -31,7 +30,7 @@ export class AzNopolyButton extends Phaser.GameObjects.Container { private onClick: () => void; - constructor(scene: Scene, title: string, x: number, y: number, onClick: () => void) { + constructor(scene: Phaser.Scene, title: string, x: number, y: number, onClick: () => void) { super(scene); this.onClick = onClick; diff --git a/src/ui/random-selection-wheel.ts b/src/ui/random-selection-wheel.ts new file mode 100644 index 0000000..25b3637 --- /dev/null +++ b/src/ui/random-selection-wheel.ts @@ -0,0 +1,150 @@ +import { COLOR_PRIMARY, COLOR_PRIMARY_2, FONT_STYLE_BODY } from "../style"; + +const SPIN_TIME = 2000; +const NUM_CHOICE_CHANGES = 50; +const FADE_IN_TIME = 100; +const PADDING = 10; +export default class RandomSelectionWheel extends Phaser.GameObjects.Container { + + private choiceIndex!: number; + + private titleText!: Phaser.GameObjects.Text; + private choiceTexts: Phaser.GameObjects.Text[] = []; + private graphics: Phaser.GameObjects.Graphics; + + private spinTimer: number = 0; + private choiceChangeTimer: number = 0; + private indexSpinOffset: number = 0; + + private fadeInTimer: number = 0; + private startPos!: Phaser.Math.Vector2; + + private heightPerChoice: number; + private fadeinCallback?: () => void; + private selectionCallback?: () => void; + + constructor(scene: Phaser.Scene, x: number, y: number, bounds = {width: 300, height: 200}) { + super(scene, x, y); + this.startPos = new Phaser.Math.Vector2(x, y); + + this.width = bounds.width; + this.heightPerChoice = bounds.height; + + this.graphics = new Phaser.GameObjects.Graphics(scene); + + this.titleText = new Phaser.GameObjects.Text(scene, this.width*0.5, -PADDING - 45, "MINIGAMES", FONT_STYLE_BODY); + this.titleText.setOrigin(0.5, 0); + + this.add(this.graphics); + this.choiceTexts.forEach(text => this.add(text)); + this.add(this.titleText); + } + + private redrawUi() { + this.graphics.clear(); + this.graphics.fillStyle(COLOR_PRIMARY_2); + this.graphics.fillRect(-PADDING, -PADDING - 50, this.width + PADDING * 2, this.height + PADDING * 2 + 50); + + this.graphics.fillStyle(COLOR_PRIMARY); + this.graphics.fillRect(-PADDING + 5, -PADDING - 50 + 5, this.width + PADDING * 2 - 10, 45); + } + + public startSpin(fakeChoices: string[], choice: string) : Promise { + this.height = this.heightPerChoice * (fakeChoices.length + 1); + this.setPosition(this.startPos.x - this.width / 2, this.startPos.y - this.height / 2); + this.redrawUi(); + + this.choiceTexts.forEach(text => this.remove(text, true)); + + const choices = [...fakeChoices, choice].sort(() => Math.random() - 0.5); + this.choiceIndex = choices.indexOf(choice); + + + this.choiceTexts = choices.map((choice, i) => { + const text = this.scene.add.text(0, 0, choice, FONT_STYLE_BODY); + text.x = this.width / 2; + text.y = i * this.heightPerChoice; + text.setOrigin(0.5, 0); + return text; + }); + this.choiceTexts.forEach(text => this.add(text)); + + this.fadeIn().then(() => { + this.spinTimer = SPIN_TIME; + this.choiceChangeTimer = 0; + this.indexSpinOffset = (this.choiceIndex - NUM_CHOICE_CHANGES + 1) % this.choiceTexts.length; + }) + + return new Promise(resolve => { + this.selectionCallback = resolve; + }); + } + + private setChoiceSelected(index: number) { + this.choiceTexts.forEach((text, i) => { + text.setStyle(FONT_STYLE_BODY); + if(i === index) { + text.setStyle({color: '#ffffff'}); + } + }); + } + + preUpdate(time: number, delta: number) { + this.fadeInUpdate(delta); + this.spinUpdate(delta); + } + + private fadeInUpdate(delta: number) { + if(this.fadeInTimer <= 0) { + return; + } + this.fadeInTimer -= delta; + + const progress = 1 - this.fadeInTimer / FADE_IN_TIME; + this.alpha = progress * 0.5 + 0.5; + this.scale = progress * 0.5 + 0.5; + this.setPosition(this.startPos.x - this.width * 0.5 * this.scale, this.startPos.y - this.height * 0.5 * this.scale); + + if (this.fadeInTimer <= 0) { + this.fadeinCallback?.(); + } + } + + private spinUpdate(delta: number) { + if(this.spinTimer <= 0) { + return; + } + this.spinTimer -= delta; + + if (this.choiceChangeTimer > 0) { + this.choiceChangeTimer -= delta; + return; + } + + const timeSpun = SPIN_TIME - this.spinTimer; + const timePerSpin = SPIN_TIME / NUM_CHOICE_CHANGES; + const index = Math.floor(this.interpolateSpin(timeSpun / timePerSpin) + this.indexSpinOffset) % this.choiceTexts.length; + this.setChoiceSelected(index); + + if(this.spinTimer <= 0) { + this.selectionCallback?.(); + } + } + + public fadeIn() : Promise { + this.fadeInTimer = FADE_IN_TIME; + this.alpha = 0.0; + this.scale = 0.5; + this.setPosition(this.startPos.x - this.width * 0.5 * this.scale, this.startPos.y - this.height * 0.5 * this.scale); + + return new Promise(resolve => { + this.fadeinCallback = resolve; + }); + } + + private interpolateSpin(t: number) { + return Math.sin(t / NUM_CHOICE_CHANGES * Math.PI * 0.5) * NUM_CHOICE_CHANGES; + } + + +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 347ae50..36eba00 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,3 @@ -import { Scene } from "phaser"; -import { FONT_STYLE_BUTTON, FONT_STYLE_BUTTON_HOVER } from "./style"; import convert from 'color-convert'; export function easeOutElastic(x: number): number { @@ -12,23 +10,6 @@ export function easeOutElastic(x: number): number { : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; } -/* DEPRECATED */ -export function makeButton(scene: Scene, text: string, x: number, y: number, onClick: () => void) : Phaser.GameObjects.Text { - const btn = scene.add.text(x, y, text, FONT_STYLE_BUTTON); - btn.setInteractive(); - btn.on('pointerover', () => { - btn.setX(110); - btn.setStyle(FONT_STYLE_BUTTON_HOVER); - }); - btn.on('pointerout', () => { - btn.setX(100); - btn.setStyle(FONT_STYLE_BUTTON); - }); - btn.on('pointerdown', onClick); - return btn; -} - - export function getColorFromUUID(uuid: string) { let hash = 0; for (let i = 0; i < uuid.length; i++) { diff --git a/test/base-scene-controller.test.ts b/test/base-scene-controller.test.ts new file mode 100644 index 0000000..5d40a0c --- /dev/null +++ b/test/base-scene-controller.test.ts @@ -0,0 +1,106 @@ +import AzNopolyGame from '../src/game' +import NetworkSceneController from '../src/scene/base/base-scene-controller'; +import {EventEmitter} from 'eventemitter3'; +import Phaser from 'phaser'; + + +it("should create a new controller", () => { + class TestController extends NetworkSceneController { + onSceneCreate(): void { + } + public bobo: number = 0; + public exampleMethod(num: number) { + this.bobo = num; + } + } + + let scene = {} as Phaser.Scene; + let aznopoly = {} as AzNopolyGame; + let controller!: TestController; + + beforeEach(() => { + scene = { + events: new EventEmitter() + } as any as Phaser.Scene; + + aznopoly = { + broadcastPacket: () => {}, + isPlayerHost: () => true, + } as any as AzNopolyGame; + + + (global as any).Phaser = { + Scenes: { + Events: { + SHUTDOWN: 'shutdown', + CREATE: 'create', + } + } + }; + controller = new TestController(scene, aznopoly); + }) + + describe('Proxy call', () => { + controller.registerSyncedMethod(controller.exampleMethod, false); + + controller.syncProxy.exampleMethod(5) + expect(controller.bobo).toBe(5); + }) + + describe('Proxy call not allowed', () => { + expect(() => controller.syncProxy.exampleMethod(5)).toThrow(); + }) + + describe('Proxy call not allowed by host', () => { + controller.registerSyncedMethod(controller.exampleMethod, true); + + (aznopoly as any).isHost = false; + expect(() => controller.syncProxy.exampleMethod(5)).toThrow(); + }) + + describe('Proxy call allowed by host', () => { + controller.registerSyncedMethod(controller.exampleMethod, true); + + (aznopoly as any).isHost = true; + controller.syncProxy.exampleMethod(5); + expect(controller.bobo).toBe(5); + }) + + describe('Proxy call sends packet', () => { + controller.registerSyncedMethod(controller.exampleMethod, false); + + let packet; + aznopoly.broadcastPacket = (p: any) => packet = p; + + controller.syncProxy.exampleMethod(5); + expect(packet).toEqual({ + type: 'CLIENT_MINIGAME_TESTCONTROLLER', + data: { + method: 'exampleMethod', + arguments: [5], + } + }); + }) + + describe('Receiving packet will call method', () => { + controller.registerSyncedMethod(controller.exampleMethod, false); + + let listener!: EventListener; + (aznopoly as any).addPacketListener = (_: string, l: EventListener) => { + listener = l; + }; + + scene.events.emit(global.Phaser.Scenes.Events.CREATE); + expect(listener).toBeDefined(); + + let packet = { + type: 'CLIENT_MINIGAME_TESTCONTROLLER', + data: { + method: 'exampleMethod', + arguments: [5], + } + } + listener(new CustomEvent('test', {detail: packet}) as any); + expect(controller.bobo).toBe(5); + }); +}); \ No newline at end of file diff --git a/test/synced-scene-controller.test.ts b/test/synced-scene-controller.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/yarn.lock b/yarn.lock index 76a4578..f4104c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,7 +1352,7 @@ esprima@^4.0.0: eventemitter3@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== execa@^5.0.0: