Merge branch 'develop' into iframe-api-refactor

This commit is contained in:
jonnytest1 2021-05-28 00:38:12 +02:00 committed by GitHub
commit b35207a578
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
97 changed files with 3192 additions and 707 deletions

View file

@ -10,6 +10,8 @@ START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. # If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
# Keep empty if you are sharing hard coded / clear text credentials. # Keep empty if you are sharing hard coded / clear text credentials.
TURN_STATIC_AUTH_SECRET= TURN_STATIC_AUTH_SECRET=
DISABLE_NOTIFICATIONS=true
SKIP_RENDER_OPTIMIZATIONS=false
# The email address used by Let's encrypt to send renewal warnings (compulsory) # The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL= ACME_EMAIL=

View file

@ -8,12 +8,17 @@
### Updates ### Updates
- Added the emote feature to WorkAdventure. (@Kharhamel, @Tabascoeye)
- The emote menu can be opened by clicking on your character.
- Clicking on one of its element will close the menu and play an emote above your character.
- This emote can be seen by other players.
- Mobile support has been improved - Mobile support has been improved
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
- Mouse wheel support to zoom in / out - Mouse wheel support to zoom in / out
- Pinch support on mobile to zoom in / out - Pinch support on mobile to zoom in / out
- Improved virtual joystick size (adapts to the zoom level) - Improved virtual joystick size (adapts to the zoom level)
- New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes ### Bug Fixes

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,6 +33,8 @@ services:
STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install STARTUP_COMMAND_2: yarn install
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349" TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS"
SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS"
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: "" TURN_USER: ""

View file

@ -33,6 +33,8 @@ services:
STARTUP_COMMAND_2: yarn install STARTUP_COMMAND_2: yarn install
STUN_SERVER: "stun:stun.l.google.com:19302" STUN_SERVER: "stun:stun.l.google.com:19302"
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349" TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS"
SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS"
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: "" TURN_USER: ""

View file

@ -38,6 +38,8 @@
<div class="main-container" id="main-container"> <div class="main-container" id="main-container">
<!-- Create the editor container --> <!-- Create the editor container -->
<div id="game" class="game"> <div id="game" class="game">
<div id="svelte-overlay">
</div>
<div id="game-overlay" class="game-overlay"> <div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section"> <div id="main-section" class="main-section">
</div> </div>
@ -71,7 +73,6 @@
<img id="microphone-close" src="resources/logos/microphone-close.svg"> <img id="microphone-close" src="resources/logos/microphone-close.svg">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="cowebsite" class="cowebsite hidden"> <div id="cowebsite" class="cowebsite hidden">
@ -106,7 +107,7 @@
</div> </div>
</div> </div>
<div class="audioplayer"> <div class="audioplayer">
<label id="label-audioplayer_decrease_while_talking" for="audiooplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations"> <label id="label-audioplayer_decrease_while_talking" for="audioplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
reduce in conversations reduce in conversations
<input type="checkbox" id="audioplayer_decrease_while_talking" checked /> <input type="checkbox" id="audioplayer_decrease_while_talking" checked />
</label> </label>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -4,21 +4,30 @@
"main": "index.js", "main": "index.js",
"license": "SEE LICENSE IN LICENSE.txt", "license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": { "devDependencies": {
"@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3", "@types/google-protobuf": "^3.7.3",
"@types/jasmine": "^3.5.10", "@types/jasmine": "^3.5.10",
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.3.0",
"@types/quill": "^1.3.7", "@types/quill": "^1.3.7",
"@types/webpack-dev-server": "^3.11.4", "@types/webpack-dev-server": "^3.11.4",
"@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",
"css-loader": "^5.2.4", "css-loader": "^5.2.4",
"eslint": "^7.26.0", "eslint": "^7.26.0",
"fork-ts-checker-webpack-plugin": "^6.2.9",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2",
"sass": "^1.32.12", "sass": "^1.32.12",
"sass-loader": "^11.1.0", "sass-loader": "^11.1.0",
"svelte": "^3.38.2",
"svelte-loader": "^3.1.1",
"svelte-preprocess": "^4.7.3",
"ts-loader": "^9.1.2", "ts-loader": "^9.1.2",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"webpack": "^5.37.0", "webpack": "^5.37.0",
"webpack-cli": "^4.7.0", "webpack-cli": "^4.7.0",
@ -39,9 +48,9 @@
"socket.io-client": "^2.3.0" "socket.io-client": "^2.3.0"
}, },
"scripts": { "scripts": {
"start": "webpack serve --open", "start": "TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "NODE_ENV=production webpack", "build": "TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts"
} }

View file

@ -9,6 +9,8 @@ import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenPopupEvent } from './OpenPopupEvent';
import type { OpenTabEvent } from './OpenTabEvent'; import type { OpenTabEvent } from './OpenTabEvent';
import type { UserInputChatEvent } from './UserInputChatEvent'; import type { UserInputChatEvent } from './UserInputChatEvent';
import type {LoadSoundEvent} from "./LoadSoundEvent";
import type {PlaySoundEvent} from "./PlaySoundEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
@ -29,6 +31,9 @@ export type IframeEventMap = {
restorePlayerControls: null restorePlayerControls: null
displayBubble: null displayBubble: null
removeBubble: null removeBubble: null
loadSound: LoadSoundEvent
playSound: PlaySoundEvent
stopSound: null
} }
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
<script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import {menuIconVisible} from "../Stores/MenuStore";
</script>
<div>
<!-- {#if $menuIconVisible}
<MenuIcon />
{/if} -->
</div>

View file

@ -0,0 +1,33 @@
<script lang="typescript">
</script>
<main class="menuIcon">
<section>
<button>
<img src="/static/images/menu.svg" alt="Open menu">
</button>
</section>
</main>
<style lang="scss">
.menuIcon button {
background-color: black;
color: white;
border-radius: 7px;
padding: 2px 8px;
img {
width: 14px;
padding-top: 0;
/*cursor: url('/resources/logos/cursor_pointer.png'), pointer;*/
}
}
.menuIcon section {
margin: 10px;
}
@media only screen and (max-height: 700px) {
.menuIcon section {
margin: 2px;
}
}
</style>

View file

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

View file

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

View file

@ -2,9 +2,10 @@ const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json'; const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json';
const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost'; const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost';
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost'; const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
const ADMIN_URL = process.env.ADMIN_URL || "//workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || ""; const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
const TURN_USER: string = process.env.TURN_USER || ''; const TURN_USER: string = process.env.TURN_USER || '';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ''; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
@ -19,9 +20,10 @@ export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window
export { export {
DEBUG_MODE, DEBUG_MODE,
START_ROOM_URL, START_ROOM_URL,
SKIP_RENDER_OPTIMIZATIONS,
DISABLE_NOTIFICATIONS,
PUSHER_URL, PUSHER_URL,
UPLOADER_URL, UPLOADER_URL,
ADMIN_URL,
POSITION_DELAY, POSITION_DELAY,
MAX_EXTRAPOLATION_TIME, MAX_EXTRAPOLATION_TIME,
STUN_SERVER, STUN_SERVER,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,14 +21,10 @@ export class RemotePlayer extends Character {
companion: string|null, companion: string|null,
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1); super(Scene, x, y, texturesPromise, name, direction, moving, 1, companion, companionTexturePromise);
//set data //set data
this.userId = userId; this.userId = userId;
if (typeof companion === 'string') {
this.addCompanion(companion, companionTexturePromise);
}
} }
updatePosition(position: PointInterface): void { updatePosition(position: PointInterface): void {
@ -42,4 +38,8 @@ export class RemotePlayer extends Character {
this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections); this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections);
} }
} }
isClickable(): boolean {
return false; //todo: make remote players clickable if they are logged in.
}
} }

View file

@ -0,0 +1,8 @@
//this file contains all the depth indexes which will be used in our game
export const DEPTH_TILE_INDEX = 0;
//Note: Player characters use their y coordinate as their depth to simulate a perspective.
//See the Character class.
export const DEPTH_OVERLAY_INDEX = 10000;
export const DEPTH_INGAME_TEXT_INDEX = 100000;
export const DEPTH_UI_INDEX = 1000000;

View file

@ -3,6 +3,7 @@ import GameObject = Phaser.GameObjects.GameObject;
import Events = Phaser.Scenes.Events; import Events = Phaser.Scenes.Events;
import AnimationEvents = Phaser.Animations.Events; import AnimationEvents = Phaser.Animations.Events;
import StructEvents = Phaser.Structs.Events; import StructEvents = Phaser.Structs.Events;
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
/** /**
* A scene that can track its dirty/pristine state. * A scene that can track its dirty/pristine state.
@ -11,6 +12,7 @@ export abstract class DirtyScene extends ResizableScene {
private isAlreadyTracking: boolean = false; private isAlreadyTracking: boolean = false;
protected dirty:boolean = true; protected dirty:boolean = true;
private objectListChanged:boolean = true; private objectListChanged:boolean = true;
private physicsEnabled: boolean = false;
/** /**
* Track all objects added to the scene and adds a callback each time an animation is added. * Track all objects added to the scene and adds a callback each time an animation is added.
@ -19,7 +21,7 @@ export abstract class DirtyScene extends ResizableScene {
* Note: this does not work with animations from sprites inside containers. * Note: this does not work with animations from sprites inside containers.
*/ */
protected trackDirtyAnims(): void { protected trackDirtyAnims(): void {
if (this.isAlreadyTracking) { if (this.isAlreadyTracking || SKIP_RENDER_OPTIMIZATIONS) {
return; return;
} }
this.isAlreadyTracking = true; this.isAlreadyTracking = true;
@ -36,6 +38,27 @@ export abstract class DirtyScene extends ResizableScene {
this.events.on(Events.RENDER, () => { this.events.on(Events.RENDER, () => {
this.objectListChanged = false; this.objectListChanged = false;
}); });
this.physics.disableUpdate();
this.events.on(Events.POST_UPDATE, () => {
let objectMoving = false;
for (const body of this.physics.world.bodies.entries) {
if (body.velocity.x !== 0 || body.velocity.y !== 0) {
this.objectListChanged = true;
objectMoving = true;
if (!this.physicsEnabled) {
this.physics.enableUpdate();
this.physicsEnabled = true;
}
break;
}
}
if (!objectMoving && this.physicsEnabled) {
this.physics.disableUpdate();
this.physicsEnabled = false;
}
});
} }
private trackAnimation(): void { private trackAnimation(): void {

View file

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

View file

@ -1,3 +1,8 @@
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import {waScaleManager} from "../Services/WaScaleManager";
import {ResizableScene} from "../Login/ResizableScene";
const Events = Phaser.Core.Events; const Events = Phaser.Core.Events;
/** /**
@ -5,8 +10,27 @@ const Events = Phaser.Core.Events;
* It comes with an optimization to skip rendering. * It comes with an optimization to skip rendering.
* *
* Beware, the "step" function might vary in future versions of Phaser. * Beware, the "step" function might vary in future versions of Phaser.
*
* It also automatically calls "onResize" on any scenes extending ResizableScene.
*/ */
export class Game extends Phaser.Game { export class Game extends Phaser.Game {
private _isDirty = false;
constructor(GameConfig: Phaser.Types.Core.GameConfig) {
super(GameConfig);
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) public step(time: number, delta: number)
{ {
// @ts-ignore // @ts-ignore
@ -35,7 +59,7 @@ export class Game extends Phaser.Game {
eventEmitter.emit(Events.POST_STEP, time, delta); eventEmitter.emit(Events.POST_STEP, time, delta);
// This "if" is the changed introduced by the new "Game" class to avoid rendering unnecessarily. // This "if" is the changed introduced by the new "Game" class to avoid rendering unnecessarily.
if (this.isDirty()) { if (SKIP_RENDER_OPTIMIZATIONS || this.isDirty()) {
const renderer = this.renderer; const renderer = this.renderer;
// Run the Pre-render (clearing the canvas, setting background colors, etc) // Run the Pre-render (clearing the canvas, setting background colors, etc)
@ -62,6 +86,11 @@ export class Game extends Phaser.Game {
} }
private isDirty(): boolean { private isDirty(): boolean {
if (this._isDirty) {
this._isDirty = false;
return true;
}
// Loop through the scenes in forward order // Loop through the scenes in forward order
for (let i = 0; i < this.scene.scenes.length; i++) for (let i = 0; i < this.scene.scenes.length; i++)
{ {
@ -85,4 +114,11 @@ export class Game extends Phaser.Game {
return false; return false;
} }
/**
* Marks the game as needing to be redrawn.
*/
public markDirty(): void {
this._isDirty = true;
}
} }

View file

@ -9,7 +9,7 @@ import type {
PositionInterface, PositionInterface,
RoomJoinedMessageInterface RoomJoinedMessageInterface
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player";
import { import {
DEBUG_MODE, DEBUG_MODE,
JITSI_PRIVATE_MODE, JITSI_PRIVATE_MODE,
@ -52,6 +52,7 @@ import {mediaManager} from "../../WebRtc/MediaManager";
import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
import type {ActionableItem} from "../Items/ActionableItem"; import type {ActionableItem} from "../Items/ActionableItem";
import {UserInputManager} from "../UserInput/UserInputManager"; import {UserInputManager} from "../UserInput/UserInputManager";
import {soundManager} from "./SoundManager";
import type {UserMovedMessage} from "../../Messages/generated/messages_pb"; import type {UserMovedMessage} from "../../Messages/generated/messages_pb";
import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils";
import {connectionManager} from "../../Connexion/ConnectionManager"; import {connectionManager} from "../../Connexion/ConnectionManager";
@ -90,7 +91,10 @@ import {TextUtils} from "../Components/TextUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager"; import {waScaleManager} from "../Services/WaScaleManager";
import {peerStore} from "../../Stores/PeerStore";
import {EmoteManager} from "./EmoteManager";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface|null, initPosition: PointInterface|null,
@ -131,7 +135,7 @@ const defaultStartLayerName = 'start';
export class GameScene extends DirtyScene implements CenterListener { export class GameScene extends DirtyScene implements CenterListener {
Terrains : Array<Phaser.Tilemaps.Tileset>; Terrains : Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: CurrentGamerInterface; CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>(); MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap; Map!: Phaser.Tilemaps.Tilemap;
@ -187,7 +191,7 @@ export class GameScene extends DirtyScene implements CenterListener {
private originalMapUrl: string|undefined; private originalMapUrl: string|undefined;
private pinchManager: PinchManager|undefined; private pinchManager: PinchManager|undefined;
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private onVisibilityChangeCallback: () => void; private emoteManager!: EmoteManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
super({ super({
@ -207,7 +211,6 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => { this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
this.connectionAnswerPromiseResolve = resolve; this.connectionAnswerPromiseResolve = resolve;
}); });
this.onVisibilityChangeCallback = this.onVisibilityChange.bind(this);
} }
//hook preload scene //hook preload scene
@ -225,6 +228,11 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickBaseKey, joystickBaseImg);
this.load.image(joystickThumbKey, joystickThumbImg); this.load.image(joystickThumbKey, joystickThumbImg);
} }
//todo: in an emote manager.
this.load.spritesheet('emote-music', 'resources/emotes/pipo-popupemotes005.png', {
frameHeight: 32,
frameWidth: 32,
});
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
// If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments)
if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) {
@ -420,7 +428,7 @@ export class GameScene extends DirtyScene implements CenterListener {
} }
} }
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000; depth = DEPTH_OVERLAY_INDEX;
} }
if (layer.type === 'objectgroup') { if (layer.type === 'objectgroup') {
for (const object of layer.objects) { for (const object of layer.objects) {
@ -507,7 +515,7 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connect(); this.connect();
} }
document.addEventListener('visibilitychange', this.onVisibilityChangeCallback); this.emoteManager = new EmoteManager(this);
} }
/** /**
@ -612,6 +620,7 @@ export class GameScene extends DirtyScene implements CenterListener {
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
peerStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection); this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
@ -629,7 +638,6 @@ export class GameScene extends DirtyScene implements CenterListener {
self.chatModeSprite.setVisible(false); self.chatModeSprite.setVisible(false);
self.openChatIcon.setVisible(false); self.openChatIcon.setVisible(false);
audioManager.restoreVolume(); audioManager.restoreVolume();
self.onVisibilityChange();
} }
} }
}) })
@ -867,9 +875,28 @@ ${escapedMessage}
this.userInputManager.disableControls(); this.userInputManager.disableControls();
})); }));
this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=>
{
const url = new URL(playSoundEvent.url, this.MapUrlFile);
soundManager.playSound(this.load,this.sound,url.toString(),playSoundEvent.config);
}))
this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=>
{
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
soundManager.stopSound(this.sound,url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=>
{
const url = new URL(loadSoundEvent.url, this.MapUrlFile);
soundManager.loadSound(this.load,this.sound,url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{
this.userInputManager.restoreControls(); this.userInputManager.restoreControls();
})); }));
let scriptedBubbleSprite : Sprite; let scriptedBubbleSprite : Sprite;
this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{
scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white');
@ -929,12 +956,11 @@ ${escapedMessage}
this.messageSubscription?.unsubscribe(); this.messageSubscription?.unsubscribe();
this.userInputManager.destroy(); this.userInputManager.destroy();
this.pinchManager?.destroy(); this.pinchManager?.destroy();
this.emoteManager.destroy();
for(const iframeEvents of this.iframeSubscriptionList){ for(const iframeEvents of this.iframeSubscriptionList){
iframeEvents.unsubscribe(); iframeEvents.unsubscribe();
} }
document.removeEventListener('visibilitychange', this.onVisibilityChangeCallback);
} }
private removeAllRemotePlayers(): void { private removeAllRemotePlayers(): void {
@ -1042,10 +1068,10 @@ ${escapedMessage}
} }
//todo: push that into the gameManager //todo: push that into the gameManager
private async loadNextGame(exitSceneIdentifier: string){ private loadNextGame(exitSceneIdentifier: string): void {
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
const room = new Room(roomId); const room = new Room(roomId);
await gameManager.loadMap(room, this.scene); gameManager.loadMap(room, this.scene).catch(() => {});
} }
private startUser(layer: ITiledMapTileLayer): PositionInterface { private startUser(layer: ITiledMapTileLayer): PositionInterface {
@ -1087,7 +1113,6 @@ ${escapedMessage}
} }
createCollisionWithPlayer() { createCollisionWithPlayer() {
this.physics.disableUpdate();
//add collision layer //add collision layer
this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => {
this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => {
@ -1121,6 +1146,12 @@ ${escapedMessage}
this.companion, this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
); );
this.CurrentPlayer.on('pointerdown', () => {
this.emoteManager.getMenuImages().then((emoteMenuElements) => this.CurrentPlayer.openOrCloseEmoteMenu(emoteMenuElements))
})
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
this.connection?.emitEmoteEvent(emoteKey);
})
}catch (err){ }catch (err){
if(err instanceof TextureError) { if(err instanceof TextureError) {
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
@ -1221,17 +1252,7 @@ ${escapedMessage}
this.dirty = false; this.dirty = false;
mediaManager.updateScene(); mediaManager.updateScene();
this.currentTick = time; this.currentTick = time;
if (this.CurrentPlayer.isMoving()) {
this.dirty = true;
}
this.CurrentPlayer.moveUser(delta); this.CurrentPlayer.moveUser(delta);
if (this.CurrentPlayer.isMoving()) {
this.dirty = true;
this.physics.enableUpdate();
} else {
this.physics.disableUpdate();
}
// Let's handle all events // Let's handle all events
while (this.pendingEvents.length !== 0) { while (this.pendingEvents.length !== 0) {
@ -1499,8 +1520,6 @@ ${escapedMessage}
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => { mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
this.stopJitsi(); this.stopJitsi();
}); });
this.onVisibilityChange();
} }
public stopJitsi(): void { public stopJitsi(): void {
@ -1509,7 +1528,6 @@ ${escapedMessage}
mediaManager.showGameOverlay(); mediaManager.showGameOverlay();
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi'); mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
this.onVisibilityChange();
} }
//todo: put this into an 'orchestrator' scene (EntryScene?) //todo: put this into an 'orchestrator' scene (EntryScene?)
@ -1549,20 +1567,4 @@ ${escapedMessage}
waScaleManager.zoomModifier *= zoomFactor; waScaleManager.zoomModifier *= zoomFactor;
this.updateCameraOffset(); this.updateCameraOffset();
} }
private onVisibilityChange(): void {
// If the overlay is not displayed, we are in Jitsi. We don't need the webcam.
if (!mediaManager.isGameOverlayVisible()) {
mediaManager.blurCamera();
return;
}
if (document.visibilityState === 'visible') {
mediaManager.focusCamera();
} else {
if (this.simplePeer.getNbConnections() === 0) {
mediaManager.blurCamera();
}
}
}
} }

View file

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

View file

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

View file

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

View file

@ -9,6 +9,8 @@ import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager"; import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu'; const gameMenuKey = 'gameMenu';
@ -53,6 +55,7 @@ export class MenuScene extends Phaser.Scene {
} }
create() { create() {
menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0); this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu'); MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
@ -322,7 +325,7 @@ export class MenuScene extends Phaser.Scene {
if (valueVideo !== this.videoQualityValue) { if (valueVideo !== this.videoQualityValue) {
this.videoQualityValue = valueVideo; this.videoQualityValue = valueVideo;
localUserStore.setVideoQualityValue(valueVideo); localUserStore.setVideoQualityValue(valueVideo);
mediaManager.updateCameraQuality(valueVideo); videoConstraintStore.setFrameRate(valueVideo);
} }
this.closeGameQualityMenu(); this.closeGameQualityMenu();
} }

View file

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

View file

@ -1,4 +1,3 @@
import ScaleManager = Phaser.Scale.ScaleManager;
interface Size { interface Size {
width: number; width: number;
@ -13,8 +12,7 @@ export class HdpiManager {
* @param minRecommendedGamePixelsNumber The minimum number of pixels we want to display "by default" to the user * @param minRecommendedGamePixelsNumber The minimum number of pixels we want to display "by default" to the user
* @param absoluteMinPixelNumber The very minimum of game pixels to display. Below, we forbid zooming more * @param absoluteMinPixelNumber The very minimum of game pixels to display. Below, we forbid zooming more
*/ */
public constructor(private minRecommendedGamePixelsNumber: number, private absoluteMinPixelNumber: number) { public constructor(private minRecommendedGamePixelsNumber: number, private absoluteMinPixelNumber: number) {}
}
/** /**
* Returns the optimal size in "game pixels" based on the screen size in "real pixels". * Returns the optimal size in "game pixels" based on the screen size in "real pixels".
@ -36,16 +34,12 @@ export class HdpiManager {
}; };
} }
let i = 1; const optimalZoomLevel = this.getOptimalZoomLevel(realPixelNumber);
while (realPixelNumber > this.minRecommendedGamePixelsNumber * i * i) {
i++;
}
// Has the canvas more pixels than the screen? This is forbidden // Has the canvas more pixels than the screen? This is forbidden
if ((i - 1) * this._zoomModifier < 1) { if (optimalZoomLevel * this._zoomModifier < 1) {
// Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter) // Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter)
this._zoomModifier = 1 / (i - 1); this._zoomModifier = 1 / optimalZoomLevel;
return { return {
game: { game: {
@ -59,8 +53,8 @@ export class HdpiManager {
} }
} }
const gameWidth = Math.ceil(realPixelScreenSize.width / (i - 1) / this._zoomModifier); const gameWidth = Math.ceil(realPixelScreenSize.width / optimalZoomLevel / this._zoomModifier);
const gameHeight = Math.ceil(realPixelScreenSize.height / (i - 1) / this._zoomModifier); const gameHeight = Math.ceil(realPixelScreenSize.height / optimalZoomLevel / this._zoomModifier);
// Let's ensure we display a minimum of pixels, even if crazily zoomed in. // Let's ensure we display a minimum of pixels, even if crazily zoomed in.
if (gameWidth * gameHeight < this.absoluteMinPixelNumber) { if (gameWidth * gameHeight < this.absoluteMinPixelNumber) {
@ -68,7 +62,7 @@ export class HdpiManager {
const minGameWidth = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.width / realPixelScreenSize.height); const minGameWidth = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.width / realPixelScreenSize.height);
// Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter) // Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter)
this._zoomModifier = realPixelScreenSize.width / minGameWidth / (i - 1); this._zoomModifier = realPixelScreenSize.width / minGameWidth / optimalZoomLevel;
return { return {
game: { game: {
@ -89,12 +83,24 @@ export class HdpiManager {
height: gameHeight, height: gameHeight,
}, },
real: { real: {
width: Math.ceil(realPixelScreenSize.width / (i - 1)) * (i - 1), width: Math.ceil(realPixelScreenSize.width / optimalZoomLevel) * optimalZoomLevel,
height: Math.ceil(realPixelScreenSize.height / (i - 1)) * (i - 1), height: Math.ceil(realPixelScreenSize.height / optimalZoomLevel) * optimalZoomLevel,
} }
} }
} }
/**
* We only accept integer but we make an exception for 1.5
*/
private getOptimalZoomLevel(realPixelNumber: number): number {
const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber);
if (1.5 <= result && result < 2) {
return 1.5
} else {
return Math.floor(result);
}
}
public get zoomModifier(): number { public get zoomModifier(): number {
return this._zoomModifier; return this._zoomModifier;
} }

View file

@ -1,18 +1,22 @@
import {HdpiManager} from "./HdpiManager"; import {HdpiManager} from "./HdpiManager";
import ScaleManager = Phaser.Scale.ScaleManager; import ScaleManager = Phaser.Scale.ScaleManager;
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import type {Game} from "../Game/Game";
class WaScaleManager { class WaScaleManager {
private hdpiManager: HdpiManager; private hdpiManager: HdpiManager;
private scaleManager!: ScaleManager; private scaleManager!: ScaleManager;
private game!: Game;
private actualZoom: number = 1;
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) { public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber); this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
} }
public setScaleManager(scaleManager: ScaleManager) { public setGame(game: Game): void {
this.scaleManager = scaleManager; this.scaleManager = game.scale;
this.game = game;
} }
public applyNewSize() { public applyNewSize() {
@ -25,6 +29,7 @@ class WaScaleManager {
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio}); const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio});
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height); this.scaleManager.resize(gameSize.width, gameSize.height);
@ -32,6 +37,8 @@ class WaScaleManager {
const style = this.scaleManager.canvas.style; const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px'; style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px';
style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px'; style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px';
this.game.markDirty();
} }
public get zoomModifier(): number { public get zoomModifier(): number {
@ -42,6 +49,14 @@ class WaScaleManager {
this.hdpiManager.zoomModifier = zoomModifier; this.hdpiManager.zoomModifier = zoomModifier;
this.applyNewSize(); this.applyNewSize();
} }
/**
* This is used to scale back the ui components to counter-act the zoom.
*/
public get uiScalingFactor(): number {
return this.actualZoom > 1 ? 1 : 1.2;
}
} }
export const waScaleManager = new WaScaleManager(640*480, 196*196); export const waScaleManager = new WaScaleManager(640*480, 196*196);

View file

@ -173,7 +173,7 @@ export class UserInputManager {
} }
destroy(): void { destroy(): void {
this.joystick.destroy(); this.joystick?.destroy();
} }
private initMouseWheel() { private initMouseWheel() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { Subject } from "rxjs";
import type { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import type { GoToPageEvent } from "./Api/Events/GoToPageEvent";
import { IframeResponseEventMap, isIframeResponseEventWrapper } from "./Api/Events/IframeEvent"; import { IframeResponseEventMap, isIframeResponseEventWrapper } from "./Api/Events/IframeEvent";
import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent";
import type { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import type { OpenTabEvent } from "./Api/Events/OpenTabEvent";
import { isUserInputChatEvent, UserInputChatEvent } from "./Api/Events/UserInputChatEvent"; import { isUserInputChatEvent, UserInputChatEvent } from "./Api/Events/UserInputChatEvent";
@ -30,6 +31,11 @@ type ObjectOfKey<Key extends ApiKeys, O = WorkadventureCommandClasses> = O exten
type ShouldAddAttribute<Key extends ApiKeys> = ObjectWithKeyOfUnion<Key>; type ShouldAddAttribute<Key extends ApiKeys> = ObjectWithKeyOfUnion<Key>;
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;
type WorkadventureFunctions = { [K in ApiKeys]: ObjectWithKeyOfUnion<K> extends Function ? K : never }[ApiKeys] type WorkadventureFunctions = { [K in ApiKeys]: ObjectWithKeyOfUnion<K> extends Function ? K : never }[ApiKeys]
type WorkadventureFunctionsFilteredByRoot = { [K in WorkadventureFunctions]: ObjectOfKey<K>["addMethodsAtRoot"] extends true ? K : never }[WorkadventureFunctions] type WorkadventureFunctionsFilteredByRoot = { [K in WorkadventureFunctions]: ObjectOfKey<K>["addMethodsAtRoot"] extends true ? K : never }[WorkadventureFunctions]
@ -55,6 +61,7 @@ export interface WorkAdventureApi extends WorkAdventureApiFiles {
restorePlayerControls(): void; restorePlayerControls(): void;
displayBubble(): void; displayBubble(): void;
removeBubble(): void; removeBubble(): void;
loadSound(url : string): Sound;
} }
@ -70,9 +77,40 @@ declare global {
const userInputChatStream: Subject<UserInputChatEvent> = new Subject(); const userInputChatStream: Subject<UserInputChatEvent> = new Subject();
export class Sound {
constructor(private url: string) {
window.parent.postMessage({
"type" : 'loadSound',
"data": {
url: this.url,
} as LoadSoundEvent
},'*');
}
public play(config : SoundConfig) {
window.parent.postMessage({
"type" : 'playSound',
"data": {
url: this.url,
config
} as PlaySoundEvent
},'*');
return this.url;
}
public stop() {
window.parent.postMessage({
"type" : 'stopSound',
"data": {
url: this.url,
} as StopSoundEvent
},'*');
return this.url;
}
}
window.WA = { window.WA = {
disablePlayerControls(): void { disablePlayerControls(): void {
@ -100,7 +138,11 @@ window.WA = {
}, '*'); }, '*');
}, },
goToPage(url: string): void { loadSound(url: string) : Sound {
return new Sound(url);
},
goToPage(url : string) : void{
window.parent.postMessage({ window.parent.postMessage({
"type": 'goToPage', "type": 'goToPage',
"data": { "data": {

View file

@ -1,6 +1,6 @@
import 'phaser'; import 'phaser';
import GameConfig = Phaser.Types.Core.GameConfig; import GameConfig = Phaser.Types.Core.GameConfig;
import "../dist/resources/style/index.scss"; import "../style/index.scss";
import {DEBUG_MODE, isMobile} from "./Enum/EnvironmentVariable"; import {DEBUG_MODE, isMobile} from "./Enum/EnvironmentVariable";
import {LoginScene} from "./Phaser/Login/LoginScene"; import {LoginScene} from "./Phaser/Login/LoginScene";
@ -21,6 +21,8 @@ import { SelectCharacterMobileScene } from './Phaser/Login/SelectCharacterMobile
import {HdpiManager} from "./Phaser/Services/HdpiManager"; import {HdpiManager} from "./Phaser/Services/HdpiManager";
import {waScaleManager} from "./Phaser/Services/WaScaleManager"; import {waScaleManager} from "./Phaser/Services/WaScaleManager";
import {Game} from "./Phaser/Game/Game"; import {Game} from "./Phaser/Game/Game";
import App from './Components/App.svelte';
import {HtmlUtils} from "./WebRtc/HtmlUtils";
const {width, height} = coWebsiteManager.getGameSize(); const {width, height} = coWebsiteManager.getGameSize();
@ -127,19 +129,12 @@ const config: GameConfig = {
//const game = new Phaser.Game(config); //const game = new Phaser.Game(config);
const game = new Game(config); const game = new Game(config);
waScaleManager.setScaleManager(game.scale); waScaleManager.setGame(game);
window.addEventListener('resize', function (event) { window.addEventListener('resize', function (event) {
coWebsiteManager.resetStyle(); coWebsiteManager.resetStyle();
waScaleManager.applyNewSize(); waScaleManager.applyNewSize();
// Let's trigger the onResize method of any active scene that is a ResizableScene
for (const scene of game.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
scene.onResize(event);
}
}
}); });
coWebsiteManager.onResize.subscribe(() => { coWebsiteManager.onResize.subscribe(() => {
@ -147,3 +142,10 @@ coWebsiteManager.onResize.subscribe(() => {
}); });
iframeListener.init(); iframeListener.init();
const app = new App({
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
props: { },
})
export default app

View file

@ -32,7 +32,7 @@
position: absolute; position: absolute;
background: none; background: none;
border: none; border: none;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
img { img {
height: 25px; height: 25px;

View file

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 979 B

View file

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

View file

@ -1,9 +1,9 @@
*{ *{
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
cursor: url('/resources/logos/cursor_normal.png'), auto; cursor: url('./images/cursor_normal.png'), auto;
} }
* a, button, select{ * a, button, select{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
body{ body{
overflow: hidden; overflow: hidden;
@ -39,7 +39,7 @@ body .message-info.warning{
position: relative; position: relative;
transition: all 0.2s ease; transition: all 0.2s ease;
background-color: #00000099; background-color: #00000099;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.video-container i{ .video-container i{
position: absolute; position: absolute;
@ -75,7 +75,7 @@ body .message-info.warning{
.video-container button.report{ .video-container button.report{
display: block; display: block;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
background: none; background: none;
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
border: none; border: none;
@ -108,7 +108,7 @@ body .message-info.warning{
left: 5px; left: 5px;
margin: 0; margin: 0;
padding: 0; padding: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
width: 25px; width: 25px;
height: 25px; height: 25px;
} }
@ -118,7 +118,7 @@ body .message-info.warning{
left: 36px; left: 36px;
color: white; color: white;
font-size: 16px; font-size: 16px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.video-container img.active { .video-container img.active {
display: block !important; display: block !important;
@ -126,7 +126,7 @@ body .message-info.warning{
.video-container video{ .video-container video{
height: 100%; height: 100%;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.video-container video:focus{ .video-container video:focus{
@ -143,6 +143,11 @@ body .message-info.warning{
bottom: 30px; bottom: 30px;
border-radius: 15px 15px 15px 15px; border-radius: 15px 15px 15px 15px;
max-height: 20%; max-height: 20%;
transition: right 350ms;
}
#div-myCamVideo.hide {
right: -20vw;
} }
video#myCamVideo{ video#myCamVideo{
@ -196,17 +201,17 @@ video#myCamVideo{
display: inline-flex; display: inline-flex;
bottom: 10px; bottom: 10px;
right: 15px; right: 15px;
width: 15vw; width: 180px;
height: 40px; height: 40px;
text-align: center; text-align: center;
align-content: center; align-content: center;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-end;
justify-items: center; justify-items: center;
} }
/*btn animation*/ /*btn animation*/
.btn-cam-action div{ .btn-cam-action div{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
/*position: absolute;*/ /*position: absolute;*/
border: solid 0px black; border: solid 0px black;
width: 44px; width: 44px;
@ -216,7 +221,6 @@ video#myCamVideo{
border-radius: 48px; border-radius: 48px;
transform: translateY(20px); transform: translateY(20px);
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
margin-bottom: 20px;
margin: 0 4%; margin: 0 4%;
} }
.btn-cam-action div.disabled { .btn-cam-action div.disabled {
@ -248,6 +252,12 @@ video#myCamVideo{
transition: all .2s; transition: all .2s;
/*right: 224px;*/ /*right: 224px;*/
} }
.btn-monitor.hide {
transform: translateY(60px);
}
.btn-cam-action:hover .btn-monitor.hide{
transform: translateY(60px);
}
.btn-copy{ .btn-copy{
pointer-events: auto; pointer-events: auto;
transition: all .3s; transition: all .3s;
@ -260,7 +270,7 @@ video#myCamVideo{
top: calc(48px - 37px); top: calc(48px - 37px);
left: calc(48px - 41px); left: calc(48px - 41px);
position: relative; position: relative;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
/* Spinner */ /* Spinner */
@ -346,6 +356,8 @@ video#myCamVideo{
#myCamVideoSetup { #myCamVideoSetup {
width: 100%; width: 100%;
height: 100%; height: 100%;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
} }
.webrtcsetup.active{ .webrtcsetup.active{
display: block; display: block;
@ -572,7 +584,7 @@ input[type=range]:focus::-ms-fill-upper {
margin: 2%; margin: 2%;
flex-basis: 96%; flex-basis: 96%;
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, flex-basis 0.2s; transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, flex-basis 0.2s;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
pointer-events: auto; pointer-events: auto;
/*flex-shrink: 2;*/ /*flex-shrink: 2;*/
} }
@ -590,7 +602,7 @@ input[type=range]:focus::-ms-fill-upper {
.sidebar > div { .sidebar > div {
margin: 2%; margin: 2%;
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s, max-width 0.2s; transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s, max-width 0.2s;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
border-radius: 15px 15px 15px 15px; border-radius: 15px 15px 15px 15px;
pointer-events: auto; pointer-events: auto;
} }
@ -600,7 +612,7 @@ input[type=range]:focus::-ms-fill-upper {
} }
.sidebar > div video { .sidebar > div video {
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
/* Let's make sure videos are vertically centered if they need to be cropped */ /* Let's make sure videos are vertically centered if they need to be cropped */
@ -625,7 +637,7 @@ input[type=range]:focus::-ms-fill-upper {
margin: 1%; margin: 1%;
max-height: 96%; max-height: 96%;
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s; transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.chat-mode > div:hover { .chat-mode > div:hover {
@ -715,7 +727,7 @@ input[type=range]:focus::-ms-fill-upper {
margin-top: 6px; margin-top: 6px;
width: 30px; width: 30px;
height: 30px; height: 30px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
padding: 0 5px; padding: 0 5px;
transition: all .5s ease; transition: all .5s ease;
transform: rotateY(0); transform: rotateY(0);
@ -739,7 +751,7 @@ input[type=range]:focus::-ms-fill-upper {
.main-console div.console:hover, .main-console div.console:hover,
.message-container div.clear:hover { .message-container div.clear:hover {
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
top: calc(100% + 5px); top: calc(100% + 5px);
transform: scale(1.2) translateY(3px); transform: scale(1.2) translateY(3px);
} }
@ -772,7 +784,7 @@ input[type=range]:focus::-ms-fill-upper {
transition: all .2s ease; transition: all .2s ease;
} }
.main-console .btn-action .btn:hover{ .main-console .btn-action .btn:hover{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
background-color: #ffda01; background-color: #ffda01;
color: black; color: black;
border: 1px solid black; border: 1px solid black;
@ -787,7 +799,7 @@ input[type=range]:focus::-ms-fill-upper {
.main-console .menu span { .main-console .menu span {
margin: 20px; margin: 20px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.main-console .menu span.active { .main-console .menu span.active {
@ -821,10 +833,10 @@ input[type=range]:focus::-ms-fill-upper {
} }
.main-console section div.upload label img{ .main-console section div.upload label img{
height: 150px; height: 150px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.main-console section div.upload label img{ .main-console section div.upload label img{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
@ -917,7 +929,7 @@ div.modal-report-user{
right: 0; right: 0;
left: auto; left: auto;
top: 0; top: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
width: 15px; width: 15px;
height: 15px; height: 15px;
margin: 10px; margin: 10px;
@ -936,7 +948,7 @@ div.modal-report-user{
transition: all .2s ease; transition: all .2s ease;
} }
.modal-report-user button:hover{ .modal-report-user button:hover{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
background-color: #ffda01; background-color: #ffda01;
color: black; color: black;
border: 1px solid black; border: 1px solid black;
@ -979,7 +991,7 @@ div.modal-report-user{
} }
.discussion .active-btn{ .discussion .active-btn{
display: none; display: none;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
height: 50px; height: 50px;
width: 50px; width: 50px;
background-color: #2d2d2dba; background-color: #2d2d2dba;
@ -1008,7 +1020,7 @@ div.modal-report-user{
right: 10px; right: 10px;
background: none; background: none;
border: none; border: none;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.discussion .close-btn img{ .discussion .close-btn img{
height: 15px; height: 15px;
@ -1033,7 +1045,7 @@ div.modal-report-user{
background-color: #ffffff69; background-color: #ffffff69;
padding: 5px; padding: 5px;
border-radius: 15px; border-radius: 15px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
} }
.discussion .participants .participant:hover{ .discussion .participants .participant:hover{
@ -1066,7 +1078,7 @@ div.modal-report-user{
} }
.discussion .participants .participant button.report-btn{ .discussion .participants .participant button.report-btn{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
position: absolute; position: absolute;
background-color: #2d2d2dba; background-color: #2d2d2dba;
right: 34px; right: 34px;
@ -1176,7 +1188,7 @@ div.action.danger{
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
} }
div.action p.action-body{ div.action p.action-body{
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('./images/cursor_pointer.png'), pointer;
padding: 10px; padding: 10px;
background-color: #2d2d2dba; background-color: #2d2d2dba;
color: #fff; color: #fff;
@ -1225,3 +1237,11 @@ div.action.danger p.action-body{
50% {bottom: 30px;} 50% {bottom: 30px;}
100% {bottom: 40px;} 100% {bottom: 40px;}
} }
#svelte-overlay {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}

View file

@ -50,6 +50,6 @@ describe("Test HdpiManager", () => {
const result = hdpiManager.getOptimalGameSize({ width: 1280, height: 768 }); const result = hdpiManager.getOptimalGameSize({ width: 1280, height: 768 });
expect(result.game.width).toEqual(1280); expect(result.game.width).toEqual(1280);
expect(result.game.height).toEqual(768); expect(result.game.height).toEqual(768);
expect(hdpiManager.zoomModifier).toEqual(1); expect(hdpiManager.zoomModifier).toEqual(2 / 3);
}); });
}); });

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*", "./tests/**/*"],
"compilerOptions": {
"module": "CommonJS",
"lib": ["es2015","dom"],
"types": ["svelte", "node"]
},
}

View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"esModuleInterop": true
}
}

View file

@ -1,9 +1,13 @@
{ {
// "include": ["src/**/*", "webpack.config.ts"],
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"moduleResolution": "node", "moduleResolution": "node",
"module": "CommonJS", //"module": "CommonJS",
"module": "ESNext",
"target": "ES2017", "target": "ES2017",
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,

View file

@ -1,10 +1,12 @@
import {Configuration} from "webpack"; import type {Configuration} from "webpack";
import WebpackDevServer from "webpack-dev-server"; import type WebpackDevServer from "webpack-dev-server";
import path from 'path';
const path = require('path'); import webpack from 'webpack';
const webpack = require('webpack'); import HtmlWebpackPlugin from 'html-webpack-plugin';
const HtmlWebpackPlugin = require('html-webpack-plugin'); import MiniCssExtractPlugin from 'mini-css-extract-plugin';
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); import sveltePreprocess from 'svelte-preprocess';
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
const mode = process.env.NODE_ENV ?? 'development'; const mode = process.env.NODE_ENV ?? 'development';
const isProduction = mode === 'production'; const isProduction = mode === 'production';
@ -33,17 +35,85 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: 'ts-loader', //use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
loader: 'ts-loader',
options: {
transpileOnly: true,
},
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader?url=false', 'sass-loader'], exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader, {
loader: 'css-loader',
options: {
//url: false,
sourceMap: true
}
}, 'sass-loader'],
}, },
{
test: /\.css$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
//url: false,
sourceMap: true
}
}
]
},
{
test: /\.(html|svelte)$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
compilerOptions: {
// Dev mode must be enabled for HMR to work!
dev: isDevelopment
},
emitCss: isProduction,
hotReload: isDevelopment,
hotOptions: {
// List of options and defaults: https://www.npmjs.com/package/svelte-loader-hot#usage
noPreserveState: false,
optimistic: true,
},
preprocess: sveltePreprocess({
scss: true,
sass: true,
})
}
}
},
// Required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4
// See: https://github.com/sveltejs/svelte-loader#usage
{
test: /node_modules\/svelte\/.*\.mjs$/,
resolve: {
fullySpecified: false
}
},
{
test: /\.(ttf|eot|svg|png|gif|jpg)$/,
exclude: /node_modules/,
type: 'asset'
}
], ],
}, },
resolve: { resolve: {
extensions: [ '.tsx', '.ts', '.js' ], alias: {
svelte: path.resolve('node_modules', 'svelte')
},
extensions: [ '.tsx', '.ts', '.js', '.svelte' ],
mainFields: ['svelte', 'browser', 'module', 'main']
}, },
output: { output: {
filename: (pathData) => { filename: (pathData) => {
@ -54,11 +124,14 @@ module.exports = {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
publicPath: '/' publicPath: '/'
}, },
/*externals:[
require('webpack-require-http')
],*/
plugins: [ plugins: [
new MiniCssExtractPlugin({filename: 'style.[contenthash].css'}), new webpack.HotModuleReplacementPlugin(),
new ForkTsCheckerWebpackPlugin({
eslint: {
files: './src/**/*.ts'
}
}),
new MiniCssExtractPlugin({filename: '[name].[contenthash].css'}),
new HtmlWebpackPlugin( new HtmlWebpackPlugin(
{ {
template: './dist/index.tmpl.html.tmp', template: './dist/index.tmpl.html.tmp',
@ -77,8 +150,11 @@ module.exports = {
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Phaser: 'phaser' Phaser: 'phaser'
}), }),
new NodePolyfillPlugin(),
new webpack.EnvironmentPlugin({ new webpack.EnvironmentPlugin({
'API_URL': null, 'API_URL': null,
'SKIP_RENDER_OPTIMIZATIONS': false,
'DISABLE_NOTIFICATIONS': false,
'PUSHER_URL': undefined, 'PUSHER_URL': undefined,
'UPLOADER_URL': null, 'UPLOADER_URL': null,
'ADMIN_URL': null, 'ADMIN_URL': null,

File diff suppressed because it is too large Load diff

View file

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

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

Binary file not shown.

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

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
maps/tests/Audience.mp3 Normal file

Binary file not shown.

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

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

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

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

BIN
maps/tests/Win.ogg Normal file

Binary file not shown.

View file

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

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

Binary file not shown.

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

Binary file not shown.

View file

@ -20,5 +20,8 @@
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */ "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */
} },
"include": [
"**/*.ts"
]
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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