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

Turn on your camera and microphone

+
+ {#if $localStreamStore.stream} + + {:else } +
+ +
+ {/if} + + +
+ + {#if $cameraListStore.length > 1 } +
+ Camera +
+ +
+
+ {/if} + + {#if $microphoneListStore.length > 1 } +
+ Microphone +
+ +
+
+ {/if} + +
+
+ +
+
+ + + 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 @@ + + +
+
+

Camera / Microphone access needed

+

Permission denied

+

You must allow camera and microphone access in your browser.

+

+ {#if isFirefox } +

Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.

+ + {:else if isChrome && !isAndroid } + + {/if} +

+
+
+ + +
+
+ + + 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 @@ + + +
+
+ WorkAdventure logo +
+
+

Enter your name

+
+ {startValidating = true}} class:is-error={name.trim() === '' && startValidating} /> +
+ {#if name.trim() === '' && startValidating } +

The name is empty

+ {/if} +
+ + {#if DISPLAY_TERMS_OF_USE} +
+

By continuing, you are agreeing our terms of use, privacy policy and cookie policy.

+
+ {/if} +
+ +
+
+ + 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 @@ + + +
+
+

Select your companion

+ + +
+
+ + +
+
+ + 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 playing +

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'} +
+
+

Sorry

+

This user doesn't have a contact card.

+
+ +
+

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

Select your WOKA

+ + +
+
+ + +
+
+ + \ 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 + +