diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 72161d86..1693844b 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -150,6 +150,7 @@ export class GameRoom { position, false, this.positionNotifier, + joinRoomMessage.getAway(), socket, joinRoomMessage.getTagList(), joinRoomMessage.getVisitcardurl(), diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 516a6b08..2f0dad54 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -32,6 +32,7 @@ export class User implements Movable { private position: PointInterface, public silent: boolean, private positionNotifier: PositionNotifier, + private away: boolean, public readonly socket: UserSocket, public readonly tags: string[], public readonly visitCardUrl: string | null, @@ -89,6 +90,10 @@ export class User implements Movable { return this.outlineColor; } + public isAway(): boolean { + return this.away; + } + get following(): User | undefined { return this._following; } @@ -129,6 +134,11 @@ export class User implements Movable { } this.voiceIndicatorShown = details.getShowvoiceindicator()?.getValue(); + const away = details.getAway(); + if (away) { + this.away = away.getValue(); + } + const playerDetails = new SetPlayerDetailsMessage(); if (this.outlineColor !== undefined) { @@ -137,6 +147,9 @@ export class User implements Movable { if (this.voiceIndicatorShown !== undefined) { playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown)); } + if (details.getAway() !== undefined) { + playerDetails.setAway(new BoolValue().setValue(this.away)); + } this.positionNotifier.updatePlayerDetails(this, playerDetails); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index b9dfacd4..6698ed43 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -331,6 +331,7 @@ export class SocketManager { userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUseruuid(thing.uuid); userJoinedZoneMessage.setName(thing.name); + userJoinedZoneMessage.setAway(thing.isAway()); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone)); @@ -658,6 +659,7 @@ export class SocketManager { userJoinedMessage.setUserid(thing.id); userJoinedMessage.setUseruuid(thing.uuid); userJoinedMessage.setName(thing.name); + userJoinedMessage.setAway(thing.isAway()); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); if (thing.visitCardUrl) { diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index f21bb4b2..7673ed3c 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -41,6 +41,7 @@ describe("PositionNotifier", () => { }, false, positionNotifier, + false, {} as UserSocket, [], null, @@ -60,6 +61,7 @@ describe("PositionNotifier", () => { }, false, positionNotifier, + false, {} as UserSocket, [], null, @@ -149,6 +151,7 @@ describe("PositionNotifier", () => { }, false, positionNotifier, + false, {} as UserSocket, [], null, @@ -168,6 +171,7 @@ describe("PositionNotifier", () => { }, false, positionNotifier, + false, {} as UserSocket, [], null, diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 1c49b210..0e32c9ed 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -14,6 +14,7 @@ export interface MessageUserPositionInterface { name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; + away: boolean; visitCardUrl: string | null; companion: string | null; userUuid: string; @@ -29,6 +30,7 @@ export interface MessageUserJoined { name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; + away: boolean; visitCardUrl: string | null; companion: string | null; userUuid: string; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 912e7bbc..e71f0924 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -518,6 +518,20 @@ export class RoomConnection implements RoomConnection { this.socket.send(bytes); } + public emitPlayerAway(away: boolean): void { + const message = SetPlayerDetailsMessageTsProto.fromPartial({ + away, + }); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "setPlayerDetailsMessage", + setPlayerDetailsMessage: message, + }, + }).finish(); + + this.socket.send(bytes); + } + public emitPlayerOutlineColor(color: number | null) { let message: SetPlayerDetailsMessageTsProto; if (color === null) { @@ -654,6 +668,7 @@ export class RoomConnection implements RoomConnection { characterLayers, visitCardUrl: message.visitCardUrl, position: ProtobufClientUtils.toPointInterface(position), + away: message.away, companion: companion ? companion.name : null, userUuid: message.userUuid, outlineColor: message.hasOutline ? message.outlineColor : undefined, diff --git a/front/src/Phaser/Components/PlayerStatusDot.ts b/front/src/Phaser/Components/PlayerStatusDot.ts new file mode 100644 index 00000000..af893b2f --- /dev/null +++ b/front/src/Phaser/Components/PlayerStatusDot.ts @@ -0,0 +1,65 @@ +import { Easing } from "../../types"; + +export class PlayerStatusDot extends Phaser.GameObjects.Container { + private graphics: Phaser.GameObjects.Graphics; + + private away: boolean; + + private readonly COLORS = { + // online: 0x00ff00, + // away: 0xffff00, + online: 0x8cc43f, + onlineOutline: 0x427a25, + away: 0xf5931e, + awayOutline: 0x875d13, + }; + + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y); + + this.away = false; + + this.graphics = this.scene.add.graphics(); + this.add(this.graphics); + this.redraw(); + + this.scene.add.existing(this); + } + + public setAway(away: boolean = true, instant: boolean = false): void { + if (this.away === away) { + return; + } + this.away = away; + if (instant) { + this.redraw(); + } else { + this.playStatusChangeAnimation(); + } + } + + private playStatusChangeAnimation(): void { + this.scale = 1; + this.scene.tweens.add({ + targets: [this], + duration: 200, + yoyo: true, + ease: Easing.BackEaseIn, + scale: 0, + onYoyo: () => { + this.redraw(); + }, + onComplete: () => { + this.scale = 1; + }, + }); + } + + private redraw(): void { + this.graphics.clear(); + this.graphics.fillStyle(this.away ? this.COLORS.away : this.COLORS.online); + this.graphics.lineStyle(1, this.away ? this.COLORS.awayOutline : this.COLORS.onlineOutline); + this.graphics.fillCircle(0, 0, 3); + this.graphics.strokeCircle(0, 0, 3); + } +} diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index fab18ce1..a4066cf2 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -24,6 +24,7 @@ import type { OutlineableInterface } from "../Game/OutlineableInterface"; import type CancelablePromise from "cancelable-promise"; import { TalkIcon } from "../Components/TalkIcon"; import { Deferred } from "ts-deferred"; +import { PlayerStatusDot } from "../Components/PlayerStatusDot"; const playerNameY = -25; const interactiveRadius = 35; @@ -32,6 +33,7 @@ export abstract class Character extends Container implements OutlineableInterfac private bubble: SpeechBubble | null = null; private readonly playerNameText: Text; private readonly talkIcon: TalkIcon; + protected readonly statusDot: PlayerStatusDot; public playerName: string; public sprites: Map; protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; @@ -137,7 +139,8 @@ export abstract class Character extends Container implements OutlineableInterfac }); } this.playerNameText.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX); - this.add(this.playerNameText); + this.statusDot = new PlayerStatusDot(scene, this.playerNameText.getLeftCenter().x - 6, playerNameY - 1); + this.add([this.playerNameText, this.statusDot]); this.setClickable(isClickable); @@ -238,6 +241,10 @@ export abstract class Character extends Container implements OutlineableInterfac this.talkIcon.show(show, forceClose); } + public setAwayStatus(away: boolean = true, instant: boolean = false): void { + this.statusDot.setAway(away, instant); + } + public addCompanion(name: string, texturePromise?: CancelablePromise): void { if (typeof texturePromise !== "undefined") { this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index d0ca4850..0d753c9a 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -202,6 +202,8 @@ export class GameMap { /** * Registers a callback called when the user moves inside another zone. + * + * @deprecated */ public onEnterZone(callback: zoneChangeCallback) { this.enterZoneCallbacks.push(callback); @@ -209,6 +211,8 @@ export class GameMap { /** * Registers a callback called when the user moves outside another zone. + * + * @deprecated */ public onLeaveZone(callback: zoneChangeCallback) { this.leaveZoneCallbacks.push(callback); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 85a72204..c6336e8b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -102,6 +102,7 @@ import CancelablePromise from "cancelable-promise"; import { Deferred } from "ts-deferred"; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; import { PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages"; +import { privacyShutdownStore } from "../../Stores/PrivacyShutdownStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -177,6 +178,7 @@ export class GameScene extends DirtyScene { private localVolumeStoreUnsubscriber: Unsubscriber | undefined; private followUsersColorStoreUnsubscribe!: Unsubscriber; + private privacyShutdownStoreUnsubscribe!: Unsubscriber; private userIsJitsiDominantSpeakerStoreUnsubscriber!: Unsubscriber; private jitsiParticipantsCountStoreUnsubscriber!: Unsubscriber; @@ -705,6 +707,10 @@ export class GameScene extends DirtyScene { } }); + this.privacyShutdownStoreUnsubscribe = privacyShutdownStore.subscribe((away) => { + this.connection?.emitPlayerAway(away); + }); + Promise.all([ this.connectionAnswerPromiseDeferred.promise as Promise, ...scriptPromises, @@ -763,6 +769,7 @@ export class GameScene extends DirtyScene { characterLayers: message.characterLayers, name: message.name, position: message.position, + away: message.away, visitCardUrl: message.visitCardUrl, companion: message.companion, userUuid: message.userUuid, @@ -1568,6 +1575,7 @@ ${escapedMessage} this.emoteUnsubscribe(); this.emoteMenuUnsubscribe(); this.followUsersColorStoreUnsubscribe(); + this.privacyShutdownStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe(); this.userIsJitsiDominantSpeakerStoreUnsubscriber(); this.jitsiParticipantsCountStoreUnsubscriber(); @@ -1940,6 +1948,9 @@ ${escapedMessage} if (addPlayerData.outlineColor !== undefined) { player.setApiOutlineColor(addPlayerData.outlineColor); } + if (addPlayerData.away !== undefined) { + player.setAwayStatus(addPlayerData.away, true); + } this.MapPlayers.add(player); this.MapPlayersByKey.set(player.userId, player); player.updatePosition(addPlayerData.position); @@ -2091,6 +2102,9 @@ ${escapedMessage} if (message.details?.showVoiceIndicator !== undefined) { character.showTalkIcon(message.details?.showVoiceIndicator); } + if (message.details?.away !== undefined) { + character.setAwayStatus(message.details?.away); + } } /** diff --git a/front/src/Phaser/Game/PlayerInterface.ts b/front/src/Phaser/Game/PlayerInterface.ts index 0a3d7543..571bf3cb 100644 --- a/front/src/Phaser/Game/PlayerInterface.ts +++ b/front/src/Phaser/Game/PlayerInterface.ts @@ -7,6 +7,7 @@ export interface PlayerInterface { visitCardUrl: string | null; companion: string | null; userUuid: string; + away: boolean; color?: string; outlineColor?: number; } diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index db3fcbe0..cae7b496 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -28,7 +28,7 @@ export class Player extends Character { companionTexturePromise?: CancelablePromise ) { super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise); - + this.statusDot.setVisible(false); //the current player model should be push away by other players to prevent conflict this.getBody().setImmovable(false); } diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts index 0676235a..9dc78780 100644 --- a/front/src/Stores/PlayersStore.ts +++ b/front/src/Stores/PlayersStore.ts @@ -30,6 +30,7 @@ function createPlayersStore() { visitCardUrl: message.visitCardUrl, companion: message.companion, userUuid: message.userUuid, + away: message.away, color: getRandomColor(), }); return users; @@ -59,6 +60,7 @@ function createPlayersStore() { characterLayers: [], visitCardUrl: null, companion: null, + away: false, userUuid: "dummy", color: getRandomColor(), }); diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 637d4aa1..fae82184 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -53,6 +53,7 @@ message SetPlayerDetailsMessage { google.protobuf.UInt32Value outlineColor = 3; google.protobuf.BoolValue removeOutlineColor = 4; google.protobuf.BoolValue showVoiceIndicator = 5; + google.protobuf.BoolValue away = 6; } message UserMovesMessage { @@ -206,6 +207,7 @@ message UserJoinedMessage { string userUuid = 7; uint32 outlineColor = 8; bool hasOutline = 9; + bool away = 10; } message UserLeftMessage { @@ -344,6 +346,7 @@ message JoinRoomMessage { CompanionMessage companion = 8; string visitCardUrl = 9; string userRoomToken = 10; + bool away = 11; } message UserJoinedZoneMessage { @@ -357,6 +360,7 @@ message UserJoinedZoneMessage { string userUuid = 8; uint32 outlineColor = 9; bool hasOutline = 10; + bool away = 11; } message UserLeftZoneMessage { diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 3cc535a7..ca4646a4 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -49,6 +49,7 @@ export class UserDescriptor { private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, + private away: boolean, private visitCardUrl: string | null, private companion?: CompanionMessage, private outlineColor?: number @@ -69,6 +70,7 @@ export class UserDescriptor { message.getName(), message.getCharacterlayersList(), position, + message.getAway(), message.getVisitcardurl(), message.getCompanion(), message.getHasoutline() ? message.getOutlinecolor() : undefined @@ -89,6 +91,10 @@ export class UserDescriptor { } else { this.outlineColor = playerDetails.getOutlinecolor()?.getValue(); } + const away = playerDetails.getAway(); + if (away) { + this.away = away.getValue(); + } } public toUserJoinedMessage(): UserJoinedMessage { @@ -98,6 +104,7 @@ export class UserDescriptor { userJoinedMessage.setName(this.name); userJoinedMessage.setCharacterlayersList(this.characterLayers); userJoinedMessage.setPosition(this.position); + userJoinedMessage.setAway(this.away); if (this.visitCardUrl) { userJoinedMessage.setVisitcardurl(this.visitCardUrl); }