merge latest dev

This commit is contained in:
_Bastler 2021-05-26 19:14:18 +02:00
commit 7bdbc191b5
72 changed files with 2036 additions and 651 deletions

View File

@ -8,12 +8,17 @@
### Updates ### 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 - 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 - 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 - Mouse wheel support to zoom in / out
- Pinch support on mobile to zoom in / out - Pinch support on mobile to zoom in / out
- Improved virtual joystick size (adapts to the zoom level) - Improved virtual joystick size (adapts to the zoom level)
- New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes ### Bug Fixes

View File

@ -2,12 +2,12 @@ import {PointInterface} from "./Websocket/PointInterface";
import {Group} from "./Group"; import {Group} from "./Group";
import {User, UserSocket} from "./User"; import {User, UserSocket} from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import {PositionInterface} from "_Model/PositionInterface";
import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
import {PositionNotifier} from "./PositionNotifier"; import {PositionNotifier} from "./PositionNotifier";
import {Movable} from "_Model/Movable"; import {Movable} from "_Model/Movable";
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
import {arrayIntersect} from "../Services/ArrayHelper"; import {arrayIntersect} from "../Services/ArrayHelper";
import {JoinRoomMessage} from "../Messages/generated/messages_pb"; import {EmoteEventMessage, JoinRoomMessage} from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {ZoneSocket} from "src/RoomManager"; import {ZoneSocket} from "src/RoomManager";
import {Admin} from "../Model/Admin"; import {Admin} from "../Model/Admin";
@ -51,8 +51,9 @@ export class GameRoom {
groupRadius: number, groupRadius: number,
onEnters: EntersCallback, onEnters: EntersCallback,
onMoves: MovesCallback, onMoves: MovesCallback,
onLeaves: LeavesCallback) onLeaves: LeavesCallback,
{ onEmote: EmoteCallback,
) {
this.roomId = roomId; this.roomId = roomId;
if (isRoomAnonymous(roomId)) { if (isRoomAnonymous(roomId)) {
@ -74,7 +75,7 @@ export class GameRoom {
this.minDistance = minDistance; this.minDistance = minDistance;
this.groupRadius = groupRadius; this.groupRadius = groupRadius;
// A zone is 10 sprites wide. // A zone is 10 sprites wide.
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves); this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
} }
public getGroups(): Group[] { public getGroups(): Group[] {
@ -325,4 +326,8 @@ export class GameRoom {
this.versionNumber++ this.versionNumber++
return this.versionNumber; return this.versionNumber;
} }
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
}
} }

View File

@ -8,10 +8,12 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player. * number of players around the current player.
*/ */
import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
import {Movable} from "_Model/Movable"; import {Movable} from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface"; import {PositionInterface} from "_Model/PositionInterface";
import {ZoneSocket} from "../RoomManager"; import {ZoneSocket} from "../RoomManager";
import {User} from "_Model/User";
import {EmoteEventMessage} from "../Messages/generated/messages_pb";
interface ZoneDescriptor { interface ZoneDescriptor {
i: number; i: number;
@ -24,7 +26,7 @@ export class PositionNotifier {
private zones: Zone[][] = []; private zones: Zone[][] = [];
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) { constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) {
} }
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
@ -77,7 +79,7 @@ export class PositionNotifier {
let zone = this.zones[j][i]; let zone = this.zones[j][i];
if (zone === undefined) { if (zone === undefined) {
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j); zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j);
this.zones[j][i] = zone; this.zones[j][i] = zone;
} }
return zone; return zone;
@ -93,4 +95,11 @@ export class PositionNotifier {
const zone = this.getZone(x, y); const zone = this.getZone(x, y);
zone.removeListener(call); zone.removeListener(call);
} }
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
zone.emitEmoteEvent(emoteEventMessage);
}
} }

View File

@ -3,21 +3,19 @@ import {PositionInterface} from "_Model/PositionInterface";
import {Movable} from "./Movable"; import {Movable} from "./Movable";
import {Group} from "./Group"; import {Group} from "./Group";
import {ZoneSocket} from "../RoomManager"; import {ZoneSocket} from "../RoomManager";
import {EmoteEventMessage} from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void; export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void;
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void; export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void;
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export class Zone { export class Zone {
private things: Set<Movable> = new Set<Movable>(); private things: Set<Movable> = new Set<Movable>();
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>(); private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
/**
* @param x For debugging purpose only constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { }
* @param y For debugging purpose only
*/
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, public readonly x: number, public readonly y: number) {
}
/** /**
* A user/thing leaves the zone * A user/thing leaves the zone
@ -41,9 +39,7 @@ export class Zone {
*/ */
private notifyLeft(thing: Movable, newZone: Zone|null) { private notifyLeft(thing: Movable, newZone: Zone|null) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
//if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) { this.onLeaves(thing, newZone, listener);
this.onLeaves(thing, newZone, listener);
//}
} }
} }
@ -57,15 +53,6 @@ export class Zone {
*/ */
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
/*if (listener === thing) {
continue;
}
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
this.onEnters(thing, listener);
} else {
this.onMoves(thing, position, listener);
}*/
this.onEnters(thing, oldZone, listener); this.onEnters(thing, oldZone, listener);
} }
} }
@ -85,28 +72,6 @@ export class Zone {
} }
} }
/*public startListening(listener: User): void {
for (const thing of this.things) {
if (thing !== listener) {
this.onEnters(thing, listener);
}
}
this.listeners.add(listener);
listener.listenedZones.add(this);
}
public stopListening(listener: User): void {
for (const thing of this.things) {
if (thing !== listener) {
this.onLeaves(thing, listener);
}
}
this.listeners.delete(listener);
listener.listenedZones.delete(this);
}*/
public getThings(): Set<Movable> { public getThings(): Set<Movable> {
return this.things; return this.things;
} }
@ -119,4 +84,11 @@ export class Zone {
public removeListener(socket: ZoneSocket): void { public removeListener(socket: ZoneSocket): void {
this.listeners.delete(socket); this.listeners.delete(socket);
} }
public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) {
for (const listener of this.listeners) {
this.onEmote(emoteEventMessage, listener);
}
}
} }

View File

@ -5,6 +5,7 @@ import {
AdminPusherToBackMessage, AdminPusherToBackMessage,
AdminRoomMessage, AdminRoomMessage,
BanMessage, BanMessage,
EmotePromptMessage,
EmptyMessage, EmptyMessage,
ItemEventMessage, ItemEventMessage,
JoinRoomMessage, JoinRoomMessage,
@ -71,6 +72,8 @@ const roomManager: IRoomManagerServer = {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasQueryjitsijwtmessage()){ } else if (message.hasQueryjitsijwtmessage()){
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
} else if (message.hasEmotepromptmessage()){
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage);
}else if (message.hasSendusermessage()) { }else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) { if(sendUserMessage !== undefined) {

View File

@ -26,7 +26,8 @@ import {
GroupLeftZoneMessage, GroupLeftZoneMessage,
WorldFullWarningMessage, WorldFullWarningMessage,
UserLeftZoneMessage, UserLeftZoneMessage,
BanUserMessage, RefreshRoomMessage, EmoteEventMessage,
BanUserMessage, RefreshRoomMessage, EmotePromptMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User"; import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
@ -67,6 +68,7 @@ export class SocketManager {
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>(); private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
constructor() { constructor() {
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
gaugeManager.incNbClientPerRoomGauge(roomId); gaugeManager.incNbClientPerRoomGauge(roomId);
}); });
@ -263,7 +265,8 @@ export class SocketManager {
GROUP_RADIUS, GROUP_RADIUS,
(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener),
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), (thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener),
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener) (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener),
(emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener),
); );
gaugeManager.incNbRoomGauge(); gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world); this.rooms.set(roomId, world);
@ -339,6 +342,14 @@ export class SocketManager {
} }
} }
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
const subMessage = new SubToPusherMessage();
subMessage.setEmoteeventmessage(emoteEventMessage);
emitZoneMessage(subMessage, client);
}
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void { private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void {
const position = group.getPosition(); const position = group.getPosition();
const pointMessage = new PointMessage(); const pointMessage = new PointMessage();
@ -751,6 +762,13 @@ export class SocketManager {
recipient.socket.write(clientMessage); recipient.socket.write(clientMessage);
}); });
} }
handleEmoteEventMessage(room: GameRoom, user: User, emotePromptMessage: EmotePromptMessage) {
const emoteEventMessage = new EmoteEventMessage();
emoteEventMessage.setEmote(emotePromptMessage.getEmote());
emoteEventMessage.setActoruserid(user.id);
room.emitEmoteEvent(user, emoteEventMessage);
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

View File

@ -5,6 +5,7 @@ import {Group} from "../src/Model/Group";
import {User, UserSocket} from "_Model/User"; import {User, UserSocket} from "_Model/User";
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb"; import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {EmoteCallback} from "_Model/Zone";
function createMockUser(userId: number): User { function createMockUser(userId: number): User {
return { return {
@ -33,6 +34,8 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
return joinRoomMessage; return joinRoomMessage;
} }
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
describe("GameRoom", () => { describe("GameRoom", () => {
it("should connect user1 and user2", () => { it("should connect user1 and user2", () => {
let connectCalledNumber: number = 0; let connectCalledNumber: number = 0;
@ -43,7 +46,8 @@ describe("GameRoom", () => {
} }
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
@ -72,7 +76,7 @@ describe("GameRoom", () => {
} }
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100)); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
@ -101,7 +105,7 @@ describe("GameRoom", () => {
disconnectCallNumber++; disconnectCallNumber++;
} }
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100)); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));

View File

@ -23,7 +23,7 @@ describe("PositionNotifier", () => {
moveTriggered = true; moveTriggered = true;
}, (thing: Movable) => { }, (thing: Movable) => {
leaveTriggered = true; leaveTriggered = true;
}); }, () => {});
const user1 = new User(1, 'test', '10.0.0.2', { const user1 = new User(1, 'test', '10.0.0.2', {
x: 500, x: 500,
@ -98,7 +98,7 @@ describe("PositionNotifier", () => {
moveTriggered = true; moveTriggered = true;
}, (thing: Movable) => { }, (thing: Movable) => {
leaveTriggered = true; leaveTriggered = true;
}); }, () => {});
const user1 = new User(1, 'test', '10.0.0.2', { const user1 = new User(1, 'test', '10.0.0.2', {
x: 500, x: 500,

View File

@ -1251,9 +1251,9 @@ has-values@^1.0.0:
kind-of "^4.0.0" kind-of "^4.0.0"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.8" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-errors@1.7.2: http-errors@1.7.2:
version "1.7.2" version "1.7.2"

View File

@ -230,9 +230,9 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.8", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
}, },
"indent-string": { "indent-string": {
"version": "2.1.0", "version": "2.1.0",

View File

@ -169,8 +169,8 @@ graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.8" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
indent-string@^2.1.0: indent-string@^2.1.0:
version "2.1.0" version "2.1.0"

View File

@ -1,90 +1,90 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<head> <!-- TRACK CODE -->
<meta charset="UTF-8"> <!-- END TRACK CODE -->
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- TRACK CODE --> <link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<!-- END TRACK CODE --> <link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicons/favicon-16x16.png">
<link rel="manifest" href="static/images/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicons/favicon-16x16.png">
<link rel="manifest" href="static/images/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<link href="/static/style/font.css" rel="stylesheet"> <link href="/static/style/font.css" rel="stylesheet">
<base href="/"> <base href="/">
<title>Partey</title> <title>Partey</title>
</head> </head>
<body id="body" style="margin: 0; background-color: #000">
<body id="body" style="margin: 0; background-color: #000"> <div class="main-container" id="main-container">
<div class="main-container" id="main-container"> <!-- Create the editor container -->
<!-- Create the editor container --> <div id="game" class="game">
<div id="game" class="game"> <div id="svelte-overlay">
<div id="svelte-overlay"></div>
<div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section">
</div> </div>
<aside id="sidebar" class="sidebar"> <div id="game-overlay" class="game-overlay">
</aside> <div id="main-section" class="main-section">
<div id="chat-mode" class="chat-mode three-col" style="display: none;"> </div>
</div> <aside id="sidebar" class="sidebar">
<div id="activeCam" class="activeCam"> </aside>
<div id="div-myCamVideo" class="video-container nes-container is-rounded is-dark"> <div id="chat-mode" class="chat-mode three-col" style="display: none;">
<video id="myCamVideo" autoplay muted></video> </div>
<div id="mySoundMeter" class="sound-progress"> <div id="activeCam" class="activeCam">
<span></span> <div id="div-myCamVideo" class="video-container nes-container is-rounded is-dark">
<span></span> <video id="myCamVideo" autoplay muted></video>
<span></span> <div id="mySoundMeter" class="sound-progress">
<span></span> <span></span>
<span></span> <span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
<div class="btn-cam-action">
<div id="btn-micro" class="btn-micro nes-btn is-dark">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div id="btn-video" class="btn-video nes-btn is-dark">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div id="btn-monitor" class="btn-monitor nes-btn is-dark">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div> </div>
</div> </div>
</div> </div>
<div class="btn-cam-action">
<div id="btn-micro" class="btn-micro nes-btn is-dark">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div id="btn-video" class="btn-video nes-btn is-dark">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div id="btn-monitor" class="btn-monitor nes-btn is-dark">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div>
</div>
</div> </div>
</div> <div id="cowebsite" class="cowebsite hidden">
<div id="cowebsite" class="cowebsite hidden"> <aside id="cowebsite-aside">
<aside id="cowebsite-aside"> <img src="/static/images/menu.svg" alt="hold to resize"/>
<img src="/static/images/menu.svg" alt="hold to resize" /> </aside>
</aside> <main id="cowebsite-main">
<main id="cowebsite-main"> </main>
</main> <button class="top-right-btn" id="cowebsite-fullscreen" alt="fullscreen mode">
<button class="top-right-btn" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/> <img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/> <img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
</button> </button>
<button class="top-right-btn" id="cowebsite-close" alt="close the iframe"> <button class="top-right-btn" id="cowebsite-close" alt="close the iframe">
<img src="resources/logos/close.svg"/> <img src="resources/logos/close.svg"/>
</button> </button>
<button class="top-right-btn" id="cowebsite-focus" alt="focus the iframe"> <button class="top-right-btn" id="cowebsite-focus" alt="focus the iframe">
@ -93,9 +93,9 @@
</button> </button>
</div> </div>
<div id="audioplayerctrl" class="hidden"> <div id="audioplayerctrl" class="hidden">
<div class="audioplayer"> <div class="audioplayer">
<button type="button" id="audioplayer_mute" class="fa fa-volump-up"> <button type="button" id="audioplayer_mute" class="fa fa-volump-up">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" /> <path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<g id="audioplayer_volume_icon_playing"> <g id="audioplayer_volume_icon_playing">
@ -111,39 +111,38 @@
</g> </g>
</svg> </svg>
</button> </button>
<div class="audioplayer"> <div class="audioplayer">
<input type="range" id="audioplayer_volume" min="0" max="1" step="0.025" value="1" /> <input type="range" id="audioplayer_volume" min="0" max="1" step="0.025" value="1" />
</div>
</div> </div>
</div> <div class="audioplayer">
<div class="audioplayer"> <label id="label-audioplayer_decrease_while_talking" for="audioplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
<label id="label-audioplayer_decrease_while_talking" for="audiooplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
reduce in conversations reduce in conversations
<input type="checkbox" id="audioplayer_decrease_while_talking" checked /> <input type="checkbox" id="audioplayer_decrease_while_talking" checked />
</label> </label>
<div id="audioplayer" style="visibility: hidden"></div> <div id="audioplayer" style="visibility: hidden"></div>
</div>
</div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg" />
</div> </div>
</div> </div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg" />
</div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active"> <div id="activeScreenSharing" class="active-screen-sharing active">
</div> </div>
<div id="webRtcSetup" class="webrtcsetup"> <div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg"> <img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video> <video id="myCamVideoSetup" autoplay muted></video>
</div> </div>
<audio id="audio-webrtc-in"> <audio id="audio-webrtc-in">
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3"> <source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
</audio> </audio>
<audio id="audio-webrtc-out"> <audio id="audio-webrtc-out">
<source src="/resources/objects/webrtc-out.mp3" type="audio/mp3"> <source src="/resources/objects/webrtc-out.mp3" type="audio/mp3">
</audio> </audio>
<audio id="report-message"> <audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3"> <source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio> </audio>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -10,6 +10,8 @@ import type { OpenPopupEvent } from './OpenPopupEvent';
import type { OpenTabEvent } from './OpenTabEvent'; import type { OpenTabEvent } from './OpenTabEvent';
import type { UserInputChatEvent } from './UserInputChatEvent'; import type { UserInputChatEvent } from './UserInputChatEvent';
import type { ExitUrlEvent } from './ExitUrlEvent'; import type { ExitUrlEvent } from './ExitUrlEvent';
import type {LoadSoundEvent} from "./LoadSoundEvent";
import type {PlaySoundEvent} from "./PlaySoundEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
@ -32,6 +34,9 @@ export type IframeEventMap = {
removeBubble: null removeBubble: null
exitUrl : ExitUrlEvent exitUrl : ExitUrlEvent
closeChatMessage : null closeChatMessage : null
loadSound: LoadSoundEvent
playSound: PlaySoundEvent
stopSound: null
} }
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isLoadSoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>;

View File

@ -0,0 +1,24 @@
import * as tg from "generic-type-guard";
const isSoundConfig =
new tg.IsInterface().withProperties({
volume: tg.isOptional(tg.isNumber),
loop: tg.isOptional(tg.isBoolean),
mute: tg.isOptional(tg.isBoolean),
rate: tg.isOptional(tg.isNumber),
detune: tg.isOptional(tg.isNumber),
seek: tg.isOptional(tg.isNumber),
delay: tg.isOptional(tg.isNumber)
}).get();
export const isPlaySoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
config : tg.isOptional(isSoundConfig),
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type PlaySoundEvent = tg.GuardedType<typeof isPlaySoundEvent>;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isStopSoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type StopSoundEvent = tg.GuardedType<typeof isStopSoundEvent>;

View File

@ -1,3 +1,4 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
@ -12,8 +13,9 @@ import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent";
import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
import { isExitUrlEvent } from './Events/ExitUrlEvent'; import { isExitUrlEvent } from './Events/ExitUrlEvent';
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
@ -58,6 +60,15 @@ class IframeListener {
private readonly _exitUrlStream: Subject<string> = new Subject(); private readonly _exitUrlStream: Subject<string> = new Subject();
public readonly exitUrlStream = this._exitUrlStream.asObservable(); public readonly exitUrlStream = this._exitUrlStream.asObservable();
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable();
private readonly _stopSoundStream: Subject<StopSoundEvent> = new Subject();
public readonly stopSoundStream = this._stopSoundStream.asObservable();
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
@ -92,6 +103,15 @@ class IframeListener {
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url); scriptUtils.goToPage(payload.data.url);
} }
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
const scriptUrl = [...this.scripts.keys()].find(key => { const scriptUrl = [...this.scripts.keys()].find(key => {
return this.scripts.get(key)?.contentWindow == message.source return this.scripts.get(key)?.contentWindow == message.source

View File

@ -0,0 +1,19 @@
import {Subject} from "rxjs";
interface EmoteEvent {
userId: number,
emoteName: string,
}
class EmoteEventStream {
private _stream:Subject<EmoteEvent> = new Subject();
public stream = this._stream.asObservable();
fire(userId: number, emoteName:string) {
this._stream.next({userId, emoteName});
}
}
export const emoteEventStream = new EmoteEventStream();

View File

@ -27,6 +27,8 @@ import {
SendJitsiJwtMessage, SendJitsiJwtMessage,
CharacterLayerMessage, CharacterLayerMessage,
PingMessage, PingMessage,
EmoteEventMessage,
EmotePromptMessage,
SendUserMessage, SendUserMessage,
BanUserMessage BanUserMessage
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb"
@ -47,6 +49,7 @@ import {adminMessagesService} from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream"; import {worldFullMessageStream} from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream"; import {worldFullWarningStream} from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager"; import {connectionManager} from "./ConnectionManager";
import {emoteEventStream} from "./EmoteEventStream";
const manualPingDelay = 20000; const manualPingDelay = 20000;
@ -124,7 +127,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) { if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) { for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
let event: string; let event: string|null = null;
let payload; let payload;
if (subMessage.hasUsermovedmessage()) { if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED; event = EventMessage.USER_MOVED;
@ -144,11 +147,16 @@ export class RoomConnection implements RoomConnection {
} else if (subMessage.hasItemeventmessage()) { } else if (subMessage.hasItemeventmessage()) {
event = EventMessage.ITEM_EVENT; event = EventMessage.ITEM_EVENT;
payload = subMessage.getItemeventmessage(); payload = subMessage.getItemeventmessage();
} else if (subMessage.hasEmoteeventmessage()) {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else { } else {
throw new Error('Unexpected batch message type'); throw new Error('Unexpected batch message type');
} }
this.dispatch(event, payload); if (event) {
this.dispatch(event, payload);
}
} }
} else if (message.hasRoomjoinedmessage()) { } else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
@ -599,4 +607,14 @@ export class RoomConnection implements RoomConnection {
public isAdmin(): boolean { public isAdmin(): boolean {
return this.hasTag('admin'); return this.hasTag('admin');
} }
public emitEmoteEvent(emoteName: string): void {
const emoteMessage = new EmotePromptMessage();
emoteMessage.setEmote(emoteName)
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setEmotepromptmessage(emoteMessage);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
} }

View File

@ -1,3 +1,5 @@
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
export class ChatModeIcon extends Phaser.GameObjects.Sprite { export class ChatModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 3); super(scene, x, y, 'layout_modes', 3);
@ -6,6 +8,6 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite {
this.setOrigin(0, 1); this.setOrigin(0, 1);
this.setInteractive(); this.setInteractive();
this.setVisible(false); this.setVisible(false);
this.setDepth(99999); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
} }
} }

View File

@ -1,5 +1,6 @@
import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js'; import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js';
import {waScaleManager} from "../Services/WaScaleManager"; 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 //the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free
export const joystickBaseKey = 'joystickBase'; export const joystickBaseKey = 'joystickBase';
@ -19,8 +20,8 @@ export class MobileJoystick extends VirtualJoystick {
x: -1000, x: -1000,
y: -1000, y: -1000,
radius: radius * window.devicePixelRatio, radius: radius * window.devicePixelRatio,
base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * 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(99999), thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
enable: true, enable: true,
dir: "8dir", dir: "8dir",
}); });

View File

@ -1,4 +1,5 @@
import {discussionManager} from "../../WebRtc/DiscussionManager"; import {discussionManager} from "../../WebRtc/DiscussionManager";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
export const openChatIconName = 'openChatIcon'; export const openChatIconName = 'openChatIcon';
export class OpenChatIcon extends Phaser.GameObjects.Image { export class OpenChatIcon extends Phaser.GameObjects.Image {
@ -9,7 +10,7 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
this.setOrigin(0, 1); this.setOrigin(0, 1);
this.setInteractive(); this.setInteractive();
this.setVisible(false); this.setVisible(false);
this.setDepth(99999); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
this.on("pointerup", () => discussionManager.showDiscussionPart()); this.on("pointerup", () => discussionManager.showDiscussionPart());
} }

View File

@ -1,3 +1,5 @@
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
export class PresentationModeIcon extends Phaser.GameObjects.Sprite { export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 0); super(scene, x, y, 'layout_modes', 0);
@ -6,6 +8,6 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
this.setOrigin(0, 1); this.setOrigin(0, 1);
this.setInteractive(); this.setInteractive();
this.setVisible(false); this.setVisible(false);
this.setDepth(99999); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
} }
} }

View File

@ -0,0 +1,74 @@
import Sprite = Phaser.GameObjects.Sprite;
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
export interface RadialMenuItem {
image: string,
name: string,
}
export const RadialMenuClickEvent = 'radialClick';
export class RadialMenu extends Phaser.GameObjects.Container {
private resizeCallback: OmitThisParameter<() => void>;
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();
this.resize();
this.resizeCallback = this.resize.bind(this);
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
}
private initItems() {
const itemsNumber = this.items.length;
const menuRadius = 70 + (waScaleManager.uiScalingFactor - 1) * 20;
this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber, menuRadius))
}
private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number, menuRadius: number) {
const image = new Sprite(this.scene, 0, menuRadius, item.image);
this.add(image);
this.scene.sys.updateList.add(image);
const scalingFactor = waScaleManager.uiScalingFactor * 0.075;
image.setScale(scalingFactor)
image.setInteractive({
useHandCursor: true,
});
image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item));
image.on('pointerover', () => {
this.scene.tweens.add({
targets: image,
props: {
scale: 2 * scalingFactor,
},
duration: 500,
ease: 'Power3',
})
});
image.on('pointerout', () => {
this.scene.tweens.add({
targets: image,
props: {
scale: scalingFactor,
},
duration: 500,
ease: 'Power3',
})
});
const angle = 2 * Math.PI * index / itemsNumber;
Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius);
}
private resize() {
this.setScale(waScaleManager.uiScalingFactor);
}
public destroy() {
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
super.destroy();
}
}

View File

@ -5,6 +5,11 @@ import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import {TextureError} from "../../Exception/TextureError"; import {TextureError} from "../../Exception/TextureError";
import {Companion} from "../Companion/Companion"; import {Companion} from "../Companion/Companion";
import type {GameScene} from "../Game/GameScene";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
const playerNameY = - 25;
interface AnimationData { interface AnimationData {
key: string; key: string;
@ -14,6 +19,8 @@ interface AnimationData {
frames : number[] frames : number[]
} }
const interactiveRadius = 35;
export abstract class Character extends Container { export abstract class Character extends Container {
private bubble: SpeechBubble|null = null; private bubble: SpeechBubble|null = null;
private readonly playerName: BitmapText; private readonly playerName: BitmapText;
@ -23,15 +30,19 @@ export abstract class Character extends Container {
//private teleportation: Sprite; //private teleportation: Sprite;
private invisible: boolean; private invisible: boolean;
public companion?: Companion; public companion?: Companion;
private emote: Phaser.GameObjects.Sprite | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null;
constructor(scene: Phaser.Scene, constructor(scene: GameScene,
x: number, x: number,
y: number, y: number,
texturesPromise: Promise<string[]>, texturesPromise: Promise<string[]>,
name: string, name: string,
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
frame?: string | number frame: string | number,
companion: string|null,
companionTexturePromise?: Promise<string>
) { ) {
super(scene, x, y/*, texture, frame*/); super(scene, x, y/*, texture, frame*/);
this.PlayerValue = name; this.PlayerValue = name;
@ -45,19 +56,18 @@ export abstract class Character extends Container {
this.invisible = false this.invisible = false
}) })
/*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3); this.playerName = new BitmapText(scene, 0, playerNameY, 'main_font', name, 7);
this.teleportation.setInteractive(); this.playerName.setOrigin(0.5).setCenterAlign().setDepth(DEPTH_INGAME_TEXT_INDEX);
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, - 25, 'main_font', name, 7);
this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999);
this.add(this.playerName); 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); scene.add.existing(this);
this.scene.physics.world.enableBody(this); this.scene.physics.world.enableBody(this);
@ -69,6 +79,10 @@ export abstract class Character extends Container {
this.setDepth(-1); this.setDepth(-1);
this.playAnimation(direction, moving); this.playAnimation(direction, moving);
if (typeof companion === 'string') {
this.addCompanion(companion, companionTexturePromise);
}
} }
public addCompanion(name: string, texturePromise?: Promise<string>): void { public addCompanion(name: string, texturePromise?: Promise<string>): void {
@ -77,13 +91,14 @@ export abstract class Character extends Container {
} }
} }
public abstract isClickable(): boolean;
public addTextures(textures: string[], frame?: string | number): void { public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) { for (const texture of textures) {
if(!this.scene.textures.exists(texture)){ if(!this.scene.textures.exists(texture)){
throw new TextureError('texture not found'); throw new TextureError('texture not found');
} }
const sprite = new Sprite(this.scene, 0, 0, texture, frame); const sprite = new Sprite(this.scene, 0, 0, texture, frame);
sprite.setInteractive({useHandCursor: true});
this.add(sprite); this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => { this.getPlayerAnimations(texture).forEach(d => {
this.scene.anims.create({ this.scene.anims.create({
@ -225,7 +240,84 @@ export abstract class Character extends Container {
this.scene.sys.updateList.remove(sprite); this.scene.sys.updateList.remove(sprite);
} }
} }
this.list.forEach(objectContaining => objectContaining.destroy())
super.destroy(); super.destroy();
this.playerName.destroy(); }
playEmote(emoteKey: string) {
this.cancelPreviousEmote();
const scalingFactor = waScaleManager.uiScalingFactor * 0.05;
const emoteY = -30 - scalingFactor * 10;
this.playerName.setVisible(false);
this.emote = new Sprite(this.scene, 0, 0, emoteKey);
this.emote.setAlpha(0);
this.emote.setScale(0.1 * scalingFactor);
this.add(this.emote);
this.scene.sys.updateList.add(this.emote);
this.createStartTransition(scalingFactor, emoteY);
}
private createStartTransition(scalingFactor: number, emoteY: number) {
this.emoteTween = this.scene.tweens.add({
targets: this.emote,
props: {
scale: scalingFactor,
alpha: 1,
y: emoteY,
},
ease: 'Power2',
duration: 500,
onComplete: () => {
this.startPulseTransition(emoteY, scalingFactor);
}
});
}
private startPulseTransition(emoteY: number, scalingFactor: number) {
this.emoteTween = this.scene.tweens.add({
targets: this.emote,
props: {
y: emoteY * 1.3,
scale: scalingFactor * 1.1
},
duration: 250,
yoyo: true,
repeat: 1,
completeDelay: 200,
onComplete: () => {
this.startExitTransition(emoteY);
}
});
}
private startExitTransition(emoteY: number) {
this.emoteTween = this.scene.tweens.add({
targets: this.emote,
props: {
alpha: 0,
y: 2 * emoteY,
},
ease: 'Power2',
duration: 500,
onComplete: () => {
this.destroyEmote();
}
});
}
cancelPreviousEmote() {
if (!this.emote) return;
this.emoteTween?.remove();
this.destroyEmote()
}
private destroyEmote() {
this.emote?.destroy();
this.emote = null;
this.playerName.setVisible(true);
} }
} }

View File

@ -2,6 +2,10 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type {CharacterTexture} from "../../Connexion/LocalUser"; import type {CharacterTexture} from "../../Connexion/LocalUser";
import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures"; import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures";
export interface FrameConfig {
frameWidth: number,
frameHeight: number,
}
export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => { export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => {
const returnArray:BodyResourceDescriptionInterface[][] = []; const returnArray:BodyResourceDescriptionInterface[][] = [];
@ -26,7 +30,10 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => { export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => {
const name = 'customCharacterTexture'+texture.id; const name = 'customCharacterTexture'+texture.id;
const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level} const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level}
return createLoadingPromise(loaderPlugin, playerResourceDescriptor); return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
frameWidth: 32,
frameHeight: 32
});
} }
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => { export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
@ -36,7 +43,10 @@ export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, textur
//TODO refactor //TODO refactor
const playerResourceDescriptor = getRessourceDescriptor(textureKey); const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor)); promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, {
frameWidth: 32,
frameHeight: 32
}));
} }
}catch (err){ }catch (err){
console.error(err); console.error(err);
@ -69,15 +79,12 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio
throw 'Could not find a data for texture '+textureName; throw 'Could not find a data for texture '+textureName;
} }
const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) => { export const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface, frameConfig: FrameConfig) => {
return new Promise<BodyResourceDescriptionInterface>((res) => { return new Promise<BodyResourceDescriptionInterface>((res) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor); return res(playerResourceDescriptor);
} }
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, { loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
frameWidth: 32,
frameHeight: 32
});
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor)); loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor));
}); });
} }

View File

@ -21,14 +21,10 @@ export class RemotePlayer extends Character {
companion: string|null, companion: string|null,
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1); super(Scene, x, y, texturesPromise, name, direction, moving, 1, companion, companionTexturePromise);
//set data //set data
this.userId = userId; this.userId = userId;
if (typeof companion === 'string') {
this.addCompanion(companion, companionTexturePromise);
}
} }
updatePosition(position: PointInterface): void { updatePosition(position: PointInterface): void {
@ -42,4 +38,8 @@ export class RemotePlayer extends Character {
this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections); 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.
}
} }

View File

@ -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;

View File

@ -12,6 +12,7 @@ export abstract class DirtyScene extends ResizableScene {
private isAlreadyTracking: boolean = false; private isAlreadyTracking: boolean = false;
protected dirty:boolean = true; protected dirty:boolean = true;
private objectListChanged:boolean = true; private objectListChanged:boolean = true;
private physicsEnabled: boolean = false;
/** /**
* Track all objects added to the scene and adds a callback each time an animation is added. * Track all objects added to the scene and adds a callback each time an animation is added.
@ -37,6 +38,27 @@ export abstract class DirtyScene extends ResizableScene {
this.events.on(Events.RENDER, () => { this.events.on(Events.RENDER, () => {
this.objectListChanged = false; this.objectListChanged = false;
}); });
this.physics.disableUpdate();
this.events.on(Events.POST_UPDATE, () => {
let objectMoving = false;
for (const body of this.physics.world.bodies.entries) {
if (body.velocity.x !== 0 || body.velocity.y !== 0) {
this.objectListChanged = true;
objectMoving = true;
if (!this.physicsEnabled) {
this.physics.enableUpdate();
this.physicsEnabled = true;
}
break;
}
}
if (!objectMoving && this.physicsEnabled) {
this.physics.disableUpdate();
this.physicsEnabled = false;
}
});
} }
private trackAnimation(): void { private trackAnimation(): void {

View File

@ -0,0 +1,73 @@
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {emoteEventStream} from "../../Connexion/EmoteEventStream";
import type {GameScene} from "./GameScene";
import type {RadialMenuItem} from "../Components/RadialMenu";
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type {Subscription} from "rxjs";
interface RegisteredEmote extends BodyResourceDescriptionInterface {
name: string;
img: string;
}
export const emotes: {[key: string]: RegisteredEmote} = {
'emote-heart': {name: 'emote-heart', img: 'resources/emotes/heart-emote.png'},
'emote-clap': {name: 'emote-clap', img: 'resources/emotes/clap-emote.png'},
'emote-hand': {name: 'emote-hand', img: 'resources/emotes/hand-emote.png'},
'emote-thanks': {name: 'emote-thanks', img: 'resources/emotes/thanks-emote.png'},
'emote-thumb-up': {name: 'emote-thumb-up', img: 'resources/emotes/thumb-up-emote.png'},
'emote-thumb-down': {name: 'emote-thumb-down', img: 'resources/emotes/thumb-down-emote.png'},
};
export class EmoteManager {
private subscription: Subscription;
constructor(private scene: GameScene) {
this.subscription = emoteEventStream.stream.subscribe((event) => {
const actor = this.scene.MapPlayersByKey.get(event.userId);
if (actor) {
this.lazyLoadEmoteTexture(event.emoteName).then(emoteKey => {
actor.playEmote(emoteKey);
})
}
})
}
createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) {
return new Promise<string>((res) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor.name);
}
loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img);
loadPlugin.once('filecomplete-image-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor.name));
});
}
lazyLoadEmoteTexture(textureKey: string): Promise<string> {
const emoteDescriptor = emotes[textureKey];
if (emoteDescriptor === undefined) {
throw 'Emote not found!';
}
const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor);
this.scene.load.start();
return loadPromise
}
getMenuImages(): Promise<RadialMenuItem[]> {
const promises = [];
for (const key in emotes) {
const promise = this.lazyLoadEmoteTexture(key).then((textureKey) => {
return {
image: textureKey,
name: textureKey,
}
});
promises.push(promise);
}
return Promise.all(promises);
}
destroy() {
this.subscription.unsubscribe();
}
}

View File

@ -9,7 +9,7 @@ import type {
PositionInterface, PositionInterface,
RoomJoinedMessageInterface RoomJoinedMessageInterface
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player";
import { import {
DEBUG_MODE, DEBUG_MODE,
JITSI_PRIVATE_MODE, JITSI_PRIVATE_MODE,
@ -52,6 +52,7 @@ import {mediaManager} from "../../WebRtc/MediaManager";
import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
import type {ActionableItem} from "../Items/ActionableItem"; import type {ActionableItem} from "../Items/ActionableItem";
import {UserInputManager} from "../UserInput/UserInputManager"; import {UserInputManager} from "../UserInput/UserInputManager";
import {soundManager} from "./SoundManager";
import type {UserMovedMessage} from "../../Messages/generated/messages_pb"; import type {UserMovedMessage} from "../../Messages/generated/messages_pb";
import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils";
import {connectionManager} from "../../Connexion/ConnectionManager"; import {connectionManager} from "../../Connexion/ConnectionManager";
@ -91,8 +92,11 @@ import {TextUtils} from "../Components/TextUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager"; import {waScaleManager} from "../Services/WaScaleManager";
import AnimatedTiles from "phaser-animated-tiles"; import AnimatedTiles from "phaser-animated-tiles";
import {peerStore} from "../../Stores/PeerStore";
import {EmoteManager} from "./EmoteManager";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface|null, initPosition: PointInterface|null,
@ -133,7 +137,7 @@ const defaultStartLayerName = 'start';
export class GameScene extends DirtyScene implements CenterListener { export class GameScene extends DirtyScene implements CenterListener {
Terrains : Array<Phaser.Tilemaps.Tileset>; Terrains : Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: CurrentGamerInterface; CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>(); MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap; Map!: Phaser.Tilemaps.Tilemap;
@ -189,9 +193,8 @@ export class GameScene extends DirtyScene implements CenterListener {
private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>(); private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
private originalMapUrl: string|undefined; private originalMapUrl: string|undefined;
private pinchManager: PinchManager|undefined; private pinchManager: PinchManager|undefined;
private physicsEnabled: boolean = true;
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private onVisibilityChangeCallback: () => void; private emoteManager!: EmoteManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
super({ super({
@ -211,7 +214,6 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => { this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
this.connectionAnswerPromiseResolve = resolve; this.connectionAnswerPromiseResolve = resolve;
}); });
this.onVisibilityChangeCallback = this.onVisibilityChange.bind(this);
} }
//hook preload scene //hook preload scene
@ -229,6 +231,11 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickBaseKey, joystickBaseImg);
this.load.image(joystickThumbKey, joystickThumbImg); this.load.image(joystickThumbKey, joystickThumbImg);
} }
//todo: in an emote manager.
this.load.spritesheet('emote-music', 'resources/emotes/pipo-popupemotes005.png', {
frameHeight: 32,
frameWidth: 32,
});
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
// If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments)
if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) {
@ -425,7 +432,7 @@ export class GameScene extends DirtyScene implements CenterListener {
} }
} }
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000; depth = DEPTH_OVERLAY_INDEX;
} }
if (layer.type === 'objectgroup') { if (layer.type === 'objectgroup') {
for (const object of layer.objects) { for (const object of layer.objects) {
@ -513,7 +520,7 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connect(); this.connect();
} }
document.addEventListener('visibilitychange', this.onVisibilityChangeCallback); this.emoteManager = new EmoteManager(this);
} }
/** /**
@ -618,6 +625,7 @@ export class GameScene extends DirtyScene implements CenterListener {
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
peerStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection); this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
@ -635,7 +643,6 @@ export class GameScene extends DirtyScene implements CenterListener {
self.chatModeSprite.setVisible(false); self.chatModeSprite.setVisible(false);
self.openChatIcon.setVisible(false); self.openChatIcon.setVisible(false);
audioManager.restoreVolume(); audioManager.restoreVolume();
self.onVisibilityChange();
} }
} }
}) })
@ -879,9 +886,28 @@ export class GameScene extends DirtyScene implements CenterListener {
this.userInputManager.disableControls(); this.userInputManager.disableControls();
})); }));
this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=>
{
const url = new URL(playSoundEvent.url, this.MapUrlFile);
soundManager.playSound(this.load,this.sound,url.toString(),playSoundEvent.config);
}))
this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=>
{
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
soundManager.stopSound(this.sound,url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=>
{
const url = new URL(loadSoundEvent.url, this.MapUrlFile);
soundManager.loadSound(this.load,this.sound,url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{
this.userInputManager.restoreControls(); this.userInputManager.restoreControls();
})); }));
let scriptedBubbleSprite : Sprite; let scriptedBubbleSprite : Sprite;
this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{
scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white');
@ -970,12 +996,11 @@ export class GameScene extends DirtyScene implements CenterListener {
this.messageSubscription?.unsubscribe(); this.messageSubscription?.unsubscribe();
this.userInputManager.destroy(); this.userInputManager.destroy();
this.pinchManager?.destroy(); this.pinchManager?.destroy();
this.emoteManager.destroy();
for(const iframeEvents of this.iframeSubscriptionList){ for(const iframeEvents of this.iframeSubscriptionList){
iframeEvents.unsubscribe(); iframeEvents.unsubscribe();
} }
document.removeEventListener('visibilitychange', this.onVisibilityChangeCallback);
} }
private removeAllRemotePlayers(): void { private removeAllRemotePlayers(): void {
@ -1128,8 +1153,6 @@ export class GameScene extends DirtyScene implements CenterListener {
} }
createCollisionWithPlayer() { createCollisionWithPlayer() {
this.physics.disableUpdate();
this.physicsEnabled = false;
//add collision layer //add collision layer
this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => {
this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => {
@ -1163,6 +1186,12 @@ export class GameScene extends DirtyScene implements CenterListener {
this.companion, this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined 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){ }catch (err){
if(err instanceof TextureError) { if(err instanceof TextureError) {
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
@ -1263,20 +1292,7 @@ export class GameScene extends DirtyScene implements CenterListener {
this.dirty = false; this.dirty = false;
mediaManager.updateScene(); mediaManager.updateScene();
this.currentTick = time; this.currentTick = time;
if (this.CurrentPlayer.isMoving()) {
this.dirty = true;
}
this.CurrentPlayer.moveUser(delta); this.CurrentPlayer.moveUser(delta);
if (this.CurrentPlayer.isMoving()) {
this.dirty = true;
if (!this.physicsEnabled) {
this.physics.enableUpdate();
this.physicsEnabled = true;
}
} else if (this.physicsEnabled) {
this.physics.disableUpdate();
this.physicsEnabled = false;
}
// Let's handle all events // Let's handle all events
while (this.pendingEvents.length !== 0) { while (this.pendingEvents.length !== 0) {
@ -1544,8 +1560,6 @@ export class GameScene extends DirtyScene implements CenterListener {
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => { mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
this.stopJitsi(); this.stopJitsi();
}); });
this.onVisibilityChange();
} }
public stopJitsi(): void { public stopJitsi(): void {
@ -1581,7 +1595,6 @@ export class GameScene extends DirtyScene implements CenterListener {
openJitsiRoomFunction(); openJitsiRoomFunction();
}, this.userInputManager); }, this.userInputManager);
} }
this.onVisibilityChange();
} }
//todo: put this into an 'orchestrator' scene (EntryScene?) //todo: put this into an 'orchestrator' scene (EntryScene?)
@ -1621,20 +1634,4 @@ export class GameScene extends DirtyScene implements CenterListener {
waScaleManager.zoomModifier *= zoomFactor; waScaleManager.zoomModifier *= zoomFactor;
this.updateCameraOffset(); this.updateCameraOffset();
} }
private onVisibilityChange(): void {
// If the overlay is not displayed, we are in Jitsi. We don't need the webcam.
if (!mediaManager.isGameOverlayVisible()) {
mediaManager.blurCamera();
return;
}
if (document.visibilityState === 'visible') {
mediaManager.focusCamera();
} else {
if (this.simplePeer.getNbConnections() === 0) {
mediaManager.blurCamera();
}
}
}
} }

View File

@ -0,0 +1,37 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import BaseSoundManager = Phaser.Sound.BaseSoundManager;
import BaseSound = Phaser.Sound.BaseSound;
import SoundConfig = Phaser.Types.Sound.SoundConfig;
class SoundManager {
private soundPromises : Map<string,Promise<BaseSound>> = new Map<string, Promise<Phaser.Sound.BaseSound>>();
public loadSound (loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string) : Promise<BaseSound> {
let soundPromise = this.soundPromises.get(soundUrl);
if (soundPromise !== undefined) {
return soundPromise;
}
soundPromise = new Promise<BaseSound>((res) => {
const sound = soundManager.get(soundUrl);
if (sound !== null) {
return res(sound);
}
loadPlugin.audio(soundUrl, soundUrl);
loadPlugin.once('filecomplete-audio-' + soundUrl, () => res(soundManager.add(soundUrl)));
loadPlugin.start();
});
this.soundPromises.set(soundUrl,soundPromise);
return soundPromise;
}
public async playSound(loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string, config: SoundConfig|undefined) : Promise<void> {
const sound = await this.loadSound(loadPlugin,soundManager,soundUrl);
if (config === undefined) sound.play();
else sound.play(config);
}
public stopSound(soundManager : BaseSoundManager,soundUrl : string){
soundManager.get(soundUrl).stop();
}
}
export const soundManager = new SoundManager();

View File

@ -10,6 +10,14 @@ import {PinchManager} from "../UserInput/PinchManager";
import Zone = Phaser.GameObjects.Zone; import Zone = Phaser.GameObjects.Zone;
import { MenuScene } from "../Menu/MenuScene"; import { MenuScene } from "../Menu/MenuScene";
import {ResizableScene} from "./ResizableScene"; import {ResizableScene} from "./ResizableScene";
import {
audioConstraintStore,
enableCameraSceneVisibilityStore,
localStreamStore,
mediaStreamConstraintsStore,
videoConstraintStore
} from "../../Stores/MediaStore";
import type {Unsubscriber} from "svelte/store";
export const EnableCameraSceneName = "EnableCameraScene"; export const EnableCameraSceneName = "EnableCameraScene";
enum LoginTextures { enum LoginTextures {
@ -40,6 +48,7 @@ export class EnableCameraScene extends ResizableScene {
private enableCameraSceneElement!: Phaser.GameObjects.DOMElement; private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
private mobileTapZone!: Zone; private mobileTapZone!: Zone;
private localStreamStoreUnsubscriber!: Unsubscriber;
constructor() { constructor() {
super({ super({
@ -119,9 +128,20 @@ export class EnableCameraScene extends ResizableScene {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active'); HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
const mediaPromise = mediaManager.getCamera(); this.localStreamStoreUnsubscriber = localStreamStore.subscribe((result) => {
if (result.type === 'error') {
// TODO: proper handling of the error
throw result.error;
}
this.getDevices();
if (result.stream !== null) {
this.setupStream(result.stream);
}
});
/*const mediaPromise = mediaManager.getCamera();
mediaPromise.then(this.getDevices.bind(this)); mediaPromise.then(this.getDevices.bind(this));
mediaPromise.then(this.setupStream.bind(this)); mediaPromise.then(this.setupStream.bind(this));*/
this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this)); this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this));
this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this)); this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this));
@ -133,6 +153,8 @@ export class EnableCameraScene extends ResizableScene {
this.add.existing(this.soundMeterSprite); this.add.existing(this.soundMeterSprite);
this.onResize(); this.onResize();
enableCameraSceneVisibilityStore.showEnableCameraScene();
} }
private previousCam(): void { private previousCam(): void {
@ -140,7 +162,9 @@ export class EnableCameraScene extends ResizableScene {
return; return;
} }
this.cameraSelected--; this.cameraSelected--;
mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this)); videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
} }
private nextCam(): void { private nextCam(): void {
@ -148,8 +172,10 @@ export class EnableCameraScene extends ResizableScene {
return; return;
} }
this.cameraSelected++; this.cameraSelected++;
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
// TODO: the change of camera should be OBSERVED (reactive) // TODO: the change of camera should be OBSERVED (reactive)
mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this)); //mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
} }
private previousMic(): void { private previousMic(): void {
@ -157,7 +183,8 @@ export class EnableCameraScene extends ResizableScene {
return; return;
} }
this.microphoneSelected--; this.microphoneSelected--;
mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this)); audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
} }
private nextMic(): void { private nextMic(): void {
@ -165,8 +192,9 @@ export class EnableCameraScene extends ResizableScene {
return; return;
} }
this.microphoneSelected++; this.microphoneSelected++;
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
// TODO: the change of camera should be OBSERVED (reactive) // TODO: the change of camera should be OBSERVED (reactive)
mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this)); //mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
} }
/** /**
@ -260,15 +288,20 @@ export class EnableCameraScene extends ResizableScene {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none'; HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.soundMeter.stop(); this.soundMeter.stop();
mediaManager.stopCamera(); enableCameraSceneVisibilityStore.hideEnableCameraScene();
mediaManager.stopMicrophone(); this.localStreamStoreUnsubscriber();
//mediaManager.stopCamera();
//mediaManager.stopMicrophone();
this.scene.sleep(EnableCameraSceneName) this.scene.sleep(EnableCameraSceneName);
gameManager.goToStartingMap(this.scene); gameManager.goToStartingMap(this.scene);
} }
private async getDevices() { private async getDevices() {
// TODO: switch this in a store.
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices(); const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
this.microphonesList = [];
this.camerasList = [];
for (const mediaDeviceInfo of mediaDeviceInfos) { for (const mediaDeviceInfo of mediaDeviceInfos) {
if (mediaDeviceInfo.kind === 'audioinput') { if (mediaDeviceInfo.kind === 'audioinput') {
this.microphonesList.push(mediaDeviceInfo); this.microphonesList.push(mediaDeviceInfo);

View File

@ -2,6 +2,8 @@ import {mediaManager} from "../../WebRtc/MediaManager";
import {HtmlUtils} from "../../WebRtc/HtmlUtils"; import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {localUserStore} from "../../Connexion/LocalUserStore"; import {localUserStore} from "../../Connexion/LocalUserStore";
import {DirtyScene} from "../Game/DirtyScene"; import {DirtyScene} from "../Game/DirtyScene";
import {get} from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene'; export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
const helpCameraSettings = 'helpCameraSettings'; const helpCameraSettings = 'helpCameraSettings';
@ -41,7 +43,7 @@ export class HelpCameraSettingsScene extends DirtyScene {
} }
}); });
if(!localUserStore.getHelpCameraSettingsShown() && (!mediaManager.constraintsMedia.audio || !mediaManager.constraintsMedia.video)){ if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
this.openHelpCameraSettingsOpened(); this.openHelpCameraSettingsOpened();
localUserStore.setHelpCameraSettingsShown(); localUserStore.setHelpCameraSettingsShown();
} }

View File

@ -10,6 +10,7 @@ import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore"; import {menuIconVisible} from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu'; const gameMenuKey = 'gameMenu';
@ -324,7 +325,7 @@ export class MenuScene extends Phaser.Scene {
if (valueVideo !== this.videoQualityValue) { if (valueVideo !== this.videoQualityValue) {
this.videoQualityValue = valueVideo; this.videoQualityValue = valueVideo;
localUserStore.setVideoQualityValue(valueVideo); localUserStore.setVideoQualityValue(valueVideo);
mediaManager.updateCameraQuality(valueVideo); videoConstraintStore.setFrameRate(valueVideo);
} }
this.closeGameQualityMenu(); this.closeGameQualityMenu();
} }

View File

@ -2,17 +2,17 @@ import {PlayerAnimationDirections} from "./Animation";
import type {GameScene} from "../Game/GameScene"; import type {GameScene} from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character"; import {Character} from "../Entity/Character";
import {userMovingStore} from "../../Stores/GameStore";
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
export const hasMovedEventName = "hasMoved"; export const hasMovedEventName = "hasMoved";
export interface CurrentGamerInterface extends Character{ export const requestEmoteEventName = "requestEmote";
moveUser(delta: number) : void;
say(text : string) : void;
isMoving(): boolean;
}
export class Player extends Character implements CurrentGamerInterface { export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down; private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false; private wasMoving: boolean = false;
private emoteMenu: RadialMenu|null = null;
private updateListener: () => void;
constructor( constructor(
Scene: GameScene, Scene: GameScene,
@ -26,14 +26,18 @@ export class Player extends Character implements CurrentGamerInterface {
companion: string|null, companion: string|null,
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
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 //the current player model should be push away by other players to prevent conflict
this.getBody().setImmovable(false); this.getBody().setImmovable(false);
if (typeof companion === 'string') { this.updateListener = () => {
this.addCompanion(companion, companionTexturePromise); if (this.emoteMenu) {
} this.emoteMenu.x = this.x;
this.emoteMenu.y = this.y;
}
};
this.scene.events.addListener('postupdate', this.updateListener);
} }
moveUser(delta: number): void { moveUser(delta: number): void {
@ -83,9 +87,43 @@ export class Player extends Character implements CurrentGamerInterface {
this.previousDirection = direction; this.previousDirection = direction;
} }
this.wasMoving = moving; this.wasMoving = moving;
userMovingStore.set(moving);
} }
public isMoving(): boolean { public isMoving(): boolean {
return this.wasMoving; 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, this.x, this.y, emotes)
this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => {
this.closeEmoteMenu();
this.emit(requestEmoteEventName, item.name);
this.playEmote(item.name);
});
}
closeEmoteMenu(): void {
if (!this.emoteMenu) return;
this.emoteMenu.destroy();
this.emoteMenu = null;
}
destroy() {
this.scene.events.removeListener('postupdate', this.updateListener);
super.destroy();
}
} }

View File

@ -8,6 +8,7 @@ class WaScaleManager {
private hdpiManager: HdpiManager; private hdpiManager: HdpiManager;
private scaleManager!: ScaleManager; private scaleManager!: ScaleManager;
private game!: Game; private game!: Game;
private actualZoom: number = 1;
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) { public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber); this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
@ -28,6 +29,7 @@ class WaScaleManager {
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio}); const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio});
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height); this.scaleManager.resize(gameSize.width, gameSize.height);
@ -48,6 +50,13 @@ class WaScaleManager {
this.applyNewSize(); this.applyNewSize();
} }
/**
* This is used to scale back the ui components to counter-act the zoom.
*/
public get uiScalingFactor(): number {
return this.actualZoom > 1 ? 1 : 1.2;
}
} }
export const waScaleManager = new WaScaleManager(640*480, 196*196); export const waScaleManager = new WaScaleManager(640*480, 196*196);

View File

@ -0,0 +1,3 @@
import { derived, writable, Writable } from "svelte/store";
export const userMovingStore = writable(false);

View File

@ -0,0 +1,510 @@
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
import {peerStore} from "./PeerStore";
import {localUserStore} from "../Connexion/LocalUserStore";
import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap";
import {userMovingStore} from "./GameStore";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
/**
* A store that contains the camera state requested by the user (on or off).
*/
function createRequestedCameraState() {
const { subscribe, set, update } = writable(true);
return {
subscribe,
enableWebcam: () => set(true),
disableWebcam: () => set(false),
};
}
/**
* A store that contains the microphone state requested by the user (on or off).
*/
function createRequestedMicrophoneState() {
const { subscribe, set, update } = writable(true);
return {
subscribe,
enableMicrophone: () => set(true),
disableMicrophone: () => set(false),
};
}
/**
* A store containing whether the current page is visible or not.
*/
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) {
const onVisibilityChange = () => {
set(document.visibilityState === 'visible');
};
document.addEventListener('visibilitychange', onVisibilityChange);
return function stop() {
document.removeEventListener('visibilitychange', onVisibilityChange);
};
});
/**
* A store that contains whether the game overlay is shown or not.
* Typically, the overlay is hidden when entering Jitsi meet.
*/
function createGameOverlayVisibilityStore() {
const { subscribe, set, update } = writable(false);
return {
subscribe,
showGameOverlay: () => set(true),
hideGameOverlay: () => set(false),
};
}
/**
* A store that contains whether the EnableCameraScene is shown or not.
*/
function createEnableCameraSceneVisibilityStore() {
const { subscribe, set, update } = writable(false);
return {
subscribe,
showEnableCameraScene: () => set(true),
hideEnableCameraScene: () => set(false),
};
}
export const requestedCameraState = createRequestedCameraState();
export const requestedMicrophoneState = createRequestedMicrophoneState();
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();
export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore();
/**
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
*/
function createPrivacyShutdownStore() {
let privacyEnabled = false;
const { subscribe, set, update } = writable(privacyEnabled);
visibilityStore.subscribe((isVisible) => {
if (!isVisible && get(peerStore).size === 0) {
privacyEnabled = true;
set(true);
}
if (isVisible) {
privacyEnabled = false;
set(false);
}
});
peerStore.subscribe((peers) => {
if (peers.size === 0 && get(visibilityStore) === false) {
privacyEnabled = true;
set(true);
}
});
return {
subscribe,
};
}
export const privacyShutdownStore = createPrivacyShutdownStore();
/**
* A store containing whether the webcam was enabled in the last 10 seconds
*/
const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null;
const unsubscribe = requestedCameraState.subscribe((enabled) => {
if (enabled === true) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
set(false);
}, 10000);
set(true);
} else {
set(false);
}
})
return function stop() {
unsubscribe();
};
});
/**
* A store containing whether the webcam was enabled in the last 5 seconds
*/
const userMoved5SecondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null;
const unsubscribe = userMovingStore.subscribe((moving) => {
if (moving === true) {
if (timeout) {
clearTimeout(timeout);
}
set(true);
} else {
timeout = setTimeout(() => {
set(false);
}, 5000);
}
})
return function stop() {
unsubscribe();
};
});
/**
* A store containing whether the mouse is getting close the bottom right corner.
*/
const mouseInBottomRight = readable(false, function start(set) {
let lastInBottomRight = false;
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
const detectInBottomRight = (event: MouseEvent) => {
const rect = gameDiv.getBoundingClientRect();
const inBottomRight = event.x - rect.left > rect.width * 3 / 4 && event.y - rect.top > rect.height * 3 / 4;
if (inBottomRight !== lastInBottomRight) {
lastInBottomRight = inBottomRight;
set(inBottomRight);
}
};
document.addEventListener('mousemove', detectInBottomRight);
return function stop() {
document.removeEventListener('mousemove', detectInBottomRight);
}
});
/**
* A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation.
*/
export const cameraEnergySavingStore = derived([userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight], ([$userMoved5SecondsAgoStore,$peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
return !$mouseInBottomRight && !$userMoved5SecondsAgoStore && $peerStore.size === 0 && !$enabledWebCam10secondsAgoStore;
});
/**
* A store that contains video constraints.
*/
function createVideoConstraintStore() {
const { subscribe, set, update } = writable({
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 400, ideal: 720 },
frameRate: { ideal: localUserStore.getVideoQualityValue() },
facingMode: "user",
resizeMode: 'crop-and-scale',
aspectRatio: 1.777777778
} as MediaTrackConstraints);
return {
subscribe,
setDeviceId: (deviceId: string) => update((constraints) => {
constraints.deviceId = {
exact: deviceId
};
return constraints;
}),
setFrameRate: (frameRate: number) => update((constraints) => {
constraints.frameRate = { ideal: frameRate };
return constraints;
})
};
}
export const videoConstraintStore = createVideoConstraintStore();
/**
* A store that contains video constraints.
*/
function createAudioConstraintStore() {
const { subscribe, set, update } = writable({
//TODO: make these values configurable in the game settings menu and store them in localstorage
autoGainControl: false,
echoCancellation: true,
noiseSuppression: true
} as boolean|MediaTrackConstraints);
let selectedDeviceId = null;
return {
subscribe,
setDeviceId: (deviceId: string) => update((constraints) => {
selectedDeviceId = deviceId;
if (typeof(constraints) === 'boolean') {
constraints = {}
}
constraints.deviceId = {
exact: selectedDeviceId
};
return constraints;
})
};
}
export const audioConstraintStore = createAudioConstraintStore();
let timeout: NodeJS.Timeout;
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
/**
* A store containing the media constraints we want to apply.
*/
export const mediaStreamConstraintsStore = derived(
[
requestedCameraState,
requestedMicrophoneState,
gameOverlayVisibilityStore,
enableCameraSceneVisibilityStore,
videoConstraintStore,
audioConstraintStore,
privacyShutdownStore,
cameraEnergySavingStore,
], (
[
$requestedCameraState,
$requestedMicrophoneState,
$gameOverlayVisibilityStore,
$enableCameraSceneVisibilityStore,
$videoConstraintStore,
$audioConstraintStore,
$privacyShutdownStore,
$cameraEnergySavingStore,
], set
) => {
let currentVideoConstraint: boolean|MediaTrackConstraints = $videoConstraintStore;
let currentAudioConstraint: boolean|MediaTrackConstraints = $audioConstraintStore;
if ($enableCameraSceneVisibilityStore) {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
return;
}
// Disable webcam if the user requested so
if ($requestedCameraState === false) {
currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== 'boolean') {
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
}
if (typeof previousComputedAudioConstraint !== 'boolean') {
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
}, 100);
}
}, {
video: false,
audio: false
} as MediaStreamConstraints);
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
interface StreamSuccessValue {
type: "success",
stream: MediaStream|null,
// The constraints that we got (and not the one that have been requested)
constraints: MediaStreamConstraints
}
interface StreamErrorValue {
type: "error",
error: Error,
constraints: MediaStreamConstraints
}
let currentStream : MediaStream|null = null;
/**
* Stops the camera from filming
*/
function stopCamera(): void {
if (currentStream) {
for (const track of currentStream.getVideoTracks()) {
track.stop();
}
}
}
/**
* Stops the microphone from listening
*/
function stopMicrophone(): void {
if (currentStream) {
for (const track of currentStream.getAudioTracks()) {
track.stop();
}
}
}
/**
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
*/
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(mediaStreamConstraintsStore, ($mediaStreamConstraintsStore, set) => {
const constraints = { ...$mediaStreamConstraintsStore };
if (navigator.mediaDevices === undefined) {
if (window.location.protocol === 'http:') {
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
set({
type: 'error',
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'),
constraints
});
} else {
//throw new Error('Unable to access your camera or microphone. Your browser is too old.');
set({
type: 'error',
error: new Error('Unable to access your camera or microphone. Your browser is too old.'),
constraints
});
}
}
if (constraints.audio === false) {
stopMicrophone();
}
if (constraints.video === false) {
stopCamera();
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({
type: 'success',
stream: null,
constraints
});
return;
}
(async () => {
try {
stopMicrophone();
stopCamera();
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'success',
stream: currentStream,
constraints
});
return;
} catch (e) {
if (constraints.video !== false) {
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
// TODO: does it make sense to pop this error when retrying?
set({
type: 'error',
error: e,
constraints
});
// Let's try without video constraints
requestedCameraState.disableWebcam();
} else {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({
type: 'error',
error: e,
constraints
});
}
/*constraints.video = false;
if (constraints.audio === false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({
type: 'error',
error: e,
constraints
});
// Let's make as if the user did not ask.
requestedCameraState.disableWebcam();
} else {
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
try {
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'success',
stream: currentStream,
constraints
});
return;
} catch (e2) {
console.info("Error. Unable to get microphone fallback access.", $mediaStreamConstraintsStore, e2);
set({
type: 'error',
error: e,
constraints
});
}
}*/
}
})();
});
/**
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
*/
export const obtainedMediaConstraintStore = derived(localStreamStore, ($localStreamStore) => {
return $localStreamStore.constraints;
});

View File

@ -0,0 +1,36 @@
import { derived, writable, Writable } from "svelte/store";
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
import type {SimplePeer} from "../WebRtc/SimplePeer";
/**
* A store that contains the camera state requested by the user (on or off).
*/
function createPeerStore() {
let users = new Map<number, UserSimplePeerInterface>();
const { subscribe, set, update } = writable(users);
return {
subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => {
users = new Map<number, UserSimplePeerInterface>();
set(users);
simplePeer.registerPeerConnectionListener({
onConnect(user: UserSimplePeerInterface) {
update(users => {
users.set(user.userId, user);
return users;
});
},
onDisconnect(userId: number) {
update(users => {
users.delete(userId);
return users;
});
}
})
}
};
}
export const peerStore = createPeerStore();

View File

@ -0,0 +1,192 @@
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
import {peerStore} from "./PeerStore";
import {localUserStore} from "../Connexion/LocalUserStore";
import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap";
import {userMovingStore} from "./GameStore";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import {
audioConstraintStore, cameraEnergySavingStore,
enableCameraSceneVisibilityStore,
gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore,
requestedCameraState,
requestedMicrophoneState, videoConstraintStore
} from "./MediaStore";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* A store that contains the camera state requested by the user (on or off).
*/
function createRequestedScreenSharingState() {
const { subscribe, set, update } = writable(false);
return {
subscribe,
enableScreenSharing: () => set(true),
disableScreenSharing: () => set(false),
};
}
export const requestedScreenSharingState = createRequestedScreenSharingState();
let currentStream : MediaStream|null = null;
/**
* Stops the camera from filming
*/
function stopScreenSharing(): void {
if (currentStream) {
for (const track of currentStream.getVideoTracks()) {
track.stop();
}
}
currentStream = null;
}
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
/**
* A store containing the media constraints we want to apply.
*/
export const screenSharingConstraintsStore = derived(
[
requestedScreenSharingState,
gameOverlayVisibilityStore,
peerStore,
], (
[
$requestedScreenSharingState,
$gameOverlayVisibilityStore,
$peerStore,
], set
) => {
let currentVideoConstraint: boolean|MediaTrackConstraints = true;
let currentAudioConstraint: boolean|MediaTrackConstraints = false;
// Disable screen sharing if the user requested so
if (!$requestedScreenSharingState) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable screen sharing when in a Jitsi
if (!$gameOverlayVisibilityStore) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable screen sharing if no peers
if ($peerStore.size === 0) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
/*if (typeof previousComputedVideoConstraint !== 'boolean') {
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
}
if (typeof previousComputedAudioConstraint !== 'boolean') {
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
}*/
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
}
}, {
video: false,
audio: false
} as MediaStreamConstraints);
/**
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
*/
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(screenSharingConstraintsStore, ($screenSharingConstraintsStore, set) => {
const constraints = $screenSharingConstraintsStore;
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
set({
type: 'success',
stream: null,
constraints
});
return;
}
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({constraints});
} else if (navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
} else {
stopScreenSharing();
set({
type: 'error',
error: new Error('Your browser does not support sharing screen'),
constraints
});
return;
}
(async () => {
try {
stopScreenSharing();
currentStream = await currentStreamPromise;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: 'success',
stream: null,
constraints: {
video: false,
audio: false
}
});
};
}
set({
type: 'success',
stream: currentStream,
constraints
});
return;
} catch (e) {
currentStream = null;
console.info("Error. Unable to share screen.", e);
set({
type: 'error',
error: e,
constraints
});
}
})();
});
/**
* A store containing whether the screen sharing button should be displayed or hidden.
*/
export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) => {
if (!navigator.getDisplayMedia && !navigator.mediaDevices.getDisplayMedia) {
set(false);
return;
}
set($peerStore.size !== 0);
});

View File

@ -1,6 +1,8 @@
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import {JITSI_URL} from "../Enum/EnvironmentVariable";
import {mediaManager} from "./MediaManager"; import {mediaManager} from "./MediaManager";
import {coWebsiteManager} from "./CoWebsiteManager"; import {coWebsiteManager} from "./CoWebsiteManager";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import {get} from "svelte/store";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
interface jitsiConfigInterface { interface jitsiConfigInterface {
@ -10,10 +12,9 @@ interface jitsiConfigInterface {
} }
const getDefaultConfig = () : jitsiConfigInterface => { const getDefaultConfig = () : jitsiConfigInterface => {
const constraints = mediaManager.getConstraintRequestedByUser();
return { return {
startWithAudioMuted: !constraints.audio, startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: constraints.video === false, startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false prejoinPageEnabled: false
} }
} }
@ -72,7 +73,6 @@ class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
private audioCallback = this.onAudioChange.bind(this); private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this); private videoCallback = this.onVideoChange.bind(this);
private previousConfigMeet! : jitsiConfigInterface;
private jitsiScriptLoaded: boolean = false; private jitsiScriptLoaded: boolean = false;
/** /**
@ -83,9 +83,6 @@ class JitsiFactory {
} }
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void { public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void {
//save previous config
this.previousConfigMeet = getDefaultConfig();
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => { coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
// Jitsi meet external API maintains some data in local storage // Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a // which is sent via the appData URL parameter when joining a
@ -134,27 +131,22 @@ class JitsiFactory {
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback); this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback); this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);
this.jitsiApi?.dispose(); this.jitsiApi?.dispose();
//restore previous config
if(this.previousConfigMeet?.startWithAudioMuted){
await mediaManager.disableMicrophone();
}else{
await mediaManager.enableMicrophone();
}
if(this.previousConfigMeet?.startWithVideoMuted){
await mediaManager.disableCamera();
}else{
await mediaManager.enableCamera();
}
} }
private onAudioChange({muted}: {muted: boolean}): void { private onAudioChange({muted}: {muted: boolean}): void {
this.previousConfigMeet.startWithAudioMuted = muted; if (muted) {
requestedMicrophoneState.disableMicrophone();
} else {
requestedMicrophoneState.enableMicrophone();
}
} }
private onVideoChange({muted}: {muted: boolean}): void { private onVideoChange({muted}: {muted: boolean}): void {
this.previousConfigMeet.startWithVideoMuted = muted; if (muted) {
requestedCameraState.disableWebcam();
} else {
requestedCameraState.enableWebcam();
}
} }
private async loadJitsiScript(domain: string): Promise<void> { private async loadJitsiScript(domain: string): Promise<void> {

View File

@ -6,10 +6,21 @@ import {localUserStore} from "../Connexion/LocalUserStore";
import type {UserSimplePeerInterface} from "./SimplePeer"; import type {UserSimplePeerInterface} from "./SimplePeer";
import {SoundMeter} from "../Phaser/Components/SoundMeter"; import {SoundMeter} from "../Phaser/Components/SoundMeter";
import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable"; import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable";
import {
gameOverlayVisibilityStore, localStreamStore,
mediaStreamConstraintsStore,
requestedCameraState,
requestedMicrophoneState
} from "../Stores/MediaStore";
import {
requestedScreenSharingState,
screenSharingAvailableStore,
screenSharingLocalStreamStore
} from "../Stores/ScreenSharingStore";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
let videoConstraint: boolean|MediaTrackConstraints = { const videoConstraint: boolean|MediaTrackConstraints = {
width: { min: 640, ideal: 1280, max: 1920 }, width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 400, ideal: 720 }, height: { min: 400, ideal: 720 },
frameRate: { ideal: localUserStore.getVideoQualityValue() }, frameRate: { ideal: localUserStore.getVideoQualityValue() },
@ -31,7 +42,6 @@ export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void; export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
export type HelpCameraSettingsCallBack = () => void; export type HelpCameraSettingsCallBack = () => void;
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
export class MediaManager { export class MediaManager {
localStream: MediaStream|null = null; localStream: MediaStream|null = null;
localScreenCapture: MediaStream|null = null; localScreenCapture: MediaStream|null = null;
@ -47,10 +57,6 @@ export class MediaManager {
//FIX ME SOUNDMETER: check stalability of sound meter calculation //FIX ME SOUNDMETER: check stalability of sound meter calculation
//mySoundMeterElement: HTMLDivElement; //mySoundMeterElement: HTMLDivElement;
private webrtcOutAudio: HTMLAudioElement; private webrtcOutAudio: HTMLAudioElement;
constraintsMedia : MediaStreamConstraints = {
audio: audioConstraint,
video: videoConstraint
};
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>(); updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
@ -61,11 +67,8 @@ export class MediaManager {
private cinemaBtn: HTMLDivElement; private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement; private monitorBtn: HTMLDivElement;
private previousConstraint : MediaStreamConstraints;
private focused : boolean = true; private focused : boolean = true;
private hasCamera = true;
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>(); private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
@ -88,14 +91,12 @@ export class MediaManager {
this.microphoneClose.style.display = "none"; this.microphoneClose.style.display = "none";
this.microphoneClose.addEventListener('click', (e: MouseEvent) => { this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enableMicrophone(); requestedMicrophoneState.enableMicrophone();
//update tracking
}); });
this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone'); this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone.addEventListener('click', (e: MouseEvent) => { this.microphone.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disableMicrophone(); requestedMicrophoneState.disableMicrophone();
//update tracking
}); });
this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video'); this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video');
@ -103,14 +104,12 @@ export class MediaManager {
this.cinemaClose.style.display = "none"; this.cinemaClose.style.display = "none";
this.cinemaClose.addEventListener('click', (e: MouseEvent) => { this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enableCamera(); requestedCameraState.enableWebcam();
//update tracking
}); });
this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema'); this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema.addEventListener('click', (e: MouseEvent) => { this.cinema.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disableCamera(); requestedCameraState.disableWebcam();
//update tracking
}); });
this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor'); this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
@ -118,21 +117,20 @@ export class MediaManager {
this.monitorClose.style.display = "block"; this.monitorClose.style.display = "block";
this.monitorClose.addEventListener('click', (e: MouseEvent) => { this.monitorClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enableScreenSharing(); //this.enableScreenSharing();
//update tracking requestedScreenSharingState.enableScreenSharing();
}); });
this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor'); this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor');
this.monitor.style.display = "none"; this.monitor.style.display = "none";
this.monitor.addEventListener('click', (e: MouseEvent) => { this.monitor.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disableScreenSharing(); //this.disableScreenSharing();
//update tracking requestedScreenSharingState.disableScreenSharing();
}); });
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
this.pingCameraStatus(); this.pingCameraStatus();
//FIX ME SOUNDMETER: check stalability of sound meter calculation //FIX ME SOUNDMETER: check stability of sound meter calculation
/*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter')); /*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => { this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
this.mySoundMeterElement.children.item(index)?.classList.remove('active'); this.mySoundMeterElement.children.item(index)?.classList.remove('active');
@ -140,37 +138,98 @@ export class MediaManager {
//Check of ask notification navigator permission //Check of ask notification navigator permission
this.getNotification(); this.getNotification();
localStreamStore.subscribe((result) => {
if (result.type === 'error') {
console.error(result.error);
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => {
this.showHelpCameraSettingsCallBack();
}, this.userInputManager);
return;
}
if (result.constraints.video !== false) {
HtmlUtils.getElementByIdOrFail('div-myCamVideo').classList.remove('hide');
} else {
HtmlUtils.getElementByIdOrFail('div-myCamVideo').classList.add('hide');
}/*
if (result.constraints.audio !== false) {
this.enableMicrophoneStyle();
} else {
this.disableMicrophoneStyle();
}*/
this.localStream = result.stream;
this.myCamVideo.srcObject = this.localStream;
// TODO: migrate all listeners to the store directly.
this.triggerUpdatedLocalStreamCallbacks(result.stream);
});
requestedCameraState.subscribe((enabled) => {
if (enabled) {
this.enableCameraStyle();
} else {
this.disableCameraStyle();
}
});
requestedMicrophoneState.subscribe((enabled) => {
if (enabled) {
this.enableMicrophoneStyle();
} else {
this.disableMicrophoneStyle();
}
});
//let screenSharingStream : MediaStream|null;
screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === 'error') {
console.error(result.error);
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check navigators permissions.', () => {
this.showHelpCameraSettingsCallBack();
}, this.userInputManager);
return;
}
if (result.stream !== null) {
this.enableScreenSharingStyle();
mediaManager.localScreenCapture = result.stream;
// TODO: migrate this out of MediaManager
this.triggerStartedScreenSharingCallbacks(result.stream);
//screenSharingStream = result.stream;
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = result.stream;
} else {
this.disableScreenSharingStyle();
this.removeActiveScreenSharingVideo('me');
// FIXME: we need the old stream that is being stopped!
if (this.localScreenCapture) {
this.triggerStoppedScreenSharingCallbacks(this.localScreenCapture);
this.localScreenCapture = null;
}
//screenSharingStream = null;
}
});
screenSharingAvailableStore.subscribe((available) => {
if (available) {
document.querySelector('.btn-monitor')?.classList.remove('hide');
} else {
document.querySelector('.btn-monitor')?.classList.add('hide');
}
});
} }
public updateScene(){ public updateScene(){
//FIX ME SOUNDMETER: check stalability of sound meter calculation //FIX ME SOUNDMETER: check stability of sound meter calculation
//this.updateSoudMeter(); //this.updateSoudMeter();
} }
public blurCamera() {
if(!this.focused){
return;
}
this.focused = false;
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
this.disableCamera();
}
/**
* Returns the constraint that the user wants (independently of the visibility / jitsi state...)
*/
public getConstraintRequestedByUser(): MediaStreamConstraints {
return this.previousConstraint ?? this.constraintsMedia;
}
public focusCamera() {
if(this.focused){
return;
}
this.focused = true;
this.applyPreviousConfig();
}
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.add(callback); this.updatedLocalStreamCallBacks.add(callback);
} }
@ -214,6 +273,8 @@ export class MediaManager {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} }
buttonCloseFrame.removeEventListener('click', functionTrigger); buttonCloseFrame.removeEventListener('click', functionTrigger);
gameOverlayVisibilityStore.showGameOverlay();
} }
public hideGameOverlay(): void { public hideGameOverlay(): void {
@ -225,110 +286,8 @@ export class MediaManager {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} }
buttonCloseFrame.addEventListener('click', functionTrigger); buttonCloseFrame.addEventListener('click', functionTrigger);
}
public isGameOverlayVisible(): boolean { gameOverlayVisibilityStore.hideGameOverlay();
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
return gameOverlay.classList.contains('active');
}
public updateCameraQuality(value: number) {
this.enableCameraStyle();
const newVideoConstraint = JSON.parse(JSON.stringify(videoConstraint));
newVideoConstraint.frameRate = {exact: value, ideal: value};
videoConstraint = newVideoConstraint;
this.constraintsMedia.video = videoConstraint;
this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
});
}
public async enableCamera() {
this.constraintsMedia.video = videoConstraint;
try {
const stream = await this.getCamera()
//TODO show error message tooltip upper of camera button
//TODO message : please check camera permission of your navigator
if(stream.getVideoTracks().length === 0) {
throw new Error('Video track is empty, please check camera permission of your navigator')
}
this.enableCameraStyle();
this.triggerUpdatedLocalStreamCallbacks(stream);
} catch(err) {
console.error(err);
this.disableCameraStyle();
this.stopCamera();
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => {
this.showHelpCameraSettingsCallBack();
}, this.userInputManager);
}
}
public async disableCamera() {
this.disableCameraStyle();
this.stopCamera();
if (this.constraintsMedia.audio !== false) {
const stream = await this.getCamera();
this.triggerUpdatedLocalStreamCallbacks(stream);
} else {
this.triggerUpdatedLocalStreamCallbacks(null);
}
}
public async enableMicrophone() {
this.constraintsMedia.audio = audioConstraint;
try {
const stream = await this.getCamera();
//TODO show error message tooltip upper of camera button
//TODO message : please check microphone permission of your navigator
if (stream.getAudioTracks().length === 0) {
throw Error('Audio track is empty, please check microphone permission of your navigator')
}
this.enableMicrophoneStyle();
this.triggerUpdatedLocalStreamCallbacks(stream);
} catch(err) {
console.error(err);
this.disableMicrophoneStyle();
layoutManager.addInformation('warning', 'Microphone access denied. Click here and check navigators permissions.', () => {
this.showHelpCameraSettingsCallBack();
}, this.userInputManager);
}
}
public async disableMicrophone() {
this.disableMicrophoneStyle();
this.stopMicrophone();
if (this.constraintsMedia.video !== false) {
const stream = await this.getCamera();
this.triggerUpdatedLocalStreamCallbacks(stream);
} else {
this.triggerUpdatedLocalStreamCallbacks(null);
}
}
private applyPreviousConfig() {
this.constraintsMedia = this.previousConstraint;
if(!this.constraintsMedia.video){
this.disableCameraStyle();
}else{
this.enableCameraStyle();
}
if(!this.constraintsMedia.audio){
this.disableMicrophoneStyle()
}else{
this.enableMicrophoneStyle()
}
this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
});
} }
private enableCameraStyle(){ private enableCameraStyle(){
@ -341,8 +300,6 @@ export class MediaManager {
this.cinemaClose.style.display = "block"; this.cinemaClose.style.display = "block";
this.cinema.style.display = "none"; this.cinema.style.display = "none";
this.cinemaBtn.classList.add("disabled"); this.cinemaBtn.classList.add("disabled");
this.constraintsMedia.video = false;
this.myCamVideo.srcObject = null;
} }
private enableMicrophoneStyle(){ private enableMicrophoneStyle(){
@ -355,185 +312,18 @@ export class MediaManager {
this.microphoneClose.style.display = "block"; this.microphoneClose.style.display = "block";
this.microphone.style.display = "none"; this.microphone.style.display = "none";
this.microphoneBtn.classList.add("disabled"); this.microphoneBtn.classList.add("disabled");
this.constraintsMedia.audio = false;
} }
private enableScreenSharing() { private enableScreenSharingStyle(){
this.getScreenMedia().then((stream) => { this.monitorClose.style.display = "none";
this.triggerStartedScreenSharingCallbacks(stream); this.monitor.style.display = "block";
this.monitorClose.style.display = "none"; this.monitorBtn.classList.add("enabled");
this.monitor.style.display = "block";
this.monitorBtn.classList.add("enabled");
}, () => {
this.monitorClose.style.display = "block";
this.monitor.style.display = "none";
this.monitorBtn.classList.remove("enabled");
layoutManager.addInformation('warning', 'Screen sharing access denied. Click here and check navigators permissions.', () => {
this.showHelpCameraSettingsCallBack();
}, this.userInputManager);
});
} }
private disableScreenSharing() { private disableScreenSharingStyle(){
this.monitorClose.style.display = "block"; this.monitorClose.style.display = "block";
this.monitor.style.display = "none"; this.monitor.style.display = "none";
this.monitorBtn.classList.remove("enabled"); this.monitorBtn.classList.remove("enabled");
this.removeActiveScreenSharingVideo('me');
this.localScreenCapture?.getTracks().forEach((track: MediaStreamTrack) => {
track.stop();
});
if (this.localScreenCapture === null) {
console.warn('Weird: trying to remove a screen sharing that is not enabled');
return;
}
const localScreenCapture = this.localScreenCapture;
this.getCamera().then((stream) => {
this.triggerStoppedScreenSharingCallbacks(localScreenCapture);
}).catch((err) => { //catch error get camera
console.error(err);
this.triggerStoppedScreenSharingCallbacks(localScreenCapture);
});
this.localScreenCapture = null;
}
//get screen
getScreenMedia() : Promise<MediaStream>{
try {
return this._startScreenCapture()
.then((stream: MediaStream) => {
this.localScreenCapture = stream;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of stream.getTracks()) {
track.onended = () => {
this.disableScreenSharing();
};
}
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = stream;
return stream;
})
.catch((err: unknown) => {
console.error("Error => getScreenMedia => ", err);
throw err;
});
}catch (err) {
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
reject(err);
});
}
}
private _startScreenCapture() {
if (navigator.getDisplayMedia) {
return navigator.getDisplayMedia({video: true});
} else if (navigator.mediaDevices.getDisplayMedia) {
return navigator.mediaDevices.getDisplayMedia({video: true});
} else {
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
reject("error sharing screen");
});
}
}
//get camera
async getCamera(): Promise<MediaStream> {
if (navigator.mediaDevices === undefined) {
if (window.location.protocol === 'http:') {
throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
} else {
throw new Error('Unable to access your camera or microphone. Your browser is too old.');
}
}
return this.getLocalStream().catch((err) => {
console.info('Error get camera, trying with video option at null =>', err);
this.disableCameraStyle();
this.stopCamera();
return this.getLocalStream().then((stream : MediaStream) => {
this.hasCamera = false;
return stream;
}).catch((err) => {
this.disableMicrophoneStyle();
console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err);
throw err;
});
});
//TODO resize remote cam
/*console.log(this.localStream.getTracks());
let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video");
let {width, height} = videoMediaStreamTrack.getSettings();
console.info(`${width}x${height}`); // 6*/
}
private getLocalStream() : Promise<MediaStream> {
return navigator.mediaDevices.getUserMedia(this.constraintsMedia).then((stream : MediaStream) => {
this.localStream = stream;
this.myCamVideo.srcObject = this.localStream;
//FIX ME SOUNDMETER: check stalability of sound meter calculation
/*this.mySoundMeter = null;
if(this.constraintsMedia.audio){
this.mySoundMeter = new SoundMeter();
this.mySoundMeter.connectToSource(stream, new AudioContext());
}*/
return stream;
}).catch((err: Error) => {
throw err;
});
}
/**
* Stops the camera from filming
*/
public stopCamera(): void {
if (this.localStream) {
for (const track of this.localStream.getVideoTracks()) {
track.stop();
}
}
}
/**
* Stops the microphone from listening
*/
public stopMicrophone(): void {
if (this.localStream) {
for (const track of this.localStream.getAudioTracks()) {
track.stop();
}
}
//this.mySoundMeter?.stop();
}
setCamera(id: string): Promise<MediaStream> {
let video = this.constraintsMedia.video;
if (typeof(video) === 'boolean' || video === undefined) {
video = {}
}
video.deviceId = {
exact: id
};
return this.getCamera();
}
setMicrophone(id: string): Promise<MediaStream> {
let audio = this.constraintsMedia.audio;
if (typeof(audio) === 'boolean' || audio === undefined) {
audio = {}
}
audio.deviceId = {
exact: id
};
return this.getCamera();
} }
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){ addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){

View File

@ -14,6 +14,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
import {connectionManager} from "../Connexion/ConnectionManager"; import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager"; import {GameConnexionTypes} from "../Url/UrlManager";
import {blackListManager} from "./BlackListManager"; import {blackListManager} from "./BlackListManager";
import {get} from "svelte/store";
import {localStreamStore, obtainedMediaConstraintStore} from "../Stores/MediaStore";
export interface UserSimplePeerInterface{ export interface UserSimplePeerInterface{
userId: number; userId: number;
@ -82,11 +84,10 @@ export class SimplePeer {
}); });
mediaManager.showGameOverlay(); mediaManager.showGameOverlay();
mediaManager.getCamera().finally(() => {
//receive message start //receive message start
this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => { this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
this.receiveWebrtcStart(message); this.receiveWebrtcStart(message);
});
}); });
this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => { this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => {
@ -344,8 +345,15 @@ export class SimplePeer {
if (!PeerConnection) { if (!PeerConnection) {
throw new Error('While adding media, cannot find user with ID ' + userId); throw new Error('While adding media, cannot find user with ID ' + userId);
} }
const localStream: MediaStream | null = mediaManager.localStream;
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia}))); const result = get(localStreamStore);
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...result.constraints})));
if (result.type === 'error') {
return;
}
const localStream: MediaStream | null = result.stream;
if(!localStream){ if(!localStream){
return; return;

View File

@ -5,6 +5,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager"; import {blackListManager} from "./BlackListManager";
import type {Subscription} from "rxjs"; import type {Subscription} from "rxjs";
import type {UserSimplePeerInterface} from "./SimplePeer"; import type {UserSimplePeerInterface} from "./SimplePeer";
import {get} from "svelte/store";
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@ -191,7 +193,7 @@ export class VideoPeer extends Peer {
private pushVideoToRemoteUser() { private pushVideoToRemoteUser() {
try { try {
const localStream: MediaStream | null = mediaManager.localStream; const localStream: MediaStream | null = mediaManager.localStream;
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia}))); this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)})));
if(!localStream){ if(!localStream){
return; return;

View File

@ -10,6 +10,10 @@ import type { OpenTabEvent } from "./Api/Events/OpenTabEvent";
import type { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import type { GoToPageEvent } from "./Api/Events/GoToPageEvent";
import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent";
import {ExitUrlEvent} from "./Api/Events/ExitUrlEvent"; import {ExitUrlEvent} from "./Api/Events/ExitUrlEvent";
import type {PlaySoundEvent} from "./Api/Events/PlaySoundEvent";
import type {StopSoundEvent} from "./Api/Events/StopSoundEvent";
import type {LoadSoundEvent} from "./Api/Events/LoadSoundEvent";
import SoundConfig = Phaser.Types.Sound.SoundConfig;
interface WorkAdventureApi { interface WorkAdventureApi {
sendChatMessage(message: string, author: string): void; sendChatMessage(message: string, author: string): void;
@ -27,6 +31,7 @@ interface WorkAdventureApi {
displayBubble(): void; displayBubble(): void;
removeBubble(): void; removeBubble(): void;
exitUrl(url : string) : void; exitUrl(url : string) : void;
loadSound(url : string): Sound;
} }
declare global { declare global {
@ -81,6 +86,41 @@ class Popup {
} }
} }
export class Sound {
constructor(private url: string) {
window.parent.postMessage({
"type" : 'loadSound',
"data": {
url: this.url,
} as LoadSoundEvent
},'*');
}
public play(config : SoundConfig) {
window.parent.postMessage({
"type" : 'playSound',
"data": {
url: this.url,
config
} as PlaySoundEvent
},'*');
return this.url;
}
public stop() {
window.parent.postMessage({
"type" : 'stopSound',
"data": {
url: this.url,
} as StopSoundEvent
},'*');
return this.url;
}
}
window.WA = { window.WA = {
/** /**
* Send a message in the chat. * Send a message in the chat.
@ -125,7 +165,11 @@ window.WA = {
}, '*'); }, '*');
}, },
goToPage(url: string): void { loadSound(url: string) : Sound {
return new Sound(url);
},
goToPage(url : string) : void{
window.parent.postMessage({ window.parent.postMessage({
"type": 'goToPage', "type": 'goToPage',
"data": { "data": {

View File

@ -142,6 +142,11 @@ body .message-info.warning {
right: 15px; right: 15px;
bottom: 30px; bottom: 30px;
max-height: 20%; max-height: 20%;
transition: right 350ms;
}
#div-myCamVideo.hide {
right: -20vw;
} }
video#myCamVideo { video#myCamVideo {
@ -202,12 +207,12 @@ video#myCamVideo {
display: inline-flex; display: inline-flex;
bottom: 10px; bottom: 10px;
right: 15px; right: 15px;
width: 15vw; width: 180px;
height: 40px; height: 40px;
text-align: center; text-align: center;
align-content: center; align-content: center;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-end;
justify-items: center; justify-items: center;
} }
@ -220,7 +225,6 @@ video#myCamVideo {
width: auto; width: auto;
transform: translateY(20px); transform: translateY(20px);
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
margin-bottom: 20px;
margin: 0 4%; margin: 0 4%;
} }
@ -259,8 +263,13 @@ video#myCamVideo {
transition: all .2s; transition: all .2s;
/*right: 224px;*/ /*right: 224px;*/
} }
.btn-monitor.hide {
.btn-copy { transform: translateY(60px);
}
.btn-cam-action:hover .btn-monitor.hide{
transform: translateY(60px);
}
.btn-copy{
pointer-events: auto; pointer-events: auto;
transition: all .3s; transition: all .3s;
right: 44px; right: 44px;
@ -359,6 +368,8 @@ video#myCamVideo {
#myCamVideoSetup { #myCamVideoSetup {
width: 100%; width: 100%;
height: 100%; height: 100%;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
} }
.webrtcsetup.active { .webrtcsetup.active {

View File

@ -5,6 +5,12 @@ var targetObjectTutoBubble ='Tutobubble';
var targetObjectTutoChat ='tutoChat'; var targetObjectTutoChat ='tutoChat';
var targetObjectTutoExplanation ='tutoExplanation'; var targetObjectTutoExplanation ='tutoExplanation';
var popUpExplanation = undefined; var popUpExplanation = undefined;
var enterSoundUrl = "webrtc-in.mp3";
var exitSoundUrl = "webrtc-out.mp3";
var soundConfig = {
volume : 0.2,
loop : false
}
function launchTuto (){ function launchTuto (){
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [ WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
{ {
@ -25,7 +31,8 @@ function launchTuto (){
label: "Got it!", label: "Got it!",
className : "success",callback:(popup2 => { className : "success",callback:(popup2 => {
popup2.close(); popup2.close();
WA.restorePlayerControls(); WA.restorePlayerControl();
WA.loadSound(winSoundUrl).play(soundConfig);
}) })
} }
]) ])
@ -36,13 +43,14 @@ function launchTuto (){
} }
} }
]); ]);
WA.disablePlayerControls(); WA.disablePlayerControl();
} }
WA.onEnterZone('popupZone', () => { WA.onEnterZone('popupZone', () => {
WA.displayBubble(); WA.displayBubble();
WA.loadSound(enterSoundUrl).play(soundConfig);
if (!isFirstTimeTuto) { if (!isFirstTimeTuto) {
isFirstTimeTuto = true; isFirstTimeTuto = true;
launchTuto(); launchTuto();
@ -71,4 +79,5 @@ WA.onEnterZone('popupZone', () => {
WA.onLeaveZone('popupZone', () => { WA.onLeaveZone('popupZone', () => {
if (popUpExplanation !== undefined) popUpExplanation.close(); if (popUpExplanation !== undefined) popUpExplanation.close();
WA.removeBubble(); WA.removeBubble();
WA.loadSound(exitSoundUrl).play(soundConfig);
}) })

BIN
maps/Tuto/webrtc-in.mp3 Normal file

Binary file not shown.

BIN
maps/Tuto/webrtc-out.mp3 Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
maps/tests/Audience.mp3 Normal file

Binary file not shown.

44
maps/tests/SoundScript.js Normal file
View File

@ -0,0 +1,44 @@
var zonePlaySound = "PlaySound";
var zonePlaySoundLoop = "playSoundLoop";
var stopSound = "StopSound";
var loopConfig ={
volume : 0.5,
loop : true
}
var configBase = {
volume : 0.5,
loop : false
}
var enterSoundUrl = "webrtc-in.mp3";
var exitSoundUrl = "webrtc-out.mp3";
var winSoundUrl = "Win.ogg";
var enterSound;
var exitSound;
var winSound;
loadAllSounds();
winSound.play(configBase);
WA.onEnterZone(zonePlaySound, () => {
enterSound.play(configBase);
})
WA.onEnterZone(zonePlaySoundLoop, () => {
winSound.play(loopConfig);
})
WA.onLeaveZone(zonePlaySoundLoop, () => {
winSound.stop();
})
WA.onEnterZone('popupZone', () => {
});
WA.onLeaveZone('popupZone', () => {
})
function loadAllSounds(){
winSound = WA.loadSound(winSoundUrl);
enterSound = WA.loadSound(enterSoundUrl);
exitSound = WA.loadSound(exitSoundUrl);
}

154
maps/tests/SoundTest.json Normal file
View File

@ -0,0 +1,154 @@
{ "compressionlevel":-1,
"height":20,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":20,
"id":4,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":3,
"name":"playSound",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"PlaySound"
}],
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":6,
"name":"playSoundLoop",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"playSoundLoop"
}],
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":19.296875,
"id":2,
"name":"",
"rotation":0,
"text":
{
"text":"Play Sound",
"wrap":true
},
"type":"",
"visible":true,
"width":107.109375,
"x":258.4453125,
"y":197.018229166667
},
{
"height":19.296875,
"id":3,
"name":"",
"rotation":0,
"text":
{
"text":"Bonjour Monde",
"wrap":true
},
"type":"",
"visible":true,
"width":107.109375,
"x":-348.221354166667,
"y":257.018229166667
},
{
"height":55.296875,
"id":4,
"name":"",
"rotation":0,
"text":
{
"text":"Play Sound Loop\nexit Zone Stop Sound \n",
"wrap":true
},
"type":"",
"visible":true,
"width":176.442708333333,
"x":243.778645833333,
"y":368.3515625
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":5,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"SoundScript.js"
}],
"renderorder":"right-down",
"tiledversion":"1.5.0",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":20
}

BIN
maps/tests/Win.ogg Normal file

Binary file not shown.

View File

@ -42,6 +42,14 @@
<a href="#" class="testLink" data-testmap="script_api.json" target="_blank">Testing scripting API with a script</a> <a href="#" class="testLink" data-testmap="script_api.json" target="_blank">Testing scripting API with a script</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-scripting-sound"> Success <input type="radio" name="test-scripting-sound"> Failure <input type="radio" name="test-scripting-sound" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="SoundTest.json" target="_blank">Testing scripting API loadSound() function</a>
</td>
</tr>
<tr> <tr>
<td> <td>
<input type="radio" name="test-autoresize"> Success <input type="radio" name="test-autoresize"> Failure <input type="radio" name="test-autoresize" checked> Pending <input type="radio" name="test-autoresize"> Success <input type="radio" name="test-autoresize"> Failure <input type="radio" name="test-autoresize" checked> Pending

BIN
maps/tests/webrtc-in.mp3 Normal file

Binary file not shown.

BIN
maps/tests/webrtc-out.mp3 Normal file

Binary file not shown.

View File

@ -665,9 +665,9 @@ has@^1.0.3:
function-bind "^1.1.1" function-bind "^1.1.1"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.8" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
iconv-lite@^0.4.24: iconv-lite@^0.4.24:
version "0.4.24" version "0.4.24"

View File

@ -66,6 +66,15 @@ message ReportPlayerMessage {
string reportComment = 2; string reportComment = 2;
} }
message EmotePromptMessage {
string emote = 2;
}
message EmoteEventMessage {
int32 actorUserId = 1;
string emote = 2;
}
message QueryJitsiJwtMessage { message QueryJitsiJwtMessage {
string jitsiRoom = 1; string jitsiRoom = 1;
string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map! string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map!
@ -84,6 +93,7 @@ message ClientToServerMessage {
StopGlobalMessage stopGlobalMessage = 10; StopGlobalMessage stopGlobalMessage = 10;
ReportPlayerMessage reportPlayerMessage = 11; ReportPlayerMessage reportPlayerMessage = 11;
QueryJitsiJwtMessage queryJitsiJwtMessage = 12; QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
EmotePromptMessage emotePromptMessage = 13;
} }
} }
@ -122,6 +132,7 @@ message SubMessage {
UserJoinedMessage userJoinedMessage = 4; UserJoinedMessage userJoinedMessage = 4;
UserLeftMessage userLeftMessage = 5; UserLeftMessage userLeftMessage = 5;
ItemEventMessage itemEventMessage = 6; ItemEventMessage itemEventMessage = 6;
EmoteEventMessage emoteEventMessage = 7;
} }
} }
@ -247,6 +258,7 @@ message ServerToClientMessage {
WorldFullMessage worldFullMessage = 16; WorldFullMessage worldFullMessage = 16;
RefreshRoomMessage refreshRoomMessage = 17; RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18; WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19;
} }
} }
@ -317,6 +329,7 @@ message PusherToBackMessage {
QueryJitsiJwtMessage queryJitsiJwtMessage = 11; QueryJitsiJwtMessage queryJitsiJwtMessage = 11;
SendUserMessage sendUserMessage = 12; SendUserMessage sendUserMessage = 12;
BanUserMessage banUserMessage = 13; BanUserMessage banUserMessage = 13;
EmotePromptMessage emotePromptMessage = 14;
} }
} }
@ -334,6 +347,7 @@ message SubToPusherMessage {
ItemEventMessage itemEventMessage = 6; ItemEventMessage itemEventMessage = 6;
SendUserMessage sendUserMessage = 7; SendUserMessage sendUserMessage = 7;
BanUserMessage banUserMessage = 8; BanUserMessage banUserMessage = 8;
EmoteEventMessage emoteEventMessage = 9;
} }
} }

View File

@ -2097,9 +2097,9 @@ highlight.js@^9.12.0:
integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
version "2.8.8" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-tag@^2.0.0: html-tag@^2.0.0:
version "2.0.0" version "2.0.0"

View File

@ -12,7 +12,8 @@ import {
WebRtcSignalToServerMessage, WebRtcSignalToServerMessage,
PlayGlobalMessage, PlayGlobalMessage,
ReportPlayerMessage, ReportPlayerMessage,
QueryJitsiJwtMessage, SendUserMessage, ServerToClientMessage, CompanionMessage EmoteEventMessage,
QueryJitsiJwtMessage, SendUserMessage, ServerToClientMessage, CompanionMessage, EmotePromptMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {UserMovesMessage} from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb";
import {TemplatedApp} from "uWebSockets.js" import {TemplatedApp} from "uWebSockets.js"
@ -182,7 +183,7 @@ export class IoSocketController {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn('Cannot find user with uuid "'+userUuid+'". Performing an anonymous login instead.'); console.warn('Cannot find user with uuid "'+userUuid+'". Performing an anonymous login instead.');
} else if(err?.response?.status == 403) { } else if(err?.response?.status == 403) {
// If we get an HTTP 404, the world is full. We need to broadcast a special error to the client. // If we get an HTTP 403, the world is full. We need to broadcast a special error to the client.
// we finish immediately the upgrade then we will close the socket as soon as it starts opening. // we finish immediately the upgrade then we will close the socket as soon as it starts opening.
return res.upgrade({ return res.upgrade({
rejected: true, rejected: true,
@ -330,6 +331,8 @@ export class IoSocketController {
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
} else if (message.hasQueryjitsijwtmessage()){ } else if (message.hasQueryjitsijwtmessage()){
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
} else if (message.hasEmotepromptmessage()){
socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage);
} }
/* Ok is false if backpressure was built up, wait for drain */ /* Ok is false if backpressure was built up, wait for drain */

View File

@ -6,13 +6,11 @@ import {
PointMessage, PositionMessage, UserJoinedMessage, PointMessage, PositionMessage, UserJoinedMessage,
UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage,
ZoneMessage, ZoneMessage,
EmoteEventMessage,
CompanionMessage CompanionMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import * as messages_pb from "../Messages/generated/messages_pb";
import {ClientReadableStream} from "grpc"; import {ClientReadableStream} from "grpc";
import {PositionDispatcher} from "_Model/PositionDispatcher"; import {PositionDispatcher} from "_Model/PositionDispatcher";
import {socketManager} from "../Services/SocketManager";
import {ProtobufUtils} from "_Model/Websocket/ProtobufUtils";
import Debug from "debug"; import Debug from "debug";
const debug = Debug("zone"); const debug = Debug("zone");
@ -24,6 +22,7 @@ export interface ZoneEventListener {
onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void;
onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void;
onGroupLeaves(groupId: number, listener: ExSocketInterface): void; onGroupLeaves(groupId: number, listener: ExSocketInterface): void;
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void;
} }
/*export type EntersCallback = (thing: Movable, listener: User) => void; /*export type EntersCallback = (thing: Movable, listener: User) => void;
@ -184,6 +183,9 @@ export class Zone {
userDescriptor.update(userMovedMessage); userDescriptor.update(userMovedMessage);
this.notifyUserMove(userDescriptor); this.notifyUserMove(userDescriptor);
} else if(message.hasEmoteeventmessage()) {
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
this.notifyEmote(emoteEventMessage);
} else { } else {
throw new Error('Unexpected message'); throw new Error('Unexpected message');
} }
@ -262,6 +264,15 @@ export class Zone {
} }
} }
private notifyEmote(emoteMessage: EmoteEventMessage) {
for (const listener of this.listeners) {
if (listener.userId === emoteMessage.getActoruserid()) {
continue;
}
this.socketListener.onEmote(emoteMessage, listener);
}
}
/** /**
* Notify listeners of this zone that this group left * Notify listeners of this zone that this group left
*/ */

View File

@ -23,7 +23,8 @@ import {
WorldConnexionMessage, WorldConnexionMessage,
AdminPusherToBackMessage, AdminPusherToBackMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage, RefreshRoomMessage EmoteEventMessage,
UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage, RefreshRoomMessage, EmotePromptMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable";
@ -73,6 +74,7 @@ export class SocketManager implements ZoneEventListener {
client.adminConnection = adminRoomStream; client.adminConnection = adminRoomStream;
adminRoomStream.on('data', (message: ServerToAdminClientMessage) => { adminRoomStream.on('data', (message: ServerToAdminClientMessage) => {
if (message.hasUserjoinedroom()) { if (message.hasUserjoinedroom()) {
const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage;
if (!client.disconnecting) { if (!client.disconnecting) {
@ -254,6 +256,15 @@ export class SocketManager implements ZoneEventListener {
this.handleViewport(client, viewport.toObject()) this.handleViewport(client, viewport.toObject())
} }
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void {
const subMessage = new SubMessage();
subMessage.setEmoteeventmessage(emoteMessage);
emitInBatch(listener, subMessage);
}
// Useless now, will be useful again if we allow editing details in game // Useless now, will be useful again if we allow editing details in game
handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) {
const pusherToBackMessage = new PusherToBackMessage(); const pusherToBackMessage = new PusherToBackMessage();
@ -321,6 +332,7 @@ export class SocketManager implements ZoneEventListener {
const room: PusherRoom | undefined = this.rooms.get(socket.roomId); const room: PusherRoom | undefined = this.rooms.get(socket.roomId);
if (room) { if (room) {
debug('Leaving room %s.', socket.roomId); debug('Leaving room %s.', socket.roomId);
room.leave(socket); room.leave(socket);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(socket.roomId); this.rooms.delete(socket.roomId);
@ -578,6 +590,13 @@ export class SocketManager implements ZoneEventListener {
this.updateRoomWithAdminData(room); this.updateRoomWithAdminData(room);
} }
handleEmotePromptMessage(client: ExSocketInterface, emoteEventmessage: EmotePromptMessage) {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setEmotepromptmessage(emoteEventmessage);
client.backConnection.write(pusherToBackMessage);
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

View File

@ -1251,9 +1251,9 @@ has-values@^1.0.0:
kind-of "^4.0.0" kind-of "^4.0.0"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.8" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-errors@1.7.2: http-errors@1.7.2:
version "1.7.2" version "1.7.2"
@ -1704,9 +1704,9 @@ lodash.once@^4.0.0:
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
version "4.17.20" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
long@~3: long@~3:
version "3.2.0" version "3.2.0"

View File

@ -811,9 +811,9 @@ has@^1.0.3:
function-bind "^1.1.1" function-bind "^1.1.1"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.8" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-errors@1.7.2: http-errors@1.7.2:
version "1.7.2" version "1.7.2"