diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index 48a7bae9..3e4b0fff 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -2,7 +2,7 @@ name: Build, push and deploy Docker image
on:
push:
- branches: [master]
+ branches: [master, develop]
release:
types: [created]
pull_request:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2028e3b7..1dd2c973 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,12 +8,31 @@
### Updates
-- Mobile support has been improved
- - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
- - Mouse wheel support to zoom in / out
- - Pinch support on mobile to zoom in / out
- - Improved virtual joystick size (adapts to the zoom level)
+- 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.
+- Player names were improved. (@Kharhamel)
+ - We now create a GameObject.Text instead of GameObject.BitmapText
+ - now use the 'Press Start 2P' font family and added an outline
+ - As a result, we can now allow non-standard letters like french accents or chinese characters!
+- Added the contact card feature. (@Kharhamel)
+ - Click on another player to see its contact info.
+ - Premium-only feature unfortunately. I need to find a way to make it available for all.
+ - If no contact data is found (either because the user is anonymous or because no admin backend), display an error card.
+
+- Mobile support has been improved
+ - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
+ - Mouse wheel support to zoom in / out
+ - Pinch support on mobile to zoom in / out
+ - Improved virtual joystick size (adapts to the zoom level)
+- Redesigned intermediate scenes
+ - Redesigned Select Companion scene
+ - Redesigned Enter Your Name scene
+ - Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use
+- New scripting API features:
+ - Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes
diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts
index 4436fb60..22ea8ca5 100644
--- a/back/src/Model/GameRoom.ts
+++ b/back/src/Model/GameRoom.ts
@@ -2,12 +2,12 @@ import {PointInterface} from "./Websocket/PointInterface";
import {Group} from "./Group";
import {User, UserSocket} from "./User";
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 {Movable} from "_Model/Movable";
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
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 {ZoneSocket} from "src/RoomManager";
import {Admin} from "../Model/Admin";
@@ -51,8 +51,9 @@ export class GameRoom {
groupRadius: number,
onEnters: EntersCallback,
onMoves: MovesCallback,
- onLeaves: LeavesCallback)
- {
+ onLeaves: LeavesCallback,
+ onEmote: EmoteCallback,
+ ) {
this.roomId = roomId;
if (isRoomAnonymous(roomId)) {
@@ -74,7 +75,7 @@ export class GameRoom {
this.minDistance = minDistance;
this.groupRadius = groupRadius;
// 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[] {
@@ -88,7 +89,10 @@ export class GameRoom {
public getUserByUuid(uuid: string): User|undefined {
return this.usersByUuid.get(uuid);
}
-
+ public getUserById(id: number): User|undefined {
+ return this.users.get(id);
+ }
+
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User {
const positionMessage = joinRoomMessage.getPositionmessage();
if (positionMessage === undefined) {
@@ -325,4 +329,8 @@ export class GameRoom {
this.versionNumber++
return this.versionNumber;
}
+
+ public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
+ this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
+ }
}
diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts
index 6eff17a3..275bf9d0 100644
--- a/back/src/Model/PositionNotifier.ts
+++ b/back/src/Model/PositionNotifier.ts
@@ -8,10 +8,12 @@
* 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.
*/
-import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
+import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
import {Movable} from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface";
import {ZoneSocket} from "../RoomManager";
+import {User} from "_Model/User";
+import {EmoteEventMessage} from "../Messages/generated/messages_pb";
interface ZoneDescriptor {
i: number;
@@ -24,7 +26,7 @@ export class PositionNotifier {
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 {
@@ -77,7 +79,7 @@ export class PositionNotifier {
let zone = this.zones[j][i];
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;
}
return zone;
@@ -93,4 +95,11 @@ export class PositionNotifier {
const zone = this.getZone(x, y);
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);
+
+ }
}
diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts
index ca695317..ffb172bb 100644
--- a/back/src/Model/Zone.ts
+++ b/back/src/Model/Zone.ts
@@ -3,21 +3,19 @@ import {PositionInterface} from "_Model/PositionInterface";
import {Movable} from "./Movable";
import {Group} from "./Group";
import {ZoneSocket} from "../RoomManager";
+import {EmoteEventMessage} from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone|null, 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 EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export class Zone {
private things: Set = new Set();
private listeners: Set = new Set();
-
- /**
- * @param x For debugging purpose only
- * @param y For debugging purpose only
- */
- constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, public readonly x: number, public readonly y: number) {
- }
+
+
+ constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { }
/**
* A user/thing leaves the zone
@@ -41,9 +39,7 @@ export class Zone {
*/
private notifyLeft(thing: Movable, newZone: Zone|null) {
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) {
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);
}
}
@@ -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 {
return this.things;
}
@@ -119,4 +84,11 @@ export class Zone {
public removeListener(socket: ZoneSocket): void {
this.listeners.delete(socket);
}
+
+ public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) {
+ for (const listener of this.listeners) {
+ this.onEmote(emoteEventMessage, listener);
+ }
+
+ }
}
diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts
index 54215698..a0f983e0 100644
--- a/back/src/RoomManager.ts
+++ b/back/src/RoomManager.ts
@@ -5,12 +5,13 @@ import {
AdminPusherToBackMessage,
AdminRoomMessage,
BanMessage,
+ EmotePromptMessage,
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
PlayGlobalMessage,
PusherToBackMessage,
- QueryJitsiJwtMessage, RefreshRoomPromptMessage,
+ QueryJitsiJwtMessage, RefreshRoomPromptMessage, RequestVisitCardMessage,
ServerToAdminClientMessage,
ServerToClientMessage,
SilentMessage,
@@ -71,6 +72,10 @@ const roomManager: IRoomManagerServer = {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasQueryjitsijwtmessage()){
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
+ } else if (message.hasEmotepromptmessage()){
+ socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage);
+ } else if (message.hasRequestvisitcardmessage()) {
+ socketManager.handleRequestVisitCardMessage(room, user, message.getRequestvisitcardmessage() as RequestVisitCardMessage);
}else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) {
diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts
new file mode 100644
index 00000000..09b092bf
--- /dev/null
+++ b/back/src/Services/AdminApi.ts
@@ -0,0 +1,22 @@
+import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
+import Axios from "axios";
+
+
+class AdminApi {
+
+ fetchVisitCardUrl(membershipUuid: string): Promise {
+ if (ADMIN_API_URL) {
+ return Axios.get(ADMIN_API_URL + '/api/membership/'+membershipUuid,
+ {headers: {"Authorization": `${ADMIN_API_TOKEN}`}}
+ ).then((res) => {
+ return res.data;
+ }).catch(() => {
+ return 'INVALID';
+ });
+ } else {
+ return Promise.resolve('INVALID')
+ }
+ }
+}
+
+export const adminApi = new AdminApi();
diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts
index 647afc95..f8fe7cd3 100644
--- a/back/src/Services/SocketManager.ts
+++ b/back/src/Services/SocketManager.ts
@@ -26,7 +26,8 @@ import {
GroupLeftZoneMessage,
WorldFullWarningMessage,
UserLeftZoneMessage,
- BanUserMessage, RefreshRoomMessage,
+ EmoteEventMessage,
+ BanUserMessage, RefreshRoomMessage, EmotePromptMessage, RequestVisitCardMessage, VisitCardMessage,
} from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
@@ -50,6 +51,7 @@ import {Zone} from "_Model/Zone";
import Debug from "debug";
import {Admin} from "_Model/Admin";
import crypto from "crypto";
+import {adminApi} from "./AdminApi";
const debug = Debug('sockermanager');
@@ -67,6 +69,7 @@ export class SocketManager {
private rooms: Map = new Map();
constructor() {
+
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
gaugeManager.incNbClientPerRoomGauge(roomId);
});
@@ -263,7 +266,8 @@ export class SocketManager {
GROUP_RADIUS,
(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, 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();
this.rooms.set(roomId, world);
@@ -339,6 +343,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 {
const position = group.getPosition();
const pointMessage = new PointMessage();
@@ -751,6 +763,28 @@ export class SocketManager {
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);
+ }
+
+ async handleRequestVisitCardMessage(room: GameRoom, user: User, requestvisitcardmessage: RequestVisitCardMessage): Promise {
+ const targetUser = room.getUserById(requestvisitcardmessage.getTargetuserid());
+ if (!targetUser) {
+ throw 'Could not find user for id '+requestvisitcardmessage.getTargetuserid();
+ }
+ const url = await adminApi.fetchVisitCardUrl(targetUser.uuid);
+
+ const visitCardMessage = new VisitCardMessage();
+ visitCardMessage.setUrl(url);
+ const clientMessage = new ServerToClientMessage();
+ clientMessage.setVisitcardmessage(visitCardMessage);
+
+ user.socket.write(clientMessage);
+ }
}
export const socketManager = new SocketManager();
diff --git a/back/tests/GameRoomTest.ts b/back/tests/GameRoomTest.ts
index 45721334..6bdc6912 100644
--- a/back/tests/GameRoomTest.ts
+++ b/back/tests/GameRoomTest.ts
@@ -5,6 +5,7 @@ import {Group} from "../src/Model/Group";
import {User, UserSocket} from "_Model/User";
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
import Direction = PositionMessage.Direction;
+import {EmoteCallback} from "_Model/Zone";
function createMockUser(userId: number): User {
return {
@@ -33,6 +34,8 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
return joinRoomMessage;
}
+const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
+
describe("GameRoom", () => {
it("should connect user1 and user2", () => {
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));
@@ -101,7 +105,7 @@ describe("GameRoom", () => {
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));
diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts
index 5901202f..24b171d9 100644
--- a/back/tests/PositionNotifierTest.ts
+++ b/back/tests/PositionNotifierTest.ts
@@ -23,7 +23,7 @@ describe("PositionNotifier", () => {
moveTriggered = true;
}, (thing: Movable) => {
leaveTriggered = true;
- });
+ }, () => {});
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
@@ -98,7 +98,7 @@ describe("PositionNotifier", () => {
moveTriggered = true;
}, (thing: Movable) => {
leaveTriggered = true;
- });
+ }, () => {});
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
diff --git a/back/yarn.lock b/back/yarn.lock
index 9469a69d..8af760c8 100644
--- a/back/yarn.lock
+++ b/back/yarn.lock
@@ -1251,9 +1251,9 @@ has-values@^1.0.0:
kind-of "^4.0.0"
hosted-git-info@^2.1.4:
- version "2.8.8"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
- integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
+ version "2.8.9"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+ integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-errors@1.7.2:
version "1.7.2"
diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json
index 8d4db6cf..72d0aae4 100644
--- a/benchmark/package-lock.json
+++ b/benchmark/package-lock.json
@@ -230,9 +230,9 @@
}
},
"hosted-git-info": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
- "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
},
"indent-string": {
"version": "2.1.0",
diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock
index d93e3667..f1209dcf 100644
--- a/benchmark/yarn.lock
+++ b/benchmark/yarn.lock
@@ -169,8 +169,8 @@ graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
hosted-git-info@^2.1.4:
- version "2.8.8"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
+ version "2.8.9"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
indent-string@^2.1.0:
version "2.1.0"
diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html
index adbbfe44..aa63229f 100644
--- a/front/dist/index.tmpl.html
+++ b/front/dist/index.tmpl.html
@@ -29,7 +29,6 @@
-
WorkAdventure
@@ -47,33 +46,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -108,30 +80,17 @@
-
-
-
-
-
-
-
-
-
diff --git a/front/dist/resources/emotes/clap-emote.png b/front/dist/resources/emotes/clap-emote.png
new file mode 100644
index 00000000..a64f2e5f
Binary files /dev/null and b/front/dist/resources/emotes/clap-emote.png differ
diff --git a/front/dist/resources/emotes/hand-emote.png b/front/dist/resources/emotes/hand-emote.png
new file mode 100644
index 00000000..3bc01acf
Binary files /dev/null and b/front/dist/resources/emotes/hand-emote.png differ
diff --git a/front/dist/resources/emotes/heart-emote.png b/front/dist/resources/emotes/heart-emote.png
new file mode 100644
index 00000000..867d6be1
Binary files /dev/null and b/front/dist/resources/emotes/heart-emote.png differ
diff --git a/front/dist/resources/emotes/thanks-emote.png b/front/dist/resources/emotes/thanks-emote.png
new file mode 100644
index 00000000..8e326ed5
Binary files /dev/null and b/front/dist/resources/emotes/thanks-emote.png differ
diff --git a/front/dist/resources/emotes/thumb-down-emote.png b/front/dist/resources/emotes/thumb-down-emote.png
new file mode 100644
index 00000000..8ec7c961
Binary files /dev/null and b/front/dist/resources/emotes/thumb-down-emote.png differ
diff --git a/front/dist/resources/emotes/thumb-up-emote.png b/front/dist/resources/emotes/thumb-up-emote.png
new file mode 100644
index 00000000..eecb0e57
Binary files /dev/null and b/front/dist/resources/emotes/thumb-up-emote.png differ
diff --git a/front/dist/resources/fonts/fonts.css b/front/dist/resources/fonts/fonts.css
new file mode 100644
index 00000000..a3d3cf71
--- /dev/null
+++ b/front/dist/resources/fonts/fonts.css
@@ -0,0 +1,5 @@
+/*This file is a workaround to allow phaser to load directly this font */
+@font-face {
+ font-family: "Press Start 2P";
+ src: url("/fonts/press-start-2p-latin-400-normal.woff2") format('woff2');
+}
\ No newline at end of file
diff --git a/front/dist/resources/html/CustomCharacterScene.html b/front/dist/resources/html/CustomCharacterScene.html
deleted file mode 100644
index 0bc050ea..00000000
--- a/front/dist/resources/html/CustomCharacterScene.html
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
diff --git a/front/dist/resources/html/EnableCameraScene.html b/front/dist/resources/html/EnableCameraScene.html
deleted file mode 100644
index 2dda6cc1..00000000
--- a/front/dist/resources/html/EnableCameraScene.html
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
diff --git a/front/dist/resources/html/SelectCompanionScene.html b/front/dist/resources/html/SelectCompanionScene.html
deleted file mode 100644
index cffa7880..00000000
--- a/front/dist/resources/html/SelectCompanionScene.html
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
-
diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html
index 6abf2753..399cf349 100644
--- a/front/dist/resources/html/gameMenu.html
+++ b/front/dist/resources/html/gameMenu.html
@@ -1,7 +1,4 @@
-
-
diff --git a/front/dist/resources/html/loginScene.html b/front/dist/resources/html/loginScene.html
deleted file mode 100644
index 38e798e5..00000000
--- a/front/dist/resources/html/loginScene.html
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
diff --git a/front/dist/resources/html/selectCharacterScene.html b/front/dist/resources/html/selectCharacterScene.html
deleted file mode 100644
index c51731df..00000000
--- a/front/dist/resources/html/selectCharacterScene.html
+++ /dev/null
@@ -1,142 +0,0 @@
-
-
-
diff --git a/front/dist/resources/html/warningContainer.html b/front/dist/resources/html/warningContainer.html
index 4989c49d..832ac4da 100644
--- a/front/dist/resources/html/warningContainer.html
+++ b/front/dist/resources/html/warningContainer.html
@@ -1,7 +1,4 @@
\ No newline at end of file
diff --git a/front/src/Components/EnableCamera/EnableCameraScene.svelte b/front/src/Components/EnableCamera/EnableCameraScene.svelte
new file mode 100644
index 00000000..537e8bdb
--- /dev/null
+++ b/front/src/Components/EnableCamera/EnableCameraScene.svelte
@@ -0,0 +1,217 @@
+
+
+
+
+
+
diff --git a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte
new file mode 100644
index 00000000..84352ebb
--- /dev/null
+++ b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+ {#each [...Array(NB_BARS).keys()] as i}
+
+ {/each}
+
+
+
diff --git a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte
new file mode 100644
index 00000000..8f4de785
--- /dev/null
+++ b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/dist/resources/objects/help-setting-camera-permission-chrome.png b/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png
similarity index 100%
rename from front/dist/resources/objects/help-setting-camera-permission-chrome.png
rename to front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png
diff --git a/front/dist/resources/objects/help-setting-camera-permission-firefox.png b/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png
similarity index 100%
rename from front/dist/resources/objects/help-setting-camera-permission-firefox.png
rename to front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png
diff --git a/front/src/Components/Login/LoginScene.svelte b/front/src/Components/Login/LoginScene.svelte
new file mode 100644
index 00000000..dbe3daaf
--- /dev/null
+++ b/front/src/Components/Login/LoginScene.svelte
@@ -0,0 +1,123 @@
+
+
+
+
+
diff --git a/front/src/Components/MyCamera.svelte b/front/src/Components/MyCamera.svelte
new file mode 100644
index 00000000..3ff88d89
--- /dev/null
+++ b/front/src/Components/MyCamera.svelte
@@ -0,0 +1,46 @@
+
+
+
+
diff --git a/front/src/Components/SelectCompanion/SelectCompanionScene.svelte b/front/src/Components/SelectCompanion/SelectCompanionScene.svelte
new file mode 100644
index 00000000..205a18ee
--- /dev/null
+++ b/front/src/Components/SelectCompanion/SelectCompanionScene.svelte
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/front/src/Components/SoundMeterWidget.svelte b/front/src/Components/SoundMeterWidget.svelte
new file mode 100644
index 00000000..40c467b1
--- /dev/null
+++ b/front/src/Components/SoundMeterWidget.svelte
@@ -0,0 +1,53 @@
+
+
+
+
+ 1}>
+ 2}>
+ 3}>
+ 4}>
+ 5}>
+
diff --git a/front/src/Components/UI/AudioPlaying.svelte b/front/src/Components/UI/AudioPlaying.svelte
new file mode 100644
index 00000000..8889ac52
--- /dev/null
+++ b/front/src/Components/UI/AudioPlaying.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
Audio message
+
+
+
+
diff --git a/front/src/Components/UI/images/megaphone.svg b/front/src/Components/UI/images/megaphone.svg
new file mode 100644
index 00000000..708f860c
--- /dev/null
+++ b/front/src/Components/UI/images/megaphone.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/front/src/Components/VisitCard/VisitCard.svelte b/front/src/Components/VisitCard/VisitCard.svelte
new file mode 100644
index 00000000..8c3706b0
--- /dev/null
+++ b/front/src/Components/VisitCard/VisitCard.svelte
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+ {#if visitCardUrl === 'INVALID'}
+
+
+
+
+ Maybe he is offline, or this feature is deactivated.
+
+
+ {:else}
+
+ {/if}
+
+
+
+
+
diff --git a/front/src/Components/images/cinema-close.svg b/front/src/Components/images/cinema-close.svg
new file mode 100644
index 00000000..aa1d9b17
--- /dev/null
+++ b/front/src/Components/images/cinema-close.svg
@@ -0,0 +1,41 @@
+
+
+
diff --git a/front/dist/resources/logos/cinema.svg b/front/src/Components/images/cinema.svg
similarity index 100%
rename from front/dist/resources/logos/cinema.svg
rename to front/src/Components/images/cinema.svg
diff --git a/front/src/Components/images/logo.png b/front/src/Components/images/logo.png
new file mode 100644
index 00000000..f4440ad5
Binary files /dev/null and b/front/src/Components/images/logo.png differ
diff --git a/front/src/Components/images/microphone-close.svg b/front/src/Components/images/microphone-close.svg
new file mode 100644
index 00000000..16731829
--- /dev/null
+++ b/front/src/Components/images/microphone-close.svg
@@ -0,0 +1,27 @@
+
+
+
diff --git a/front/dist/resources/logos/microphone.svg b/front/src/Components/images/microphone.svg
similarity index 100%
rename from front/dist/resources/logos/microphone.svg
rename to front/src/Components/images/microphone.svg
diff --git a/front/dist/resources/logos/monitor-close.svg b/front/src/Components/images/monitor-close.svg
similarity index 100%
rename from front/dist/resources/logos/monitor-close.svg
rename to front/src/Components/images/monitor-close.svg
diff --git a/front/dist/resources/logos/monitor.svg b/front/src/Components/images/monitor.svg
similarity index 100%
rename from front/dist/resources/logos/monitor.svg
rename to front/src/Components/images/monitor.svg
diff --git a/front/src/Components/selectCharacter/SelectCharacterScene.svelte b/front/src/Components/selectCharacter/SelectCharacterScene.svelte
new file mode 100644
index 00000000..e227771c
--- /dev/null
+++ b/front/src/Components/selectCharacter/SelectCharacterScene.svelte
@@ -0,0 +1,92 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts
index 932fb1fc..8112ba17 100644
--- a/front/src/Connexion/ConnectionManager.ts
+++ b/front/src/Connexion/ConnectionManager.ts
@@ -4,7 +4,7 @@ import {RoomConnection} from "./RoomConnection";
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
import {localUserStore} from "./LocalUserStore";
-import {LocalUser} from "./LocalUser";
+import {CharacterTexture, LocalUser} from "./LocalUser";
import {Room} from "./Room";
@@ -46,8 +46,8 @@ class ConnectionManager {
urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
- const localUser = localUserStore.getLocalUser();
+ let localUser = localUserStore.getLocalUser();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
this.localUser = localUser;
try {
@@ -57,16 +57,42 @@ class ConnectionManager {
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
await this.anonymousLogin();
}
- } else {
+ }else{
await this.anonymousLogin();
}
- let roomId: string
+
+ localUser = localUserStore.getLocalUser();
+ if(!localUser){
+ throw "Error to store local user data";
+ }
+
+ let roomId: string;
if (connexionType === GameConnexionTypes.empty) {
roomId = START_ROOM_URL;
} else {
roomId = window.location.pathname + window.location.search + window.location.hash;
}
- return Promise.resolve(new Room(roomId));
+
+ //get detail map for anonymous login and set texture in local storage
+ const room = new Room(roomId);
+ const mapDetail = await room.getMapDetail();
+ if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
+ //check if texture was changed
+ if(localUser.textures.length === 0){
+ localUser.textures = mapDetail.textures;
+ }else{
+ mapDetail.textures.forEach((newTexture) => {
+ const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
+ if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
+ return;
+ }
+ localUser?.textures.push(newTexture)
+ });
+ }
+ this.localUser = localUser;
+ localUserStore.saveUser(localUser);
+ }
+ return Promise.resolve(room);
}
return Promise.reject(new Error('Invalid URL'));
diff --git a/front/src/Connexion/EmoteEventStream.ts b/front/src/Connexion/EmoteEventStream.ts
new file mode 100644
index 00000000..9a639697
--- /dev/null
+++ b/front/src/Connexion/EmoteEventStream.ts
@@ -0,0 +1,19 @@
+import {Subject} from "rxjs";
+
+interface EmoteEvent {
+ userId: number,
+ emoteName: string,
+}
+
+class EmoteEventStream {
+
+ private _stream:Subject = new Subject();
+ public stream = this._stream.asObservable();
+
+
+ fire(userId: number, emoteName:string) {
+ this._stream.next({userId, emoteName});
+ }
+}
+
+export const emoteEventStream = new EmoteEventStream();
\ No newline at end of file
diff --git a/front/src/Connexion/LocalUser.ts b/front/src/Connexion/LocalUser.ts
index 0793a938..c877d119 100644
--- a/front/src/Connexion/LocalUser.ts
+++ b/front/src/Connexion/LocalUser.ts
@@ -9,9 +9,8 @@ export interface CharacterTexture {
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
-export function isUserNameValid(value: string): boolean {
- const regexp = new RegExp('^[A-Za-z0-9]{1,'+maxUserNameLength+'}$');
- return regexp.test(value);
+export function isUserNameValid(value: unknown): boolean {
+ return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && value.indexOf(' ') === -1;
}
export function areCharacterLayersValid(value: string[] | null): boolean {
@@ -25,6 +24,6 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
}
export class LocalUser {
- constructor(public readonly uuid:string, public readonly jwtToken: string, public readonly textures: CharacterTexture[]) {
+ constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
}
}
diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts
index 05d94440..3ae8d2ed 100644
--- a/front/src/Connexion/Room.ts
+++ b/front/src/Connexion/Room.ts
@@ -1,10 +1,17 @@
import Axios from "axios";
import {PUSHER_URL} from "../Enum/EnvironmentVariable";
+import type {CharacterTexture} from "./LocalUser";
+
+export class MapDetail{
+ constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) {
+ }
+}
export class Room {
public readonly id: string;
public readonly isPublic: boolean;
private mapUrl: string|undefined;
+ private textures: CharacterTexture[]|undefined;
private instance: string|undefined;
private _search: URLSearchParams;
@@ -50,10 +57,10 @@ export class Room {
return {roomId, hash}
}
- public async getMapUrl(): Promise {
- return new Promise((resolve, reject) => {
- if (this.mapUrl !== undefined) {
- resolve(this.mapUrl);
+ public async getMapDetail(): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.mapUrl !== undefined && this.textures != undefined) {
+ resolve(new MapDetail(this.mapUrl, this.textures));
return;
}
@@ -61,7 +68,7 @@ export class Room {
const match = /_\/[^/]+\/(.+)/.exec(this.id);
if (!match) throw new Error('Could not extract url from "'+this.id+'"');
this.mapUrl = window.location.protocol+'//'+match[1];
- resolve(this.mapUrl);
+ resolve(new MapDetail(this.mapUrl, this.textures));
return;
} else {
// We have a private ID, we need to query the map URL from the server.
@@ -71,7 +78,7 @@ export class Room {
params: urlParts
}).then(({data}) => {
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
- resolve(data.mapUrl);
+ resolve(data);
return;
}).catch((reason) => {
reject(reason);
diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts
index 6edb9c45..ae9a1986 100644
--- a/front/src/Connexion/RoomConnection.ts
+++ b/front/src/Connexion/RoomConnection.ts
@@ -27,8 +27,10 @@ import {
SendJitsiJwtMessage,
CharacterLayerMessage,
PingMessage,
+ EmoteEventMessage,
+ EmotePromptMessage,
SendUserMessage,
- BanUserMessage
+ BanUserMessage, RequestVisitCardMessage
} from "../Messages/generated/messages_pb"
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
@@ -47,6 +49,8 @@ import {adminMessagesService} from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager";
+import {emoteEventStream} from "./EmoteEventStream";
+import {requestVisitCardsStore} from "../Stores/GameStore";
const manualPingDelay = 20000;
@@ -124,7 +128,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
- let event: string;
+ let event: string|null = null;
let payload;
if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED;
@@ -144,11 +148,16 @@ export class RoomConnection implements RoomConnection {
} else if (subMessage.hasItemeventmessage()) {
event = EventMessage.ITEM_EVENT;
payload = subMessage.getItemeventmessage();
+ } else if (subMessage.hasEmoteeventmessage()) {
+ const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
+ emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else {
throw new Error('Unexpected batch message type');
}
- this.dispatch(event, payload);
+ if (event) {
+ this.dispatch(event, payload);
+ }
}
} else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
@@ -195,6 +204,8 @@ export class RoomConnection implements RoomConnection {
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage();
+ } else if (message.hasVisitcardmessage()) {
+ requestVisitCardsStore.set(message?.getVisitcardmessage()?.getUrl() as unknown as string);
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else {
@@ -599,4 +610,24 @@ export class RoomConnection implements RoomConnection {
public isAdmin(): boolean {
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);
+ }
+
+ public requestVisitCardUrl(targetUserId: number): void {
+ const message = new RequestVisitCardMessage();
+ message.setTargetuserid(targetUserId);
+
+ const clientToServerMessage = new ClientToServerMessage();
+ clientToServerMessage.setRequestvisitcardmessage(message);
+
+ this.socket.send(clientToServerMessage.serializeBinary().buffer);
+ }
}
diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts
index 85b63335..73f6427c 100644
--- a/front/src/Enum/EnvironmentVariable.ts
+++ b/front/src/Enum/EnvironmentVariable.ts
@@ -14,6 +14,7 @@ const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
+export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true';
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) );
diff --git a/front/src/Phaser/Components/ChatModeIcon.ts b/front/src/Phaser/Components/ChatModeIcon.ts
index 932a4d88..69449a1d 100644
--- a/front/src/Phaser/Components/ChatModeIcon.ts
+++ b/front/src/Phaser/Components/ChatModeIcon.ts
@@ -1,3 +1,5 @@
+import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
+
export class ChatModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 3);
@@ -6,6 +8,6 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite {
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
- this.setDepth(99999);
+ this.setDepth(DEPTH_INGAME_TEXT_INDEX);
}
}
\ No newline at end of file
diff --git a/front/src/Phaser/Components/MobileJoystick.ts b/front/src/Phaser/Components/MobileJoystick.ts
index fced71da..b3fc021b 100644
--- a/front/src/Phaser/Components/MobileJoystick.ts
+++ b/front/src/Phaser/Components/MobileJoystick.ts
@@ -1,5 +1,6 @@
import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js';
import {waScaleManager} from "../Services/WaScaleManager";
+import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free
export const joystickBaseKey = 'joystickBase';
@@ -19,21 +20,21 @@ export class MobileJoystick extends VirtualJoystick {
x: -1000,
y: -1000,
radius: radius * window.devicePixelRatio,
- base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(99999),
- thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(99999),
+ base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
+ thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
enable: true,
dir: "8dir",
});
this.visible = false;
this.enable = false;
- this.scene.input.on('pointerdown', (pointer: { x: number; y: number; wasTouch: boolean; event: TouchEvent }) => {
+ this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (!pointer.wasTouch) {
return;
}
// Let's only display the joystick if there is one finger on the screen
- if (pointer.event.touches.length === 1) {
+ if ((pointer.event as TouchEvent).touches.length === 1) {
this.x = pointer.x;
this.y = pointer.y;
this.visible = true;
diff --git a/front/src/Phaser/Components/OpenChatIcon.ts b/front/src/Phaser/Components/OpenChatIcon.ts
index 1e9429e8..ab07a80c 100644
--- a/front/src/Phaser/Components/OpenChatIcon.ts
+++ b/front/src/Phaser/Components/OpenChatIcon.ts
@@ -1,4 +1,5 @@
import {discussionManager} from "../../WebRtc/DiscussionManager";
+import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
export const openChatIconName = 'openChatIcon';
export class OpenChatIcon extends Phaser.GameObjects.Image {
@@ -9,7 +10,7 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
- this.setDepth(99999);
+ this.setDepth(DEPTH_INGAME_TEXT_INDEX);
this.on("pointerup", () => discussionManager.showDiscussionPart());
}
diff --git a/front/src/Phaser/Components/PresentationModeIcon.ts b/front/src/Phaser/Components/PresentationModeIcon.ts
index 49ff2ea1..09c8beb5 100644
--- a/front/src/Phaser/Components/PresentationModeIcon.ts
+++ b/front/src/Phaser/Components/PresentationModeIcon.ts
@@ -1,3 +1,5 @@
+import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
+
export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 0);
@@ -6,6 +8,6 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
- this.setDepth(99999);
+ this.setDepth(DEPTH_INGAME_TEXT_INDEX);
}
}
\ No newline at end of file
diff --git a/front/src/Phaser/Components/RadialMenu.ts b/front/src/Phaser/Components/RadialMenu.ts
new file mode 100644
index 00000000..1094f73a
--- /dev/null
+++ b/front/src/Phaser/Components/RadialMenu.ts
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/front/src/Phaser/Components/SoundMeter.ts b/front/src/Phaser/Components/SoundMeter.ts
index 1d6f7eba..53802d31 100644
--- a/front/src/Phaser/Components/SoundMeter.ts
+++ b/front/src/Phaser/Components/SoundMeter.ts
@@ -1,3 +1,5 @@
+import type {IAnalyserNode, IAudioContext, IMediaStreamAudioSourceNode} from 'standardized-audio-context';
+
/**
* Class to measure the sound volume of a media stream
*/
@@ -5,10 +7,10 @@ export class SoundMeter {
private instant: number;
private clip: number;
//private script: ScriptProcessorNode;
- private analyser: AnalyserNode|undefined;
+ private analyser: IAnalyserNode|undefined;
private dataArray: Uint8Array|undefined;
- private context: AudioContext|undefined;
- private source: MediaStreamAudioSourceNode|undefined;
+ private context: IAudioContext|undefined;
+ private source: IMediaStreamAudioSourceNode|undefined;
constructor() {
this.instant = 0.0;
@@ -16,7 +18,7 @@ export class SoundMeter {
//this.script = context.createScriptProcessor(2048, 1, 1);
}
- private init(context: AudioContext) {
+ private init(context: IAudioContext) {
this.context = context;
this.analyser = this.context.createAnalyser();
@@ -25,8 +27,12 @@ export class SoundMeter {
this.dataArray = new Uint8Array(bufferLength);
}
- public connectToSource(stream: MediaStream, context: AudioContext): void
+ public connectToSource(stream: MediaStream, context: IAudioContext): void
{
+ if (this.source !== undefined) {
+ this.stop();
+ }
+
this.init(context);
this.source = this.context?.createMediaStreamSource(stream);
@@ -81,56 +87,3 @@ export class SoundMeter {
}
-
-// Meter class that generates a number correlated to audio volume.
-// The meter class itself displays nothing, but it makes the
-// instantaneous and time-decaying volumes available for inspection.
-// It also reports on the fraction of samples that were at or near
-// the top of the measurement range.
-/*function SoundMeter(context) {
- this.context = context;
- this.instant = 0.0;
- this.slow = 0.0;
- this.clip = 0.0;
- this.script = context.createScriptProcessor(2048, 1, 1);
- const that = this;
- this.script.onaudioprocess = function(event) {
- const input = event.inputBuffer.getChannelData(0);
- let i;
- let sum = 0.0;
- let clipcount = 0;
- for (i = 0; i < input.length; ++i) {
- sum += input[i] * input[i];
- if (Math.abs(input[i]) > 0.99) {
- clipcount += 1;
- }
- }
- that.instant = Math.sqrt(sum / input.length);
- that.slow = 0.95 * that.slow + 0.05 * that.instant;
- that.clip = clipcount / input.length;
- };
-}
-
-SoundMeter.prototype.connectToSource = function(stream, callback) {
- console.log('SoundMeter connecting');
- try {
- this.mic = this.context.createMediaStreamSource(stream);
- this.mic.connect(this.script);
- // necessary to make sample run, but should not be.
- this.script.connect(this.context.destination);
- if (typeof callback !== 'undefined') {
- callback(null);
- }
- } catch (e) {
- console.error(e);
- if (typeof callback !== 'undefined') {
- callback(e);
- }
- }
-};
-
-SoundMeter.prototype.stop = function() {
- this.mic.disconnect();
- this.script.disconnect();
-};
-*/
diff --git a/front/src/Phaser/Components/SoundMeterSprite.ts b/front/src/Phaser/Components/SoundMeterSprite.ts
deleted file mode 100644
index 582617f9..00000000
--- a/front/src/Phaser/Components/SoundMeterSprite.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import Container = Phaser.GameObjects.Container;
-import type {Scene} from "phaser";
-import GameObject = Phaser.GameObjects.GameObject;
-import Rectangle = Phaser.GameObjects.Rectangle;
-
-
-export class SoundMeterSprite extends Container {
- private rectangles: Rectangle[] = new Array();
- private static readonly NB_BARS = 20;
-
- constructor(scene: Scene, x?: number, y?: number, children?: GameObject[]) {
- super(scene, x, y, children);
-
- for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
- const rectangle = new Rectangle(scene, i * 13, 0, 10, 20, (Math.round(255 - i * 255 / SoundMeterSprite.NB_BARS) << 8) + (Math.round(i * 255 / SoundMeterSprite.NB_BARS) << 16));
- this.add(rectangle);
- this.rectangles.push(rectangle);
- }
- }
-
- /**
- * A number between 0 and 100
- *
- * @param volume
- */
- public setVolume(volume: number): void {
-
- const normalizedVolume = volume / 100 * SoundMeterSprite.NB_BARS;
- for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
- if (normalizedVolume < i) {
- this.rectangles[i].alpha = 0.5;
- } else {
- this.rectangles[i].alpha = 1;
- }
- }
- }
-
- public getWidth(): number {
- return SoundMeterSprite.NB_BARS * 13;
- }
-
-
-
-}
diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts
index 9f2bd1fd..9c3273ec 100644
--- a/front/src/Phaser/Entity/Character.ts
+++ b/front/src/Phaser/Entity/Character.ts
@@ -1,10 +1,15 @@
import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation";
import {SpeechBubble} from "./SpeechBubble";
-import BitmapText = Phaser.GameObjects.BitmapText;
+import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite;
import {TextureError} from "../../Exception/TextureError";
import {Companion} from "../Companion/Companion";
+import type {GameScene} from "../Game/GameScene";
+import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
+import {waScaleManager} from "../Services/WaScaleManager";
+
+const playerNameY = - 25;
interface AnimationData {
key: string;
@@ -14,24 +19,30 @@ interface AnimationData {
frames : number[]
}
+const interactiveRadius = 35;
+
export abstract class Character extends Container {
private bubble: SpeechBubble|null = null;
- private readonly playerName: BitmapText;
+ private readonly playerName: Text;
public PlayerValue: string;
public sprites: Map;
private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
//private teleportation: Sprite;
private invisible: boolean;
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,
y: number,
texturesPromise: Promise,
name: string,
direction: PlayerAnimationDirections,
moving: boolean,
- frame?: string | number
+ frame: string | number,
+ companion: string|null,
+ companionTexturePromise?: Promise
) {
super(scene, x, y/*, texture, frame*/);
this.PlayerValue = name;
@@ -44,20 +55,19 @@ export abstract class Character extends Container {
this.addTextures(textures, frame);
this.invisible = false
})
-
- /*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3);
- this.teleportation.setInteractive();
- this.teleportation.visible = false;
- this.teleportation.on('pointerup', () => {
- this.report.visible = false;
- this.teleportation.visible = false;
- });
- this.add(this.teleportation);*/
-
- this.playerName = new BitmapText(scene, 0, - 25, 'main_font', name, 7);
- this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999);
+
+ this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"});
+ this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName);
+ if (this.isClickable()) {
+ this.setInteractive({
+ hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius),
+ hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
+ useHandCursor: true,
+ });
+ }
+
scene.add.existing(this);
this.scene.physics.world.enableBody(this);
@@ -69,6 +79,10 @@ export abstract class Character extends Container {
this.setDepth(-1);
this.playAnimation(direction, moving);
+
+ if (typeof companion === 'string') {
+ this.addCompanion(companion, companionTexturePromise);
+ }
}
public addCompanion(name: string, texturePromise?: Promise): void {
@@ -76,6 +90,8 @@ export abstract class Character extends Container {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
}
}
+
+ public abstract isClickable(): boolean;
public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) {
@@ -83,7 +99,6 @@ export abstract class Character extends Container {
throw new TextureError('texture not found');
}
const sprite = new Sprite(this.scene, 0, 0, texture, frame);
- sprite.setInteractive({useHandCursor: true});
this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => {
this.scene.anims.create({
@@ -225,7 +240,84 @@ export abstract class Character extends Container {
this.scene.sys.updateList.remove(sprite);
}
}
+ this.list.forEach(objectContaining => objectContaining.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);
}
}
diff --git a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts
index 6d8b84c2..95f00a9e 100644
--- a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts
+++ b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts
@@ -2,6 +2,10 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type {CharacterTexture} from "../../Connexion/LocalUser";
import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures";
+export interface FrameConfig {
+ frameWidth: number,
+ frameHeight: number,
+}
export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => {
const returnArray:BodyResourceDescriptionInterface[][] = [];
@@ -26,7 +30,10 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise => {
const name = 'customCharacterTexture'+texture.id;
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): Promise => {
@@ -36,7 +43,10 @@ export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, textur
//TODO refactor
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
- promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor));
+ promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, {
+ frameWidth: 32,
+ frameHeight: 32
+ }));
}
}catch (err){
console.error(err);
@@ -69,15 +79,12 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio
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((res) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor);
}
- loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, {
- frameWidth: 32,
- frameHeight: 32
- });
+ loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor));
});
}
diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts
index 4787d1f2..c2384357 100644
--- a/front/src/Phaser/Entity/RemotePlayer.ts
+++ b/front/src/Phaser/Entity/RemotePlayer.ts
@@ -3,6 +3,8 @@ import type {PointInterface} from "../../Connexion/ConnexionModels";
import {Character} from "../Entity/Character";
import type {PlayerAnimationDirections} from "../Player/Animation";
+export const playerClickedEvent = 'playerClickedEvent';
+
/**
* Class representing the sprite of a remote player (a player that plays on another computer)
*/
@@ -21,14 +23,14 @@ export class RemotePlayer extends Character {
companion: string|null,
companionTexturePromise?: Promise
) {
- super(Scene, x, y, texturesPromise, name, direction, moving, 1);
-
+ super(Scene, x, y, texturesPromise, name, direction, moving, 1, companion, companionTexturePromise);
+
//set data
this.userId = userId;
-
- if (typeof companion === 'string') {
- this.addCompanion(companion, companionTexturePromise);
- }
+
+ this.on('pointerdown', () => {
+ this.emit(playerClickedEvent, this.userId);
+ })
}
updatePosition(position: PointInterface): void {
@@ -42,4 +44,8 @@ export class RemotePlayer extends Character {
this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections);
}
}
+
+ isClickable(): boolean {
+ return true; //todo: make remote players clickable if they are logged in.
+ }
}
diff --git a/front/src/Phaser/Game/DepthIndexes.ts b/front/src/Phaser/Game/DepthIndexes.ts
new file mode 100644
index 00000000..d2d38328
--- /dev/null
+++ b/front/src/Phaser/Game/DepthIndexes.ts
@@ -0,0 +1,8 @@
+//this file contains all the depth indexes which will be used in our game
+
+export const DEPTH_TILE_INDEX = 0;
+//Note: Player characters use their y coordinate as their depth to simulate a perspective.
+//See the Character class.
+export const DEPTH_OVERLAY_INDEX = 10000;
+export const DEPTH_INGAME_TEXT_INDEX = 100000;
+export const DEPTH_UI_INDEX = 1000000;
diff --git a/front/src/Phaser/Game/DirtyScene.ts b/front/src/Phaser/Game/DirtyScene.ts
index e44ce07b..3e1f3cdf 100644
--- a/front/src/Phaser/Game/DirtyScene.ts
+++ b/front/src/Phaser/Game/DirtyScene.ts
@@ -12,6 +12,7 @@ export abstract class DirtyScene extends ResizableScene {
private isAlreadyTracking: boolean = false;
protected dirty: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.
@@ -37,6 +38,27 @@ export abstract class DirtyScene extends ResizableScene {
this.events.on(Events.RENDER, () => {
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 {
@@ -47,7 +69,7 @@ export abstract class DirtyScene extends ResizableScene {
return this.dirty || this.objectListChanged;
}
- public onResize(ev: UIEvent): void {
+ public onResize(): void {
this.objectListChanged = true;
}
}
diff --git a/front/src/Phaser/Game/EmoteManager.ts b/front/src/Phaser/Game/EmoteManager.ts
new file mode 100644
index 00000000..2e0bbd67
--- /dev/null
+++ b/front/src/Phaser/Game/EmoteManager.ts
@@ -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((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 {
+ 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 {
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/front/src/Phaser/Game/Game.ts b/front/src/Phaser/Game/Game.ts
index 01aecf9f..e267e80a 100644
--- a/front/src/Phaser/Game/Game.ts
+++ b/front/src/Phaser/Game/Game.ts
@@ -21,14 +21,22 @@ export class Game extends Phaser.Game {
constructor(GameConfig: Phaser.Types.Core.GameConfig) {
super(GameConfig);
- window.addEventListener('resize', (event) => {
+ this.scale.on(Phaser.Scale.Events.RESIZE, () => {
+ for (const scene of this.scene.getScenes(true)) {
+ if (scene instanceof ResizableScene) {
+ scene.onResize();
+ }
+ }
+ })
+
+ /*window.addEventListener('resize', (event) => {
// Let's trigger the onResize method of any active scene that is a ResizableScene
for (const scene of this.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
scene.onResize(event);
}
}
- });
+ });*/
}
public step(time: number, delta: number)
diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts
index 2a1d3d8a..cd2575c0 100644
--- a/front/src/Phaser/Game/GameManager.ts
+++ b/front/src/Phaser/Game/GameManager.ts
@@ -2,11 +2,13 @@ import {GameScene} from "./GameScene";
import {connectionManager} from "../../Connexion/ConnectionManager";
import type {Room} from "../../Connexion/Room";
import {MenuScene, MenuSceneName} from "../Menu/MenuScene";
-import {HelpCameraSettingsScene, HelpCameraSettingsSceneName} from "../Menu/HelpCameraSettingsScene";
import {LoginSceneName} from "../Login/LoginScene";
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene";
import {EnableCameraSceneName} from "../Login/EnableCameraScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
+import {get} from "svelte/store";
+import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
+import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
export interface HasMovedEvent {
direction: string;
@@ -76,11 +78,11 @@ export class GameManager {
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise {
const roomID = room.id;
- const mapUrl = await room.getMapUrl();
+ const mapDetail = await room.getMapDetail();
const gameIndex = scenePlugin.getIndex(roomID);
if(gameIndex === -1){
- const game : Phaser.Scene = new GameScene(room, mapUrl);
+ const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
scenePlugin.add(roomID, game, false);
}
}
@@ -89,7 +91,11 @@ export class GameManager {
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
scenePlugin.launch(MenuSceneName);
- scenePlugin.launch(HelpCameraSettingsSceneName);//700
+
+ if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
+ helpCameraSettingsVisibleStore.set(true);
+ localUserStore.setHelpCameraSettingsShown();
+ }
}
public gameSceneIsCreated(scene: GameScene) {
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index 748897c5..e6e40df6 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -9,7 +9,7 @@ import type {
PositionInterface,
RoomJoinedMessageInterface
} from "../../Connexion/ConnexionModels";
-import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
+import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player";
import {
DEBUG_MODE,
JITSI_PRIVATE_MODE,
@@ -29,7 +29,7 @@ import type {AddPlayerInterface} from "./AddPlayerInterface";
import {PlayerAnimationDirections} from "../Player/Animation";
import {PlayerMovement} from "./PlayerMovement";
import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator";
-import {RemotePlayer} from "../Entity/RemotePlayer";
+import {playerClickedEvent, RemotePlayer} from "../Entity/RemotePlayer";
import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
@@ -52,6 +52,7 @@ import {mediaManager} from "../../WebRtc/MediaManager";
import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
import type {ActionableItem} from "../Items/ActionableItem";
import {UserInputManager} from "../UserInput/UserInputManager";
+import {soundManager} from "./SoundManager";
import type {UserMovedMessage} from "../../Messages/generated/messages_pb";
import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils";
import {connectionManager} from "../../Connexion/ConnectionManager";
@@ -90,7 +91,10 @@ import {TextUtils} from "../Components/TextUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
+import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
+import {peerStore} from "../../Stores/PeerStore";
+import {EmoteManager} from "./EmoteManager";
export interface GameSceneInitInterface {
initPosition: PointInterface|null,
@@ -131,7 +135,7 @@ const defaultStartLayerName = 'start';
export class GameScene extends DirtyScene implements CenterListener {
Terrains : Array;
- CurrentPlayer!: CurrentGamerInterface;
+ CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey : Map = new Map();
Map!: Phaser.Tilemaps.Tilemap;
@@ -156,6 +160,7 @@ export class GameScene extends DirtyScene implements CenterListener {
private createPromise: Promise;
private createPromiseResolve!: (value?: void | PromiseLike) => void;
private iframeSubscriptionList! : Array;
+ private peerStoreUnsubscribe!: () => void;
MapUrlFile: string;
RoomId: string;
instance: string;
@@ -186,9 +191,8 @@ export class GameScene extends DirtyScene implements CenterListener {
private popUpElements : Map = new Map();
private originalMapUrl: string|undefined;
private pinchManager: PinchManager|undefined;
- private physicsEnabled: boolean = true;
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) {
super({
@@ -208,7 +212,6 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connectionAnswerPromise = new Promise((resolve, reject): void => {
this.connectionAnswerPromiseResolve = resolve;
});
- this.onVisibilityChangeCallback = this.onVisibilityChange.bind(this);
}
//hook preload scene
@@ -226,6 +229,11 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.image(joystickBaseKey, joystickBaseImg);
this.load.image(joystickThumbKey, joystickThumbImg);
}
+ this.load.audio('audio-webrtc-in', '/resources/objects/webrtc-in.mp3');
+ this.load.audio('audio-webrtc-out', '/resources/objects/webrtc-out.mp3');
+ //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3');
+ this.sound.pauseOnBlur = false;
+
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 (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) {
@@ -272,6 +280,14 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32});
this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (this.load as any).rexWebFont({
+ custom: {
+ families: ['Press Start 2P'],
+ urls: ['/resources/fonts/fonts.css'],
+ testString: 'abcdefg'
+ },
+ });
//this function must stay at the end of preload function
addLoader(this);
@@ -421,7 +437,7 @@ export class GameScene extends DirtyScene implements CenterListener {
}
}
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
- depth = 10000;
+ depth = DEPTH_OVERLAY_INDEX;
}
if (layer.type === 'objectgroup') {
for (const object of layer.objects) {
@@ -508,7 +524,22 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connect();
}
- document.addEventListener('visibilitychange', this.onVisibilityChangeCallback);
+ this.emoteManager = new EmoteManager(this);
+
+ let oldPeerNumber = 0;
+ this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
+ const newPeerNumber = peers.size;
+ if (newPeerNumber > oldPeerNumber) {
+ this.sound.play('audio-webrtc-in', {
+ volume: 0.2
+ });
+ } else if (newPeerNumber < oldPeerNumber) {
+ this.sound.play('audio-webrtc-out', {
+ volume: 0.2
+ });
+ }
+ oldPeerNumber = newPeerNumber;
+ });
}
/**
@@ -613,6 +644,7 @@ export class GameScene extends DirtyScene implements CenterListener {
// When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
+ peerStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
@@ -630,7 +662,6 @@ export class GameScene extends DirtyScene implements CenterListener {
self.chatModeSprite.setVisible(false);
self.openChatIcon.setVisible(false);
audioManager.restoreVolume();
- self.onVisibilityChange();
}
}
})
@@ -868,9 +899,28 @@ ${escapedMessage}
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.userInputManager.restoreControls();
}));
+
let scriptedBubbleSprite : Sprite;
this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{
scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white');
@@ -930,12 +980,14 @@ ${escapedMessage}
this.messageSubscription?.unsubscribe();
this.userInputManager.destroy();
this.pinchManager?.destroy();
+ this.emoteManager.destroy();
+ this.peerStoreUnsubscribe();
+
+ mediaManager.hideGameOverlay();
for(const iframeEvents of this.iframeSubscriptionList){
iframeEvents.unsubscribe();
}
-
- document.removeEventListener('visibilitychange', this.onVisibilityChangeCallback);
}
private removeAllRemotePlayers(): void {
@@ -1088,8 +1140,6 @@ ${escapedMessage}
}
createCollisionWithPlayer() {
- this.physics.disableUpdate();
- this.physicsEnabled = false;
//add collision layer
this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => {
this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => {
@@ -1123,6 +1173,15 @@ ${escapedMessage}
this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
);
+ this.CurrentPlayer.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
+ if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
+ return; //we don't want the menu to open when pinching on a touch screen.
+ }
+ this.emoteManager.getMenuImages().then((emoteMenuElements) => this.CurrentPlayer.openOrCloseEmoteMenu(emoteMenuElements))
+ })
+ this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
+ this.connection?.emitEmoteEvent(emoteKey);
+ })
}catch (err){
if(err instanceof TextureError) {
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
@@ -1223,20 +1282,7 @@ ${escapedMessage}
this.dirty = false;
mediaManager.updateScene();
this.currentTick = time;
- if (this.CurrentPlayer.isMoving()) {
- this.dirty = true;
- }
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
while (this.pendingEvents.length !== 0) {
@@ -1333,6 +1379,9 @@ ${escapedMessage}
addPlayerData.companion,
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
);
+ player.on(playerClickedEvent, (userID:number) => {
+ this.connection?.requestVisitCardUrl(userID);
+ })
this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player);
player.updatePosition(addPlayerData.position);
@@ -1436,8 +1485,8 @@ ${escapedMessage}
this.connection?.emitActionableEvent(itemId, eventName, state, parameters);
}
- public onResize(ev: UIEvent): void {
- super.onResize(ev);
+ public onResize(): void {
+ super.onResize();
this.reposition();
// Send new viewport to server
@@ -1504,8 +1553,6 @@ ${escapedMessage}
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
this.stopJitsi();
});
-
- this.onVisibilityChange();
}
public stopJitsi(): void {
@@ -1514,7 +1561,6 @@ ${escapedMessage}
mediaManager.showGameOverlay();
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
- this.onVisibilityChange();
}
//todo: put this into an 'orchestrator' scene (EntryScene?)
@@ -1554,20 +1600,4 @@ ${escapedMessage}
waScaleManager.zoomModifier *= zoomFactor;
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();
- }
- }
- }
}
diff --git a/front/src/Phaser/Game/SoundManager.ts b/front/src/Phaser/Game/SoundManager.ts
new file mode 100644
index 00000000..47614fca
--- /dev/null
+++ b/front/src/Phaser/Game/SoundManager.ts
@@ -0,0 +1,39 @@
+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> = new Map>();
+ public loadSound (loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string) : Promise {
+ let soundPromise = this.soundPromises.get(soundUrl);
+ if (soundPromise !== undefined) {
+ return soundPromise;
+ }
+ soundPromise = new Promise((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 {
+ 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();
diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts
index 8b9a9a7a..3d85cdd5 100644
--- a/front/src/Phaser/Login/CustomizeScene.ts
+++ b/front/src/Phaser/Login/CustomizeScene.ts
@@ -11,6 +11,10 @@ import {AbstractCharacterScene} from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import { MenuScene } from "../Menu/MenuScene";
import { SelectCharacterSceneName } from "./SelectCharacterScene";
+import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
+import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
+import {waScaleManager} from "../Services/WaScaleManager";
+import {isMobile} from "../../Enum/EnvironmentVariable";
export const CustomizeSceneName = "CustomizeScene";
@@ -22,10 +26,10 @@ export class CustomizeScene extends AbstractCharacterScene {
private selectedLayers: number[] = [0];
private containersRow: Container[][] = [];
- private activeRow:number = 0;
+ public activeRow:number = 0;
private layers: BodyResourceDescriptionInterface[][] = [];
- private customizeSceneElement!: Phaser.GameObjects.DOMElement;
+ protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
constructor() {
super({
@@ -36,7 +40,6 @@ export class CustomizeScene extends AbstractCharacterScene {
preload() {
this.load.html(customizeSceneKey, 'resources/html/CustomCharacterScene.html');
- this.layers = loadAllLayers(this.load);
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if(bodyResourceDescription.level == undefined || bodyResourceDescription.level < 0 || bodyResourceDescription.level > 5 ){
@@ -44,43 +47,28 @@ export class CustomizeScene extends AbstractCharacterScene {
}
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
});
+ this.lazyloadingAttempt = true;
});
+ this.layers = loadAllLayers(this.load);
+ this.lazyloadingAttempt = false;
+
+
//this function must stay at the end of preload function
addLoader(this);
}
create() {
- this.customizeSceneElement = this.add.dom(-1000, 0).createFromCache(customizeSceneKey);
- this.centerXDomElement(this.customizeSceneElement, 150);
- MenuScene.revealMenusAfterInit(this.customizeSceneElement, customizeSceneKey);
-
- this.customizeSceneElement.addListener('click');
- this.customizeSceneElement.on('click', (event:MouseEvent) => {
- event.preventDefault();
- if((event?.target as HTMLInputElement).id === 'customizeSceneButtonLeft') {
- this.moveCursorHorizontally(-1);
- }else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonRight') {
- this.moveCursorHorizontally(1);
- }else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonDown') {
- this.moveCursorVertically(1);
- }else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonUp') {
- this.moveCursorVertically(-1);
- }else if((event?.target as HTMLInputElement).id === 'customizeSceneFormBack') {
- if(this.activeRow > 0){
- this.moveCursorVertically(-1);
- }else{
- this.backToPreviousScene();
- }
- }else if((event?.target as HTMLButtonElement).id === 'customizeSceneFormSubmit') {
- if(this.activeRow < 5){
- this.moveCursorVertically(1);
- }else{
- this.nextSceneToCamera();
- }
- }
+ customCharacterSceneVisibleStore.set(true);
+ this.events.addListener('wake', () => {
+ waScaleManager.saveZoom();
+ waScaleManager.zoomModifier = isMobile() ? 3 : 1;
+ customCharacterSceneVisibleStore.set(true);
});
+ waScaleManager.saveZoom();
+ waScaleManager.zoomModifier = isMobile() ? 3 : 1;
+
this.Rectangle = this.add.rectangle(this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.y + this.cameras.main.height / 3, 32, 33)
this.Rectangle.setStrokeStyle(2, 0xFFFFFF);
this.add.existing(this.Rectangle);
@@ -116,7 +104,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.onResize();
}
- private moveCursorHorizontally(index: number): void {
+ public moveCursorHorizontally(index: number): void {
this.selectedLayers[this.activeRow] += index;
if (this.selectedLayers[this.activeRow] < 0) {
this.selectedLayers[this.activeRow] = 0
@@ -128,27 +116,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.saveInLocalStorage();
}
- private moveCursorVertically(index:number): void {
-
- if(index === -1 && this.activeRow === 5){
- const button = this.customizeSceneElement.getChildByID('customizeSceneFormSubmit') as HTMLButtonElement;
- button.innerHTML = `Next `;
- }
-
- if(index === 1 && this.activeRow === 4){
- const button = this.customizeSceneElement.getChildByID('customizeSceneFormSubmit') as HTMLButtonElement;
- button.innerText = 'Finish';
- }
-
- if(index === -1 && this.activeRow === 1){
- const button = this.customizeSceneElement.getChildByID('customizeSceneFormBack') as HTMLButtonElement;
- button.innerText = `Return`;
- }
-
- if(index === 1 && this.activeRow === 0){
- const button = this.customizeSceneElement.getChildByID('customizeSceneFormBack') as HTMLButtonElement;
- button.innerHTML = `Back `;
- }
+ public moveCursorVertically(index:number): void {
this.activeRow += index;
if (this.activeRow < 0) {
@@ -262,6 +230,10 @@ export class CustomizeScene extends AbstractCharacterScene {
update(time: number, delta: number): void {
+ if(this.lazyloadingAttempt){
+ this.moveLayers();
+ this.lazyloadingAttempt = false;
+ }
}
public onResize(): void {
@@ -269,8 +241,6 @@ export class CustomizeScene extends AbstractCharacterScene {
this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2;
this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 3;
-
- this.centerXDomElement(this.customizeSceneElement, 150);
}
private nextSceneToCamera(){
@@ -288,12 +258,16 @@ export class CustomizeScene extends AbstractCharacterScene {
gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName);
- this.scene.remove(SelectCharacterSceneName);
+ waScaleManager.restoreZoom();
+ this.events.removeListener('wake');
gameManager.tryResumingGame(this, EnableCameraSceneName);
+ customCharacterSceneVisibleStore.set(false);
}
private backToPreviousScene(){
this.scene.sleep(CustomizeSceneName);
+ waScaleManager.restoreZoom();
this.scene.run(SelectCharacterSceneName);
+ customCharacterSceneVisibleStore.set(false);
}
}
diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts
index 755ac9a0..ba27cd07 100644
--- a/front/src/Phaser/Login/EnableCameraScene.ts
+++ b/front/src/Phaser/Login/EnableCameraScene.ts
@@ -3,279 +3,48 @@ import {TextField} from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import {mediaManager} from "../../WebRtc/MediaManager";
import {SoundMeter} from "../Components/SoundMeter";
-import {SoundMeterSprite} from "../Components/SoundMeterSprite";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import Zone = Phaser.GameObjects.Zone;
import { MenuScene } from "../Menu/MenuScene";
import {ResizableScene} from "./ResizableScene";
+import {
+ enableCameraSceneVisibilityStore,
+} from "../../Stores/MediaStore";
export const EnableCameraSceneName = "EnableCameraScene";
-enum LoginTextures {
- playButton = "play_button",
- icon = "icon",
- mainFont = "main_font",
- arrowRight = "arrow_right",
- arrowUp = "arrow_up"
-}
-
-const enableCameraSceneKey = 'enableCameraScene';
export class EnableCameraScene extends ResizableScene {
- private textField!: TextField;
- private cameraNameField!: TextField;
- private arrowLeft!: Image;
- private arrowRight!: Image;
- private arrowDown!: Image;
- private arrowUp!: Image;
- private microphonesList: MediaDeviceInfo[] = new Array();
- private camerasList: MediaDeviceInfo[] = new Array();
- private cameraSelected: number = 0;
- private microphoneSelected: number = 0;
- private soundMeter: SoundMeter;
- private soundMeterSprite!: SoundMeterSprite;
- private microphoneNameField!: TextField;
-
- private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
-
- private mobileTapZone!: Zone;
constructor() {
super({
key: EnableCameraSceneName
});
- this.soundMeter = new SoundMeter();
}
preload() {
-
- this.load.html(enableCameraSceneKey, 'resources/html/EnableCameraScene.html');
-
- this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
- this.load.image(LoginTextures.arrowRight, "resources/objects/arrow_right.png");
- this.load.image(LoginTextures.arrowUp, "resources/objects/arrow_up.png");
- // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
- this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
}
create() {
- this.enableCameraSceneElement = this.add.dom(-1000, 0).createFromCache(enableCameraSceneKey);
- this.centerXDomElement(this.enableCameraSceneElement, 300);
-
- MenuScene.revealMenusAfterInit(this.enableCameraSceneElement, enableCameraSceneKey);
-
- const continuingButton = this.enableCameraSceneElement.getChildByID('enableCameraSceneFormSubmit') as HTMLButtonElement;
- continuingButton.addEventListener('click', (e) => {
- e.preventDefault();
- this.login();
- });
-
- if (touchScreenManager.supportTouchScreen) {
- new PinchManager(this);
- }
- //this.scale.setZoom(ZOOM_LEVEL);
- //Phaser.Display.Align.In.BottomCenter(this.pressReturnField, zone);
-
- /* FIX ME */
- this.textField = new TextField(this, this.scale.width / 2, 20, '');
-
- // For mobile purposes - we need a big enough touchable area.
- this.mobileTapZone = this.add.zone(this.scale.width / 2,this.scale.height - 30,200,50)
- .setInteractive().on("pointerdown", () => {
- this.login();
- });
-
- this.cameraNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 60, '');
-
- this.microphoneNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 40, '');
-
- this.arrowRight = new Image(this, 0, 0, LoginTextures.arrowRight);
- this.arrowRight.setVisible(false);
- this.arrowRight.setInteractive().on('pointerdown', this.nextCam.bind(this));
- this.add.existing(this.arrowRight);
-
- this.arrowLeft = new Image(this, 0, 0, LoginTextures.arrowRight);
- this.arrowLeft.setVisible(false);
- this.arrowLeft.flipX = true;
- this.arrowLeft.setInteractive().on('pointerdown', this.previousCam.bind(this));
- this.add.existing(this.arrowLeft);
-
- this.arrowUp = new Image(this, 0, 0, LoginTextures.arrowUp);
- this.arrowUp.setVisible(false);
- this.arrowUp.setInteractive().on('pointerdown', this.previousMic.bind(this));
- this.add.existing(this.arrowUp);
-
- this.arrowDown = new Image(this, 0, 0, LoginTextures.arrowUp);
- this.arrowDown.setVisible(false);
- this.arrowDown.flipY = true;
- this.arrowDown.setInteractive().on('pointerdown', this.nextMic.bind(this));
- this.add.existing(this.arrowDown);
-
this.input.keyboard.on('keyup-ENTER', () => {
this.login();
});
- HtmlUtils.getElementByIdOrFail('webRtcSetup').classList.add('active');
-
- const mediaPromise = mediaManager.getCamera();
- mediaPromise.then(this.getDevices.bind(this));
- mediaPromise.then(this.setupStream.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-DOWN', this.nextMic.bind(this));
- this.input.keyboard.on('keydown-UP', this.previousMic.bind(this));
-
- this.soundMeterSprite = new SoundMeterSprite(this, 50, 50);
- this.soundMeterSprite.setVisible(false);
- this.add.existing(this.soundMeterSprite);
-
- this.onResize();
- }
-
- private previousCam(): void {
- if (this.cameraSelected === 0 || this.camerasList.length === 0) {
- return;
- }
- this.cameraSelected--;
- mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
- }
-
- private nextCam(): void {
- if (this.cameraSelected === this.camerasList.length - 1 || this.camerasList.length === 0) {
- return;
- }
- this.cameraSelected++;
- // TODO: the change of camera should be OBSERVED (reactive)
- mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
- }
-
- private previousMic(): void {
- if (this.microphoneSelected === 0 || this.microphonesList.length === 0) {
- return;
- }
- this.microphoneSelected--;
- mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
- }
-
- private nextMic(): void {
- if (this.microphoneSelected === this.microphonesList.length - 1 || this.microphonesList.length === 0) {
- return;
- }
- this.microphoneSelected++;
- // TODO: the change of camera should be OBSERVED (reactive)
- mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
- }
-
- /**
- * Function called each time a camera is changed
- */
- private setupStream(stream: MediaStream): void {
- const img = HtmlUtils.getElementByIdOrFail('webRtcSetupNoVideo');
- img.style.display = 'none';
-
- const div = HtmlUtils.getElementByIdOrFail('myCamVideoSetup');
- div.srcObject = stream;
-
- this.soundMeter.connectToSource(stream, new window.AudioContext());
- this.soundMeterSprite.setVisible(true);
-
- this.updateWebCamName();
- }
-
- private updateWebCamName(): void {
- if (this.camerasList.length > 1) {
- let label = this.camerasList[this.cameraSelected].label;
- // remove text in parenthesis
- label = label.replace(/\([^()]*\)/g, '').trim();
- // remove accents
- label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
- this.cameraNameField.text = label;
-
- this.arrowRight.setVisible(this.cameraSelected < this.camerasList.length - 1);
- this.arrowLeft.setVisible(this.cameraSelected > 0);
- }
- if (this.microphonesList.length > 1) {
- let label = this.microphonesList[this.microphoneSelected].label;
- // remove text in parenthesis
- label = label.replace(/\([^()]*\)/g, '').trim();
- // remove accents
- label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
-
- this.microphoneNameField.text = label;
-
- this.arrowDown.setVisible(this.microphoneSelected < this.microphonesList.length - 1);
- this.arrowUp.setVisible(this.microphoneSelected > 0);
-
- }
+ enableCameraSceneVisibilityStore.showEnableCameraScene();
}
public onResize(): void {
- let div = HtmlUtils.getElementByIdOrFail('myCamVideoSetup');
- let bounds = div.getBoundingClientRect();
- if (!div.srcObject) {
- div = HtmlUtils.getElementByIdOrFail('webRtcSetup');
- bounds = div.getBoundingClientRect();
- }
-
- this.textField.x = this.game.renderer.width / 2;
- this.mobileTapZone.x = this.game.renderer.width / 2;
- this.cameraNameField.x = this.game.renderer.width / 2;
- this.microphoneNameField.x = this.game.renderer.width / 2;
-
- this.cameraNameField.y = bounds.top / this.scale.zoom - 8;
-
- this.soundMeterSprite.x = this.game.renderer.width / 2 - this.soundMeterSprite.getWidth() / 2;
- this.soundMeterSprite.y = bounds.bottom / this.scale.zoom + 16;
-
- this.microphoneNameField.y = this.soundMeterSprite.y + 22;
-
- this.arrowRight.x = bounds.right / this.scale.zoom + 16;
- this.arrowRight.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
-
- this.arrowLeft.x = bounds.left / this.scale.zoom - 16;
- this.arrowLeft.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
-
- this.arrowDown.x = this.microphoneNameField.x + this.microphoneNameField.width / 2 + 16;
- this.arrowDown.y = this.microphoneNameField.y;
-
- this.arrowUp.x = this.microphoneNameField.x - this.microphoneNameField.width / 2 - 16;
- this.arrowUp.y = this.microphoneNameField.y;
-
- const actionBtn = document.querySelector('#enableCameraScene .action');
- if (actionBtn !== null) {
- actionBtn.style.top = (this.scale.height - 65) + 'px';
- }
}
update(time: number, delta: number): void {
- this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
-
- this.centerXDomElement(this.enableCameraSceneElement, 300);
}
- private login(): void {
- HtmlUtils.getElementByIdOrFail('webRtcSetup').style.display = 'none';
- this.soundMeter.stop();
+ public login(): void {
+ enableCameraSceneVisibilityStore.hideEnableCameraScene();
- mediaManager.stopCamera();
- mediaManager.stopMicrophone();
-
- this.scene.sleep(EnableCameraSceneName)
+ this.scene.sleep(EnableCameraSceneName);
gameManager.goToStartingMap(this.scene);
}
-
- private async getDevices() {
- const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
- for (const mediaDeviceInfo of mediaDeviceInfos) {
- if (mediaDeviceInfo.kind === 'audioinput') {
- this.microphonesList.push(mediaDeviceInfo);
- } else if (mediaDeviceInfo.kind === 'videoinput') {
- this.camerasList.push(mediaDeviceInfo);
- }
- }
- this.updateWebCamName();
- }
}
diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts
index 435592f2..39a8f5f3 100644
--- a/front/src/Phaser/Login/LoginScene.ts
+++ b/front/src/Phaser/Login/LoginScene.ts
@@ -1,17 +1,12 @@
import {gameManager} from "../Game/GameManager";
import {SelectCharacterSceneName} from "./SelectCharacterScene";
import {ResizableScene} from "./ResizableScene";
-import { localUserStore } from "../../Connexion/LocalUserStore";
-import {MenuScene} from "../Menu/MenuScene";
-import { isUserNameValid } from "../../Connexion/LocalUser";
+import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
export const LoginSceneName = "LoginScene";
-const loginSceneKey = 'loginScene';
-
export class LoginScene extends ResizableScene {
- private loginSceneElement!: Phaser.GameObjects.DOMElement;
private name: string = '';
constructor() {
@@ -22,65 +17,25 @@ export class LoginScene extends ResizableScene {
}
preload() {
- this.load.html(loginSceneKey, 'resources/html/loginScene.html');
}
create() {
- this.loginSceneElement = this.add.dom(-1000, 0).createFromCache(loginSceneKey);
- this.centerXDomElement(this.loginSceneElement, 200);
- MenuScene.revealMenusAfterInit(this.loginSceneElement, loginSceneKey);
-
- const pErrorElement = this.loginSceneElement.getChildByID('errorLoginScene') as HTMLInputElement;
- const inputElement = this.loginSceneElement.getChildByID('loginSceneName') as HTMLInputElement;
- inputElement.value = localUserStore.getName() ?? '';
- inputElement.focus();
- inputElement.addEventListener('keypress', (event: KeyboardEvent) => {
- if(inputElement.value.length > 7){
- event.preventDefault();
- return;
- }
- pErrorElement.innerHTML = '';
- if(inputElement.value && !isUserNameValid(inputElement.value)){
- pErrorElement.innerHTML = 'Invalid user name: only letters and numbers are allowed. No spaces.';
- }
- if (event.key === 'Enter') {
- event.preventDefault();
- this.login(inputElement);
- return;
- }
- });
-
- const continuingButton = this.loginSceneElement.getChildByID('loginSceneFormSubmit') as HTMLButtonElement;
- continuingButton.addEventListener('click', (e) => {
- e.preventDefault();
- this.login(inputElement);
- });
+ loginSceneVisibleStore.set(true);
}
- private login(inputElement: HTMLInputElement): void {
- const pErrorElement = this.loginSceneElement.getChildByID('errorLoginScene') as HTMLInputElement;
- this.name = inputElement.value;
- if (this.name === '') {
- pErrorElement.innerHTML = 'The name is empty';
- return
- }
- if(!isUserNameValid(this.name)){
- pErrorElement.innerHTML = 'Invalid user name: only letters and numbers are allowed. No spaces.';
- return
- }
- if (this.name === '') return
- gameManager.setPlayerName(this.name);
+ public login(name: string): void {
+ name = name.trim();
+ gameManager.setPlayerName(name);
this.scene.stop(LoginSceneName)
gameManager.tryResumingGame(this, SelectCharacterSceneName);
- this.scene.remove(LoginSceneName)
+ this.scene.remove(LoginSceneName);
+ loginSceneVisibleStore.set(false);
}
update(time: number, delta: number): void {
-
}
- public onResize(ev: UIEvent): void {
- this.centerXDomElement(this.loginSceneElement, 200);
+ public onResize(): void {
}
}
diff --git a/front/src/Phaser/Login/ResizableScene.ts b/front/src/Phaser/Login/ResizableScene.ts
index 39e2d74b..d06cb66c 100644
--- a/front/src/Phaser/Login/ResizableScene.ts
+++ b/front/src/Phaser/Login/ResizableScene.ts
@@ -2,7 +2,7 @@ import {Scene} from "phaser";
import DOMElement = Phaser.GameObjects.DOMElement;
export abstract class ResizableScene extends Scene {
- public abstract onResize(ev: UIEvent): void;
+ public abstract onResize(): void;
/**
* Centers the DOM element on the X axis.
@@ -17,7 +17,7 @@ export abstract class ResizableScene extends Scene {
&& object.node
&& object.node.getBoundingClientRect().width > 0
? (object.node.getBoundingClientRect().width / 2 / this.scale.zoom)
- : (300 / this.scale.zoom)
+ : (defaultWidth / this.scale.zoom)
);
}
}
diff --git a/front/src/Phaser/Login/SelectCharacterMobileScene.ts b/front/src/Phaser/Login/SelectCharacterMobileScene.ts
index b9c4b5a8..0d8e49d5 100644
--- a/front/src/Phaser/Login/SelectCharacterMobileScene.ts
+++ b/front/src/Phaser/Login/SelectCharacterMobileScene.ts
@@ -4,49 +4,50 @@ export class SelectCharacterMobileScene extends SelectCharacterScene {
create(){
super.create();
+ this.onResize();
this.selectedRectangle.destroy();
}
- protected defineSetupPlayer(numero: number){
+ protected defineSetupPlayer(num: number){
const deltaX = 30;
const deltaY = 2;
let [playerX, playerY] = this.getCharacterPosition();
let playerVisible = true;
let playerScale = 1.5;
- let playserOpactity = 1;
+ let playerOpacity = 1;
- if( this.currentSelectUser !== numero ){
+ if( this.currentSelectUser !== num ){
playerVisible = false;
}
- if( numero === (this.currentSelectUser + 1) ){
+ if( num === (this.currentSelectUser + 1) ){
playerY -= deltaY;
playerX += deltaX;
playerScale = 0.8;
- playserOpactity = 0.6;
+ playerOpacity = 0.6;
playerVisible = true;
}
- if( numero === (this.currentSelectUser + 2) ){
+ if( num === (this.currentSelectUser + 2) ){
playerY -= deltaY;
playerX += (deltaX * 2);
playerScale = 0.8;
- playserOpactity = 0.6;
+ playerOpacity = 0.6;
playerVisible = true;
}
- if( numero === (this.currentSelectUser - 1) ){
+ if( num === (this.currentSelectUser - 1) ){
playerY -= deltaY;
playerX -= deltaX;
playerScale = 0.8;
- playserOpactity = 0.6;
+ playerOpacity = 0.6;
playerVisible = true;
}
- if( numero === (this.currentSelectUser - 2) ){
+ if( num === (this.currentSelectUser - 2) ){
playerY -= deltaY;
playerX -= (deltaX * 2);
playerScale = 0.8;
- playserOpactity = 0.6;
+ playerOpacity = 0.6;
playerVisible = true;
}
- return {playerX, playerY, playerScale, playserOpactity, playerVisible}
+ return {playerX, playerY, playerScale, playerOpacity, playerVisible}
}
/**
diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts
index ecbb9c64..4eed17fe 100644
--- a/front/src/Phaser/Login/SelectCharacterScene.ts
+++ b/front/src/Phaser/Login/SelectCharacterScene.ts
@@ -11,6 +11,10 @@ import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import {MenuScene} from "../Menu/MenuScene";
+import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
+import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
+import {waScaleManager} from "../Services/WaScaleManager";
+import {isMobile} from "../../Enum/EnvironmentVariable";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
@@ -27,6 +31,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
protected selectCharacterSceneElement!: Phaser.GameObjects.DOMElement;
protected currentSelectUser = 0;
+ protected pointerClicked: boolean = false;
+
+ protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
constructor() {
super({
@@ -41,44 +48,36 @@ export class SelectCharacterScene extends AbstractCharacterScene {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription);
});
- })
+ this.lazyloadingAttempt = true;
+ });
this.playerModels = loadAllDefaultModels(this.load);
+ this.lazyloadingAttempt = false;
//this function must stay at the end of preload function
addLoader(this);
}
create() {
-
- this.selectCharacterSceneElement = this.add.dom(-1000, 0).createFromCache(selectCharacterKey);
- this.centerXDomElement(this.selectCharacterSceneElement, 150);
- MenuScene.revealMenusAfterInit(this.selectCharacterSceneElement, selectCharacterKey);
-
- this.selectCharacterSceneElement.addListener('click');
- this.selectCharacterSceneElement.on('click', (event:MouseEvent) => {
- event.preventDefault();
- if((event?.target as HTMLInputElement).id === 'selectCharacterButtonLeft') {
- this.moveToLeft();
- }else if((event?.target as HTMLInputElement).id === 'selectCharacterButtonRight') {
- this.moveToRight();
- }else if((event?.target as HTMLInputElement).id === 'selectCharacterSceneFormSubmit') {
- this.nextSceneToCameraScene();
- }else if((event?.target as HTMLInputElement).id === 'selectCharacterSceneFormCustomYourOwnSubmit') {
- this.nextSceneToCustomizeScene();
- }
+ selectCharacterSceneVisibleStore.set(true);
+ this.events.addListener('wake', () => {
+ waScaleManager.saveZoom();
+ waScaleManager.zoomModifier = isMobile() ? 2 : 1;
+ selectCharacterSceneVisibleStore.set(true);
});
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
+ waScaleManager.saveZoom();
+ waScaleManager.zoomModifier = isMobile() ? 2 : 1;
+
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF);
this.selectedRectangle.setDepth(2);
/*create user*/
this.createCurrentPlayer();
- const playerNumber = localUserStore.getPlayerCharacterIndex();
this.input.keyboard.on('keyup-ENTER', () => {
return this.nextSceneToCameraScene();
@@ -106,9 +105,12 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return;
}
this.scene.stop(SelectCharacterSceneName);
+ waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(this, EnableCameraSceneName);
- this.scene.remove(SelectCharacterSceneName);
+ this.players = [];
+ selectCharacterSceneVisibleStore.set(false);
+ this.events.removeListener('wake');
}
protected nextSceneToCustomizeScene(): void {
@@ -116,7 +118,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return;
}
this.scene.sleep(SelectCharacterSceneName);
+ waScaleManager.restoreZoom();
this.scene.run(CustomizeSceneName);
+ selectCharacterSceneVisibleStore.set(false);
}
createCurrentPlayer(): void {
@@ -133,15 +137,16 @@ export class SelectCharacterScene extends AbstractCharacterScene {
repeat: -1
});
player.setInteractive().on("pointerdown", () => {
- if(this.currentSelectUser === i){
+ if (this.pointerClicked || this.currentSelectUser === i) {
return;
}
+ this.pointerClicked = true;
this.currentSelectUser = i;
this.moveUser();
+ setTimeout(() => {this.pointerClicked = false;}, 100);
});
this.players.push(player);
}
-
this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
}
@@ -186,35 +191,35 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.moveUser();
}
- protected defineSetupPlayer(numero: number){
+ protected defineSetupPlayer(num: number){
const deltaX = 32;
const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
- playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (numero % this.nbCharactersPerRow)) ); // calcul position on line users
- playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(numero / this.nbCharactersPerRow) )) ); // calcul position on column users
+ playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users
+ playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users
const playerVisible = true;
const playerScale = 1;
- const playserOpactity = 1;
+ const playerOpacity = 1;
// if selected
- if( numero === this.currentSelectUser ){
+ if( num === this.currentSelectUser ){
this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY);
}
- return {playerX, playerY, playerScale, playserOpactity, playerVisible}
+ return {playerX, playerY, playerScale, playerOpacity, playerVisible}
}
- protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, numero: number){
+ protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){
- const {playerX, playerY, playerScale, playserOpactity, playerVisible} = this.defineSetupPlayer(numero);
+ const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
player.setBounce(0.2);
- player.setCollideWorldBounds(true);
+ player.setCollideWorldBounds(false);
player.setVisible( playerVisible );
player.setScale(playerScale, playerScale);
- player.setAlpha(playserOpactity);
+ player.setAlpha(playerOpacity);
player.setX(playerX);
player.setY(playerY);
}
@@ -238,12 +243,14 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
update(time: number, delta: number): void {
+ if(this.lazyloadingAttempt){
+ this.moveUser();
+ this.lazyloadingAttempt = false;
+ }
}
- public onResize(ev: UIEvent): void {
+ public onResize(): void {
//move position of user
this.moveUser();
-
- this.centerXDomElement(this.selectCharacterSceneElement, 150);
}
}
diff --git a/front/src/Phaser/Login/SelectCompanionScene.ts b/front/src/Phaser/Login/SelectCompanionScene.ts
index 203fd557..9caa88f7 100644
--- a/front/src/Phaser/Login/SelectCompanionScene.ts
+++ b/front/src/Phaser/Login/SelectCompanionScene.ts
@@ -10,17 +10,18 @@ import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingM
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import { MenuScene } from "../Menu/MenuScene";
+import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
+import {waScaleManager} from "../Services/WaScaleManager";
+import {isMobile} from "../../Enum/EnvironmentVariable";
export const SelectCompanionSceneName = "SelectCompanionScene";
-const selectCompanionSceneKey = 'selectCompanionScene';
-
export class SelectCompanionScene extends ResizableScene {
private selectedCompanion!: Phaser.Physics.Arcade.Sprite;
private companions: Array = new Array();
private companionModels: Array = [];
+ private saveZoom: number = 0;
- private selectCompanionSceneElement!: Phaser.GameObjects.DOMElement;
private currentCompanion = 0;
constructor() {
@@ -30,8 +31,6 @@ export class SelectCompanionScene extends ResizableScene {
}
preload() {
- this.load.html(selectCompanionSceneKey, 'resources/html/SelectCompanionScene.html');
-
getAllCompanionResources(this.load).forEach(model => {
this.companionModels.push(model);
});
@@ -42,30 +41,17 @@ export class SelectCompanionScene extends ResizableScene {
create() {
- this.selectCompanionSceneElement = this.add.dom(-1000, 0).createFromCache(selectCompanionSceneKey);
- this.centerXDomElement(this.selectCompanionSceneElement, 150);
- MenuScene.revealMenusAfterInit(this.selectCompanionSceneElement, selectCompanionSceneKey);
+ selectCompanionSceneVisibleStore.set(true);
- this.selectCompanionSceneElement.addListener('click');
- this.selectCompanionSceneElement.on('click', (event:MouseEvent) => {
- event.preventDefault();
- if((event?.target as HTMLInputElement).id === 'selectCharacterButtonLeft') {
- this.moveToLeft();
- }else if((event?.target as HTMLInputElement).id === 'selectCharacterButtonRight') {
- this.moveToRight();
- }else if((event?.target as HTMLInputElement).id === 'selectCompanionSceneFormSubmit') {
- this.nextScene();
- }else if((event?.target as HTMLInputElement).id === 'selectCompanionSceneFormBack') {
- this._nextScene();
- }
- });
+ waScaleManager.saveZoom();
+ waScaleManager.zoomModifier = isMobile() ? 2 : 1;
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
// input events
- this.input.keyboard.on('keyup-ENTER', this.nextScene.bind(this));
+ this.input.keyboard.on('keyup-ENTER', this.selectCompanion.bind(this));
this.input.keyboard.on('keydown-RIGHT', this.moveToRight.bind(this));
this.input.keyboard.on('keydown-LEFT', this.moveToLeft.bind(this));
@@ -89,18 +75,20 @@ export class SelectCompanionScene extends ResizableScene {
}
- private nextScene(): void {
+ public selectCompanion(): void {
localUserStore.setCompanion(this.companionModels[this.currentCompanion].name);
gameManager.setCompanion(this.companionModels[this.currentCompanion].name);
- this._nextScene();
+ this.closeScene();
}
- private _nextScene(){
+ public closeScene(){
// next scene
this.scene.stop(SelectCompanionSceneName);
+ waScaleManager.restoreZoom();
gameManager.tryResumingGame(this, EnableCameraSceneName);
this.scene.remove(SelectCompanionSceneName);
+ selectCompanionSceneVisibleStore.set(false);
}
private createCurrentCompanion(): void {
@@ -126,10 +114,8 @@ export class SelectCompanionScene extends ResizableScene {
this.selectedCompanion = this.companions[this.currentCompanion];
}
- public onResize(ev: UIEvent): void {
+ public onResize(): void {
this.moveCompanion();
-
- this.centerXDomElement(this.selectCompanionSceneElement, 150);
}
private updateSelectedCompanion(): void {
@@ -147,15 +133,7 @@ export class SelectCompanionScene extends ResizableScene {
this.updateSelectedCompanion();
}
- private moveToLeft(){
- if(this.currentCompanion === 0){
- return;
- }
- this.currentCompanion -= 1;
- this.moveCompanion();
- }
-
- private moveToRight(){
+ public moveToRight(){
if(this.currentCompanion === (this.companions.length - 1)){
return;
}
@@ -163,38 +141,46 @@ export class SelectCompanionScene extends ResizableScene {
this.moveCompanion();
}
- private defineSetupCompanion(numero: number){
+ public moveToLeft(){
+ if(this.currentCompanion === 0){
+ return;
+ }
+ this.currentCompanion -= 1;
+ this.moveCompanion();
+ }
+
+ private defineSetupCompanion(num: number){
const deltaX = 30;
const deltaY = 2;
let [companionX, companionY] = this.getCompanionPosition();
let companionVisible = true;
let companionScale = 1.5;
let companionOpactity = 1;
- if( this.currentCompanion !== numero ){
+ if( this.currentCompanion !== num ){
companionVisible = false;
}
- if( numero === (this.currentCompanion + 1) ){
+ if( num === (this.currentCompanion + 1) ){
companionY -= deltaY;
companionX += deltaX;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
- if( numero === (this.currentCompanion + 2) ){
+ if( num === (this.currentCompanion + 2) ){
companionY -= deltaY;
companionX += (deltaX * 2);
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
- if( numero === (this.currentCompanion - 1) ){
+ if( num === (this.currentCompanion - 1) ){
companionY -= deltaY;
companionX -= deltaX;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
- if( numero === (this.currentCompanion - 2) ){
+ if( num === (this.currentCompanion - 2) ){
companionY -= deltaY;
companionX -= (deltaX * 2);
companionScale = 0.8;
diff --git a/front/src/Phaser/Menu/HelpCameraSettingsScene.ts b/front/src/Phaser/Menu/HelpCameraSettingsScene.ts
deleted file mode 100644
index 6e80b8d4..00000000
--- a/front/src/Phaser/Menu/HelpCameraSettingsScene.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import {mediaManager} from "../../WebRtc/MediaManager";
-import {HtmlUtils} from "../../WebRtc/HtmlUtils";
-import {localUserStore} from "../../Connexion/LocalUserStore";
-import {DirtyScene} from "../Game/DirtyScene";
-
-export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
-const helpCameraSettings = 'helpCameraSettings';
-/**
- * The scene that show how to permit Camera and Microphone access if there are not already allowed
- */
-export class HelpCameraSettingsScene extends DirtyScene {
- private helpCameraSettingsElement!: Phaser.GameObjects.DOMElement;
- private helpCameraSettingsOpened: boolean = false;
-
- constructor() {
- super({key: HelpCameraSettingsSceneName});
- }
-
- preload() {
- this.load.html(helpCameraSettings, 'resources/html/helpCameraSettings.html');
- }
-
- create(){
- this.createHelpCameraSettings();
- }
-
- private createHelpCameraSettings() : void {
- const middleX = this.getMiddleX();
- this.helpCameraSettingsElement = this.add.dom(middleX, -800, undefined, {overflow: 'scroll'}).createFromCache(helpCameraSettings);
- this.revealMenusAfterInit(this.helpCameraSettingsElement, helpCameraSettings);
- this.helpCameraSettingsElement.addListener('click');
- this.helpCameraSettingsElement.on('click', (event:MouseEvent) => {
- if((event?.target as HTMLInputElement).id === 'mailto') {
- return;
- }
- event.preventDefault();
- if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormRefresh') {
- window.location.reload();
- }else if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormContinue') {
- this.closeHelpCameraSettingsOpened();
- }
- });
-
- if(!localUserStore.getHelpCameraSettingsShown() && (!mediaManager.constraintsMedia.audio || !mediaManager.constraintsMedia.video)){
- this.openHelpCameraSettingsOpened();
- localUserStore.setHelpCameraSettingsShown();
- }
-
- mediaManager.setHelpCameraSettingsCallBack(() => {
- this.openHelpCameraSettingsOpened();
- });
- }
-
- private openHelpCameraSettingsOpened(): void{
- HtmlUtils.getElementByIdOrFail('webRtcSetup').style.display = 'none';
- this.helpCameraSettingsOpened = true;
- try{
- if(window.navigator.userAgent.includes('Firefox')){
- HtmlUtils.getElementByIdOrFail('browserHelpSetting').innerHTML ='';
- }else if(window.navigator.userAgent.includes('Chrome')){
- HtmlUtils.getElementByIdOrFail('browserHelpSetting').innerHTML ='';
- }
- }catch(err) {
- console.error('openHelpCameraSettingsOpened => getElementByIdOrFail => error', err);
- }
- const middleY = this.getMiddleY();
- const middleX = this.getMiddleX();
- this.tweens.add({
- targets: this.helpCameraSettingsElement,
- y: middleY,
- x: middleX,
- duration: 1000,
- ease: 'Power3',
- overflow: 'scroll'
- });
-
- this.dirty = true;
- }
-
- private closeHelpCameraSettingsOpened(): void{
- const middleX = this.getMiddleX();
- /*const helpCameraSettingsInfo = this.helpCameraSettingsElement.getChildByID('helpCameraSettings') as HTMLParagraphElement;
- helpCameraSettingsInfo.innerText = '';
- helpCameraSettingsInfo.style.display = 'none';*/
- this.helpCameraSettingsOpened = false;
- this.tweens.add({
- targets: this.helpCameraSettingsElement,
- y: -1000,
- x: middleX,
- duration: 1000,
- ease: 'Power3',
- overflow: 'scroll'
- });
-
- this.dirty = true;
- }
-
- private revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
- //Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect.
- //To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
- setTimeout(() => {
- (menuElement.getChildByID(rootDomId) as HTMLElement).hidden = false;
- }, 250);
- }
-
- update(time: number, delta: number): void {
- this.dirty = false;
- }
-
- public onResize(ev: UIEvent): void {
- super.onResize(ev);
- if (this.helpCameraSettingsOpened) {
- const middleX = this.getMiddleX();
- const middleY = this.getMiddleY();
- this.tweens.add({
- targets: this.helpCameraSettingsElement,
- x: middleX,
- y: middleY,
- duration: 1000,
- ease: 'Power3'
- });
- this.dirty = true;
- }
- }
-
- private getMiddleX() : number{
- return (this.scale.width / 2) -
- (
- this.helpCameraSettingsElement
- && this.helpCameraSettingsElement.node
- && this.helpCameraSettingsElement.node.getBoundingClientRect().width > 0
- ? (this.helpCameraSettingsElement.node.getBoundingClientRect().width / (2 * this.scale.zoom))
- : (400 / 2)
- );
- }
-
- private getMiddleY() : number{
- const middleY = ((this.scale.height) - (
- (this.helpCameraSettingsElement
- && this.helpCameraSettingsElement.node
- && this.helpCameraSettingsElement.node.getBoundingClientRect().height > 0
- ? this.helpCameraSettingsElement.node.getBoundingClientRect().height : 400 /*FIXME to use a const will be injected in HTMLElement*/)/this.scale.zoom)) / 2;
- return (middleY > 0 ? middleY : 0);
- }
-
- public isDirty(): boolean {
- return this.dirty;
- }
-}
-
diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts
index 76bf520f..54fa395a 100644
--- a/front/src/Phaser/Menu/MenuScene.ts
+++ b/front/src/Phaser/Menu/MenuScene.ts
@@ -10,6 +10,7 @@ import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore";
+import {videoConstraintStore} from "../../Stores/MediaStore";
export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu';
@@ -324,7 +325,7 @@ export class MenuScene extends Phaser.Scene {
if (valueVideo !== this.videoQualityValue) {
this.videoQualityValue = valueVideo;
localUserStore.setVideoQualityValue(valueVideo);
- mediaManager.updateCameraQuality(valueVideo);
+ videoConstraintStore.setFrameRate(valueVideo);
}
this.closeGameQualityMenu();
}
diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts
index 6044ba84..b7f31aad 100644
--- a/front/src/Phaser/Player/Player.ts
+++ b/front/src/Phaser/Player/Player.ts
@@ -2,17 +2,17 @@ import {PlayerAnimationDirections} from "./Animation";
import type {GameScene} from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character";
+import {userMovingStore} from "../../Stores/GameStore";
+import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
export const hasMovedEventName = "hasMoved";
-export interface CurrentGamerInterface extends Character{
- moveUser(delta: number) : void;
- say(text : string) : void;
- isMoving(): boolean;
-}
+export const requestEmoteEventName = "requestEmote";
-export class Player extends Character implements CurrentGamerInterface {
+export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false;
+ private emoteMenu: RadialMenu|null = null;
+ private updateListener: () => void;
constructor(
Scene: GameScene,
@@ -26,14 +26,18 @@ export class Player extends Character implements CurrentGamerInterface {
companion: string|null,
companionTexturePromise?: Promise
) {
- super(Scene, x, y, texturesPromise, name, direction, moving, 1);
+ super(Scene, x, y, texturesPromise, name, direction, moving, 1, companion, companionTexturePromise);
//the current player model should be push away by other players to prevent conflict
this.getBody().setImmovable(false);
- if (typeof companion === 'string') {
- this.addCompanion(companion, companionTexturePromise);
- }
+ this.updateListener = () => {
+ if (this.emoteMenu) {
+ this.emoteMenu.x = this.x;
+ this.emoteMenu.y = this.y;
+ }
+ };
+ this.scene.events.addListener('postupdate', this.updateListener);
}
moveUser(delta: number): void {
@@ -83,9 +87,43 @@ export class Player extends Character implements CurrentGamerInterface {
this.previousDirection = direction;
}
this.wasMoving = moving;
+ userMovingStore.set(moving);
}
public isMoving(): boolean {
return this.wasMoving;
}
+
+ openOrCloseEmoteMenu(emotes:RadialMenuItem[]) {
+ if(this.emoteMenu) {
+ this.closeEmoteMenu();
+ } else {
+ this.openEmoteMenu(emotes);
+ }
+ }
+
+ isClickable(): boolean {
+ return true;
+ }
+
+ openEmoteMenu(emotes:RadialMenuItem[]): void {
+ this.cancelPreviousEmote();
+ this.emoteMenu = new RadialMenu(this.scene, 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();
+ }
}
diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts
index 9b013e32..4e0e9208 100644
--- a/front/src/Phaser/Services/WaScaleManager.ts
+++ b/front/src/Phaser/Services/WaScaleManager.ts
@@ -2,12 +2,15 @@ import {HdpiManager} from "./HdpiManager";
import ScaleManager = Phaser.Scale.ScaleManager;
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import type {Game} from "../Game/Game";
+import {ResizableScene} from "../Login/ResizableScene";
class WaScaleManager {
private hdpiManager: HdpiManager;
private scaleManager!: ScaleManager;
private game!: Game;
+ private actualZoom: number = 1;
+ private _saveZoom: number = 1;
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
@@ -28,13 +31,20 @@ class WaScaleManager {
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio});
- this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
+ this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
+ this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio)
this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px';
style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px';
+ // Note: onResize will be called twice (once here and once is Game.ts), but we have no better way.
+ for (const scene of this.game.scene.getScenes(true)) {
+ if (scene instanceof ResizableScene) {
+ scene.onResize();
+ }
+ }
this.game.markDirty();
}
@@ -48,6 +58,23 @@ class WaScaleManager {
this.applyNewSize();
}
+ public saveZoom(): void {
+ this._saveZoom = this.hdpiManager.zoomModifier;
+ console.log(this._saveZoom);
+ }
+
+ public restoreZoom(): void{
+ this.hdpiManager.zoomModifier = this._saveZoom;
+ 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);
diff --git a/front/src/Stores/CustomCharacterStore.ts b/front/src/Stores/CustomCharacterStore.ts
new file mode 100644
index 00000000..4bef7768
--- /dev/null
+++ b/front/src/Stores/CustomCharacterStore.ts
@@ -0,0 +1,3 @@
+import { derived, writable, Writable } from "svelte/store";
+
+export const customCharacterSceneVisibleStore = writable(false);
\ No newline at end of file
diff --git a/front/src/Stores/GameStore.ts b/front/src/Stores/GameStore.ts
new file mode 100644
index 00000000..8899aa12
--- /dev/null
+++ b/front/src/Stores/GameStore.ts
@@ -0,0 +1,5 @@
+import { writable } from "svelte/store";
+
+export const userMovingStore = writable(false);
+
+export const requestVisitCardsStore = writable(null);
diff --git a/front/src/Stores/HelpCameraSettingsStore.ts b/front/src/Stores/HelpCameraSettingsStore.ts
new file mode 100644
index 00000000..88373dab
--- /dev/null
+++ b/front/src/Stores/HelpCameraSettingsStore.ts
@@ -0,0 +1,3 @@
+import { writable } from "svelte/store";
+
+export const helpCameraSettingsVisibleStore = writable(false);
diff --git a/front/src/Stores/LoginSceneStore.ts b/front/src/Stores/LoginSceneStore.ts
new file mode 100644
index 00000000..6e2ea18b
--- /dev/null
+++ b/front/src/Stores/LoginSceneStore.ts
@@ -0,0 +1,3 @@
+import { writable } from "svelte/store";
+
+export const loginSceneVisibleStore = writable(false);
diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts
new file mode 100644
index 00000000..7d1911a4
--- /dev/null
+++ b/front/src/Stores/MediaStore.ts
@@ -0,0 +1,596 @@
+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('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|undefined) => update((constraints) => {
+ if (deviceId !== undefined) {
+ constraints.deviceId = {
+ exact: deviceId
+ };
+ } else {
+ delete constraints.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|undefined) => update((constraints) => {
+ selectedDeviceId = deviceId;
+
+ if (typeof(constraints) === 'boolean') {
+ constraints = {}
+ }
+ if (deviceId !== undefined) {
+ constraints.deviceId = {
+ exact: selectedDeviceId
+ };
+ } else {
+ delete constraints.deviceId;
+ }
+
+ 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, 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
+ });
+ return;
+ } 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. Please consider upgrading your browser or try using a recent version of Chrome.'),
+ constraints
+ });
+ return;
+ }
+ }
+
+ 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;
+});
+
+/**
+ * Device list
+ */
+export const deviceListStore = readable([], function start(set) {
+ let deviceListCanBeQueried = false;
+
+ const queryDeviceList = () => {
+ // Note: so far, we are ignoring any failures.
+ navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => {
+ set(mediaDeviceInfos);
+ }).catch((e) => {
+ console.error(e);
+ throw e;
+ });
+ };
+
+ const unsubscribe = localStreamStore.subscribe((streamResult) => {
+ if (streamResult.type === "success" && streamResult.stream !== null) {
+ if (deviceListCanBeQueried === false) {
+ queryDeviceList();
+ deviceListCanBeQueried = true;
+ }
+ }
+ });
+
+ if (navigator.mediaDevices) {
+ navigator.mediaDevices.addEventListener('devicechange', queryDeviceList);
+ }
+
+ return function stop() {
+ unsubscribe();
+ if (navigator.mediaDevices) {
+ navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList);
+ }
+ };
+});
+
+export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
+ return $deviceListStore.filter(device => device.kind === 'videoinput');
+});
+
+export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
+ return $deviceListStore.filter(device => device.kind === 'audioinput');
+});
+
+// TODO: detect the new webcam and automatically switch on it.
+cameraListStore.subscribe((devices) => {
+ // If the selected camera is unplugged, let's remove the constraint on deviceId
+ const constraints = get(videoConstraintStore);
+ if (!constraints.deviceId) {
+ return;
+ }
+
+ // If we cannot find the device ID, let's remove it.
+ // @ts-ignore
+ if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
+ videoConstraintStore.setDeviceId(undefined);
+ }
+});
+
+microphoneListStore.subscribe((devices) => {
+ // If the selected camera is unplugged, let's remove the constraint on deviceId
+ const constraints = get(audioConstraintStore);
+ if (typeof constraints === 'boolean') {
+ return;
+ }
+ if (!constraints.deviceId) {
+ return;
+ }
+
+ // If we cannot find the device ID, let's remove it.
+ // @ts-ignore
+ if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
+ audioConstraintStore.setDeviceId(undefined);
+ }
+});
diff --git a/front/src/Stores/PeerStore.ts b/front/src/Stores/PeerStore.ts
new file mode 100644
index 00000000..a582e692
--- /dev/null
+++ b/front/src/Stores/PeerStore.ts
@@ -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();
+
+ const { subscribe, set, update } = writable(users);
+
+ return {
+ subscribe,
+ connectToSimplePeer: (simplePeer: SimplePeer) => {
+ users = new Map();
+ 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();
diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts
new file mode 100644
index 00000000..0a7ef3e6
--- /dev/null
+++ b/front/src/Stores/ScreenSharingStore.ts
@@ -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, 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;
+ if (navigator.getDisplayMedia) {
+ currentStreamPromise = navigator.getDisplayMedia({constraints});
+ } else if (navigator.mediaDevices && 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 || !navigator.mediaDevices.getDisplayMedia)) {
+ set(false);
+ return;
+ }
+
+ set($peerStore.size !== 0);
+});
diff --git a/front/src/Stores/SelectCharacterStore.ts b/front/src/Stores/SelectCharacterStore.ts
new file mode 100644
index 00000000..094eaef3
--- /dev/null
+++ b/front/src/Stores/SelectCharacterStore.ts
@@ -0,0 +1,3 @@
+import { derived, writable, Writable } from "svelte/store";
+
+export const selectCharacterSceneVisibleStore = writable(false);
\ No newline at end of file
diff --git a/front/src/Stores/SelectCompanionStore.ts b/front/src/Stores/SelectCompanionStore.ts
new file mode 100644
index 00000000..e66f5de3
--- /dev/null
+++ b/front/src/Stores/SelectCompanionStore.ts
@@ -0,0 +1,3 @@
+import { derived, writable, Writable } from "svelte/store";
+
+export const selectCompanionSceneVisibleStore = writable(false);
diff --git a/front/src/Stores/SoundPlayingStore.ts b/front/src/Stores/SoundPlayingStore.ts
new file mode 100644
index 00000000..cf1d681c
--- /dev/null
+++ b/front/src/Stores/SoundPlayingStore.ts
@@ -0,0 +1,22 @@
+import { writable } from "svelte/store";
+
+/**
+ * A store that contains the URL of the sound currently playing
+ */
+function createSoundPlayingStore() {
+ const { subscribe, set, update } = writable(null);
+
+ return {
+ subscribe,
+ playSound: (url: string) => {
+ set(url);
+ },
+ soundEnded: () => {
+ set(null);
+ }
+
+
+ };
+}
+
+export const soundPlayingStore = createSoundPlayingStore();
diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts
index f00f6ecb..9793cd25 100644
--- a/front/src/WebRtc/CoWebsiteManager.ts
+++ b/front/src/WebRtc/CoWebsiteManager.ts
@@ -11,7 +11,7 @@ enum iframeStates {
const cowebsiteDivId = 'cowebsite'; // the id of the whole container.
const cowebsiteMainDomId = 'cowebsite-main'; // the id of the parent div of the iframe.
const cowebsiteAsideDomId = 'cowebsite-aside'; // the id of the parent div of the iframe.
-const cowebsiteCloseButtonId = 'cowebsite-close';
+export const cowebsiteCloseButtonId = 'cowebsite-close';
const cowebsiteFullScreenButtonId = 'cowebsite-fullscreen';
const cowebsiteOpenFullScreenImageId = 'cowebsite-fullscreen-open';
const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close';
@@ -64,10 +64,15 @@ class CoWebsiteManager {
this.initResizeListeners();
- HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId).addEventListener('click', () => {
+ const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
+ buttonCloseFrame.addEventListener('click', () => {
+ buttonCloseFrame.blur();
this.closeCoWebsite();
});
- HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId).addEventListener('click', () => {
+
+ const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
+ buttonFullScreenFrame.addEventListener('click', () => {
+ buttonFullScreenFrame.blur();
this.fullscreen();
});
}
@@ -152,7 +157,10 @@ class CoWebsiteManager {
setTimeout(() => {
this.fire();
}, animationTime)
- }).catch(() => this.closeCoWebsite());
+ }).catch((err) => {
+ console.error('Error loadCoWebsite => ', err);
+ this.closeCoWebsite()
+ });
}
/**
@@ -166,7 +174,10 @@ class CoWebsiteManager {
setTimeout(() => {
this.fire();
}, animationTime);
- }).catch(() => this.closeCoWebsite());
+ }).catch((err) => {
+ console.error('Error insertCoWebsite => ', err);
+ this.closeCoWebsite();
+ });
}
public closeCoWebsite(): Promise {
diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts
index 8ddbba7b..d2b9ebdd 100644
--- a/front/src/WebRtc/JitsiFactory.ts
+++ b/front/src/WebRtc/JitsiFactory.ts
@@ -1,6 +1,8 @@
import {JITSI_URL} from "../Enum/EnvironmentVariable";
import {mediaManager} from "./MediaManager";
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
interface jitsiConfigInterface {
@@ -10,10 +12,9 @@ interface jitsiConfigInterface {
}
const getDefaultConfig = () : jitsiConfigInterface => {
- const constraints = mediaManager.getConstraintRequestedByUser();
return {
- startWithAudioMuted: !constraints.audio,
- startWithVideoMuted: constraints.video === false,
+ startWithAudioMuted: !get(requestedMicrophoneState),
+ startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false
}
}
@@ -72,7 +73,6 @@ class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this);
- private previousConfigMeet! : jitsiConfigInterface;
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 {
- //save previous config
- this.previousConfigMeet = getDefaultConfig();
-
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
// Jitsi meet external API maintains some data in local storage
// 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('videoMuteStatusChanged', this.videoCallback);
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 {
- this.previousConfigMeet.startWithAudioMuted = muted;
+ if (muted) {
+ requestedMicrophoneState.disableMicrophone();
+ } else {
+ requestedMicrophoneState.enableMicrophone();
+ }
}
private onVideoChange({muted}: {muted: boolean}): void {
- this.previousConfigMeet.startWithVideoMuted = muted;
+ if (muted) {
+ requestedCameraState.disableWebcam();
+ } else {
+ requestedCameraState.enableWebcam();
+ }
}
private async loadJitsiScript(domain: string): Promise {
diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts
index b7594670..3d5d3190 100644
--- a/front/src/WebRtc/MediaManager.ts
+++ b/front/src/WebRtc/MediaManager.ts
@@ -6,23 +6,13 @@ import {localUserStore} from "../Connexion/LocalUserStore";
import type {UserSimplePeerInterface} from "./SimplePeer";
import {SoundMeter} from "../Phaser/Components/SoundMeter";
import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable";
-
-declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
-
-let videoConstraint: boolean|MediaTrackConstraints = {
- 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
-};
-const audioConstraint: boolean|MediaTrackConstraints = {
- //TODO: make these values configurable in the game settings menu and store them in localstorage
- autoGainControl: false,
- echoCancellation: true,
- noiseSuppression: true
-};
+import {
+ gameOverlayVisibilityStore, localStreamStore,
+} from "../Stores/MediaStore";
+import {
+ screenSharingLocalStreamStore
+} from "../Stores/ScreenSharingStore";
+import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void;
@@ -31,41 +21,18 @@ export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string|undefined) => 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)
+import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
+
export class MediaManager {
- localStream: MediaStream|null = null;
- localScreenCapture: MediaStream|null = null;
private remoteVideo: Map = new Map();
- myCamVideo: HTMLVideoElement;
- cinemaClose: HTMLImageElement;
- cinema: HTMLImageElement;
- monitorClose: HTMLImageElement;
- monitor: HTMLImageElement;
- microphoneClose: HTMLImageElement;
- microphone: HTMLImageElement;
- webrtcInAudio: HTMLAudioElement;
//FIX ME SOUNDMETER: check stalability of sound meter calculation
//mySoundMeterElement: HTMLDivElement;
- private webrtcOutAudio: HTMLAudioElement;
- constraintsMedia : MediaStreamConstraints = {
- audio: audioConstraint,
- video: videoConstraint
- };
- updatedLocalStreamCallBacks : Set = new Set();
startScreenSharingCallBacks : Set = new Set();
stopScreenSharingCallBacks : Set = new Set();
showReportModalCallBacks : Set = new Set();
- helpCameraSettingsCallBacks : Set = new Set();
- private microphoneBtn: HTMLDivElement;
- private cinemaBtn: HTMLDivElement;
- private monitorBtn: HTMLDivElement;
-
- private previousConstraint : MediaStreamConstraints;
private focused : boolean = true;
- private hasCamera = true;
-
private triggerCloseJistiFrame : Map = new Map();
private userInputManager?: UserInputManager;
@@ -77,62 +44,9 @@ export class MediaManager {
constructor() {
- this.myCamVideo = HtmlUtils.getElementByIdOrFail('myCamVideo');
- this.webrtcInAudio = HtmlUtils.getElementByIdOrFail('audio-webrtc-in');
- this.webrtcOutAudio = HtmlUtils.getElementByIdOrFail('audio-webrtc-out');
- this.webrtcInAudio.volume = 0.2;
- this.webrtcOutAudio.volume = 0.2;
-
- this.microphoneBtn = HtmlUtils.getElementByIdOrFail('btn-micro');
- this.microphoneClose = HtmlUtils.getElementByIdOrFail('microphone-close');
- this.microphoneClose.style.display = "none";
- this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- this.enableMicrophone();
- //update tracking
- });
- this.microphone = HtmlUtils.getElementByIdOrFail('microphone');
- this.microphone.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- this.disableMicrophone();
- //update tracking
- });
-
- this.cinemaBtn = HtmlUtils.getElementByIdOrFail('btn-video');
- this.cinemaClose = HtmlUtils.getElementByIdOrFail('cinema-close');
- this.cinemaClose.style.display = "none";
- this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- this.enableCamera();
- //update tracking
- });
- this.cinema = HtmlUtils.getElementByIdOrFail('cinema');
- this.cinema.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- this.disableCamera();
- //update tracking
- });
-
- this.monitorBtn = HtmlUtils.getElementByIdOrFail('btn-monitor');
- this.monitorClose = HtmlUtils.getElementByIdOrFail('monitor-close');
- this.monitorClose.style.display = "block";
- this.monitorClose.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- this.enableScreenSharing();
- //update tracking
- });
- this.monitor = HtmlUtils.getElementByIdOrFail('monitor');
- this.monitor.style.display = "none";
- this.monitor.addEventListener('click', (e: MouseEvent) => {
- e.preventDefault();
- this.disableScreenSharing();
- //update tracking
- });
-
- this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
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.childNodes.forEach((value: ChildNode, index) => {
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
@@ -140,404 +54,82 @@ export class MediaManager {
//Check of ask notification navigator permission
this.getNotification();
+
+ localStreamStore.subscribe((result) => {
+ if (result.type === 'error') {
+ console.error(result.error);
+ layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => {
+ helpCameraSettingsVisibleStore.set(true);
+ }, this.userInputManager);
+ return;
+ }
+ });
+
+ screenSharingLocalStreamStore.subscribe((result) => {
+ if (result.type === 'error') {
+ console.error(result.error);
+ layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => {
+ helpCameraSettingsVisibleStore.set(true);
+ }, this.userInputManager);
+ return;
+ }
+
+ if (result.stream !== null) {
+ this.addScreenSharingActiveVideo('me', DivImportance.Normal);
+ HtmlUtils.getElementByIdOrFail('screen-sharing-me').srcObject = result.stream;
+ } else {
+ this.removeActiveScreenSharingVideo('me');
+ }
+
+ });
+
+ /*screenSharingAvailableStore.subscribe((available) => {
+ if (available) {
+ document.querySelector('.btn-monitor')?.classList.remove('hide');
+ } else {
+ document.querySelector('.btn-monitor')?.classList.add('hide');
+ }
+ });*/
}
public updateScene(){
- //FIX ME SOUNDMETER: check stalability of sound meter calculation
+ //FIX ME SOUNDMETER: check stability of sound meter calculation
//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 {
- this.updatedLocalStreamCallBacks.add(callback);
- }
-
- public onStartScreenSharing(callback: StartScreenSharingCallback): void {
- this.startScreenSharingCallBacks.add(callback);
- }
-
- public onStopScreenSharing(callback: StopScreenSharingCallback): void {
- this.stopScreenSharingCallBacks.add(callback);
- }
-
- removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void {
- this.updatedLocalStreamCallBacks.delete(callback);
- }
-
- private triggerUpdatedLocalStreamCallbacks(stream: MediaStream|null): void {
- for (const callback of this.updatedLocalStreamCallBacks) {
- callback(stream);
- }
- }
-
- private triggerStartedScreenSharingCallbacks(stream: MediaStream): void {
- for (const callback of this.startScreenSharingCallBacks) {
- callback(stream);
- }
- }
-
- private triggerStoppedScreenSharingCallbacks(stream: MediaStream): void {
- for (const callback of this.stopScreenSharingCallBacks) {
- callback(stream);
- }
- }
-
public showGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active');
- const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close');
+ const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
- buttonCloseFrame.removeEventListener('click', functionTrigger);
+ buttonCloseFrame.removeEventListener('click', () => {
+ buttonCloseFrame.blur();
+ functionTrigger();
+ });
+
+ gameOverlayVisibilityStore.showGameOverlay();
}
public hideGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active');
- const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close');
+ const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
- buttonCloseFrame.addEventListener('click', functionTrigger);
- }
-
- public isGameOverlayVisible(): boolean {
- 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(){
- this.cinemaClose.style.display = "none";
- this.cinemaBtn.classList.remove("disabled");
- this.cinema.style.display = "block";
- }
-
- private disableCameraStyle(){
- this.cinemaClose.style.display = "block";
- this.cinema.style.display = "none";
- this.cinemaBtn.classList.add("disabled");
- this.constraintsMedia.video = false;
- this.myCamVideo.srcObject = null;
- }
-
- private enableMicrophoneStyle(){
- this.microphoneClose.style.display = "none";
- this.microphone.style.display = "block";
- this.microphoneBtn.classList.remove("disabled");
- }
-
- private disableMicrophoneStyle(){
- this.microphoneClose.style.display = "block";
- this.microphone.style.display = "none";
- this.microphoneBtn.classList.add("disabled");
- this.constraintsMedia.audio = false;
- }
-
- private enableScreenSharing() {
- this.getScreenMedia().then((stream) => {
- this.triggerStartedScreenSharingCallbacks(stream);
- this.monitorClose.style.display = "none";
- 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);
+ buttonCloseFrame.addEventListener('click', () => {
+ buttonCloseFrame.blur();
+ functionTrigger();
});
- }
-
- private disableScreenSharing() {
- this.monitorClose.style.display = "block";
- this.monitor.style.display = "none";
- 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{
- 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('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 {
- 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 {
- 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 {
- let video = this.constraintsMedia.video;
- if (typeof(video) === 'boolean' || video === undefined) {
- video = {}
- }
- video.deviceId = {
- exact: id
- };
-
- return this.getCamera();
- }
-
- setMicrophone(id: string): Promise {
- let audio = this.constraintsMedia.audio;
- if (typeof(audio) === 'boolean' || audio === undefined) {
- audio = {}
- }
- audio.deviceId = {
- exact: id
- };
-
- return this.getCamera();
+ gameOverlayVisibilityStore.hideGameOverlay();
}
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
- this.webrtcInAudio.play();
const userId = ''+user.userId
userName = userName.toUpperCase();
@@ -685,10 +277,6 @@ export class MediaManager {
this.removeActiveVideo(this.getScreenSharingId(userId))
}
- playWebrtcOutSound(): void {
- this.webrtcOutAudio.play();
- }
-
isConnecting(userId: string): void {
const connectingSpinnerDiv = this.getSpinner(userId);
if (connectingSpinnerDiv === null) {
@@ -803,16 +391,6 @@ export class MediaManager {
this.showReportModalCallBacks.add(callback);
}
- public setHelpCameraSettingsCallBack(callback: HelpCameraSettingsCallBack){
- this.helpCameraSettingsCallBacks.add(callback);
- }
-
- private showHelpCameraSettingsCallBack(){
- for(const callBack of this.helpCameraSettingsCallBacks){
- callBack();
- }
- }
-
//FIX ME SOUNDMETER: check stalability of sound meter calculation
/*updateSoudMeter(){
try{
@@ -858,12 +436,32 @@ export class MediaManager {
public getNotification(){
//Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
- Notification.requestPermission().catch((err) => {
- console.error(`Notification permission error`, err);
- });
+ if (this.checkNotificationPromise()) {
+ Notification.requestPermission().catch((err) => {
+ console.error(`Notification permission error`, err);
+ });
+ } else {
+ Notification.requestPermission();
+ }
}
}
+ /**
+ * Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
+ * if we are on Safari...
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
+ */
+ private checkNotificationPromise(): boolean {
+ try {
+ Notification.requestPermission().then();
+ } catch(e) {
+ return false;
+ }
+
+ return true;
+ }
+
public createNotification(userName: string){
if(this.focused){
return;
diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts
index f1786ef3..d797f59b 100644
--- a/front/src/WebRtc/ScreenSharingPeer.ts
+++ b/front/src/WebRtc/ScreenSharingPeer.ts
@@ -19,7 +19,7 @@ export class ScreenSharingPeer extends Peer {
public _connected: boolean = false;
private userId: number;
- constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
+ constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) {
super({
initiator: initiator ? initiator : false,
//reconnectTimer: 10000,
@@ -60,6 +60,7 @@ export class ScreenSharingPeer extends Peer {
const message = JSON.parse(chunk.toString('utf8'));
if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection');
+ return;
}
mediaManager.removeActiveScreenSharingVideo("" + this.userId);
});
@@ -81,7 +82,9 @@ export class ScreenSharingPeer extends Peer {
this._onFinish();
});
- this.pushScreenSharingToRemoteUser();
+ if (stream) {
+ this.addStream(stream);
+ }
}
private sendWebrtcScreenSharingSignal(data: unknown) {
@@ -141,16 +144,6 @@ export class ScreenSharingPeer extends Peer {
}
}
- private pushScreenSharingToRemoteUser() {
- const localScreenCapture: MediaStream | null = mediaManager.localScreenCapture;
- if(!localScreenCapture){
- return;
- }
-
- this.addStream(localScreenCapture);
- return;
- }
-
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream);
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true})));
diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts
index 67e72c6d..2a502bab 100644
--- a/front/src/WebRtc/SimplePeer.ts
+++ b/front/src/WebRtc/SimplePeer.ts
@@ -14,6 +14,11 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
import {blackListManager} from "./BlackListManager";
+import {get} from "svelte/store";
+import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
+import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
+import {DivImportance, layoutManager} from "./LayoutManager";
+import {HtmlUtils} from "./HtmlUtils";
export interface UserSimplePeerInterface{
userId: number;
@@ -37,9 +42,9 @@ export class SimplePeer {
private PeerScreenSharingConnectionArray: Map = new Map();
private PeerConnectionArray: Map = new Map();
- private readonly sendLocalVideoStreamCallback: UpdatedLocalStreamCallback;
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
+ private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array = new Array();
private readonly userId: number;
private lastWebrtcUserName: string|undefined;
@@ -47,13 +52,32 @@ export class SimplePeer {
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
- this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this);
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
- mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback);
- mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback);
- mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback);
+ this.unsubscribers.push(localStreamStore.subscribe((streamResult) => {
+ this.sendLocalVideoStream(streamResult);
+ }));
+
+ let localScreenCapture: MediaStream|null = null;
+
+ this.unsubscribers.push(screenSharingLocalStreamStore.subscribe((streamResult) => {
+ if (streamResult.type === 'error') {
+ // Let's ignore screen sharing errors, we will deal with those in a different way.
+ return;
+ }
+
+ if (streamResult.stream !== null) {
+ localScreenCapture = streamResult.stream;
+ this.sendLocalScreenSharingStream(localScreenCapture);
+ } else {
+ if (localScreenCapture) {
+ this.stopLocalScreenSharingStream(localScreenCapture);
+ localScreenCapture = null;
+ }
+ }
+ }));
+
this.userId = Connection.getUserId();
this.initialise();
}
@@ -82,11 +106,10 @@ export class SimplePeer {
});
mediaManager.showGameOverlay();
- mediaManager.getCamera().finally(() => {
- //receive message start
- this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
- this.receiveWebrtcStart(message);
- });
+
+ //receive message start
+ this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
+ this.receiveWebrtcStart(message);
});
this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => {
@@ -105,13 +128,19 @@ export class SimplePeer {
if(!user.initiator){
return;
}
- this.createPeerConnection(user);
+ const streamResult = get(localStreamStore);
+ let stream : MediaStream | null = null;
+ if (streamResult.type === 'success' && streamResult.stream) {
+ stream = streamResult.stream;
+ }
+
+ this.createPeerConnection(user, stream);
}
/**
* create peer connection to bind users
*/
- private createPeerConnection(user : UserSimplePeerInterface) : VideoPeer | null {
+ private createPeerConnection(user : UserSimplePeerInterface, localStream: MediaStream | null) : VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId)
if (peerConnection) {
if (peerConnection.destroyed) {
@@ -121,11 +150,11 @@ export class SimplePeer {
if (!peerConnexionDeleted) {
throw 'Error to delete peer connection';
}
- this.createPeerConnection(user);
+ //return this.createPeerConnection(user, localStream);
} else {
peerConnection.toClose = false;
+ return null;
}
- return null;
}
let name = user.name;
@@ -143,7 +172,7 @@ export class SimplePeer {
this.lastWebrtcUserName = user.webRtcUser;
this.lastWebrtcPassword = user.webRtcPassword;
- const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection);
+ const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream);
//permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
@@ -154,8 +183,9 @@ export class SimplePeer {
// When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user!
peer.on('connect', () => {
- if (mediaManager.localScreenCapture) {
- this.sendLocalScreenSharingStreamToUser(user.userId);
+ const streamResult = get(screenSharingLocalStreamStore);
+ if (streamResult.type === 'success' && streamResult.stream !== null) {
+ this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
}
});
@@ -174,7 +204,7 @@ export class SimplePeer {
/**
* create peer connection to bind users
*/
- private createPeerScreenSharingConnection(user : UserSimplePeerInterface) : ScreenSharingPeer | null{
+ private createPeerScreenSharingConnection(user : UserSimplePeerInterface, stream: MediaStream | null) : ScreenSharingPeer | null{
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
if(peerConnection){
if(peerConnection.destroyed){
@@ -184,7 +214,7 @@ export class SimplePeer {
if(!peerConnexionDeleted){
throw 'Error to delete peer connection';
}
- this.createPeerConnection(user);
+ this.createPeerConnection(user, stream);
}else {
peerConnection.toClose = false;
}
@@ -203,7 +233,7 @@ export class SimplePeer {
user.webRtcPassword = this.lastWebrtcPassword;
}
- const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection);
+ const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
@@ -216,7 +246,6 @@ export class SimplePeer {
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*/
private closeConnection(userId : number) {
- mediaManager.playWebrtcOutSound();
try {
const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) {
@@ -233,7 +262,7 @@ export class SimplePeer {
const userIndex = this.Users.findIndex(user => user.userId === userId);
if(userIndex < 0){
- throw 'Couln\'t delete user';
+ throw 'Couldn\'t delete user';
} else {
this.Users.splice(userIndex, 1);
}
@@ -293,7 +322,9 @@ export class SimplePeer {
* Unregisters any held event handler.
*/
public unregister() {
- mediaManager.removeUpdateLocalStreamEventListener(this.sendLocalVideoStreamCallback);
+ for (const unsubscriber of this.unsubscribers) {
+ unsubscriber();
+ }
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -301,7 +332,13 @@ export class SimplePeer {
try {
//if offer type, create peer connection
if(data.signal.type === "offer"){
- this.createPeerConnection(data);
+ const streamResult = get(localStreamStore);
+ let stream : MediaStream | null = null;
+ if (streamResult.type === 'success' && streamResult.stream) {
+ stream = streamResult.stream;
+ }
+
+ this.createPeerConnection(data, stream);
}
const peer = this.PeerConnectionArray.get(data.userId);
if (peer !== undefined) {
@@ -317,18 +354,26 @@ export class SimplePeer {
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
if (blackListManager.isBlackListed(data.userId)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
+ const streamResult = get(screenSharingLocalStreamStore);
+ let stream : MediaStream | null = null;
+ if (streamResult.type === 'success' && streamResult.stream !== null) {
+ stream = streamResult.stream;
+ }
+
try {
//if offer type, create peer connection
if(data.signal.type === "offer"){
- this.createPeerScreenSharingConnection(data);
+ this.createPeerScreenSharingConnection(data, stream);
}
const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
if (peer !== undefined) {
peer.signal(data.signal);
} else {
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal');
- console.info('tentative to create new peer connexion');
- this.sendLocalScreenSharingStreamToUser(data.userId);
+ console.info('Attempt to create new peer connexion');
+ if (stream) {
+ this.sendLocalScreenSharingStreamToUser(data.userId, stream);
+ }
}
} catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
@@ -338,14 +383,19 @@ export class SimplePeer {
}
}
- private pushVideoToRemoteUser(userId : number) {
+ private pushVideoToRemoteUser(userId: number, streamResult: LocalStreamStoreValue) {
try {
const PeerConnection = this.PeerConnectionArray.get(userId);
if (!PeerConnection) {
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})));
+
+ PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints})));
+
+ if (streamResult.type === 'error') {
+ return;
+ }
+ const localStream: MediaStream | null = streamResult.stream;
if(!localStream){
return;
@@ -362,15 +412,11 @@ export class SimplePeer {
}
}
- private pushScreenSharingToRemoteUser(userId : number) {
+ private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) {
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId);
}
- const localScreenCapture: MediaStream | null = mediaManager.localScreenCapture;
- if(!localScreenCapture){
- return;
- }
for (const track of localScreenCapture.getTracks()) {
PeerConnection.addTrack(track, localScreenCapture);
@@ -378,23 +424,18 @@ export class SimplePeer {
return;
}
- public sendLocalVideoStream(){
+ public sendLocalVideoStream(streamResult: LocalStreamStoreValue){
for (const user of this.Users) {
- this.pushVideoToRemoteUser(user.userId);
+ this.pushVideoToRemoteUser(user.userId, streamResult);
}
}
/**
* Triggered locally when clicking on the screen sharing button
*/
- public sendLocalScreenSharingStream() {
- if (!mediaManager.localScreenCapture) {
- console.error('Could not find localScreenCapture to share')
- return;
- }
-
+ public sendLocalScreenSharingStream(localScreenCapture: MediaStream) {
for (const user of this.Users) {
- this.sendLocalScreenSharingStreamToUser(user.userId);
+ this.sendLocalScreenSharingStreamToUser(user.userId, localScreenCapture);
}
}
@@ -407,11 +448,11 @@ export class SimplePeer {
}
}
- private sendLocalScreenSharingStreamToUser(userId: number): void {
+ private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void {
if (blackListManager.isBlackListed(userId)) return;
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
if (this.PeerScreenSharingConnectionArray.has(userId)) {
- this.pushScreenSharingToRemoteUser(userId);
+ this.pushScreenSharingToRemoteUser(userId, localScreenCapture);
return;
}
@@ -419,7 +460,7 @@ export class SimplePeer {
userId,
initiator: true
};
- const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser);
+ const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser, localScreenCapture);
if (!PeerConnectionScreenSharing) {
return;
}
diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts
index 503ca0de..5ca8952c 100644
--- a/front/src/WebRtc/VideoPeer.ts
+++ b/front/src/WebRtc/VideoPeer.ts
@@ -5,6 +5,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager";
import type {Subscription} from "rxjs";
import type {UserSimplePeerInterface} from "./SimplePeer";
+import {get} from "svelte/store";
+import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@@ -25,7 +27,7 @@ export class VideoPeer extends Peer {
private onBlockSubscribe: Subscription;
private onUnBlockSubscribe: Subscription;
- constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
+ constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) {
super({
initiator: initiator ? initiator : false,
//reconnectTimer: 10000,
@@ -105,7 +107,7 @@ export class VideoPeer extends Peer {
this._onFinish();
});
- this.pushVideoToRemoteUser();
+ this.pushVideoToRemoteUser(localStream);
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.toggleRemoteStream(false);
@@ -188,10 +190,9 @@ export class VideoPeer extends Peer {
}
}
- private pushVideoToRemoteUser() {
+ private pushVideoToRemoteUser(localStream: MediaStream | null) {
try {
- 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){
return;
diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts
index df37e53d..17b979df 100644
--- a/front/src/iframe_api.ts
+++ b/front/src/iframe_api.ts
@@ -9,6 +9,10 @@ import type { ClosePopupEvent } from "./Api/Events/ClosePopupEvent";
import type { OpenTabEvent } from "./Api/Events/OpenTabEvent";
import type { GoToPageEvent } from "./Api/Events/GoToPageEvent";
import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent";
+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 {
sendChatMessage(message: string, author: string): void;
@@ -24,6 +28,7 @@ interface WorkAdventureApi {
restorePlayerControls(): void;
displayBubble(): void;
removeBubble(): void;
+ loadSound(url : string): Sound;
}
declare global {
@@ -57,7 +62,7 @@ interface ButtonDescriptor {
callback: ButtonClickedCallback,
}
-class Popup {
+export class Popup {
constructor(private id: number) {
}
@@ -74,6 +79,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 = {
/**
* Send a message in the chat.
@@ -113,7 +153,11 @@ window.WA = {
}, '*');
},
- goToPage(url: string): void {
+ loadSound(url: string) : Sound {
+ return new Sound(url);
+ },
+
+ goToPage(url : string) : void{
window.parent.postMessage({
"type": 'goToPage',
"data": {
diff --git a/front/src/index.ts b/front/src/index.ts
index 2cdcaa19..90d4c612 100644
--- a/front/src/index.ts
+++ b/front/src/index.ts
@@ -9,11 +9,10 @@ import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene";
import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene";
import {CustomizeScene} from "./Phaser/Login/CustomizeScene";
-import {ResizableScene} from "./Phaser/Login/ResizableScene";
+import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugin.js';
import {EntryScene} from "./Phaser/Login/EntryScene";
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
import {MenuScene} from "./Phaser/Menu/MenuScene";
-import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene";
import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener";
@@ -96,7 +95,7 @@ const config: GameConfig = {
ErrorScene,
CustomizeScene,
MenuScene,
- HelpCameraSettingsScene],
+ ],
//resolution: window.devicePixelRatio / 2,
fps: fps,
dom: {
@@ -107,6 +106,13 @@ const config: GameConfig = {
roundPixels: true,
antialias: false
},
+ plugins: {
+ global: [{
+ key: 'rexWebFontLoader',
+ plugin: WebFontLoaderPlugin,
+ start: true
+ }]
+ },
physics: {
default: "arcade",
arcade: {
@@ -145,7 +151,9 @@ iframeListener.init();
const app = new App({
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
- props: { },
+ props: {
+ game: game
+ },
})
export default app
diff --git a/front/src/rex-plugins.d.ts b/front/src/rex-plugins.d.ts
index d5457702..2e160315 100644
--- a/front/src/rex-plugins.d.ts
+++ b/front/src/rex-plugins.d.ts
@@ -7,6 +7,10 @@ declare module 'phaser3-rex-plugins/plugins/gestures-plugin.js' {
const content: any; // eslint-disable-line
export default content;
}
+declare module 'phaser3-rex-plugins/plugins/webfontloader-plugin.js' {
+ const content: any; // eslint-disable-line
+ export default content;
+}
declare module 'phaser3-rex-plugins/plugins/gestures.js' {
export const Pinch: any; // eslint-disable-line
}
diff --git a/front/style/fonts.scss b/front/style/fonts.scss
new file mode 100644
index 00000000..a49d3967
--- /dev/null
+++ b/front/style/fonts.scss
@@ -0,0 +1,9 @@
+@import "~@fontsource/press-start-2p/index.css";
+
+*{
+ font-family: PixelFont-7,monospace;
+}
+
+.nes-btn {
+ font-family: "Press Start 2P";
+}
diff --git a/front/style/index.scss b/front/style/index.scss
index 67e85c5b..7ed141cd 100644
--- a/front/style/index.scss
+++ b/front/style/index.scss
@@ -1,4 +1,5 @@
@import "cowebsite.scss";
@import "cowebsite-mobile.scss";
-@import "style.css";
-@import "mobile-style.scss";
\ No newline at end of file
+@import "style";
+@import "mobile-style.scss";
+@import "fonts.scss";
diff --git a/front/style/mobile-style.scss b/front/style/mobile-style.scss
index 21753ebd..1b37053a 100644
--- a/front/style/mobile-style.scss
+++ b/front/style/mobile-style.scss
@@ -1,9 +1,24 @@
+@media (hover: none) {
+ /**
+ * If we cannot hover over elements, let's display camera button in full.
+ */
+ .btn-cam-action {
+ div {
+ transform: translateY(0px);
+ }
+ }
+}
+
@media screen and (max-width: 700px),
screen and (max-height: 700px){
- video#myCamVideo {
+ video.myCamVideo {
width: 150px;
}
+ .div-myCamVideo.hide {
+ right: -160px;
+ }
+
.sidebar {
width: 20%;
min-width: 200px;
@@ -22,21 +37,6 @@
}
}
- .btn-cam-action {
- min-width: 150px;
-
- &:hover{
- transform: translateY(20px);
- }
- div {
- margin: 0 1%;
- &:hover {
- background-color: #666;
- }
- margin-bottom: 30px;
- }
- }
-
.main-section {
position: absolute;
width: 100%;
diff --git a/front/style/style.css b/front/style/style.scss
similarity index 94%
rename from front/style/style.css
rename to front/style/style.scss
index d95ac701..f43fb240 100644
--- a/front/style/style.css
+++ b/front/style/style.scss
@@ -133,19 +133,27 @@ body .message-info.warning{
outline: none;
}
-.video-container#div-myCamVideo{
+.video-container.div-myCamVideo{
border: none;
+ background-color: transparent;
}
-#div-myCamVideo {
+.div-myCamVideo {
position: absolute;
right: 15px;
bottom: 30px;
border-radius: 15px 15px 15px 15px;
max-height: 20%;
+ transition: right 350ms;
}
-video#myCamVideo{
+.div-myCamVideo.hide {
+ right: -20vw;
+}
+
+video.myCamVideo{
+ background-color: #00000099;
+ max-height: 20vh;
width: 15vw;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
@@ -196,18 +204,20 @@ video#myCamVideo{
display: inline-flex;
bottom: 10px;
right: 15px;
- width: 15vw;
+ width: 180px;
height: 40px;
text-align: center;
align-content: center;
align-items: center;
- justify-content: center;
+ justify-content: flex-end;
justify-items: center;
}
/*btn animation*/
.btn-cam-action div{
cursor: url('./images/cursor_pointer.png'), pointer;
- /*position: absolute;*/
+ display: flex;
+ align-items: center;
+ justify-content: center;
border: solid 0px black;
width: 44px;
height: 44px;
@@ -216,7 +226,6 @@ video#myCamVideo{
border-radius: 48px;
transform: translateY(20px);
transition-timing-function: ease-in-out;
- margin-bottom: 20px;
margin: 0 4%;
}
.btn-cam-action div.disabled {
@@ -248,6 +257,12 @@ video#myCamVideo{
transition: all .2s;
/*right: 224px;*/
}
+.btn-monitor.hide {
+ transform: translateY(60px);
+}
+.btn-cam-action:hover .btn-monitor.hide{
+ transform: translateY(60px);
+}
.btn-copy{
pointer-events: auto;
transition: all .3s;
@@ -257,8 +272,6 @@ video#myCamVideo{
.btn-cam-action div img{
height: 22px;
width: 30px;
- top: calc(48px - 37px);
- left: calc(48px - 41px);
position: relative;
cursor: url('./images/cursor_pointer.png'), pointer;
}
@@ -321,37 +334,6 @@ video#myCamVideo{
}
}
-.webrtcsetup{
- display: none;
- position: absolute;
- top: 140px;
- left: 0;
- right: 0;
- margin-left: auto;
- margin-right: auto;
- height: 50%;
- width: 50%;
- border: white 6px solid;
-}
-.webrtcsetup .background-img {
- position: relative;
- display: block;
- width: 40%;
- height: 60%;
- margin-left: auto;
- margin-right: auto;
- top: 50%;
- transform: translateY(-50%);
-}
-#myCamVideoSetup {
- width: 100%;
- height: 100%;
-}
-.webrtcsetup.active{
- display: block;
-}
-
-
/* New layout */
body {
margin: 0;
@@ -828,35 +810,6 @@ input[type=range]:focus::-ms-fill-upper {
}
-
-/*audio html when audio message playing*/
-.main-container .audio-playing {
- position: absolute;
- width: 200px;
- height: 54px;
- right: -210px;
- top: 40px;
- transition: all 0.1s ease-out;
- background-color: black;
- border-radius: 30px 0 0 30px;
- display: inline-flex;
-}
-
-.main-container .audio-playing.active{
- right: 0;
-}
-.main-container .audio-playing img{
- /*width: 30px;*/
- border-radius: 50%;
- background-color: #ffda01;
- padding: 10px;
-}
-.main-container .audio-playing p{
- color: white;
- margin-left: 10px;
- margin-top: 14px;
-}
-
/* VIDEO QUALITY */
.main-console div.setting h1{
color: white;
@@ -1232,4 +1185,22 @@ div.action.danger p.action-body{
width: 100%;
height: 100%;
pointer-events: none;
+
+ & > div {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ & > div {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ }
+
+ & > div.scrollable {
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ pointer-events: auto;
+ }
+ }
}
diff --git a/front/tests/Phaser/Connexion/LocalUserTest.ts b/front/tests/Phaser/Connexion/LocalUserTest.ts
index 25b54005..4ba20745 100644
--- a/front/tests/Phaser/Connexion/LocalUserTest.ts
+++ b/front/tests/Phaser/Connexion/LocalUserTest.ts
@@ -19,8 +19,14 @@ describe("isUserNameValid()", () => {
it("should not validate spaces", () => {
expect(isUserNameValid(' ')).toBe(false);
});
- it("should not validate special characters", () => {
- expect(isUserNameValid('a&-')).toBe(false);
+ it("should validate special characters", () => {
+ expect(isUserNameValid('%&-')).toBe(true);
+ });
+ it("should validate accents", () => {
+ expect(isUserNameValid('éàëè')).toBe(true);
+ });
+ it("should validate chinese characters", () => {
+ expect(isUserNameValid('中文鍵盤')).toBe(true);
});
});
diff --git a/front/webpack.config.ts b/front/webpack.config.ts
index a277b15b..82c3935e 100644
--- a/front/webpack.config.ts
+++ b/front/webpack.config.ts
@@ -7,6 +7,7 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import sveltePreprocess from 'svelte-preprocess';
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
+import {DISPLAY_TERMS_OF_USE} from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? 'development';
const isProduction = mode === 'production';
@@ -88,7 +89,16 @@ module.exports = {
preprocess: sveltePreprocess({
scss: true,
sass: true,
- })
+ }),
+ onwarn: function (warning: { code: string }, handleWarning: (warning: { code: string }) => void) {
+ // See https://github.com/sveltejs/svelte/issues/4946#issuecomment-662168782
+
+ if (warning.code === 'a11y-no-onchange') { return }
+ if (warning.code === 'a11y-autofocus') { return }
+
+ // process as usual
+ handleWarning(warning);
+ }
}
}
},
@@ -102,9 +112,17 @@ module.exports = {
}
},
{
- test: /\.(ttf|eot|svg|png|gif|jpg)$/,
+ test: /\.(eot|svg|png|gif|jpg)$/,
exclude: /node_modules/,
type: 'asset'
+ },
+ {
+ test: /\.(woff(2)?|ttf)$/,
+ type: 'asset',
+ generator: {
+ filename: 'fonts/[name][ext]'
+ }
+
}
],
},
@@ -167,7 +185,8 @@ module.exports = {
'JITSI_PRIVATE_MODE': null,
'START_ROOM_URL': null,
'MAX_USERNAME_LENGTH': 8,
- 'MAX_PER_GROUP': 4
+ 'MAX_PER_GROUP': 4,
+ 'DISPLAY_TERMS_OF_USE': false,
})
],
diff --git a/front/yarn.lock b/front/yarn.lock
index bbdf0e06..f422713a 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -30,6 +30,13 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@babel/runtime@^7.14.0":
+ version "7.14.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
+ integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@discoveryjs/json-ext@^0.5.0":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d"
@@ -50,6 +57,11 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@fontsource/press-start-2p@^4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@fontsource/press-start-2p/-/press-start-2p-4.3.0.tgz#37124387f7fbfe7792b5fc9a1906b80d9aeda4c6"
+ integrity sha512-gmS4070EoZp5/6NUJ+tBnvtDiSmFcR+S+ClAOJ8NGFXDWOkO12yMnyGJEJaDCNCAMX0s2TQCcmr6qWKx5ad3RQ==
+
"@nodelib/fs.scandir@2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69"
@@ -773,6 +785,14 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+automation-events@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/automation-events/-/automation-events-4.0.1.tgz#93acef8a457cbea65f16fdcef8f349fd2fafe298"
+ integrity sha512-8bQx+PVtNDMD5F2H40cQs7oexZve3Z0xC9fWRQT4fltF65f/WsSpjM4jpulL4K4yLLB71oi4/xVJJCJ5I/Kjbw==
+ dependencies:
+ "@babel/runtime" "^7.14.0"
+ tslib "^2.2.0"
+
available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
@@ -4313,6 +4333,11 @@ rechoir@^0.7.0:
dependencies:
resolve "^1.9.0"
+regenerator-runtime@^0.13.4:
+ version "0.13.7"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+ integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -4877,6 +4902,15 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+standardized-audio-context@^25.2.4:
+ version "25.2.4"
+ resolved "https://registry.yarnpkg.com/standardized-audio-context/-/standardized-audio-context-25.2.4.tgz#d64dbdd70615171ec90d1b7151a0d945af94cf3d"
+ integrity sha512-uQKZXRnXrPVO1V6SwZ7PiV3RkQqRY3n7i6Q8nbTXYvoz8NftRNzfOIlwefpuC8LVLUUs9dhbKTpP+WOA82dkBw==
+ dependencies:
+ "@babel/runtime" "^7.14.0"
+ automation-events "^4.0.1"
+ tslib "^2.2.0"
+
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -5207,7 +5241,7 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.3:
+tslib@^2.0.3, tslib@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
@@ -5616,9 +5650,9 @@ wrappy@1:
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^6.2.1:
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
- integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
+ integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
dependencies:
async-limiter "~1.0.0"
diff --git a/maps/Tuto/scriptTuto.js b/maps/Tuto/scriptTuto.js
index 65962a94..8821134b 100644
--- a/maps/Tuto/scriptTuto.js
+++ b/maps/Tuto/scriptTuto.js
@@ -5,6 +5,12 @@ var targetObjectTutoBubble ='Tutobubble';
var targetObjectTutoChat ='tutoChat';
var targetObjectTutoExplanation ='tutoExplanation';
var popUpExplanation = undefined;
+var enterSoundUrl = "webrtc-in.mp3";
+var exitSoundUrl = "webrtc-out.mp3";
+var soundConfig = {
+ volume : 0.2,
+ loop : false
+}
function launchTuto (){
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
{
@@ -25,7 +31,8 @@ function launchTuto (){
label: "Got it!",
className : "success",callback:(popup2 => {
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.displayBubble();
+ WA.loadSound(enterSoundUrl).play(soundConfig);
if (!isFirstTimeTuto) {
isFirstTimeTuto = true;
launchTuto();
@@ -71,4 +79,5 @@ WA.onEnterZone('popupZone', () => {
WA.onLeaveZone('popupZone', () => {
if (popUpExplanation !== undefined) popUpExplanation.close();
WA.removeBubble();
+ WA.loadSound(exitSoundUrl).play(soundConfig);
})
diff --git a/maps/Tuto/webrtc-in.mp3 b/maps/Tuto/webrtc-in.mp3
new file mode 100644
index 00000000..34e22003
Binary files /dev/null and b/maps/Tuto/webrtc-in.mp3 differ
diff --git a/maps/Tuto/webrtc-out.mp3 b/maps/Tuto/webrtc-out.mp3
new file mode 100644
index 00000000..dcf02928
Binary files /dev/null and b/maps/Tuto/webrtc-out.mp3 differ
diff --git a/maps/Village/Village.json b/maps/Village/Village.json
index e4ee93bf..30733388 100644
--- a/maps/Village/Village.json
+++ b/maps/Village/Village.json
@@ -33,7 +33,7 @@
"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, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 1979, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 1979, 0, 1979, 1979, 0, 1979, 1979, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 1979, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 1979, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 1979, 1979, 0, 1979, 1979, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 1979, 1979, 0, 1979, 1979, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 1979, 1979, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 1979, 0, 1979, 0, 0, 1979, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 1979, 0, 0, 0, 0, 0, 0, 0, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 1979, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":39,
"id":7,
"name":"collides",
@@ -769,7 +769,20 @@
"tilecount":121,
"tileheight":32,
"tilewidth":32
- },
+ },
+ {
+ "columns":3,
+ "firstgid":4611,
+ "image":"su1 Student fmale 12.png",
+ "imageheight":128,
+ "imagewidth":96,
+ "margin":0,
+ "name":"su1 Student fmale 12",
+ "spacing":0,
+ "tilecount":12,
+ "tileheight":32,
+ "tilewidth":32
+ },
{
"columns":5,
"firstgid":4611,
diff --git a/maps/tests/Audience.mp3 b/maps/tests/Audience.mp3
new file mode 100644
index 00000000..81745d14
Binary files /dev/null and b/maps/tests/Audience.mp3 differ
diff --git a/maps/tests/SoundScript.js b/maps/tests/SoundScript.js
new file mode 100644
index 00000000..f90dfe0f
--- /dev/null
+++ b/maps/tests/SoundScript.js
@@ -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);
+ }
diff --git a/maps/tests/SoundTest.json b/maps/tests/SoundTest.json
new file mode 100644
index 00000000..f1e38761
--- /dev/null
+++ b/maps/tests/SoundTest.json
@@ -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
+}
\ No newline at end of file
diff --git a/maps/tests/Win.ogg b/maps/tests/Win.ogg
new file mode 100644
index 00000000..43880a77
Binary files /dev/null and b/maps/tests/Win.ogg differ
diff --git a/maps/tests/help_camera_setting.json b/maps/tests/help_camera_setting.json
new file mode 100644
index 00000000..2dcdec3a
--- /dev/null
+++ b/maps/tests/help_camera_setting.json
@@ -0,0 +1,164 @@
+{ "compressionlevel":-1,
+ "height":10,
+ "infinite":false,
+ "layers":[
+ {
+ "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
+ "height":10,
+ "id":3,
+ "name":"bottom",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":10,
+ "x":0,
+ "y":0
+ },
+ {
+ "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":10,
+ "id":1,
+ "name":"start",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":10,
+ "x":0,
+ "y":0
+ },
+ {
+ "draworder":"topdown",
+ "id":2,
+ "name":"floorLayer",
+ "objects":[
+ {
+ "height":254.57168784029,
+ "id":1,
+ "name":"",
+ "rotation":0,
+ "text":
+ {
+ "fontfamily":"Sans Serif",
+ "pixelsize":12,
+ "text":"Test 1 : \nBlock permission to camera and\/or microphone access.\n\nResult 1 :\nOrange popup show at the bottom of the screen.\nIf you click on it, the HelpCameraSetting popup open.\n\nTest 2 : \nReload the page and block permission to camera and\/or microphone access on the camera setting page.\n\nResult 2 : \nOrange popup show at the bottom of the screen.\nIf you click on it, the HelpCameraSetting popup open.\n",
+ "wrap":true
+ },
+ "type":"",
+ "visible":true,
+ "width":295.278811252269,
+ "x":12.2517014519056,
+ "y":49.3021778584392
+ }],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":6,
+ "nextobjectid":2,
+ "orientation":"orthogonal",
+ "renderorder":"right-down",
+ "tiledversion":"1.4.3",
+ "tileheight":32,
+ "tilesets":[
+ {
+ "columns":8,
+ "firstgid":1,
+ "image":"Validation\/tileset_dungeon.png",
+ "imageheight":256,
+ "imagewidth":256,
+ "margin":0,
+ "name":"dungeon",
+ "spacing":0,
+ "tilecount":64,
+ "tileheight":32,
+ "tiles":[
+ {
+ "id":0,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":1,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":2,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":5,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":8,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":10,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":16,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":17,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ },
+ {
+ "id":18,
+ "properties":[
+ {
+ "name":"collides",
+ "type":"bool",
+ "value":true
+ }]
+ }],
+ "tilewidth":32
+ }],
+ "tilewidth":32,
+ "type":"map",
+ "version":1.4,
+ "width":10
+}
\ No newline at end of file
diff --git a/maps/tests/index.html b/maps/tests/index.html
index 9b54a5af..9c95c281 100644
--- a/maps/tests/index.html
+++ b/maps/tests/index.html
@@ -42,6 +42,14 @@
Testing scripting API with a script
+
+
+ Success Failure Pending
+ |
+
+ Testing scripting API loadSound() function
+ |
+
Success Failure Pending
@@ -74,6 +82,14 @@
Test energy consumption
|
+
+
+ Success Failure Pending
+ |
+
+ Test the HelpCameraSettingScene
+ |
+