diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index c628d29d..4436fb60 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -105,7 +105,8 @@ export class GameRoom { socket, joinRoomMessage.getTagList(), joinRoomMessage.getName(), - ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()) + ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()), + joinRoomMessage.getCompanion() ); this.nextUserId++; this.users.set(user.id, user); diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 51a1a617..52a96b61 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -4,7 +4,7 @@ import {Zone} from "_Model/Zone"; import {Movable} from "_Model/Movable"; import {PositionNotifier} from "_Model/PositionNotifier"; import {ServerDuplexStream} from "grpc"; -import {BatchMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; +import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; export type UserSocket = ServerDuplexStream; @@ -23,7 +23,8 @@ export class User implements Movable { public readonly socket: UserSocket, public readonly tags: string[], public readonly name: string, - public readonly characterLayers: CharacterLayer[] + public readonly characterLayers: CharacterLayer[], + public readonly companion?: CompanionMessage ) { this.listenedZones = new Set(); diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 16b4d005..4a76f131 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -296,6 +296,7 @@ export class SocketManager { userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone)); + userJoinedZoneMessage.setCompanion(thing.companion); const subMessage = new SubToPusherMessage(); subMessage.setUserjoinedzonemessage(userJoinedZoneMessage); @@ -605,6 +606,7 @@ export class SocketManager { userJoinedMessage.setName(thing.name); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); + userJoinedMessage.setCompanion(thing.companion); const subMessage = new SubToPusherMessage(); subMessage.setUserjoinedzonemessage(userJoinedMessage); diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html index f0faf5c5..d5e9ad7e 100644 --- a/front/dist/resources/html/gameMenu.html +++ b/front/dist/resources/html/gameMenu.html @@ -30,6 +30,9 @@
+
+ +
diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 04c93a3e..a0edacbc 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -88,9 +88,9 @@ class ConnectionManager { this.localUser = new LocalUser('', 'test', []); } - public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise { + public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise { return new Promise((resolve, reject) => { - const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport); + const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); reject(error); @@ -111,7 +111,7 @@ class ConnectionManager { this.reconnectingTimeout = setTimeout(() => { //todo: allow a way to break recursion? //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. - this.connectToRoomSocket(roomId, name, characterLayers, position, viewport).then((connection) => resolve(connection)); + this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection)); }, 4000 + Math.floor(Math.random() * 2000) ); }); }); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 519afcd3..477e86e3 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -47,6 +47,7 @@ export interface MessageUserPositionInterface { name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; + companion: string|null; } export interface MessageUserMovedInterface { @@ -58,7 +59,8 @@ export interface MessageUserJoined { userId: number; name: string; characterLayers: BodyResourceDescriptionInterface[]; - position: PointInterface + position: PointInterface; + companion: string|null; } export interface PositionInterface { diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 48b5697b..4741fc8c 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -4,6 +4,7 @@ const playerNameKey = 'playerName'; const selectedPlayerKey = 'selectedPlayer'; const customCursorPositionKey = 'customCursorPosition'; const characterLayersKey = 'characterLayers'; +const companionKey = 'companion'; const gameQualityKey = 'gameQuality'; const videoQualityKey = 'videoQuality'; const audioPlayerVolumeKey = 'audioVolume'; @@ -49,6 +50,22 @@ class LocalUserStore { return areCharacterLayersValid(value) ? value : null; } + setCompanion(companion: string|null): void { + return localStorage.setItem(companionKey, JSON.stringify(companion)); + } + getCompanion(): string|null { + const companion = JSON.parse(localStorage.getItem(companionKey) || "null"); + + if (typeof companion !== "string" || companion === "") { + return null; + } + + return companion; + } + wasCompanionSet(): boolean { + return localStorage.getItem(companionKey) ? true : false; + } + setGameQualityValue(value: number): void { localStorage.setItem(gameQualityKey, '' + value); } diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 0220cb52..a31b7476 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -66,7 +66,7 @@ export class RoomConnection implements RoomConnection { * @param token A JWT token containing the UUID of the user * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" */ - public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { + public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null) { let url = new URL(PUSHER_URL, window.location.toString()).toString(); url = url.replace('http://', 'ws://').replace('https://', 'wss://'); if (!url.endsWith('/')) { @@ -85,6 +85,10 @@ export class RoomConnection implements RoomConnection { url += '&bottom='+Math.floor(viewport.bottom); url += '&left='+Math.floor(viewport.left); url += '&right='+Math.floor(viewport.right); + + if (typeof companion === 'string') { + url += '&companion='+encodeURIComponent(companion); + } if (RoomConnection.websocketFactory) { this.socket = RoomConnection.websocketFactory(url); @@ -322,11 +326,14 @@ export class RoomConnection implements RoomConnection { } }) + const companion = message.getCompanion(); + return { userId: message.getUserid(), name: message.getName(), characterLayers, - position: ProtobufClientUtils.toPointInterface(position) + position: ProtobufClientUtils.toPointInterface(position), + companion: companion ? companion.getName() : null } } diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts new file mode 100644 index 00000000..72491ae1 --- /dev/null +++ b/front/src/Phaser/Companion/Companion.ts @@ -0,0 +1,221 @@ +import Sprite = Phaser.GameObjects.Sprite; +import Container = Phaser.GameObjects.Container; +import { lazyLoadCompanionResource } from "./CompanionTexturesLoadingManager"; +import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation"; + +export interface CompanionStatus { + x: number; + y: number; + name: string; + moving: boolean; + direction: PlayerAnimationDirections; +} + +export class Companion extends Container { + public sprites: Map; + + private delta: number; + private invisible: boolean; + private updateListener: Function; + private target: { x: number, y: number, direction: PlayerAnimationDirections }; + + private companionName: string; + private direction: PlayerAnimationDirections; + private animationType: PlayerAnimationTypes; + + constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise) { + super(scene, x + 14, y + 4); + + this.sprites = new Map(); + + this.delta = 0; + this.invisible = true; + this.target = { x, y, direction: PlayerAnimationDirections.Down }; + + this.direction = PlayerAnimationDirections.Down; + this.animationType = PlayerAnimationTypes.Idle; + + this.companionName = name; + + texturePromise.then(resource => { + this.addResource(resource); + this.invisible = false; + }) + + this.scene.physics.world.enableBody(this); + + this.getBody().setImmovable(true); + this.getBody().setCollideWorldBounds(false); + this.setSize(16, 16); + this.getBody().setSize(16, 16); + this.getBody().setOffset(0, 8); + + this.setDepth(-1); + + this.updateListener = this.step.bind(this); + this.scene.events.addListener('update', this.updateListener); + + this.scene.add.existing(this); + } + + public setTarget(x: number, y: number, direction: PlayerAnimationDirections) { + this.target = { x, y: y + 4, direction }; + } + + public step(time: number, delta: number) { + if (typeof this.target === 'undefined') return; + + this.delta += delta; + if (this.delta < 128) { + return; + } + this.delta = 0; + + const xDist = this.target.x - this.x; + const yDist = this.target.y - this.y; + + const distance = Math.pow(xDist, 2) + Math.pow(yDist, 2); + + if (distance < 650) { + this.animationType = PlayerAnimationTypes.Idle; + this.direction = this.target.direction; + + this.getBody().stop(); + } else { + this.animationType = PlayerAnimationTypes.Walk; + + const xDir = xDist / Math.max(Math.abs(xDist), 1); + const yDir = yDist / Math.max(Math.abs(yDist), 1); + + const speed = 256; + this.getBody().setVelocity(Math.min(Math.abs(xDist * 2.5), speed) * xDir, Math.min(Math.abs(yDist * 2.5), speed) * yDir); + + if (Math.abs(xDist) > Math.abs(yDist)) { + if (xDist < 0) { + this.direction = PlayerAnimationDirections.Left; + } else { + this.direction = PlayerAnimationDirections.Right; + } + } else { + if (yDist < 0) { + this.direction = PlayerAnimationDirections.Up; + } else { + this.direction = PlayerAnimationDirections.Down; + } + } + } + + this.setDepth(this.y); + this.playAnimation(this.direction, this.animationType); + } + + public getStatus(): CompanionStatus { + const { x, y, direction, animationType, companionName } = this; + + return { + x, + y, + direction, + moving: animationType === PlayerAnimationTypes.Walk, + name: companionName + } + } + + private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void { + if (this.invisible) return; + + for (const [resource, sprite] of this.sprites.entries()) { + sprite.play(`${resource}-${direction}-${type}`, true); + } + } + + private addResource(resource: string, frame?: string | number): void { + const sprite = new Sprite(this.scene, 0, 0, resource, frame); + + this.add(sprite); + + this.getAnimations(resource).forEach(animation => { + this.scene.anims.create(animation); + }); + + this.scene.sys.updateList.add(sprite); + this.sprites.set(resource, sprite); + } + + private getAnimations(resource: string): Phaser.Types.Animations.Animation[] { + return [ + { + key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [1]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [4]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [7]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [10]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [0, 1, 2]}), + frameRate: 15, + repeat: -1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [3, 4, 5]}), + frameRate: 15, + repeat: -1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [6, 7, 8]}), + frameRate: 15, + repeat: -1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [9, 10, 11]}), + frameRate: 15, + repeat: -1 + } + ] + } + + private getBody(): Phaser.Physics.Arcade.Body { + const body = this.body; + + if (!(body instanceof Phaser.Physics.Arcade.Body)) { + throw new Error('Container does not have arcade body'); + } + + return body; + } + + public destroy(): void { + for (const sprite of this.sprites.values()) { + if (this.scene) { + this.scene.sys.updateList.remove(sprite); + } + } + + if (this.scene) { + this.scene.events.removeListener('update', this.updateListener); + } + + super.destroy(); + } +} diff --git a/front/src/Phaser/Companion/CompanionTextures.ts b/front/src/Phaser/Companion/CompanionTextures.ts new file mode 100644 index 00000000..84eaf38f --- /dev/null +++ b/front/src/Phaser/Companion/CompanionTextures.ts @@ -0,0 +1,14 @@ +export interface CompanionResourceDescriptionInterface { + name: string, + img: string, + behaviour: "dog" | "cat" +} + +export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [ + { name: "dog1", img: "resources/characters/pipoya/Dog 01-1.png", behaviour: "dog" }, + { name: "dog2", img: "resources/characters/pipoya/Dog 01-2.png", behaviour: "dog" }, + { name: "dog3", img: "resources/characters/pipoya/Dog 01-3.png", behaviour: "dog" }, + { name: "cat1", img: "resources/characters/pipoya/Cat 01-1.png", behaviour: "cat" }, + { name: "cat2", img: "resources/characters/pipoya/Cat 01-2.png", behaviour: "cat" }, + { name: "cat3", img: "resources/characters/pipoya/Cat 01-3.png", behaviour: "cat" }, +] diff --git a/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts new file mode 100644 index 00000000..75c20a48 --- /dev/null +++ b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts @@ -0,0 +1,29 @@ +import LoaderPlugin = Phaser.Loader.LoaderPlugin; +import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./CompanionTextures"; + +export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => { + COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => { + lazyLoadCompanionResource(loader, resource.name); + }); + + return COMPANION_RESOURCES; +} + +export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise => { + return new Promise((resolve, reject) => { + const resource = COMPANION_RESOURCES.find(item => item.name === name); + + if (typeof resource === 'undefined') { + return reject(`Texture '${name}' not found!`); + } + + if (loader.textureManager.exists(resource.name)) { + return resolve(resource.name); + } + + loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 }); + loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name)); + + loader.start(); // It's only automatically started during the Scene preload. + }); +} diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 797616f8..9f2bd1fd 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -4,6 +4,7 @@ import BitmapText = Phaser.GameObjects.BitmapText; import Container = Phaser.GameObjects.Container; import Sprite = Phaser.GameObjects.Sprite; import {TextureError} from "../../Exception/TextureError"; +import {Companion} from "../Companion/Companion"; interface AnimationData { key: string; @@ -21,6 +22,7 @@ export abstract class Character extends Container { private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; //private teleportation: Sprite; private invisible: boolean; + public companion?: Companion; constructor(scene: Phaser.Scene, x: number, @@ -69,6 +71,12 @@ export abstract class Character extends Container { this.playAnimation(direction, moving); } + public addCompanion(name: string, texturePromise?: Promise): void { + if (typeof texturePromise !== 'undefined') { + this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); + } + } + public addTextures(textures: string[], frame?: string | number): void { for (const texture of textures) { if(!this.scene.textures.exists(texture)){ @@ -189,6 +197,10 @@ export abstract class Character extends Container { } this.setDepth(this.y); + + if (this.companion) { + this.companion.setTarget(this.x, this.y, this.lastDirection); + } } stop(){ diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index a6bd4f40..41e2e2df 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -17,12 +17,18 @@ export class RemotePlayer extends Character { name: string, texturesPromise: Promise, direction: PlayerAnimationDirections, - moving: boolean + moving: boolean, + companion: string|null, + companionTexturePromise?: Promise ) { super(Scene, x, y, texturesPromise, name, direction, moving, 1); - + //set data this.userId = userId; + + if (typeof companion === 'string') { + this.addCompanion(companion, companionTexturePromise); + } } updatePosition(position: PointInterface): void { @@ -31,5 +37,9 @@ export class RemotePlayer extends Character { this.setY(position.y); this.setDepth(position.y); //this is to make sure the perspective (player models closer the bottom of the screen will appear in front of models nearer the top of the screen). + + if (this.companion) { + this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections); + } } } diff --git a/front/src/Phaser/Game/AddPlayerInterface.ts b/front/src/Phaser/Game/AddPlayerInterface.ts index 0edf197e..96762a66 100644 --- a/front/src/Phaser/Game/AddPlayerInterface.ts +++ b/front/src/Phaser/Game/AddPlayerInterface.ts @@ -6,4 +6,5 @@ export interface AddPlayerInterface { name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; + companion: string|null; } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index e5ed7bba..c146c06d 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -21,12 +21,14 @@ export interface HasMovedEvent { export class GameManager { private playerName: string|null; private characterLayers: string[]|null; + private companion: string|null; private startRoom!:Room; currentGameSceneName: string|null = null; constructor() { this.playerName = localUserStore.getName(); this.characterLayers = localUserStore.getCharacterLayers(); + this.companion = localUserStore.getCompanion(); } public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise { @@ -63,6 +65,14 @@ export class GameManager { return this.characterLayers; } + + setCompanion(companion: string|null): void { + this.companion = companion; + } + + getCompanion(): string|null { + return this.companion; + } public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise { const roomID = room.id; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 6c4c6e3e..a9ba86a6 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -69,6 +69,7 @@ import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import DOMElement = Phaser.GameObjects.DOMElement; import {Subscription} from "rxjs"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; +import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -159,6 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener { private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; + private companion!: string|null; private messageSubscription: Subscription|null = null; private popUpElements : Map = new Map(); private originalMapUrl: string|undefined; @@ -352,7 +354,7 @@ export class GameScene extends ResizableScene implements CenterListener { } this.playerName = playerName; this.characterLayers = gameManager.getCharacterLayers(); - + this.companion = gameManager.getCompanion(); //initalise map this.Map = this.add.tilemap(this.MapUrlFile); @@ -476,7 +478,9 @@ export class GameScene extends ResizableScene implements CenterListener { top: camera.scrollY, right: camera.scrollX + camera.width, bottom: camera.scrollY + camera.height, - }).then((onConnect: OnConnectInterface) => { + }, + this.companion + ).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; this.connection.onUserJoins((message: MessageUserJoined) => { @@ -484,7 +488,8 @@ export class GameScene extends ResizableScene implements CenterListener { userId: message.userId, characterLayers: message.characterLayers, name: message.name, - position: message.position + position: message.position, + companion: message.companion } this.addPlayer(userMessage); }); @@ -870,6 +875,11 @@ ${escapedMessage} private removeAllRemotePlayers(): void { this.MapPlayersByKey.forEach((player: RemotePlayer) => { player.destroy(); + + if (player.companion) { + player.companion.destroy(); + } + this.MapPlayers.remove(player); }); this.MapPlayersByKey = new Map(); @@ -1040,7 +1050,9 @@ ${escapedMessage} texturesPromise, PlayerAnimationDirections.Down, false, - this.userInputManager + this.userInputManager, + this.companion, + this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined ); }catch (err){ if(err instanceof TextureError) { @@ -1232,7 +1244,9 @@ ${escapedMessage} addPlayerData.name, texturesPromise, addPlayerData.position.direction as PlayerAnimationDirections, - addPlayerData.position.moving + addPlayerData.position.moving, + addPlayerData.companion, + addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined ); this.MapPlayers.add(player); this.MapPlayersByKey.set(player.userId, player); @@ -1255,6 +1269,11 @@ ${escapedMessage} console.error('Cannot find user with id ', userId); } else { player.destroy(); + + if (player.companion) { + player.companion.destroy(); + } + this.MapPlayers.remove(player); } this.MapPlayersByKey.delete(userId); diff --git a/front/src/Phaser/Login/SelectCompanionScene.ts b/front/src/Phaser/Login/SelectCompanionScene.ts new file mode 100644 index 00000000..9b5c38fb --- /dev/null +++ b/front/src/Phaser/Login/SelectCompanionScene.ts @@ -0,0 +1,236 @@ +import Image = Phaser.GameObjects.Image; +import Rectangle = Phaser.GameObjects.Rectangle; +import { addLoader } from "../Components/Loader"; +import { gameManager} from "../Game/GameManager"; +import { ResizableScene } from "./ResizableScene"; +import { TextField } from "../Components/TextField"; +import { EnableCameraSceneName } from "./EnableCameraScene"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures"; +import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingManager"; + +export const SelectCompanionSceneName = "SelectCompanionScene"; + +enum LoginTextures { + playButton = "play_button", + icon = "icon", + mainFont = "main_font" +} + +export class SelectCompanionScene extends ResizableScene { + private logo!: Image; + private textField!: TextField; + private pressReturnField!: TextField; + private readonly nbCharactersPerRow = 7; + + private selectedRectangle!: Rectangle; + + private selectedCompanion!: Phaser.Physics.Arcade.Sprite; + private companions: Array = new Array(); + private companionModels: Array = [null]; + + constructor() { + super({ + key: SelectCompanionSceneName + }); + } + + preload() { + addLoader(this); + + getAllCompanionResources(this.load).forEach(model => { + this.companionModels.push(model); + }); + + this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png"); + this.load.image(LoginTextures.playButton, "resources/objects/play_button.png"); + + // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap + this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + + addLoader(this); + } + + create() { + this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Select your companion'); + + this.pressReturnField = new TextField( + this, + this.game.renderer.width / 2, + 90 + 32 * Math.ceil(this.companionModels.length / this.nbCharactersPerRow), + 'Press enter to start' + ); + + const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; + this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); + + this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); + this.add.existing(this.logo); + + // input events + this.input.keyboard.on('keyup-ENTER', this.nextScene.bind(this)); + + this.input.keyboard.on('keydown-RIGHT', this.selectNext.bind(this)); + this.input.keyboard.on('keydown-LEFT', this.selectPrevious.bind(this)); + this.input.keyboard.on('keydown-DOWN', this.jumpToNextRow.bind(this)); + this.input.keyboard.on('keydown-UP', this.jumpToPreviousRow.bind(this)); + + this.createCurrentCompanion(); + this.selectCompanion(this.getCompanionIndex()); + } + + update(time: number, delta: number): void { + this.pressReturnField.setVisible(!!(Math.floor(time / 500) % 2)); + } + + private jumpToPreviousRow(): void { + const index = this.companions.indexOf(this.selectedCompanion) - this.nbCharactersPerRow; + if (index >= 0) { + this.selectCompanion(index); + } + } + + private jumpToNextRow(): void { + const index = this.companions.indexOf(this.selectedCompanion) + this.nbCharactersPerRow; + if (index < this.companions.length) { + this.selectCompanion(index); + } + } + + private selectPrevious(): void { + const index = this.companions.indexOf(this.selectedCompanion); + this.selectCompanion(index - 1); + } + + private selectNext(): void { + const index = this.companions.indexOf(this.selectedCompanion); + this.selectCompanion(index + 1); + } + + private selectCompanion(index?: number): void { + if (typeof index === 'undefined') { + index = this.companions.indexOf(this.selectedCompanion); + } + + // make sure index is inside possible range + index = Math.min(this.companions.length - 1, Math.max(0, index)); + + if (this.selectedCompanion === this.companions[index]) { + return; + } + + this.selectedCompanion.anims.pause(); + this.selectedCompanion = this.companions[index]; + + this.redrawSelectedRectangle(); + + const model = this.companionModels[index]; + + if (model !== null) { + this.selectedCompanion.anims.play(model.name); + } + } + + private redrawSelectedRectangle(): void { + this.selectedRectangle.setVisible(true); + this.selectedRectangle.setX(this.selectedCompanion.x); + this.selectedRectangle.setY(this.selectedCompanion.y); + this.selectedRectangle.setSize(32, 32); + } + + private storeCompanionSelection(): string|null { + const index = this.companions.indexOf(this.selectedCompanion); + const model = this.companionModels[index]; + const companion = model === null ? null : model.name; + + localUserStore.setCompanion(companion); + + return companion; + } + + private nextScene(): void { + const companion = this.storeCompanionSelection(); + + // next scene + this.scene.stop(SelectCompanionSceneName); + + gameManager.setCompanion(companion); + gameManager.tryResumingGame(this, EnableCameraSceneName); + + this.scene.remove(SelectCompanionSceneName); + } + + private createCurrentCompanion(): void { + for (let i = 0; i < this.companionModels.length; i++) { + const companionResource = this.companionModels[i]; + + const col = i % this.nbCharactersPerRow; + const row = Math.floor(i / this.nbCharactersPerRow); + + const [x, y] = this.getCharacterPosition(col, row); + + let name = "null"; + if (companionResource !== null) { + name = companionResource.name; + } + + const companion = this.physics.add.sprite(x, y, name, 0); + companion.setBounce(0.2); + companion.setCollideWorldBounds(true); + + if (companionResource !== null) { + this.anims.create({ + key: name, + frames: this.anims.generateFrameNumbers(name, {start: 0, end: 2,}), + frameRate: 10, + repeat: -1 + }); + } + + companion.setInteractive().on("pointerdown", () => { + this.selectCompanion(i); + }); + + this.companions.push(companion); + } + + this.selectedCompanion = this.companions[0]; + } + + private getCharacterPosition(x: number, y: number): [number, number] { + return [ + this.game.renderer.width / 2 + 16 + (x - this.nbCharactersPerRow / 2) * 32, + y * 32 + 90 + ]; + } + + public onResize(ev: UIEvent): void { + this.textField.x = this.game.renderer.width / 2; + this.pressReturnField.x = this.game.renderer.width / 2; + this.logo.x = this.game.renderer.width - 30; + this.logo.y = this.game.renderer.height - 20; + + for (let i = 0; i < this.companionModels.length; i++) { + const companion = this.companions[i]; + + const col = i % this.nbCharactersPerRow; + const row = Math.floor(i / this.nbCharactersPerRow); + + const [x, y] = this.getCharacterPosition(col, row); + companion.x = x; + companion.y = y; + } + + this.redrawSelectedRectangle(); + } + + private getCompanionIndex(): number { + const companion = localUserStore.getCompanion(); + + if (companion === null) { + return 0; + } + + return this.companionModels.findIndex(model => model !== null && model.name === companion); + } +} diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 58e7f0a6..f29fd39d 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -1,5 +1,6 @@ import {LoginScene, LoginSceneName} from "../Login/LoginScene"; import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; +import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; import {gameManager} from "../Game/GameManager"; import {localUserStore} from "../../Connexion/LocalUserStore"; import {mediaManager} from "../../WebRtc/MediaManager"; @@ -277,6 +278,10 @@ export class MenuScene extends Phaser.Scene { this.closeSideMenu(); gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); break; + case 'changeCompanionButton': + this.closeSideMenu(); + gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene()); + break; case 'closeButton': this.closeSideMenu(); break; diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 64ba56d0..bb961115 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -3,7 +3,6 @@ import {GameScene} from "../Game/GameScene"; import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; import {Character} from "../Entity/Character"; - export const hasMovedEventName = "hasMoved"; export interface CurrentGamerInterface extends Character{ moveUser(delta: number) : void; @@ -22,12 +21,18 @@ export class Player extends Character implements CurrentGamerInterface { texturesPromise: Promise, direction: PlayerAnimationDirections, moving: boolean, - private userInputManager: UserInputManager + private userInputManager: UserInputManager, + companion: string|null, + companionTexturePromise?: Promise ) { super(Scene, x, y, texturesPromise, name, direction, moving, 1); //the current player model should be push away by other players to prevent conflict this.getBody().setImmovable(false); + + if (typeof companion === 'string') { + this.addCompanion(companion, companionTexturePromise); + } } moveUser(delta: number): void { @@ -59,6 +64,7 @@ export class Player extends Character implements CurrentGamerInterface { direction = PlayerAnimationDirections.Right; moving = true; } + if (x !== 0 || y !== 0) { this.move(x, y); this.emit(hasMovedEventName, {moving, direction, x: this.x, y: this.y}); diff --git a/front/src/index.ts b/front/src/index.ts index c0663acd..aab45a9b 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -6,6 +6,7 @@ import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable"; import {LoginScene} from "./Phaser/Login/LoginScene"; import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene"; +import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene"; import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; @@ -74,7 +75,7 @@ const config: GameConfig = { width: width / RESOLUTION, height: height / RESOLUTION, parent: "game", - scene: [EntryScene, LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, ErrorScene, CustomizeScene, MenuScene, HelpCameraSettingsScene], + scene: [EntryScene, LoginScene, SelectCharacterScene, SelectCompanionScene, EnableCameraScene, ReconnectingScene, ErrorScene, CustomizeScene, MenuScene, HelpCameraSettingsScene], zoom: RESOLUTION, fps: fps, dom: { diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 6f8ed036..b3d4e755 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -36,6 +36,10 @@ message CharacterLayerMessage { string name = 2; } +message CompanionMessage { + string name = 1; +} + /*********** CLIENT TO SERVER MESSAGES *************/ message PingMessage { @@ -141,6 +145,7 @@ message UserJoinedMessage { string name = 2; repeated CharacterLayerMessage characterLayers = 3; PositionMessage position = 4; + CompanionMessage companion = 5; } message UserLeftMessage { @@ -251,6 +256,7 @@ message JoinRoomMessage { string roomId = 5; repeated string tag = 6; string IPAddress = 7; + CompanionMessage companion = 8; } message UserJoinedZoneMessage { @@ -259,6 +265,7 @@ message UserJoinedZoneMessage { repeated CharacterLayerMessage characterLayers = 3; PositionMessage position = 4; Zone fromZone = 5; + CompanionMessage companion = 6; } message UserLeftZoneMessage { diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index d4b0f98e..87051bbc 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -12,7 +12,7 @@ import { WebRtcSignalToServerMessage, PlayGlobalMessage, ReportPlayerMessage, - QueryJitsiJwtMessage, SendUserMessage, ServerToClientMessage + QueryJitsiJwtMessage, SendUserMessage, ServerToClientMessage, CompanionMessage } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import {TemplatedApp} from "uWebSockets.js" @@ -138,6 +138,14 @@ export class IoSocketController { const left = Number(query.left); const right = Number(query.right); const name = query.name; + + let companion: CompanionMessage|undefined = undefined; + + if (typeof query.companion === 'string') { + companion = new CompanionMessage(); + companion.setName(query.companion); + } + if (typeof name !== 'string') { throw new Error('Expecting name'); } @@ -221,6 +229,7 @@ export class IoSocketController { IPAddress, roomId, name, + companion, characterLayers: characterLayerObjs, messages: memberMessages, tags: memberTags, @@ -350,6 +359,7 @@ export class IoSocketController { client.tags = ws.tags; client.textures = ws.textures; client.characterLayers = ws.characterLayers; + client.companion = ws.companion; client.roomId = ws.roomId; client.listenedZones = new Set(); return client; diff --git a/pusher/src/Model/Websocket/ExSocketInterface.ts b/pusher/src/Model/Websocket/ExSocketInterface.ts index 56e7e5ca..5b9a4f7e 100644 --- a/pusher/src/Model/Websocket/ExSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExSocketInterface.ts @@ -3,6 +3,7 @@ import {Identificable} from "./Identificable"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import { BatchMessage, + CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage @@ -29,6 +30,7 @@ export interface ExSocketInterface extends WebSocket, Identificable { characterLayers: CharacterLayer[]; position: PointInterface; viewport: ViewportInterface; + companion?: CompanionMessage; /** * Pushes an event that will be sent in the next batch of events */ diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 12d27ff3..3f39a5ed 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -5,7 +5,8 @@ import { CharacterLayerMessage, GroupLeftZoneMessage, GroupUpdateMessage, GroupUpdateZoneMessage, PointMessage, PositionMessage, UserJoinedMessage, UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, - ZoneMessage + ZoneMessage, + CompanionMessage } from "../Messages/generated/messages_pb"; import * as messages_pb from "../Messages/generated/messages_pb"; import {ClientReadableStream} from "grpc"; @@ -30,7 +31,7 @@ export type MovesCallback = (thing: Movable, position: PositionInterface, listen export type LeavesCallback = (thing: Movable, listener: User) => void;*/ export class UserDescriptor { - private constructor(public readonly userId: number, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage) { + private constructor(public readonly userId: number, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, private companion?: CompanionMessage) { if (!Number.isInteger(this.userId)) { throw new Error('UserDescriptor.userId is not an integer: '+this.userId); } @@ -41,7 +42,7 @@ export class UserDescriptor { if (position === undefined) { throw new Error('Missing position'); } - return new UserDescriptor(message.getUserid(), message.getName(), message.getCharacterlayersList(), position); + return new UserDescriptor(message.getUserid(), message.getName(), message.getCharacterlayersList(), position, message.getCompanion()); } public update(userMovedMessage: UserMovedMessage) { @@ -59,6 +60,7 @@ export class UserDescriptor { userJoinedMessage.setName(this.name); userJoinedMessage.setCharacterlayersList(this.characterLayers); userJoinedMessage.setPosition(this.position); + userJoinedMessage.setCompanion(this.companion) return userJoinedMessage; } diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index f0734b94..6efd6f8d 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -153,6 +153,8 @@ export class SocketManager implements ZoneEventListener { joinRoomMessage.setName(client.name); joinRoomMessage.setPositionmessage(ProtobufUtils.toPositionMessage(client.position)); joinRoomMessage.setTagList(client.tags); + joinRoomMessage.setCompanion(client.companion); + for (const characterLayer of client.characterLayers) { const characterLayerMessage = new CharacterLayerMessage(); characterLayerMessage.setName(characterLayer.name);