From 35b37a6a88cb57600dd5f3290b62b6206ca8184d Mon Sep 17 00:00:00 2001 From: kharhamel Date: Mon, 10 May 2021 17:10:41 +0200 Subject: [PATCH] Added a radial menu to run emotes --- CHANGELOG.md | 4 ++ front/src/Phaser/Components/ChatModeIcon.ts | 4 +- front/src/Phaser/Components/MobileJoystick.ts | 5 +- front/src/Phaser/Components/OpenChatIcon.ts | 3 +- .../Phaser/Components/PresentationModeIcon.ts | 4 +- front/src/Phaser/Components/RadialMenu.ts | 58 +++++++++++++++++++ front/src/Phaser/Entity/Character.ts | 50 +++++++++++----- front/src/Phaser/Entity/RemotePlayer.ts | 12 ++-- front/src/Phaser/Game/DepthIndexes.ts | 8 +++ front/src/Phaser/Game/EmoteManager.ts | 46 ++++++++------- front/src/Phaser/Game/GameScene.ts | 13 ++++- front/src/Phaser/Player/Player.ts | 45 ++++++++++---- pusher/src/Services/SocketManager.ts | 2 + 13 files changed, 191 insertions(+), 63 deletions(-) create mode 100644 front/src/Phaser/Components/RadialMenu.ts create mode 100644 front/src/Phaser/Game/DepthIndexes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2028e3b7..e5d9138a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ### Updates +- Added the emote feature to Workadventure. (@Kharhamel, @Tabascoeye) + - The emote menu can be opened by clicking on your character. + - Clicking on one of its element will close the menu and play an emote above your character. + - This emote can be seen by other players. - Mobile support has been improved - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used - Mouse wheel support to zoom in / out diff --git a/front/src/Phaser/Components/ChatModeIcon.ts b/front/src/Phaser/Components/ChatModeIcon.ts index 932a4d88..69449a1d 100644 --- a/front/src/Phaser/Components/ChatModeIcon.ts +++ b/front/src/Phaser/Components/ChatModeIcon.ts @@ -1,3 +1,5 @@ +import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; + export class ChatModeIcon extends Phaser.GameObjects.Sprite { constructor(scene: Phaser.Scene, x: number, y: number) { super(scene, x, y, 'layout_modes', 3); @@ -6,6 +8,6 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite { this.setOrigin(0, 1); this.setInteractive(); this.setVisible(false); - this.setDepth(99999); + this.setDepth(DEPTH_INGAME_TEXT_INDEX); } } \ No newline at end of file diff --git a/front/src/Phaser/Components/MobileJoystick.ts b/front/src/Phaser/Components/MobileJoystick.ts index fced71da..46efcbc2 100644 --- a/front/src/Phaser/Components/MobileJoystick.ts +++ b/front/src/Phaser/Components/MobileJoystick.ts @@ -1,5 +1,6 @@ import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js'; import {waScaleManager} from "../Services/WaScaleManager"; +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; //the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free export const joystickBaseKey = 'joystickBase'; @@ -19,8 +20,8 @@ export class MobileJoystick extends VirtualJoystick { x: -1000, y: -1000, radius: radius * window.devicePixelRatio, - base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(99999), - thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(99999), + base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX), + thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX), enable: true, dir: "8dir", }); diff --git a/front/src/Phaser/Components/OpenChatIcon.ts b/front/src/Phaser/Components/OpenChatIcon.ts index 1e9429e8..ab07a80c 100644 --- a/front/src/Phaser/Components/OpenChatIcon.ts +++ b/front/src/Phaser/Components/OpenChatIcon.ts @@ -1,4 +1,5 @@ import {discussionManager} from "../../WebRtc/DiscussionManager"; +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; export const openChatIconName = 'openChatIcon'; export class OpenChatIcon extends Phaser.GameObjects.Image { @@ -9,7 +10,7 @@ export class OpenChatIcon extends Phaser.GameObjects.Image { this.setOrigin(0, 1); this.setInteractive(); this.setVisible(false); - this.setDepth(99999); + this.setDepth(DEPTH_INGAME_TEXT_INDEX); this.on("pointerup", () => discussionManager.showDiscussionPart()); } diff --git a/front/src/Phaser/Components/PresentationModeIcon.ts b/front/src/Phaser/Components/PresentationModeIcon.ts index 49ff2ea1..09c8beb5 100644 --- a/front/src/Phaser/Components/PresentationModeIcon.ts +++ b/front/src/Phaser/Components/PresentationModeIcon.ts @@ -1,3 +1,5 @@ +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; + export class PresentationModeIcon extends Phaser.GameObjects.Sprite { constructor(scene: Phaser.Scene, x: number, y: number) { super(scene, x, y, 'layout_modes', 0); @@ -6,6 +8,6 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite { this.setOrigin(0, 1); this.setInteractive(); this.setVisible(false); - this.setDepth(99999); + this.setDepth(DEPTH_INGAME_TEXT_INDEX); } } \ No newline at end of file diff --git a/front/src/Phaser/Components/RadialMenu.ts b/front/src/Phaser/Components/RadialMenu.ts new file mode 100644 index 00000000..a2a646f5 --- /dev/null +++ b/front/src/Phaser/Components/RadialMenu.ts @@ -0,0 +1,58 @@ +import Sprite = Phaser.GameObjects.Sprite; +import {DEPTH_UI_INDEX} from "../Game/DepthIndexes"; + +export interface RadialMenuItem { + sprite: string, + frame: number, + name: string, +} + +const menuRadius = 80; +export const RadialMenuClickEvent = 'radialClick'; + +export class RadialMenu extends Phaser.GameObjects.Container { + + constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) { + super(scene, x, y); + this.setDepth(DEPTH_UI_INDEX) + this.scene.add.existing(this); + this.initItems(); + } + + private initItems() { + const itemsNumber = this.items.length; + this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber)) + } + + private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number) { + const image = new Sprite(this.scene, 0, menuRadius, item.sprite, item.frame); + this.add(image); + this.scene.sys.updateList.add(image); + image.setDepth(DEPTH_UI_INDEX) + image.setInteractive({ + hitArea: new Phaser.Geom.Circle(0, 0, 25), + hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method + useHandCursor: true, + }); + image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item)); + image.on('pointerover', () => { + this.scene.tweens.add({ + targets: image, + scale: 2, + duration: 500, + ease: 'Power3', + }) + }); + image.on('pointerout', () => { + this.scene.tweens.add({ + targets: image, + scale: 1, + duration: 500, + ease: 'Power3', + }) + }); + const angle = 2 * Math.PI * index / itemsNumber; + Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius); + } + +} \ No newline at end of file diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index bb8c2fb0..bc536eb4 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -6,6 +6,8 @@ import Sprite = Phaser.GameObjects.Sprite; import {TextureError} from "../../Exception/TextureError"; import {Companion} from "../Companion/Companion"; import {getEmoteAnimName} from "../Game/EmoteManager"; +import {GameScene} from "../Game/GameScene"; +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; const playerNameY = - 25; @@ -17,6 +19,8 @@ interface AnimationData { frames : number[] } +const interactiveRadius = 40; + export abstract class Character extends Container { private bubble: SpeechBubble|null = null; private readonly playerName: BitmapText; @@ -28,14 +32,16 @@ export abstract class Character extends Container { public companion?: Companion; private emote: Phaser.GameObjects.Sprite | null = null; - constructor(scene: Phaser.Scene, + constructor(scene: GameScene, x: number, y: number, texturesPromise: Promise, name: string, direction: PlayerAnimationDirections, moving: boolean, - frame?: string | number + frame: string | number, + companion: string|null, + companionTexturePromise?: Promise ) { super(scene, x, y/*, texture, frame*/); this.PlayerValue = name; @@ -49,19 +55,18 @@ export abstract class Character extends Container { this.invisible = false }) - /*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3); - this.teleportation.setInteractive(); - this.teleportation.visible = false; - this.teleportation.on('pointerup', () => { - this.report.visible = false; - this.teleportation.visible = false; - }); - this.add(this.teleportation);*/ - this.playerName = new BitmapText(scene, 0, playerNameY, 'main_font', name, 7); - this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999); + this.playerName.setOrigin(0.5).setCenterAlign().setDepth(DEPTH_INGAME_TEXT_INDEX); this.add(this.playerName); + if (this.isClickable()) { + this.setInteractive({ + hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius), + hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method + useHandCursor: true, + }); + } + scene.add.existing(this); this.scene.physics.world.enableBody(this); @@ -73,6 +78,10 @@ export abstract class Character extends Container { this.setDepth(-1); this.playAnimation(direction, moving); + + if (typeof companion === 'string') { + this.addCompanion(companion, companionTexturePromise); + } } public addCompanion(name: string, texturePromise?: Promise): void { @@ -80,6 +89,8 @@ export abstract class Character extends Container { this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); } } + + public abstract isClickable(): boolean; public addTextures(textures: string[], frame?: string | number): void { for (const texture of textures) { @@ -87,7 +98,6 @@ export abstract class Character extends Container { throw new TextureError('texture not found'); } const sprite = new Sprite(this.scene, 0, 0, texture, frame); - sprite.setInteractive({useHandCursor: true}); this.add(sprite); this.getPlayerAnimations(texture).forEach(d => { this.scene.anims.create({ @@ -234,18 +244,26 @@ export abstract class Character extends Container { } playEmote(emoteKey: string) { - if (this.emote) return; + this.cancelPreviousEmote(); this.playerName.setVisible(false); this.emote = new Sprite(this.scene, 0, -40, emoteKey, 1); - this.emote.setDepth(99999); + this.emote.setDepth(DEPTH_INGAME_TEXT_INDEX); this.add(this.emote); this.scene.sys.updateList.add(this.emote); this.emote.play(getEmoteAnimName(emoteKey)); - this.emote.on(Phaser.Animations.Events.SPRITE_ANIMATION_COMPLETE, () => { + this.emote.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => { this.emote?.destroy(); this.emote = null; this.playerName.setVisible(true); }); } + + cancelPreviousEmote() { + if (!this.emote) return; + + this.emote?.destroy(); + this.emote = null; + this.playerName.setVisible(true); + } } diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index 4787d1f2..4e00f102 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -21,14 +21,10 @@ export class RemotePlayer extends Character { companion: string|null, companionTexturePromise?: Promise ) { - super(Scene, x, y, texturesPromise, name, direction, moving, 1); - + super(Scene, x, y, texturesPromise, name, direction, moving, 1, companion, companionTexturePromise); + //set data this.userId = userId; - - if (typeof companion === 'string') { - this.addCompanion(companion, companionTexturePromise); - } } updatePosition(position: PointInterface): void { @@ -42,4 +38,8 @@ export class RemotePlayer extends Character { this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections); } } + + isClickable(): boolean { + return false; //todo: make remote players clickable if they are logged in. + } } diff --git a/front/src/Phaser/Game/DepthIndexes.ts b/front/src/Phaser/Game/DepthIndexes.ts new file mode 100644 index 00000000..d2d38328 --- /dev/null +++ b/front/src/Phaser/Game/DepthIndexes.ts @@ -0,0 +1,8 @@ +//this file contains all the depth indexes which will be used in our game + +export const DEPTH_TILE_INDEX = 0; +//Note: Player characters use their y coordinate as their depth to simulate a perspective. +//See the Character class. +export const DEPTH_OVERLAY_INDEX = 10000; +export const DEPTH_INGAME_TEXT_INDEX = 100000; +export const DEPTH_UI_INDEX = 1000000; diff --git a/front/src/Phaser/Game/EmoteManager.ts b/front/src/Phaser/Game/EmoteManager.ts index b33952fe..0256f458 100644 --- a/front/src/Phaser/Game/EmoteManager.ts +++ b/front/src/Phaser/Game/EmoteManager.ts @@ -2,6 +2,7 @@ import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import {createLoadingPromise} from "../Entity/PlayerTexturesLoadingManager"; import {emoteEventStream} from "../../Connexion/EmoteEventStream"; import {GameScene} from "./GameScene"; +import {RadialMenuItem} from "../Components/RadialMenu"; enum RegisteredEmoteTypes { short = 1, @@ -14,10 +15,11 @@ interface RegisteredEmote extends BodyResourceDescriptionInterface { type: RegisteredEmoteTypes } +//the last 3 emotes are courtesy of @tabascoeye export const emotes: {[key: string]: RegisteredEmote} = { - 'emote-exclamation': {name: 'emote-exclamation', img: 'resources/emotes/pipo-popupemotes001.png', type: RegisteredEmoteTypes.short}, + 'emote-exclamation': {name: 'emote-exclamation', img: 'resources/emotes/pipo-popupemotes001.png', type: RegisteredEmoteTypes.short, }, 'emote-interrogation': {name: 'emote-interrogation', img: 'resources/emotes/pipo-popupemotes002.png', type: RegisteredEmoteTypes.short}, - 'emote-sleep': {name: 'emote-sleep', img: 'resources/emotes/pipo-popupemotes002.png', type: RegisteredEmoteTypes.short}, + 'emote-sleep': {name: 'emote-sleep', img: 'resources/emotes/pipo-popupemotes021.png', type: RegisteredEmoteTypes.short}, 'emote-clap': {name: 'emote-clap', img: 'resources/emotes/taba-clap-emote.png', type: RegisteredEmoteTypes.short}, 'emote-thumbsdown': {name: 'emote-thumbsdown', img: 'resources/emotes/taba-thumbsdown-emote.png', type: RegisteredEmoteTypes.long}, 'emote-thumbsup': {name: 'emote-thumbsup', img: 'resources/emotes/taba-thumbsup-emote.png', type: RegisteredEmoteTypes.long}, @@ -30,16 +32,6 @@ export const getEmoteAnimName = (emoteKey: string): string => { export class EmoteManager { constructor(private scene: GameScene) { - - //todo: use a radial menu instead? - this.registerEmoteOnKey('keyup-Y', 'emote-clap'); - this.registerEmoteOnKey('keyup-U', 'emote-thumbsup'); - this.registerEmoteOnKey('keyup-I', 'emote-thumbsdown'); - this.registerEmoteOnKey('keyup-O', 'emote-exclamation'); - this.registerEmoteOnKey('keyup-P', 'emote-interrogation'); - this.registerEmoteOnKey('keyup-T', 'emote-sleep'); - - emoteEventStream.stream.subscribe((event) => { const actor = this.scene.MapPlayersByKey.get(event.userId); if (actor) { @@ -49,16 +41,7 @@ export class EmoteManager { } }) } - - private registerEmoteOnKey(keyboardKey: string, emoteKey: string) { - this.scene.input.keyboard.on(keyboardKey, () => { - this.scene.connection?.emitEmoteEvent(emoteKey); - this.lazyLoadEmoteTexture(emoteKey).then(emoteKey => { - this.scene.CurrentPlayer.playEmote(emoteKey); - }) - }); - } - + lazyLoadEmoteTexture(textureKey: string): Promise { const emoteDescriptor = emotes[textureKey]; if (emoteDescriptor === undefined) { @@ -70,6 +53,9 @@ export class EmoteManager { }); this.scene.load.start(); return loadPromise.then(() => { + if (this.scene.anims.exists(getEmoteAnimName(textureKey))) { + return Promise.resolve(textureKey); + } const frameConfig = emoteDescriptor.type === RegisteredEmoteTypes.short ? {frames: [0,1,2]} : {frames : [0,1,2,3,4,5,6,7]}; this.scene.anims.create({ key: getEmoteAnimName(textureKey), @@ -80,4 +66,20 @@ export class EmoteManager { return textureKey; }); } + + getMenuImages(): Promise { + const promises = []; + for (const key in emotes) { + const promise = this.lazyLoadEmoteTexture(key).then((textureKey) => { + const emoteDescriptor = emotes[textureKey]; + return { + sprite: textureKey, + name: textureKey, + frame: emoteDescriptor.type === RegisteredEmoteTypes.short ? 1 : 4, + } + }); + promises.push(promise); + } + return Promise.all(promises); + } } \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d7b635c0..b6b3e57e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -9,7 +9,7 @@ import type { PositionInterface, RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; -import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; +import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, @@ -90,6 +90,7 @@ import {TextUtils} from "../Components/TextUtils"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes"; import {waScaleManager} from "../Services/WaScaleManager"; import {EmoteManager} from "./EmoteManager"; @@ -132,7 +133,7 @@ const defaultStartLayerName = 'start'; export class GameScene extends DirtyScene implements CenterListener { Terrains : Array; - CurrentPlayer!: CurrentGamerInterface; + CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayersByKey : Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; @@ -428,7 +429,7 @@ export class GameScene extends DirtyScene implements CenterListener { } } if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { - depth = 10000; + depth = DEPTH_OVERLAY_INDEX; } if (layer.type === 'objectgroup') { for (const object of layer.objects) { @@ -1132,6 +1133,12 @@ ${escapedMessage} this.companion, this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined ); + this.CurrentPlayer.on('pointerdown', () => { + this.emoteManager.getMenuImages().then((emoteMenuElements) => this.CurrentPlayer.openOrCloseEmoteMenu(emoteMenuElements)) + }) + this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => { + this.connection?.emitEmoteEvent(emoteKey); + }) }catch (err){ if(err instanceof TextureError) { gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 6044ba84..e93b25c7 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -2,17 +2,15 @@ import {PlayerAnimationDirections} from "./Animation"; import type {GameScene} from "../Game/GameScene"; import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; import {Character} from "../Entity/Character"; +import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu"; export const hasMovedEventName = "hasMoved"; -export interface CurrentGamerInterface extends Character{ - moveUser(delta: number) : void; - say(text : string) : void; - isMoving(): boolean; -} +export const requestEmoteEventName = "requestEmote"; -export class Player extends Character implements CurrentGamerInterface { +export class Player extends Character { private previousDirection: string = PlayerAnimationDirections.Down; private wasMoving: boolean = false; + private emoteMenu: RadialMenu|null = null; constructor( Scene: GameScene, @@ -26,14 +24,10 @@ export class Player extends Character implements CurrentGamerInterface { companion: string|null, companionTexturePromise?: Promise ) { - super(Scene, x, y, texturesPromise, name, direction, moving, 1); + super(Scene, x, y, texturesPromise, name, direction, moving, 1, companion, companionTexturePromise); //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 { @@ -88,4 +82,33 @@ export class Player extends Character implements CurrentGamerInterface { public isMoving(): boolean { return this.wasMoving; } + + openOrCloseEmoteMenu(emotes:RadialMenuItem[]) { + if(this.emoteMenu) { + this.closeEmoteMenu(); + } else { + this.openEmoteMenu(emotes); + } + } + + isClickable(): boolean { + return true; + } + + openEmoteMenu(emotes:RadialMenuItem[]): void { + this.cancelPreviousEmote(); + this.emoteMenu = new RadialMenu(this.scene, 0, 0, emotes) + this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => { + this.closeEmoteMenu(); + this.emit(requestEmoteEventName, item.name); + this.playEmote(item.name); + }) + this.add(this.emoteMenu); + } + + closeEmoteMenu(): void { + if (!this.emoteMenu) return; + this.emoteMenu.destroy(); + this.emoteMenu = null; + } } diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 78bbe330..3bf8467a 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -74,6 +74,7 @@ export class SocketManager implements ZoneEventListener { client.adminConnection = adminRoomStream; adminRoomStream.on('data', (message: ServerToAdminClientMessage) => { + if (message.hasUserjoinedroom()) { const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; if (!client.disconnecting) { @@ -331,6 +332,7 @@ export class SocketManager implements ZoneEventListener { const room: PusherRoom | undefined = this.rooms.get(socket.roomId); if (room) { debug('Leaving room %s.', socket.roomId); + room.leave(socket); if (room.isEmpty()) { this.rooms.delete(socket.roomId);