diff options
Diffstat (limited to 'client')
| -rw-r--r-- | client/src/akkamon/DataWrappers.ts | 4 | ||||
| -rw-r--r-- | client/src/akkamon/GameConfig.ts | 2 | ||||
| -rw-r--r-- | client/src/akkamon/client/Client.ts | 58 | ||||
| -rw-r--r-- | client/src/akkamon/client/Events.ts | 48 | ||||
| -rw-r--r-- | client/src/akkamon/client/InteractionEngine.ts | 15 | ||||
| -rw-r--r-- | client/src/akkamon/client/Session.ts | 3 | ||||
| -rw-r--r-- | client/src/akkamon/client/Socket.ts | 14 | ||||
| -rw-r--r-- | client/src/akkamon/render/UIControls.ts | 2 | ||||
| -rw-r--r-- | client/src/akkamon/render/engine/AkkamonEngine.ts | 3 | ||||
| -rw-r--r-- | client/src/akkamon/scenes/AkkamonWorldScene.ts | 30 | ||||
| -rw-r--r-- | client/src/akkamon/scenes/UIElement.ts | 236 | ||||
| -rw-r--r-- | client/src/app.ts | 19 |
12 files changed, 403 insertions, 31 deletions
diff --git a/client/src/akkamon/DataWrappers.ts b/client/src/akkamon/DataWrappers.ts index 22b25e2..6fff618 100644 --- a/client/src/akkamon/DataWrappers.ts +++ b/client/src/akkamon/DataWrappers.ts @@ -68,4 +68,8 @@ export class Stack<T> { this._data = new Array(); } + cloneData() { + return new Array(this._data); + } + } diff --git a/client/src/akkamon/GameConfig.ts b/client/src/akkamon/GameConfig.ts index 8d7e4f1..53a10a2 100644 --- a/client/src/akkamon/GameConfig.ts +++ b/client/src/akkamon/GameConfig.ts @@ -10,7 +10,7 @@ export const gameConfig: Phaser.Types.Core.GameConfig & Phaser.Types.Core.Render type: Phaser.AUTO, backgroundColor: '#125555', width: 800, - height: 600, + height: 800, pixelArt: true, scene: [BootScene, DemoScene] }; diff --git a/client/src/akkamon/client/Client.ts b/client/src/akkamon/client/Client.ts index 40aa415..54a06f1 100644 --- a/client/src/akkamon/client/Client.ts +++ b/client/src/akkamon/client/Client.ts @@ -10,6 +10,8 @@ import { UIControls } from '../render/UIControls'; import { RemotePlayerEngine } from '../render/engine/RemotePlayerEngine'; +import { InteractionEngine } from './InteractionEngine'; + import type { AkkamonClient } from './AkkamonClient'; import type { AkkamonWorldScene } from '../scenes/AkkamonWorldScene'; @@ -22,8 +24,15 @@ import { HeartBeatReplyEvent, IncomingEvent, AkkamonEvent, + BattleRequestEvent, } from './Events'; +function delay(ms: number) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + export class Client implements AkkamonClient { @@ -36,15 +45,28 @@ export class Client implements AkkamonClient private remotePlayerEngine?: RemotePlayerEngine; + private interactionEngine?: InteractionEngine constructor( - url: string + private url: string ) { + this.session = new Socket(url, this); + + } + + retryConnection() { + this.tryAgainLater(1000); + this.session = new Socket(this.url, this); + } + + async tryAgainLater(ms: number) { + await delay(ms); } in(eventString: string) { let event: IncomingEvent = JSON.parse(eventString); + // console.log(event); switch (event.type) { case EventType.HEART_BEAT: if (this.remotePlayerEngine !== undefined) { @@ -52,6 +74,16 @@ export class Client implements AkkamonClient } this.send(new HeartBeatReplyEvent()); break; + case EventType.TRAINER_REGISTRATION_REPLY: + if (event.trainerId !== undefined) { + console.log("setting Session trainerId to: " + event.trainerId); + this.session.trainerId = event.trainerId; + } + break; + case EventType.INIT_BATTLE_REPLY: + console.log("Received battle request reply!"); + console.log(event); + break; default: console.log("ignored incoming event, doesn't match EventType interface."); break; @@ -70,6 +102,7 @@ export class Client implements AkkamonClient this.controls!.update(); this.gridPhysics!.update(delta); this.remotePlayerEngine!.update(delta); + this.interactionEngine!.update(); } setUIControls(input: Phaser.Input.InputPlugin, menu: any) { @@ -109,6 +142,11 @@ export class Client implements AkkamonClient this.remotePlayerEngine = new RemotePlayerEngine( scene ); + + this.interactionEngine = new InteractionEngine( + scene + ); + this.initAnimation(scene, playerSprite); } @@ -151,4 +189,22 @@ export class Client implements AkkamonClient requestRemotePlayerData() { return this.remotePlayerEngine!.getData(); } + + sendBattleChallenge(remotePlayerName: string) { + console.log("sent a battle request!"); + this.interactionEngine!.setAwaitingResponse(); + this.send(new BattleRequestEvent( + this.getCurrentSceneKey(), + this.getSessionTrainerId(), + remotePlayerName + )); + } + + getSessionTrainerId() { + return this.session.trainerId; + } + + getCurrentSceneKey() { + return this.scene!.scene.key; + } } diff --git a/client/src/akkamon/client/Events.ts b/client/src/akkamon/client/Events.ts index 830ac23..f816da8 100644 --- a/client/src/akkamon/client/Events.ts +++ b/client/src/akkamon/client/Events.ts @@ -3,10 +3,14 @@ import type { Direction } from '../render/Direction'; export enum EventType { HEART_BEAT = "HeartBeat", - PLAYER_REGISTRATION = "PlayerRegistrationEvent", + TRAINER_REGISTRATION_REQUEST = "TrainerRegistrationRequestEvent", + TRAINER_REGISTRATION_REPLY = "TrainerRegistrationReplyEvent", START_MOVING = "StartMoving", STOP_MOVING = "StopMoving", - NEW_TILE_POS = "NewTilePos" + NEW_TILE_POS = "NewTilePos", + INIT_BATTLE_REQUEST = "InitBattleRequestEvent", + INIT_BATTLE_REPLY = "InitBattleReplyEvent", + BATTLE_ABORTED = "BattleAborted" } export interface AkkamonEvent { @@ -19,18 +23,47 @@ export type RemoteMovementQueues = { [trainerId: string]: { value: Array<Direction> } } +// INCOMING EVENTS export interface IncomingEvent extends AkkamonEvent { remoteMovementQueues?: RemoteMovementQueues + trainerId?: string } -export class PlayerRegistrationEvent implements AkkamonEvent { +export class HeartBeatReplyEvent implements IncomingEvent { - public type: EventType = EventType.PLAYER_REGISTRATION; + public type: EventType = EventType.HEART_BEAT; + + constructor( + ) { } +} + +export class PlayerRegistrationReplyEvent implements IncomingEvent { + + public type: EventType = EventType.TRAINER_REGISTRATION_REPLY; + + constructor( + public trainerId: string + ) { } +} + +export class InitBattleReplyEvent implements IncomingEvent { + + public type: EventType = EventType.INIT_BATTLE_REPLY; constructor( ) { } } +// OUTGOING EVENTS +export class PlayerRegistrationRequestEvent implements AkkamonEvent { + + public type: EventType = EventType.TRAINER_REGISTRATION_REQUEST; + + constructor( + ) { } +} + + export class StartMovingEvent implements AkkamonEvent { public type: EventType = EventType.START_MOVING; @@ -61,10 +94,13 @@ export class NewTilePosEvent implements AkkamonEvent { ) { } } -export class HeartBeatReplyEvent implements AkkamonEvent { - public type: EventType = EventType.HEART_BEAT; +export class InitBattleRequestEvent implements AkkamonEvent { + + public type: EventType = EventType.INIT_BATTLE_REQUEST; constructor( + public thisTrainer: string, + public otherTrainer: string ) { } } diff --git a/client/src/akkamon/client/InteractionEngine.ts b/client/src/akkamon/client/InteractionEngine.ts new file mode 100644 index 0000000..5746b73 --- /dev/null +++ b/client/src/akkamon/client/InteractionEngine.ts @@ -0,0 +1,15 @@ +import { AkkamonEngine } from '../render/engine/AkkamonEngine'; + +import type { AkkamonWorldScene } from '../scenes/AkkamonWorldScene'; + +export class InteractionEngine extends AkkamonEngine { + constructor(scene: AkkamonWorldScene) { + super(); + } + + update() { + } + + setAwaitingResponse() { + } +} diff --git a/client/src/akkamon/client/Session.ts b/client/src/akkamon/client/Session.ts index bbefd66..6b6f5ec 100644 --- a/client/src/akkamon/client/Session.ts +++ b/client/src/akkamon/client/Session.ts @@ -1,7 +1,8 @@ import type Player from './player'; export default interface AkkamonSession extends WebSocket { - user?: User + trainerId?: string + } interface User { diff --git a/client/src/akkamon/client/Socket.ts b/client/src/akkamon/client/Socket.ts index e0c3e0c..b46c73f 100644 --- a/client/src/akkamon/client/Socket.ts +++ b/client/src/akkamon/client/Socket.ts @@ -2,11 +2,13 @@ import Phaser from 'phaser'; import type { Client } from './Client' import type AkkamonSession from './Session' import { - PlayerRegistrationEvent + PlayerRegistrationRequestEvent } from './Events'; export class Socket extends WebSocket implements AkkamonSession { + public trainerId?: string; + constructor( url: string, client: Client @@ -15,12 +17,20 @@ export class Socket extends WebSocket implements AkkamonSession this.onopen = function echo(this: WebSocket, ev: Event) { console.log("Sending PlayerRegistrationEvent."); - client.send(new PlayerRegistrationEvent()); + client.send(new PlayerRegistrationRequestEvent()); } this.onmessage = function incomingMessage(this: WebSocket, ev: MessageEvent) { client.in(ev.data); } + + // this.onerror = function socketFailure(this: WebSocket, ev: Event) { + // client.retryConnection(); + // } + + this.onclose = function socketClose(this: WebSocket, ev: Event) { + client.retryConnection(); + } } } diff --git a/client/src/akkamon/render/UIControls.ts b/client/src/akkamon/render/UIControls.ts index 89eba32..0db6385 100644 --- a/client/src/akkamon/render/UIControls.ts +++ b/client/src/akkamon/render/UIControls.ts @@ -13,7 +13,7 @@ export class UIControls { update() { if (Phaser.Input.Keyboard.JustDown(this.cursors.left)) { - this.menu.destroyMe(); + this.menu.destroyAndGoBack(); } else if (Phaser.Input.Keyboard.JustDown(this.cursors.right)) { this.menu.confirm(); } else if (Phaser.Input.Keyboard.JustDown(this.cursors.up)) { diff --git a/client/src/akkamon/render/engine/AkkamonEngine.ts b/client/src/akkamon/render/engine/AkkamonEngine.ts index 14dae86..02f7d58 100644 --- a/client/src/akkamon/render/engine/AkkamonEngine.ts +++ b/client/src/akkamon/render/engine/AkkamonEngine.ts @@ -4,4 +4,7 @@ import { client } from '../../../app'; export class AkkamonEngine { client = client; + update(delta?: number) { + throw new Error("update must be implemented in Engines"); + } } diff --git a/client/src/akkamon/scenes/AkkamonWorldScene.ts b/client/src/akkamon/scenes/AkkamonWorldScene.ts index 6a97f4a..30af96c 100644 --- a/client/src/akkamon/scenes/AkkamonWorldScene.ts +++ b/client/src/akkamon/scenes/AkkamonWorldScene.ts @@ -55,12 +55,7 @@ export class AkkamonWorldScene extends Phaser.Scene { let akey = this.input.keyboard.addKey('a'); akey.on('down', () => { if (this.menus.isEmpty()) { - this.menus.push(new PauseMenu(this)); - console.log("here is the menu stack:"); - console.log(this.menus); - this.menuTakesUIControl(this.input, this.menus.peek()); - console.log("here is the menu stack, after taking controls:"); - console.log(this.menus); + this.pushMenu(new PauseMenu(this)); } }); @@ -86,6 +81,8 @@ export class AkkamonWorldScene extends Phaser.Scene { pushMenu(menu: AkkamonMenu) { this.menus.push(menu); this.menuTakesUIControl(this.input, menu); + console.log("New menu stack:"); + console.log(this.menus); } popMenu() { @@ -93,14 +90,14 @@ export class AkkamonWorldScene extends Phaser.Scene { } traverseMenusBackwards() { - console.log("menu stack before traversing back:"); - console.log(this.menus); this.popMenu(); if (!this.menus.isEmpty()) { this.menuTakesUIControl(this.input, this.menus.peek()); } else { this.isUsingGridControls(); } + console.log("menu stack after traversing back:"); + console.log(this.menus); } getPlayerPixelPosition(): Phaser.Math.Vector2 { @@ -115,4 +112,21 @@ export class AkkamonWorldScene extends Phaser.Scene { return Array.from(remotePlayerData.keys()); } } + + requestBattleChallenge(remotePlayerName: string): void { + this.client.sendBattleChallenge(remotePlayerName); + } + + clearMenus() { + if (this.menus.length > 0) { + this.menus.pop()!.destroyGroup(); + console.log("stack while clearing menus:"); + console.log(this.menus.cloneData()); + this.clearMenus(); + } + } + + setWaitingOnResponse() { + + } } diff --git a/client/src/akkamon/scenes/UIElement.ts b/client/src/akkamon/scenes/UIElement.ts index 1f0de90..08905ef 100644 --- a/client/src/akkamon/scenes/UIElement.ts +++ b/client/src/akkamon/scenes/UIElement.ts @@ -1,12 +1,17 @@ import type { AkkamonWorldScene } from '../scenes/AkkamonWorldScene'; import { Direction } from '../render/Direction'; +import { + Queue +} from '../DataWrappers'; class MenuText extends Phaser.GameObjects.Text { + public static TEXT_HEIGHT: number = 16; + constructor(scene: Phaser.Scene, group: Phaser.GameObjects.Group, groupDepth: number, x: number, y: number, text: string) { let style: Phaser.Types.GameObjects.Text.TextStyle = { fontFamily: 'Courier', - fontSize: '16px', + fontSize: `${MenuText.TEXT_HEIGHT}px`, fontStyle: '', backgroundColor: undefined, color: '#000000', @@ -21,6 +26,30 @@ class MenuText extends Phaser.GameObjects.Text { } } +class WrappedMenuText extends MenuText { + constructor(scene: Phaser.Scene, group: Phaser.GameObjects.Group, groupDepth: number, x: number, y: number, text: string, wrapWidth: number) { + super(scene, group, groupDepth, x, y, text); + this.setStyle({ + fontFamily: 'Courier', + fontSize: '16px', + fontStyle: '', + backgroundColor: undefined, + color: '#000000', + stroke: '#000000', + strokeThickness: 0, + align: 'left', // 'left'|'center'|'right'|'justify' + wordWrap: { + width: wrapWidth, + useAdvancedWrap: true + } + }); + } + + destroy() { + super.destroy(); + } +} + class Picker extends Phaser.GameObjects.Image { constructor(scene: Phaser.Scene, group: Phaser.GameObjects.Group, x: number, y: number, name: string) { super(scene, x, y, name); @@ -34,8 +63,9 @@ class Picker extends Phaser.GameObjects.Image { export interface AkkamonMenu { selectButton: (direction: Direction) => void - destroyMe: () => void + destroyAndGoBack: () => void confirm: () => void + destroyGroup: () => void } @@ -58,21 +88,26 @@ class Menu extends Phaser.GameObjects.Image implements AkkamonMenu { ySpacing?: number yOffsetFromTop?: number xOffsetFromRight?: number + pickerOffset?:number - destroyMe() { - this.akkamonScene.traverseMenusBackwards(); + destroyGroup() { this.group!.destroy(true); } + destroyAndGoBack() { + this.akkamonScene.traverseMenusBackwards(); + this.destroyGroup(); + } + confirm() { // communicate with client throw new Error('Confirm method should be present in a Menu implementation'); } - constructor(scene: AkkamonWorldScene) { + constructor(scene: AkkamonWorldScene, imageKey: string) { let camera = scene.cameras.main; - super(scene, camera.scrollX + camera.width, camera.scrollY, "pause-menu") + super(scene, camera.scrollX, camera.scrollY, imageKey) this.setOrigin(1,0) this.setVisible(true) this.setDisplaySize(296, 400) @@ -143,8 +178,9 @@ class Menu extends Phaser.GameObjects.Image implements AkkamonMenu { export class PauseMenu extends Menu implements AkkamonMenu { constructor(scene: AkkamonWorldScene) { - super(scene) - this.ySpacing + super(scene, "pause-menu") + let camera = scene.cameras.main; + this.setPosition(this.x + camera.width, this.y); this.setPicker(0); this.setButtons([ 'POKéDEX', @@ -173,13 +209,25 @@ class ListMenu extends Menu implements AkkamonMenu { scene: AkkamonWorldScene, options: Array<string> ) { - super(scene) + super(scene, "pause-menu") + let camera = scene.cameras.main; + this.setPosition(this.x + camera.width, this.y); this.options = options; + if (this.viewBot > this.options.length) { + this.viewBot = this.options.length; + } + this.xOffsetFromRight = 210; this.yOffsetFromTop = 50; - let contacts = new MenuText(this.scene, this.group!, this.groupDepth!, this.x - this.xOffsetFromRight, this.y + 20, "Nearby trainers:") + let contacts = new MenuText( + this.scene, + this.group!, + this.groupDepth!, + this.x - this.xOffsetFromRight, + this.y + 20, + "Nearby trainers:") // this.yOffsetFromTop // this.ySpacing @@ -232,4 +280,172 @@ class ListMenu extends Menu implements AkkamonMenu { } class RemotePlayerList extends ListMenu implements AkkamonMenu { + + confirm() { + this.akkamonScene.pushMenu(new ChallengeDialogue( + this.akkamonScene, + ['YES', 'NO'], + { + 'trainerName': this.buttons![this.index! + this.viewTop].text + })); + } +} + +class ConfirmationDialogue extends Menu implements AkkamonMenu { + dialogueData?: {[key: string]: string} + options?: Array<string> + dialogueBox?: Dialogue + + constructor(scene: AkkamonWorldScene, options: Array<string>, dialogueData: {[key: string]: string}) { + super(scene, "confirmation-dialogue"); + let camera = scene.cameras.main; + this.setDisplaySize(200, 0.83 * 200) + this.setPosition(this.x + camera.width, this.y + (camera.height - 0.28 * camera.width - this.displayHeight)); + this.xOffsetFromRight = 0.5 * this.displayWidth; + this.yOffsetFromTop = 0.33 * this.displayHeight - MenuText.TEXT_HEIGHT; + this.ySpacing! = 0.33 * this.displayHeight; + + this.setPicker(0); + this.setButtons(options); + this.groupDepth = 40; + this.group!.setDepth(this.groupDepth!); + + this.dialogueBox = new Dialogue(scene, this.group!, this.groupDepth); + } +} + +class Dialogue extends Phaser.GameObjects.Image implements AkkamonMenu { + public messageQueue: Queue<string>; + public displayedText: MenuText; + public akkamonScene: AkkamonWorldScene; + public group: Phaser.GameObjects.Group; + + constructor(scene: AkkamonWorldScene, group: Phaser.GameObjects.Group, depth: number) { + let camera = scene.cameras.main; + super(scene, camera.scrollX, camera.scrollY, "general-dialogue-box") + this.setOrigin(0,1); + this.setPosition(camera.scrollX, camera.scrollY + camera.height) + this.setDisplaySize(camera.width, 0.28 * camera.width); + + scene.add.existing(this); + group.add(this); + this.setDepth(depth); + + this.group = group; + this.akkamonScene = scene; + + this.messageQueue = new Queue(); + this.displayedText = new WrappedMenuText( + this.akkamonScene, + this.group, + depth, + this.x + 40, + this.y - this.displayHeight + 40, + '', + camera.width + ); + } + + selectButton() { + } + confirm() { + } + + destroyGroup() { + const clonedChildren = [... this.group.getChildren()] + for (let child of clonedChildren) { + console.log("destroying child with:"); + console.log(child); + console.log(child.destroy); + child.destroy(); + } + console.log("destroying group of dialogue"); + console.log(this.group); + this.group.destroy(true) + } + + destroyAndGoBack() { + console.log("Destroying dialogue box!"); + this.akkamonScene.traverseMenusBackwards(); + this.destroyGroup(); + } + + push(messageData: string | string[]): void { + if (typeof messageData === 'string') { + this.messageQueue.push(messageData); + } else if (Array.isArray(messageData)) { + this.messageQueue.pushArray(messageData); + } + } + + displayNextDialogue() { + this.displayedText.text = ''; + if (this.messageQueue.peek() !== undefined) { + this.typewriteText(this.messageQueue.pop()!); + } + } + + typewriteText(text: string) { + const length = text.length + let i = 0 + this.scene.time.addEvent({ + callback: () => { + this.displayedText.text += text[i] + ++i + }, + repeat: length - 1, + delay: 20 + }) + } + +} + +class ChallengeDialogue extends ConfirmationDialogue implements AkkamonMenu { + challengedTrainerName: string; + constructor(scene: AkkamonWorldScene, options: Array<string>, dialogueData: {[key: string]: string}) { + super(scene, options, dialogueData); + this.challengedTrainerName = dialogueData['trainerName']; + this.dialogueBox!.push( + `Do you want to challenge ${this.challengedTrainerName} to a battle?` + ); + this.dialogueBox!.displayNextDialogue(); + } + + confirm() { + if (this.buttons![this.index!].text === "YES") { + this.akkamonScene.requestBattleChallenge(this.challengedTrainerName); + this.akkamonScene.clearMenus(); + this.akkamonScene.pushMenu(new WaitingDialogue(this.akkamonScene, new Phaser.GameObjects.Group(this.scene), 20)); + } else { + this.destroyAndGoBack(); + } + } + + destroy() { + this.scene.time.removeAllEvents(); + super.destroy(); + } +} + +class WaitingDialogue extends Dialogue { + waitingPrinter: any + constructor(scene: AkkamonWorldScene, group: Phaser.GameObjects.Group, depth: number) { + super(scene, group, depth); + this.typewriteText("Waiting on reponse..."); + this.waitingPrinter = setInterval(() => { + this.displayedText.text = ''; + this.typewriteText("Waiting on reponse..."); + }, 3000); + } + + destroyAndGoBack() { + super.destroyAndGoBack(); + } + + destroy() { + console.log("destroying waiting dialogue!"); + clearInterval(this.waitingPrinter); + this.scene.time.removeAllEvents(); + super.destroy(); + } } diff --git a/client/src/app.ts b/client/src/app.ts index 197fb63..05fccb5 100644 --- a/client/src/app.ts +++ b/client/src/app.ts @@ -29,4 +29,21 @@ function destroyGame () { export let client = new Client('ws://localhost:8080'); let game: Phaser.Game | null | undefined; -if (!game) newGame(); + +function delay(ms: number) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +async function awaitRegistrationReplyAndStart() { + if (!game) { + while (client.getSessionTrainerId() === undefined) { + console.log("can't start game, this trainerId is still undefined"); + await delay(1000); + } + newGame(); + } +} + +awaitRegistrationReplyAndStart(); |
