diff --git a/front/src/Components/Companion/Companion.svelte b/front/src/Components/Companion/Companion.svelte new file mode 100644 index 00000000..984e8bba --- /dev/null +++ b/front/src/Components/Companion/Companion.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte index bf34658f..bb5a2df2 100644 --- a/front/src/Components/Menu/MenuIcon.svelte +++ b/front/src/Components/Menu/MenuIcon.svelte @@ -1,6 +1,6 @@ + + + + diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts index 75eb844f..f7f010ac 100644 --- a/front/src/Phaser/Companion/Companion.ts +++ b/front/src/Phaser/Companion/Companion.ts @@ -1,6 +1,7 @@ import Sprite = Phaser.GameObjects.Sprite; import Container = Phaser.GameObjects.Container; import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation"; +import { TexturesHelper } from "../Helpers/TexturesHelper"; export interface CompanionStatus { x: number; @@ -39,6 +40,7 @@ export class Companion extends Container { texturePromise.then((resource) => { this.addResource(resource); this.invisible = false; + this.emit("texture-loaded"); }); this.scene.physics.world.enableBody(this); @@ -123,6 +125,22 @@ export class Companion extends Container { }; } + public async getSnapshot(): Promise { + const sprites = Array.from(this.sprites.values()).map((sprite) => { + return { sprite, frame: 1 }; + }); + return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => { + console.warn(reason); + for (const sprite of this.sprites.values()) { + // it can be either cat or dog prefix + if (sprite.texture.key.includes("cat") || sprite.texture.key.includes("dog")) { + return this.scene.textures.getBase64(sprite.texture.key); + } + } + return "cat1"; + }); + } + private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void { if (this.invisible) return; diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 1211a52d..6a8e0752 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -8,10 +8,10 @@ import { TextureError } from "../../Exception/TextureError"; import { Companion } from "../Companion/Companion"; import type { GameScene } from "../Game/GameScene"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; -import { waScaleManager } from "../Services/WaScaleManager"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import { isSilentStore } from "../../Stores/MediaStore"; -import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager"; +import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager"; +import { TexturesHelper } from "../Helpers/TexturesHelper"; const playerNameY = -25; @@ -64,6 +64,7 @@ export abstract class Character extends Container { this.addTextures(textures, frame); this.invisible = false; this.playAnimation(direction, moving); + this.emit("woka-textures-loaded"); }) .catch(() => { return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => { @@ -117,13 +118,28 @@ export abstract class Character extends Container { } } - private getOutlinePlugin(): OutlinePipelinePlugin | undefined { - return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; + public async getSnapshot(): Promise { + const sprites = Array.from(this.sprites.values()).map((sprite) => { + return { sprite, frame: 1 }; + }); + return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => { + console.warn(reason); + for (const sprite of this.sprites.values()) { + // we can be sure that either predefined woka or body texture is at this point loaded + if (sprite.texture.key.includes("color") || sprite.texture.key.includes("male")) { + return this.scene.textures.getBase64(sprite.texture.key); + } + } + return "male1"; + }); } public addCompanion(name: string, texturePromise?: Promise): void { if (typeof texturePromise !== "undefined") { this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); + this.companion.once("texture-loaded", () => { + this.emit("companion-texture-loaded", this.companion?.getSnapshot()); + }); } } @@ -154,6 +170,10 @@ export abstract class Character extends Container { } } + private getOutlinePlugin(): OutlinePipelinePlugin | undefined { + return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; + } + private getPlayerAnimations(name: string): AnimationData[] { return [ { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5d0c06ab..558b4d21 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,7 +1,54 @@ import type { Subscription } from "rxjs"; +import AnimatedTiles from "phaser-animated-tiles"; +import { Queue } from "queue-typescript"; +import { get } from "svelte/store"; + import { userMessageManager } from "../../Administration/UserMessageManager"; -import { iframeListener } from "../../Api/IframeListener"; import { connectionManager } from "../../Connexion/ConnectionManager"; +import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { urlManager } from "../../Url/UrlManager"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import { gameManager } from "./GameManager"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { EmoteManager } from "./EmoteManager"; +import { soundManager } from "./SoundManager"; +import { SharedVariablesManager } from "./SharedVariablesManager"; +import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; + +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; +import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; +import { iframeListener } from "../../Api/IframeListener"; +import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { Room } from "../../Connexion/Room"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { TextureError } from "../../Exception/TextureError"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; +import { SimplePeer } from "../../WebRtc/SimplePeer"; +import { Loader } from "../Components/Loader"; +import { RemotePlayer } from "../Entity/RemotePlayer"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; +import { ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { GameMap } from "./GameMap"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; +import { DirtyScene } from "./DirtyScene"; +import { TextUtils } from "../Components/TextUtils"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; +import { StartPositionCalculator } from "./StartPositionCalculator"; +import { PropertyUtils } from "../Map/PropertyUtils"; +import { GameMapPropertiesListener } from "./GameMapPropertiesListener"; +import { analyticsClient } from "../../Administration/AnalyticsClient"; +import { GameMapProperties } from "./GameMapProperties"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -12,85 +59,36 @@ import type { PositionInterface, RoomJoinedMessageInterface, } from "../../Connexion/ConnexionModels"; -import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; - -import { Queue } from "queue-typescript"; -import { Box, ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; -import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; -import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; import type { RoomConnection } from "../../Connexion/RoomConnection"; -import { Room } from "../../Connexion/Room"; -import { jitsiFactory } from "../../WebRtc/JitsiFactory"; -import { urlManager } from "../../Url/UrlManager"; -import { TextureError } from "../../Exception/TextureError"; -import { localUserStore } from "../../Connexion/LocalUserStore"; -import { HtmlUtils } from "../../WebRtc/HtmlUtils"; -import { mediaManager } from "../../WebRtc/MediaManager"; -import { SimplePeer } from "../../WebRtc/SimplePeer"; -import { Loader } from "../Components/Loader"; -import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; -import { RemotePlayer } from "../Entity/RemotePlayer"; import type { ActionableItem } from "../Items/ActionableItem"; import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; -import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; -import { PlayerAnimationDirections } from "../Player/Animation"; -import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; -import { ErrorSceneName } from "../Reconnecting/ErrorScene"; -import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; -import { UserInputManager } from "../UserInput/UserInputManager"; import type { AddPlayerInterface } from "./AddPlayerInterface"; -import { gameManager } from "./GameManager"; -import { GameMap } from "./GameMap"; -import { PlayerMovement } from "./PlayerMovement"; -import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { CameraManager } from "./CameraManager"; +import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; +import type { Character } from "../Entity/Character"; + +import { peerStore } from "../../Stores/PeerStore"; +import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; +import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; +import { playersStore } from "../../Stores/PlayersStore"; +import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; +import { userIsAdminStore } from "../../Stores/GameStore"; +import { contactPageStore } from "../../Stores/MenuStore"; +import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; +import { UserWokaPictureStore } from "../../Stores/UserWokaPictureStore"; +import { UserCompanionPictureStore } from "../../Stores/UserCompanionPictureStore"; + +import EVENT_TYPE = Phaser.Scenes.Events; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; -import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import DOMElement = Phaser.GameObjects.DOMElement; -import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; -import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; -import { DirtyScene } from "./DirtyScene"; -import { TextUtils } from "../Components/TextUtils"; -import { touchScreenManager } from "../../Touch/TouchScreenManager"; -import { PinchManager } from "../UserInput/PinchManager"; -import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; -import { waScaleManager } from "../Services/WaScaleManager"; -import { EmoteManager } from "./EmoteManager"; -import { CameraManager } from "./CameraManager"; -import EVENT_TYPE = Phaser.Scenes.Events; -import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; - -import AnimatedTiles from "phaser-animated-tiles"; -import { StartPositionCalculator } from "./StartPositionCalculator"; -import { soundManager } from "./SoundManager"; -import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; -import { videoFocusStore } from "../../Stores/VideoFocusStore"; -import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; -import { SharedVariablesManager } from "./SharedVariablesManager"; -import { playersStore } from "../../Stores/PlayersStore"; -import { chatVisibilityStore } from "../../Stores/ChatStore"; -import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; -import { - audioManagerFileStore, - audioManagerVisibilityStore, - audioManagerVolumeStore, -} from "../../Stores/AudioManagerStore"; -import { PropertyUtils } from "../Map/PropertyUtils"; import Tileset = Phaser.Tilemaps.Tileset; -import { userIsAdminStore } from "../../Stores/GameStore"; -import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; -import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; -import { GameMapPropertiesListener } from "./GameMapPropertiesListener"; -import { analyticsClient } from "../../Administration/AnalyticsClient"; -import { get } from "svelte/store"; -import { contactPageStore } from "../../Stores/MenuStore"; -import { GameMapProperties } from "./GameMapProperties"; import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile; - +import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -206,6 +204,11 @@ export class GameScene extends DirtyScene { private objectsByType = new Map(); private embeddedWebsiteManager!: EmbeddedWebsiteManager; private loader: Loader; + private userWokaPictureStores: Map = new Map(); + private userCompanionPictureStores: Map = new Map< + number, + UserCompanionPictureStore + >(); constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -339,6 +342,24 @@ export class GameScene extends DirtyScene { this.loader.addLoader(); } + public getUserWokaPictureStore(userId: number) { + let store = this.userWokaPictureStores.get(userId); + if (!store) { + store = new UserWokaPictureStore(); + this.userWokaPictureStores.set(userId, store); + } + return store; + } + + public getUserCompanionPictureStore(userId: number) { + let store = this.userCompanionPictureStores.get(userId); + if (!store) { + store = new UserCompanionPictureStore(); + this.userCompanionPictureStores.set(userId, store); + } + return store; + } + // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. // eslint-disable-next-line @typescript-eslint/no-explicit-any private async onMapLoad(data: any): Promise { @@ -674,7 +695,6 @@ export class GameScene extends DirtyScene { this.connection = onConnect.connection; playersStore.connectToRoomConnection(this.connection); - userIsAdminStore.set(this.connection.hasTag("admin")); this.connection.onUserJoins((message: MessageUserJoined) => { @@ -1539,6 +1559,14 @@ ${escapedMessage} this.companion, this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined ); + this.CurrentPlayer.once("woka-textures-loaded", () => { + this.savePlayerWokaPicture(this.CurrentPlayer, -1); + }); + this.CurrentPlayer.once("companion-texture-loaded", (snapshotPromise: Promise) => { + snapshotPromise.then((snapshot: string) => { + this.savePlayerCompanionPicture(-1, snapshot); + }); + }); this.CurrentPlayer.on("pointerdown", (pointer: Phaser.Input.Pointer) => { if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) { return; //we don't want the menu to open when pinching on a touch screen. @@ -1566,6 +1594,15 @@ ${escapedMessage} this.createCollisionWithPlayer(); } + private async savePlayerWokaPicture(character: Character, userId: number): Promise { + const htmlImageElementSrc = await character.getSnapshot(); + this.getUserWokaPictureStore(userId).picture.set(htmlImageElementSrc); + } + + private savePlayerCompanionPicture(userId: number, snapshot: string): void { + this.getUserCompanionPictureStore(userId).picture.set(snapshot); + } + pushPlayerPosition(event: HasPlayerMovedEvent) { if (this.lastMoveEventSent === event) { return; @@ -1753,6 +1790,9 @@ ${escapedMessage} addPlayerData.companion, addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined ); + player.once("woka-textures-loaded", () => { + this.savePlayerWokaPicture(player, addPlayerData.userId); + }); this.MapPlayers.add(player); this.MapPlayersByKey.set(player.userId, player); player.updatePosition(addPlayerData.position); diff --git a/front/src/Phaser/Helpers/TexturesHelper.ts b/front/src/Phaser/Helpers/TexturesHelper.ts new file mode 100644 index 00000000..348e957a --- /dev/null +++ b/front/src/Phaser/Helpers/TexturesHelper.ts @@ -0,0 +1,34 @@ +export class TexturesHelper { + public static async getSnapshot( + scene: Phaser.Scene, + ...sprites: { sprite: Phaser.GameObjects.Sprite; frame?: string | number }[] + ): Promise { + const rt = scene.make.renderTexture({}, false); + try { + for (const { sprite, frame } of sprites) { + if (frame) { + sprite.setFrame(frame); + } + rt.draw(sprite, sprite.displayWidth * 0.5, sprite.displayHeight * 0.5); + } + return new Promise((resolve, reject) => { + try { + rt.snapshot( + (url) => { + resolve((url as HTMLImageElement).src); + rt.destroy(); + }, + "image/png", + 1 + ); + } catch (error) { + rt.destroy(); + reject(error); + } + }); + } catch (error) { + rt.destroy(); + throw new Error("Could not get the snapshot"); + } + } +} diff --git a/front/src/Stores/UserCompanionPictureStore.ts b/front/src/Stores/UserCompanionPictureStore.ts new file mode 100644 index 00000000..5483ca91 --- /dev/null +++ b/front/src/Stores/UserCompanionPictureStore.ts @@ -0,0 +1,8 @@ +import { writable, Writable } from "svelte/store"; + +/** + * A store that contains the player companion picture + */ +export class UserCompanionPictureStore { + constructor(public picture: Writable = writable(undefined)) {} +} diff --git a/front/src/Stores/UserWokaPictureStore.ts b/front/src/Stores/UserWokaPictureStore.ts new file mode 100644 index 00000000..8422ae50 --- /dev/null +++ b/front/src/Stores/UserWokaPictureStore.ts @@ -0,0 +1,8 @@ +import { writable, Writable } from "svelte/store"; + +/** + * A store that contains the player avatar picture + */ +export class UserWokaPictureStore { + constructor(public picture: Writable = writable(undefined)) {} +} diff --git a/front/style/style.scss b/front/style/style.scss index 1654156d..89437a99 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -62,8 +62,7 @@ body .message-info.warning{ background-color: black; border-radius: 50%; text-align: center; - padding-top: 32px; - font-size: 28px; + font-size: 14px; color: white; overflow: hidden; }