diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 5cd26045..d9bfe905 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -30,8 +30,12 @@ export class Room { let roomId = ''; let hash = ''; if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link - const absoluteExitSceneUrl = new URL(identifier, baseUrl); - roomId = '_/'+currentInstance+'/'+absoluteExitSceneUrl.hostname + absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId + //Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects. + //We instead use 'workadventure' as a dummy base value. + const baseUrlObject = new URL(baseUrl); + const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/'+currentInstance+'/'+baseUrlObject.hostname+baseUrlObject.pathname); + roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId + roomId = roomId.substring(1); //remove the leading slash hash = absoluteExitSceneUrl.hash; hash = hash.substring(1); //remove the leading diese } else { //absolute room Id diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 5c89447e..6df6e1a4 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -59,6 +59,7 @@ export abstract class Character extends Container { frame?: string | number ) { super(scene, x, y/*, texture, frame*/); + this.PlayerValue = name; this.sprites = new Map(); @@ -73,10 +74,9 @@ export abstract class Character extends Container { }); this.add(this.teleportation);*/ - this.PlayerValue = name; - this.playerName = new BitmapText(scene, x, y - 25, 'main_font', name, 7); + this.playerName = new BitmapText(scene, 0, - 25, 'main_font', name, 7); this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999); - scene.add.existing(this.playerName); + this.add(this.playerName); scene.add.existing(this); @@ -88,8 +88,6 @@ export abstract class Character extends Container { this.getBody().setOffset(0, 8); this.setDepth(-1); - this.scene.events.on('postupdate', this.postupdate.bind(this)); - this.playAnimation(direction, moving); } @@ -181,35 +179,21 @@ export abstract class Character extends Container { if (body.velocity.y < 0) { //moving up this.lastDirection = PlayerAnimationNames.WalkUp; this.playAnimation(PlayerAnimationNames.WalkUp, true); - //this.play(`${this.PlayerTexture}-${PlayerAnimationNames.WalkUp}`, true); } else if (body.velocity.y > 0) { //moving down this.lastDirection = PlayerAnimationNames.WalkDown; this.playAnimation(PlayerAnimationNames.WalkDown, true); - //this.play(`${this.PlayerTexture}-${PlayerAnimationNames.WalkDown}`, true); } else if (body.velocity.x > 0) { //moving right this.lastDirection = PlayerAnimationNames.WalkRight; this.playAnimation(PlayerAnimationNames.WalkRight, true); - //this.play(`${this.PlayerTexture}-${PlayerAnimationNames.WalkRight}`, true); } else if (body.velocity.x < 0) { //moving left this.lastDirection = PlayerAnimationNames.WalkLeft; this.playAnimation(PlayerAnimationNames.WalkLeft, true); - //this.anims.playReverse(`${this.PlayerTexture}-${PlayerAnimationNames.WalkLeft}`, true); - } - - //todo:remove this, use a container tech to move the bubble instead - if (this.bubble) { - this.bubble.moveBubble(this.x, this.y); } //update depth user this.setDepth(this.y); } - postupdate(time: number, delta: number) { - //super.update(delta); - this.playerName.setPosition(this.x, this.y - 25); - } - stop(){ this.getBody().setVelocity(0, 0); this.playAnimation(this.lastDirection, false); @@ -218,7 +202,6 @@ export abstract class Character extends Container { say(text: string) { if (this.bubble) return; this.bubble = new SpeechBubble(this.scene, this, text) - //todo make the bubble destroy on player movement? setTimeout(() => { if (this.bubble !== null) { this.bubble.destroy(); @@ -227,16 +210,13 @@ export abstract class Character extends Container { }, 3000) } - destroy(fromScene?: boolean): void { - if (this.scene) { - this.scene.events.removeListener('postupdate', this.postupdate.bind(this)); - } + destroy(): void { for (const sprite of this.sprites.values()) { if(this.scene) { this.scene.sys.updateList.remove(sprite); } } - super.destroy(fromScene); + super.destroy(); this.playerName.destroy(); } } diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index 08f657d4..54592389 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -29,6 +29,7 @@ export class RemotePlayer extends Character { this.playAnimation(position.direction, position.moving); this.setX(position.x); this.setY(position.y); - this.setDepth(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). } } diff --git a/front/src/Phaser/Entity/SpeechBubble.ts b/front/src/Phaser/Entity/SpeechBubble.ts index 30518890..06a64bd4 100644 --- a/front/src/Phaser/Entity/SpeechBubble.ts +++ b/front/src/Phaser/Entity/SpeechBubble.ts @@ -1,16 +1,12 @@ import Scene = Phaser.Scene; import {Character} from "./Character"; +//todo: improve this WIP export class SpeechBubble { private bubble: Phaser.GameObjects.Graphics; private content: Phaser.GameObjects.Text; - /** - * - * @param scene - * @param player - * @param text - */ + constructor(scene: Scene, player: Character, text: string = "") { const bubbleHeight = 50; @@ -19,6 +15,7 @@ export class SpeechBubble { const arrowHeight = bubbleHeight / 4; this.bubble = scene.add.graphics({ x: player.x + 16, y: player.y - 80 }); + player.add(this.bubble); // Bubble shadow this.bubble.fillStyle(0x222222, 0.5); @@ -52,31 +49,13 @@ export class SpeechBubble { this.bubble.lineBetween(point2X, point2Y, point3X, point3Y); this.bubble.lineBetween(point1X, point1Y, point3X, point3Y); - this.content = scene.add.text(0, 0, text, { fontFamily: 'Arial', fontSize: 20, color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } }); + this.content = scene.add.text(0, 0, text, { fontFamily: 'Arial', fontSize: '20', color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } }); + player.add(this.content); const bounds = this.content.getBounds(); this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); } - /** - * - * @param x - * @param y - */ - moveBubble(x : number, y : number) { - if (this.bubble) { - this.bubble.setPosition((x + 16), (y - 80)); - } - if (this.content) { - const bubbleHeight = 50; - const bubblePadding = 10; - const bubbleWidth = bubblePadding * 2 + this.content.text.length * 10; - const bounds = this.content.getBounds(); - //this.content.setPosition(x, y); - this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); - } - } - destroy(): void { this.bubble.setVisible(false) //todo find a better way this.bubble.destroy(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index f9ee234d..9dcb48c9 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -67,6 +67,7 @@ import {ChatModeIcon} from "../Components/ChatModeIcon"; import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {TextureError} from "../../Exception/TextureError"; +import {TextField} from "../Components/TextField"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -111,7 +112,7 @@ export class GameScene extends ResizableScene implements CenterListener { MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayersByKey : Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; - Layers!: Array; + Layers!: Array; Objects!: Array; mapFile!: ITiledMap; groups: Map; @@ -181,6 +182,8 @@ export class GameScene extends ResizableScene implements CenterListener { //hook preload scene preload(): void { + this.initProgressBar(); + this.load.image(openChatIconName, 'resources/objects/talk.png'); this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.scene.start(FourOFourSceneName, { @@ -208,6 +211,20 @@ export class GameScene extends ResizableScene implements CenterListener { this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); } + private initProgressBar(): void { + const loadingText = this.add.text(this.game.renderer.width / 2, 200, 'Loading'); + const progress = this.add.graphics(); + this.load.on('progress', (value: number) => { + progress.clear(); + progress.fillStyle(0xffffff, 1); + progress.fillRect(0, 270, 800 * value, 60); + }); + this.load.on('complete', () => { + loadingText.destroy(); + progress.destroy(); + }); + } + // 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 { @@ -344,11 +361,11 @@ export class GameScene extends ResizableScene implements CenterListener { this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); //add layer on map - this.Layers = new Array(); + this.Layers = new Array(); let depth = -2; for (const layer of this.mapFile.layers) { if (layer.type === 'tilelayer') { - this.addLayer(this.Map.createDynamicLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); + this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { @@ -381,42 +398,13 @@ export class GameScene extends ResizableScene implements CenterListener { //notify game manager can to create currentUser in map this.createCurrentPlayer(); - - //initialise camera + this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted + this.initCamera(); (this as any).animatedTiles.init(this.Map); // eslint-disable-line @typescript-eslint/no-explicit-any - // Let's generate the circle for the group delimiter - let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-white'); - if (circleElement) { - this.textures.remove('circleSprite-white'); - } - - circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-red'); - if (circleElement) { - this.textures.remove('circleSprite-red'); - } - - //create white circle canvas use to create sprite - this.circleTexture = this.textures.createCanvas('circleSprite-white', 96, 96); - const context = this.circleTexture.context; - context.beginPath(); - context.arc(48, 48, 48, 0, 2 * Math.PI, false); - // context.lineWidth = 5; - context.strokeStyle = '#ffffff'; - context.stroke(); - this.circleTexture.refresh(); - - //create red circle canvas use to create sprite - this.circleRedTexture = this.textures.createCanvas('circleSprite-red', 96, 96); - const contextRed = this.circleRedTexture.context; - contextRed.beginPath(); - contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); - // context.lineWidth = 5; - contextRed.strokeStyle = '#ff0000'; - contextRed.stroke(); - this.circleRedTexture.refresh(); + this.initCirclesCanvas(); // Let's pause the scene if the connection is not established yet if (this.isReconnecting) { @@ -600,6 +588,40 @@ export class GameScene extends ResizableScene implements CenterListener { }); } + //todo: into dedicated classes + private initCirclesCanvas(): void { + // Let's generate the circle for the group delimiter + let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-white'); + if (circleElement) { + this.textures.remove('circleSprite-white'); + } + + circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-red'); + if (circleElement) { + this.textures.remove('circleSprite-red'); + } + + //create white circle canvas use to create sprite + this.circleTexture = this.textures.createCanvas('circleSprite-white', 96, 96); + const context = this.circleTexture.context; + context.beginPath(); + context.arc(48, 48, 48, 0, 2 * Math.PI, false); + // context.lineWidth = 5; + context.strokeStyle = '#ffffff'; + context.stroke(); + this.circleTexture.refresh(); + + //create red circle canvas use to create sprite + this.circleRedTexture = this.textures.createCanvas('circleSprite-red', 96, 96); + const contextRed = this.circleRedTexture.context; + contextRed.beginPath(); + contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); + // context.lineWidth = 5; + contextRed.strokeStyle = '#ff0000'; + contextRed.stroke(); + this.circleRedTexture.refresh(); + } + private playAudio(url: string|number|boolean|undefined, loop=false): void { if (url === undefined) { audioManager.unloadAudio(); @@ -711,6 +733,14 @@ export class GameScene extends ResizableScene implements CenterListener { } } + private removeAllRemotePlayers(): void { + this.MapPlayersByKey.forEach((player: RemotePlayer) => { + player.destroy(); + this.MapPlayers.remove(player); + }); + this.MapPlayersByKey = new Map(); + } + private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated if(mediaManager.activatedDiscussion){ @@ -830,13 +860,13 @@ export class GameScene extends ResizableScene implements CenterListener { this.cameras.main.setZoom(ZOOM_LEVEL); } - addLayer(Layer : Phaser.Tilemaps.DynamicTilemapLayer){ + addLayer(Layer : Phaser.Tilemaps.TilemapLayer){ this.Layers.push(Layer); } createCollisionWithPlayer() { //add collision layer - this.Layers.forEach((Layer: Phaser.Tilemaps.DynamicTilemapLayer) => { + this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); @@ -1018,14 +1048,7 @@ export class GameScene extends ResizableScene implements CenterListener { */ private doInitUsersPosition(usersPosition: MessageUserPositionInterface[]): void { const currentPlayerId = this.connection.getUserId(); - - // clean map - this.MapPlayersByKey.forEach((player: RemotePlayer) => { - player.destroy(); - this.MapPlayers.remove(player); - }); - this.MapPlayersByKey = new Map(); - + this.removeAllRemotePlayers(); // load map usersPosition.forEach((userPosition : MessageUserPositionInterface) => { if(userPosition.userId === currentPlayerId){ @@ -1218,9 +1241,7 @@ export class GameScene extends ResizableScene implements CenterListener { // Let's put this in Game coordinates by applying the zoom level: xCenter /= ZOOM_LEVEL * RESOLUTION; yCenter /= ZOOM_LEVEL * RESOLUTION; - - //console.log("updateCameraOffset", array, xCenter, yCenter, this.game.renderer.width, this.game.renderer.height); - + this.cameras.main.startFollow(this.CurrentPlayer, true, 1, 1, xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2); } diff --git a/front/src/Phaser/Items/ActionableItem.ts b/front/src/Phaser/Items/ActionableItem.ts index 36b14921..a1548f41 100644 --- a/front/src/Phaser/Items/ActionableItem.ts +++ b/front/src/Phaser/Items/ActionableItem.ts @@ -43,8 +43,7 @@ export class ActionableItem { } this.isSelectable = true; this.sprite.setPipeline(OutlinePipeline.KEY); - this.sprite.pipeline.setFloat2('uTextureSize', - this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height); + this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height); } /** diff --git a/front/src/Phaser/Shaders/OutlinePipeline.ts b/front/src/Phaser/Shaders/OutlinePipeline.ts index 6b416b8a..0d074bc3 100644 --- a/front/src/Phaser/Shaders/OutlinePipeline.ts +++ b/front/src/Phaser/Shaders/OutlinePipeline.ts @@ -1,5 +1,4 @@ -export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline -{ +export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline { // the unique id of this pipeline public static readonly KEY = 'Outline'; @@ -11,7 +10,6 @@ export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTint { super({ game: game, - renderer: game.renderer, fragShader: ` precision mediump float; diff --git a/front/src/index.ts b/front/src/index.ts index c8783b90..46d38a3b 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -75,7 +75,7 @@ const config: GameConfig = { postBoot: game => { // FIXME: we should fore WebGL in the config. const renderer = game.renderer as WebGLRenderer; - renderer.addPipeline(OutlinePipeline.KEY, new OutlinePipeline(game)); + renderer.pipelines.add(OutlinePipeline.KEY, new OutlinePipeline(game)); } } }; diff --git a/front/tests/Phaser/Game/RoomTest.ts b/front/tests/Phaser/Game/RoomTest.ts index 40218d53..80624d64 100644 --- a/front/tests/Phaser/Game/RoomTest.ts +++ b/front/tests/Phaser/Game/RoomTest.ts @@ -34,6 +34,22 @@ describe("Room getIdFromIdentifier()", () => { expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json'); expect(hash).toEqual(''); }); + it("should work with a relative file link that rewrite the map domain", () => { + const {roomId, hash} = Room.getIdFromIdentifier('../../maps.workadventure.localhost/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global'); + expect(roomId).toEqual('_/global/maps.workadventure.localhost/Floor1/floor1.json'); + expect(hash).toEqual(''); + }); + it("should work with a relative file link that rewrite the map instance", () => { + const {roomId, hash} = Room.getIdFromIdentifier('../../../notglobal/maps.workadventu.re/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global'); + expect(roomId).toEqual('_/notglobal/maps.workadventu.re/Floor1/floor1.json'); + expect(hash).toEqual(''); + }); + it("should work with a relative file link that change the map type", () => { + const {roomId, hash} = Room.getIdFromIdentifier('../../../../@/tcm/is/great', 'https://maps.workadventu.re/floor0/Floor0.json', 'global'); + expect(roomId).toEqual('@/tcm/is/great'); + expect(hash).toEqual(''); + }); + it("should work with a relative file link and a hash as parameters", () => { const {roomId, hash} = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global'); expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); diff --git a/front/yarn.lock b/front/yarn.lock index 02b6052b..a31409a6 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1771,7 +1771,7 @@ eventemitter3@^2.0.3: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= -eventemitter3@^4.0.0, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -1829,7 +1829,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -exports-loader@^1.1.0: +exports-loader@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-1.1.1.tgz#88c9a6877ee6a5519d7c41a016bdd99148421e69" integrity sha512-CmyhIR2sJ3KOfVsHjsR0Yvo+0lhRhRMAevCbB8dhTVLHsZPs0lCQTvRmR9YNvBXDBxUuhmCE2f54KqEjZUaFrg== @@ -2528,7 +2528,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imports-loader@^1.1.0: +imports-loader@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-1.2.0.tgz#b06823d0bb42e6f5ff89bc893829000eda46693f" integrity sha512-zPvangKEgrrPeqeUqH0Uhc59YqK07JqZBi9a9cQ3v/EKUIqrbJHY4CvUrDus2lgQa5AmPyXuGrWP8JJTqzE5RQ== @@ -3664,13 +3664,13 @@ pbkdf2@^3.0.3: sha.js "^2.4.8" phaser@^3.22.0: - version "3.24.1" - resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.24.1.tgz#376e0c965d2a35af37c06ee78627dafbde5be017" - integrity sha512-WbrRMkbpEzarkfrq83akeauc6b8xNxsOTpDygyW7wrU2G2ne6kOYu3hji4UAaGnZaOLrVuj8ycYPjX9P1LxcDw== + version "3.51.0" + resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.51.0.tgz#b0c7ee2b21e795830d74f476dd30816a42b023bd" + integrity sha512-Z7XNToZWO60Zx/YetaoeGSeELy5ND45TPPfYB9HtQU2692ACXc/nioQaWp20NzTMgeBsgl6vYf3CI82y/DzSyg== dependencies: - eventemitter3 "^4.0.4" - exports-loader "^1.1.0" - imports-loader "^1.1.0" + eventemitter3 "^4.0.7" + exports-loader "^1.1.1" + imports-loader "^1.2.0" path "^0.12.7" picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: