Merge branch 'develop' of github.com:thecodingmachine/workadventure into develop
This commit is contained in:
commit
fb2bd1c346
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,4 +7,5 @@ docker-compose.override.yaml
|
|||||||
maps/yarn.lock
|
maps/yarn.lock
|
||||||
maps/dist/computer.js
|
maps/dist/computer.js
|
||||||
maps/dist/computer.js.map
|
maps/dist/computer.js.map
|
||||||
/node_modules/
|
node_modules
|
||||||
|
_
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -10,12 +10,23 @@
|
|||||||
- New scripting API features :
|
- New scripting API features :
|
||||||
- Use `WA.room.showLayer(): void` to show a layer
|
- Use `WA.room.showLayer(): void` to show a layer
|
||||||
- Use `WA.room.hideLayer(): void` to hide a layer
|
- Use `WA.room.hideLayer(): void` to hide a layer
|
||||||
- Use `WA.room.setProperty() : void` to add or change existing property of a layer
|
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
||||||
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
||||||
- Use `WA.room.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
|
- Use `WA.player.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
|
||||||
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
|
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
|
||||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||||
- Use `WA.room.setTiles(): void` to change an array of tiles
|
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
||||||
|
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
||||||
|
- The text chat was redesigned to be prettier and to use more features :
|
||||||
|
- The chat is now persistent bewteen discussions and always accesible
|
||||||
|
- The chat now tracks incoming and outcoming users in your conversation
|
||||||
|
- The chat allows your to see the visit card of users
|
||||||
|
- You can close the chat window with the escape key
|
||||||
|
- Added a 'Enable notifications' button in the menu.
|
||||||
|
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
|
||||||
|
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
|
||||||
|
- `/api/ban`: new endpoint to report users
|
||||||
|
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
|
||||||
|
|
||||||
## Version 1.4.3 - 1.4.4 - 1.4.5
|
## Version 1.4.3 - 1.4.4 - 1.4.5
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ Before committing, be sure to install the "Prettier" precommit hook that will re
|
|||||||
In order to enable the "Prettier" precommit hook, at the root of the project, run:
|
In order to enable the "Prettier" precommit hook, at the root of the project, run:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ yarn run install
|
$ yarn install
|
||||||
$ yarn run prepare
|
$ yarn run prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export class DebugController {
|
|||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
if (query.token !== ADMIN_API_TOKEN) {
|
||||||
return res.status(401).send("Invalid token sent!");
|
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
@ -5,8 +5,6 @@ import { PositionInterface } from "_Model/PositionInterface";
|
|||||||
import { EmoteCallback, 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 { arrayIntersect } from "../Services/ArrayHelper";
|
|
||||||
import { EmoteEventMessage, 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";
|
||||||
@ -15,12 +13,6 @@ import { Admin } from "../Model/Admin";
|
|||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
|
|
||||||
export enum GameRoomPolicyTypes {
|
|
||||||
ANONYMOUS_POLICY = 1,
|
|
||||||
MEMBERS_ONLY_POLICY,
|
|
||||||
USE_TAGS_POLICY,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GameRoom {
|
export class GameRoom {
|
||||||
private readonly minDistance: number;
|
private readonly minDistance: number;
|
||||||
private readonly groupRadius: number;
|
private readonly groupRadius: number;
|
||||||
@ -37,15 +29,12 @@ export class GameRoom {
|
|||||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
private readonly positionNotifier: PositionNotifier;
|
||||||
public readonly roomId: string;
|
public readonly roomUrl: string;
|
||||||
public readonly roomSlug: string;
|
|
||||||
public readonly worldSlug: string = "";
|
|
||||||
public readonly organizationSlug: string = "";
|
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
roomId: string,
|
roomUrl: string,
|
||||||
connectCallback: ConnectCallback,
|
connectCallback: ConnectCallback,
|
||||||
disconnectCallback: DisconnectCallback,
|
disconnectCallback: DisconnectCallback,
|
||||||
minDistance: number,
|
minDistance: number,
|
||||||
@ -55,16 +44,7 @@ export class GameRoom {
|
|||||||
onLeaves: LeavesCallback,
|
onLeaves: LeavesCallback,
|
||||||
onEmote: EmoteCallback
|
onEmote: EmoteCallback
|
||||||
) {
|
) {
|
||||||
this.roomId = roomId;
|
this.roomUrl = roomUrl;
|
||||||
|
|
||||||
if (isRoomAnonymous(roomId)) {
|
|
||||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
|
||||||
} else {
|
|
||||||
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
|
|
||||||
this.roomSlug = roomSlug;
|
|
||||||
this.organizationSlug = organizationSlug;
|
|
||||||
this.worldSlug = worldSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.users = new Map<number, User>();
|
this.users = new Map<number, User>();
|
||||||
this.usersByUuid = new Map<string, User>();
|
this.usersByUuid = new Map<string, User>();
|
||||||
@ -183,7 +163,7 @@ export class GameRoom {
|
|||||||
} else {
|
} else {
|
||||||
const closestUser: User = closestItem;
|
const closestUser: User = closestItem;
|
||||||
const group: Group = new Group(
|
const group: Group = new Group(
|
||||||
this.roomId,
|
this.roomUrl,
|
||||||
[user, closestUser],
|
[user, closestUser],
|
||||||
this.connectCallback,
|
this.connectCallback,
|
||||||
this.disconnectCallback,
|
this.disconnectCallback,
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
//helper functions to parse room IDs
|
|
||||||
|
|
||||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
|
||||||
if (roomID.startsWith("_/")) {
|
|
||||||
return true;
|
|
||||||
} else if (roomID.startsWith("@/")) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
throw new Error("Incorrect room ID: " + roomID);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
|
||||||
const idParts = roomId.split("/");
|
|
||||||
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
|
|
||||||
return idParts.slice(2).join("/");
|
|
||||||
};
|
|
||||||
export interface extractDataFromPrivateRoomIdResponse {
|
|
||||||
organizationSlug: string;
|
|
||||||
worldSlug: string;
|
|
||||||
roomSlug: string;
|
|
||||||
}
|
|
||||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
|
||||||
const idParts = roomId.split("/");
|
|
||||||
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
|
|
||||||
const organizationSlug = idParts[1];
|
|
||||||
const worldSlug = idParts[2];
|
|
||||||
const roomSlug = idParts[3];
|
|
||||||
return { organizationSlug, worldSlug, roomSlug };
|
|
||||||
};
|
|
@ -250,12 +250,12 @@ export class SocketManager {
|
|||||||
//user leave previous world
|
//user leave previous world
|
||||||
room.leave(user);
|
room.leave(user);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.rooms.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
||||||
console.log("A user left");
|
console.log("A user left");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,6 +308,7 @@ export class SocketManager {
|
|||||||
throw new Error("clientUser.userId is not an integer " + thing.id);
|
throw new Error("clientUser.userId is not an integer " + thing.id);
|
||||||
}
|
}
|
||||||
userJoinedZoneMessage.setUserid(thing.id);
|
userJoinedZoneMessage.setUserid(thing.id);
|
||||||
|
userJoinedZoneMessage.setUseruuid(thing.uuid);
|
||||||
userJoinedZoneMessage.setName(thing.name);
|
userJoinedZoneMessage.setName(thing.name);
|
||||||
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||||
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||||
@ -425,7 +426,6 @@ export class SocketManager {
|
|||||||
// Let's send 2 messages: one to the user joining the group and one to the other user
|
// Let's send 2 messages: one to the user joining the group and one to the other user
|
||||||
const webrtcStartMessage1 = new WebRtcStartMessage();
|
const webrtcStartMessage1 = new WebRtcStartMessage();
|
||||||
webrtcStartMessage1.setUserid(otherUser.id);
|
webrtcStartMessage1.setUserid(otherUser.id);
|
||||||
webrtcStartMessage1.setName(otherUser.name);
|
|
||||||
webrtcStartMessage1.setInitiator(true);
|
webrtcStartMessage1.setInitiator(true);
|
||||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
|
||||||
@ -436,14 +436,10 @@ export class SocketManager {
|
|||||||
const serverToClientMessage1 = new ServerToClientMessage();
|
const serverToClientMessage1 = new ServerToClientMessage();
|
||||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
|
||||||
user.socket.write(serverToClientMessage1);
|
user.socket.write(serverToClientMessage1);
|
||||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
|
||||||
//}
|
|
||||||
|
|
||||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||||
webrtcStartMessage2.setUserid(user.id);
|
webrtcStartMessage2.setUserid(user.id);
|
||||||
webrtcStartMessage2.setName(user.name);
|
|
||||||
webrtcStartMessage2.setInitiator(false);
|
webrtcStartMessage2.setInitiator(false);
|
||||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
@ -454,10 +450,7 @@ export class SocketManager {
|
|||||||
const serverToClientMessage2 = new ServerToClientMessage();
|
const serverToClientMessage2 = new ServerToClientMessage();
|
||||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
|
||||||
otherUser.socket.write(serverToClientMessage2);
|
otherUser.socket.write(serverToClientMessage2);
|
||||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,6 +607,7 @@ export class SocketManager {
|
|||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
const userJoinedMessage = new UserJoinedZoneMessage();
|
const userJoinedMessage = new UserJoinedZoneMessage();
|
||||||
userJoinedMessage.setUserid(thing.id);
|
userJoinedMessage.setUserid(thing.id);
|
||||||
|
userJoinedMessage.setUseruuid(thing.uuid);
|
||||||
userJoinedMessage.setName(thing.name);
|
userJoinedMessage.setName(thing.name);
|
||||||
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||||
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||||
@ -664,9 +658,9 @@ export class SocketManager {
|
|||||||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||||
room.adminLeave(admin);
|
room.adminLeave(admin);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.rooms.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
|
||||||
|
|
||||||
describe("RoomIdentifier", () => {
|
|
||||||
it("should flag public id as anonymous", () => {
|
|
||||||
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
|
||||||
});
|
|
||||||
it("should flag public id as not anonymous", () => {
|
|
||||||
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
|
||||||
});
|
|
||||||
it("should extract roomSlug from public ID", () => {
|
|
||||||
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
|
||||||
});
|
|
||||||
it("should extract correct from private ID", () => {
|
|
||||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
|
||||||
expect(organizationSlug).toBe('afup');
|
|
||||||
expect(worldSlug).toBe('afup2020');
|
|
||||||
expect(roomSlug).toBe('1floor');
|
|
||||||
});
|
|
||||||
})
|
|
@ -1,6 +1,7 @@
|
|||||||
{.section-title.accent.text-primary}
|
{.section-title.accent.text-primary}
|
||||||
# API Player functions Reference
|
# API Player functions Reference
|
||||||
|
|
||||||
|
|
||||||
### Listen to player movement
|
### Listen to player movement
|
||||||
```
|
```
|
||||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||||
|
@ -54,6 +54,7 @@ WA.room.showLayer(layerName : string): void
|
|||||||
WA.room.hideLayer(layerName : string) : void
|
WA.room.hideLayer(layerName : string) : void
|
||||||
```
|
```
|
||||||
These 2 methods can be used to show and hide a layer.
|
These 2 methods can be used to show and hide a layer.
|
||||||
|
if `layerName` is the name of a group layer, show/hide all the layer in that group layer.
|
||||||
|
|
||||||
Example :
|
Example :
|
||||||
```javascript
|
```javascript
|
||||||
@ -70,49 +71,14 @@ WA.room.setProperty(layerName : string, propertyName : string, propertyValue : s
|
|||||||
|
|
||||||
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
|
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
|
||||||
|
|
||||||
|
Note :
|
||||||
|
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
|
||||||
|
|
||||||
Example :
|
Example :
|
||||||
```javascript
|
```javascript
|
||||||
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting information on the current room
|
|
||||||
```
|
|
||||||
WA.room.getCurrentRoom(): Promise<Room>
|
|
||||||
```
|
|
||||||
Return a promise that resolves to a `Room` object with the following attributes :
|
|
||||||
* **id (string) :** ID of the current room
|
|
||||||
* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called.
|
|
||||||
* **mapUrl (string) :** Url of the JSON map file
|
|
||||||
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer
|
|
||||||
|
|
||||||
Example :
|
|
||||||
```javascript
|
|
||||||
WA.room.getCurrentRoom((room) => {
|
|
||||||
if (room.id === '42') {
|
|
||||||
console.log(room.map);
|
|
||||||
window.open(room.mapUrl, '_blank');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getting information on the current user
|
|
||||||
```
|
|
||||||
WA.player.getCurrentUser(): Promise<User>
|
|
||||||
```
|
|
||||||
Return a promise that resolves to a `User` object with the following attributes :
|
|
||||||
* **id (string) :** ID of the current user
|
|
||||||
* **nickName (string) :** name displayed above the current user
|
|
||||||
* **tags (string[]) :** list of all the tags of the current user
|
|
||||||
|
|
||||||
Example :
|
|
||||||
```javascript
|
|
||||||
WA.room.getCurrentUser().then((user) => {
|
|
||||||
if (user.nickName === 'ABC') {
|
|
||||||
console.log(user.tags);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changing tiles
|
### Changing tiles
|
||||||
```
|
```
|
||||||
WA.room.setTiles(tiles: TileDescriptor[]): void
|
WA.room.setTiles(tiles: TileDescriptor[]): void
|
||||||
@ -134,6 +100,7 @@ If `tile` is a string, it's not the id of the tile but the value of the property
|
|||||||
|
|
||||||
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor.
|
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor.
|
||||||
|
|
||||||
|
Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
|
||||||
|
|
||||||
Example :
|
Example :
|
||||||
```javascript
|
```javascript
|
||||||
|
3
front/dist/index.tmpl.html
vendored
3
front/dist/index.tmpl.html
vendored
@ -37,8 +37,7 @@
|
|||||||
<div class="main-container" id="main-container">
|
<div class="main-container" id="main-container">
|
||||||
<!-- Create the editor container -->
|
<!-- Create the editor container -->
|
||||||
<div id="game" class="game">
|
<div id="game" class="game">
|
||||||
<div id="svelte-overlay">
|
<div id="svelte-overlay"></div>
|
||||||
</div>
|
|
||||||
<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>
|
||||||
|
3
front/dist/resources/html/gameMenu.html
vendored
3
front/dist/resources/html/gameMenu.html
vendored
@ -57,6 +57,9 @@
|
|||||||
<section>
|
<section>
|
||||||
<button id="toggleFullscreen">Toggle fullscreen</button>
|
<button id="toggleFullscreen">Toggle fullscreen</button>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<button id="enableNotification">Enable notifications</button>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<button id="sparkButton">Create map</button>
|
<button id="sparkButton">Create map</button>
|
||||||
</section>
|
</section>
|
||||||
|
BIN
front/dist/resources/logos/tcm_full.png
vendored
BIN
front/dist/resources/logos/tcm_full.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 300 B |
BIN
front/dist/resources/logos/tcm_short.png
vendored
BIN
front/dist/resources/logos/tcm_short.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 216 B |
53
front/dist/resources/service-worker.js
vendored
Normal file
53
front/dist/resources/service-worker.js
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
let CACHE_NAME = 'workavdenture-cache-v1';
|
||||||
|
let urlsToCache = [
|
||||||
|
'/'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', function(event) {
|
||||||
|
// Perform install steps
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(function(cache) {
|
||||||
|
console.log('Opened cache');
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', function(event) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(function(response) {
|
||||||
|
// Cache hit - return response
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(event.request).then(
|
||||||
|
function(response) {
|
||||||
|
// Check if we received a valid response
|
||||||
|
if(!response || response.status !== 200 || response.type !== 'basic') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: Clone the response. A response is a stream
|
||||||
|
// and because we want the browser to consume the response
|
||||||
|
// as well as the cache consuming the response, we need
|
||||||
|
// to clone it so we have two streams.
|
||||||
|
var responseToCache = response.clone();
|
||||||
|
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(function(cache) {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', function(event) {
|
||||||
|
//TODO activate service worker
|
||||||
|
});
|
BIN
front/dist/static/images/favicons/icon-512x512.png
vendored
Normal file
BIN
front/dist/static/images/favicons/icon-512x512.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
11
front/dist/static/images/favicons/manifest.json
vendored
11
front/dist/static/images/favicons/manifest.json
vendored
@ -119,7 +119,13 @@
|
|||||||
"src": "/static/images/favicons/android-icon-192x192.png",
|
"src": "/static/images/favicons/android-icon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "4.0"
|
"density": "4.0",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/images/favicons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image\/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
@ -127,6 +133,7 @@
|
|||||||
"display_override": ["window-control-overlay", "minimal-ui"],
|
"display_override": ["window-control-overlay", "minimal-ui"],
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
|
"lang": "en",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
{
|
{
|
||||||
@ -134,7 +141,7 @@
|
|||||||
"short_name": "WA",
|
"short_name": "WA",
|
||||||
"description": "WorkAdventure application",
|
"description": "WorkAdventure application",
|
||||||
"url": "/",
|
"url": "/",
|
||||||
"icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192" }]
|
"icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" }]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "WorkAdventure application",
|
"description": "WorkAdventure application",
|
||||||
|
BIN
front/dist/static/images/send.png
vendored
Normal file
BIN
front/dist/static/images/send.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/press-start-2p": "^4.3.0",
|
"@fontsource/press-start-2p": "^4.3.0",
|
||||||
"@types/simple-peer": "^9.6.0",
|
"@types/simple-peer": "^9.11.1",
|
||||||
"@types/socket.io-client": "^1.4.32",
|
"@types/socket.io-client": "^1.4.32",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"queue-typescript": "^1.0.1",
|
"queue-typescript": "^1.0.1",
|
||||||
"quill": "1.3.6",
|
"quill": "1.3.6",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"simple-peer": "^9.6.2",
|
"simple-peer": "^9.11.0",
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"standardized-audio-context": "^25.2.4"
|
"standardized-audio-context": "^25.2.4"
|
||||||
},
|
},
|
||||||
@ -61,7 +61,7 @@
|
|||||||
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
|
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
|
||||||
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
|
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
|
||||||
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
|
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
|
||||||
"test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
"test": "cross-env 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",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isDataLayerEvent = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
export const isDataLayerEvent =
|
data: tg.isObject,
|
||||||
new tg.IsInterface().withProperties({
|
})
|
||||||
data: tg.isObject
|
.get();
|
||||||
}).get();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
|
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isGameStateEvent =
|
export const isGameStateEvent = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
roomId: tg.isString,
|
roomId: tg.isString,
|
||||||
mapUrl: tg.isString,
|
mapUrl: tg.isString,
|
||||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
nickname: tg.isUnion(tg.isString, tg.isNull),
|
||||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||||
tags : tg.isArray(tg.isString),
|
tags: tg.isArray(tg.isString),
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
/**
|
/**
|
||||||
* A message sent from the game to the iFrame when the gameState is received by the script
|
* A message sent from the game to the iFrame when the gameState is received by the script
|
||||||
*/
|
*/
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isHasPlayerMovedEvent = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
export const isHasPlayerMovedEvent =
|
direction: tg.isElementOf("right", "left", "up", "down"),
|
||||||
new tg.IsInterface().withProperties({
|
|
||||||
direction: tg.isElementOf('right', 'left', 'up', 'down'),
|
|
||||||
moving: tg.isBoolean,
|
moving: tg.isBoolean,
|
||||||
x: tg.isNumber,
|
x: tg.isNumber,
|
||||||
y: tg.isNumber
|
y: tg.isNumber,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message sent from the game to the iFrame to notify a movement from the current player.
|
* A message sent from the game to the iFrame to notify a movement from the current player.
|
||||||
*/
|
*/
|
||||||
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
|
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
|
||||||
|
|
||||||
|
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;
|
||||||
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void
|
|
||||||
|
@ -1,73 +1,73 @@
|
|||||||
|
import type { GameStateEvent } from "./GameStateEvent";
|
||||||
import type { GameStateEvent } from './GameStateEvent';
|
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||||
import type { ButtonClickedEvent } from './ButtonClickedEvent';
|
import type { ChatEvent } from "./ChatEvent";
|
||||||
import type { ChatEvent } from './ChatEvent';
|
import type { ClosePopupEvent } from "./ClosePopupEvent";
|
||||||
import type { ClosePopupEvent } from './ClosePopupEvent';
|
import type { EnterLeaveEvent } from "./EnterLeaveEvent";
|
||||||
import type { EnterLeaveEvent } from './EnterLeaveEvent';
|
import type { GoToPageEvent } from "./GoToPageEvent";
|
||||||
import type { GoToPageEvent } from './GoToPageEvent';
|
import type { LoadPageEvent } from "./LoadPageEvent";
|
||||||
import type { LoadPageEvent } from './LoadPageEvent';
|
import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
||||||
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 { DataLayerEvent } from "./DataLayerEvent";
|
import type { DataLayerEvent } from "./DataLayerEvent";
|
||||||
import type { LayerEvent } from './LayerEvent';
|
import type { LayerEvent } from "./LayerEvent";
|
||||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||||
import type { PlaySoundEvent } from "./PlaySoundEvent";
|
import type { PlaySoundEvent } from "./PlaySoundEvent";
|
||||||
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
|
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
|
||||||
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent';
|
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
|
||||||
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
|
||||||
import type { SetTilesEvent } from "./SetTilesEvent";
|
import type { SetTilesEvent } from "./SetTilesEvent";
|
||||||
|
|
||||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||||
data: T
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List event types sent from an iFrame to WorkAdventure
|
||||||
|
*/
|
||||||
export type IframeEventMap = {
|
export type IframeEventMap = {
|
||||||
//getState: GameStateEvent,
|
loadPage: LoadPageEvent;
|
||||||
loadPage: LoadPageEvent
|
chat: ChatEvent;
|
||||||
chat: ChatEvent,
|
openPopup: OpenPopupEvent;
|
||||||
openPopup: OpenPopupEvent
|
closePopup: ClosePopupEvent;
|
||||||
closePopup: ClosePopupEvent
|
openTab: OpenTabEvent;
|
||||||
openTab: OpenTabEvent
|
goToPage: GoToPageEvent;
|
||||||
goToPage: GoToPageEvent
|
openCoWebSite: OpenCoWebSiteEvent;
|
||||||
openCoWebSite: OpenCoWebSiteEvent
|
closeCoWebSite: null;
|
||||||
closeCoWebSite: null
|
disablePlayerControls: null;
|
||||||
disablePlayerControls: null
|
restorePlayerControls: null;
|
||||||
restorePlayerControls: null
|
displayBubble: null;
|
||||||
displayBubble: null
|
removeBubble: null;
|
||||||
removeBubble: null
|
onPlayerMove: undefined;
|
||||||
onPlayerMove: undefined
|
showLayer: LayerEvent;
|
||||||
showLayer: LayerEvent
|
hideLayer: LayerEvent;
|
||||||
hideLayer: LayerEvent
|
setProperty: SetPropertyEvent;
|
||||||
setProperty: SetPropertyEvent
|
getDataLayer: undefined;
|
||||||
getDataLayer: undefined
|
loadSound: LoadSoundEvent;
|
||||||
loadSound: LoadSoundEvent
|
playSound: PlaySoundEvent;
|
||||||
playSound: PlaySoundEvent
|
stopSound: null;
|
||||||
stopSound: null
|
getState: undefined;
|
||||||
setTiles: SetTilesEvent
|
registerMenuCommand: MenuItemRegisterEvent;
|
||||||
getState: undefined,
|
setTiles: SetTilesEvent;
|
||||||
registerMenuCommand: MenuItemRegisterEvent
|
};
|
||||||
}
|
|
||||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||||
type: T;
|
type: T;
|
||||||
data: IframeEventMap[T];
|
data: IframeEventMap[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string';
|
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
|
||||||
|
typeof event.type === "string";
|
||||||
|
|
||||||
export interface IframeResponseEventMap {
|
export interface IframeResponseEventMap {
|
||||||
userInputChat: UserInputChatEvent
|
userInputChat: UserInputChatEvent;
|
||||||
enterEvent: EnterLeaveEvent
|
enterEvent: EnterLeaveEvent;
|
||||||
leaveEvent: EnterLeaveEvent
|
leaveEvent: EnterLeaveEvent;
|
||||||
buttonClickedEvent: ButtonClickedEvent
|
buttonClickedEvent: ButtonClickedEvent;
|
||||||
gameState: GameStateEvent
|
hasPlayerMoved: HasPlayerMovedEvent;
|
||||||
hasPlayerMoved: HasPlayerMovedEvent
|
dataLayer: DataLayerEvent;
|
||||||
dataLayer: DataLayerEvent
|
menuItemClicked: MenuItemClickedEvent;
|
||||||
menuItemClicked: MenuItemClickedEvent
|
|
||||||
}
|
}
|
||||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||||
type: T;
|
type: T;
|
||||||
@ -75,4 +75,49 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';
|
export const isIframeResponseEventWrapper = (event: {
|
||||||
|
type?: string;
|
||||||
|
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
|
||||||
|
*/
|
||||||
|
export type IframeQueryMap = {
|
||||||
|
getState: {
|
||||||
|
query: undefined,
|
||||||
|
answer: GameStateEvent
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IframeQuery<T extends keyof IframeQueryMap> {
|
||||||
|
type: T;
|
||||||
|
data: IframeQueryMap[T]['query'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
||||||
|
id: number;
|
||||||
|
query: IframeQuery<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === 'string';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
|
||||||
|
|
||||||
|
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
|
||||||
|
id: number;
|
||||||
|
type: T;
|
||||||
|
data: IframeQueryMap[T]['answer'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number';
|
||||||
|
|
||||||
|
export interface IframeErrorAnswerEvent {
|
||||||
|
id: number;
|
||||||
|
type: keyof IframeQueryMap;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string';
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isLayerEvent =
|
export const isLayerEvent = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
name: tg.isString,
|
name: tg.isString,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
/**
|
/**
|
||||||
* A message sent from the iFrame to the game to show/hide a layer.
|
* A message sent from the iFrame to the game to show/hide a layer.
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isLoadPageEvent = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
export const isLoadPageEvent =
|
|
||||||
new tg.IsInterface().withProperties({
|
|
||||||
url: tg.isString,
|
url: tg.isString,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message sent from the iFrame to the game to add a message in the chat.
|
* A message sent from the iFrame to the game to add a message in the chat.
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isOpenCoWebsite = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
export const isOpenCoWebsite =
|
|
||||||
new tg.IsInterface().withProperties({
|
|
||||||
url: tg.isString,
|
url: tg.isString,
|
||||||
allowApi: tg.isBoolean,
|
allowApi: tg.isBoolean,
|
||||||
allowPolicy: tg.isString,
|
allowPolicy: tg.isString,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message sent from the iFrame to the game to add a message in the chat.
|
* A message sent from the iFrame to the game to add a message in the chat.
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isSetTilesEvent =
|
export const isSetTilesEvent = tg.isArray(
|
||||||
tg.isArray(
|
new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
x: tg.isNumber,
|
x: tg.isNumber,
|
||||||
y: tg.isNumber,
|
y: tg.isNumber,
|
||||||
tile: tg.isUnion(tg.isNumber, tg.isString),
|
tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull),
|
||||||
layer: tg.isString
|
layer: tg.isString,
|
||||||
}).get()
|
})
|
||||||
);
|
.get()
|
||||||
|
);
|
||||||
/**
|
/**
|
||||||
* A message sent from the iFrame to the game to set one or many tiles.
|
* A message sent from the iFrame to the game to set one or many tiles.
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isSetPropertyEvent =
|
export const isSetPropertyEvent = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
layerName: tg.isString,
|
layerName: tg.isString,
|
||||||
propertyName: tg.isString,
|
propertyName: tg.isString,
|
||||||
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined)))
|
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
/**
|
/**
|
||||||
* A message sent from the iFrame to the game to change the value of the property of the layer
|
* A message sent from the iFrame to the game to change the value of the property of the layer
|
||||||
*/
|
*/
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isMenuItemClickedEvent =
|
export const isMenuItemClickedEvent = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
menuItem: tg.isString
|
menuItem: tg.isString,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
/**
|
/**
|
||||||
* A message sent from the game to the iFrame when a menu item is clicked.
|
* A message sent from the game to the iFrame when a menu item is clicked.
|
||||||
*/
|
*/
|
||||||
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;
|
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
export const isMenuItemRegisterEvent =
|
export const isMenuItemRegisterEvent = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
menutItem: tg.isString
|
menutItem: tg.isString,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
/**
|
/**
|
||||||
* A message sent from the iFrame to the game to add a new menu item.
|
* A message sent from the iFrame to the game to add a new menu item.
|
||||||
*/
|
*/
|
||||||
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
|
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
|
||||||
|
|
||||||
export const isMenuItemRegisterIframeEvent =
|
export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
type: tg.isSingletonString("registerMenuCommand"),
|
type: tg.isSingletonString("registerMenuCommand"),
|
||||||
data: isMenuItemRegisterEvent
|
data: isMenuItemRegisterEvent,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
const _registerMenuCommandStream: Subject<string> = new Subject();
|
const _registerMenuCommandStream: Subject<string> = new Subject();
|
||||||
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
|
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
|
||||||
|
|
||||||
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
|
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
|
||||||
_registerMenuCommandStream.next(event.menutItem)
|
_registerMenuCommandStream.next(event.menutItem);
|
||||||
}
|
}
|
@ -1,42 +1,45 @@
|
|||||||
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";
|
||||||
import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent";
|
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
|
||||||
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
|
import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
|
||||||
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
|
import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
|
||||||
import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
|
import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
|
||||||
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
|
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
|
||||||
import {scriptUtils} from "./ScriptUtils";
|
import { scriptUtils } from "./ScriptUtils";
|
||||||
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
|
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
|
||||||
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
|
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
|
||||||
import {
|
import {
|
||||||
|
IframeErrorAnswerEvent,
|
||||||
IframeEvent,
|
IframeEvent,
|
||||||
IframeEventMap,
|
IframeEventMap, IframeQueryMap,
|
||||||
IframeResponseEvent,
|
IframeResponseEvent,
|
||||||
IframeResponseEventMap,
|
IframeResponseEventMap,
|
||||||
isIframeEventWrapper,
|
isIframeEventWrapper,
|
||||||
TypedMessageEvent
|
isIframeQueryWrapper,
|
||||||
|
TypedMessageEvent,
|
||||||
} from "./Events/IframeEvent";
|
} from "./Events/IframeEvent";
|
||||||
import type {UserInputChatEvent} from "./Events/UserInputChatEvent";
|
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
|
||||||
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
|
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
|
||||||
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
|
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
|
||||||
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
|
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
||||||
import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent";
|
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
||||||
import {isLayerEvent, LayerEvent} from "./Events/LayerEvent";
|
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
|
||||||
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent";
|
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||||
import type {DataLayerEvent} from "./Events/DataLayerEvent";
|
import type { DataLayerEvent } from "./Events/DataLayerEvent";
|
||||||
import type {GameStateEvent} from "./Events/GameStateEvent";
|
import type { GameStateEvent } from "./Events/GameStateEvent";
|
||||||
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
||||||
import {isLoadPageEvent} from "./Events/LoadPageEvent";
|
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||||
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent";
|
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||||
import {SetTilesEvent, isSetTilesEvent} from "./Events/SetTilesEvent";
|
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
||||||
|
|
||||||
|
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
class IframeListener {
|
class IframeListener {
|
||||||
|
|
||||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||||
public readonly chatStream = this._chatStream.asObservable();
|
public readonly chatStream = this._chatStream.asObservable();
|
||||||
|
|
||||||
@ -82,9 +85,6 @@ class IframeListener {
|
|||||||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||||
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
||||||
|
|
||||||
private readonly _gameStateStream: Subject<void> = new Subject();
|
|
||||||
public readonly gameStateStream = this._gameStateStream.asObservable();
|
|
||||||
|
|
||||||
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
|
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
|
||||||
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
||||||
|
|
||||||
@ -111,117 +111,147 @@ class IframeListener {
|
|||||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||||
private sendPlayerMove: boolean = false;
|
private sendPlayerMove: boolean = false;
|
||||||
|
|
||||||
|
private answerers: {
|
||||||
|
[key in keyof IframeQueryMap]?: AnswererCallback<key>
|
||||||
|
} = {};
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
window.addEventListener(
|
||||||
// Do we trust the sender of this message?
|
"message",
|
||||||
// Let's only accept messages from the iframe that are allowed.
|
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
// Do we trust the sender of this message?
|
||||||
let foundSrc: string | undefined;
|
// Let's only accept messages from the iframe that are allowed.
|
||||||
|
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||||
|
let foundSrc: string | undefined;
|
||||||
|
|
||||||
let iframe: HTMLIFrameElement;
|
let iframe: HTMLIFrameElement | undefined;
|
||||||
for (iframe of this.iframes) {
|
for (iframe of this.iframes) {
|
||||||
if (iframe.contentWindow === message.source) {
|
if (iframe.contentWindow === message.source) {
|
||||||
foundSrc = iframe.src;
|
foundSrc = iframe.src;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const payload = message.data;
|
|
||||||
|
|
||||||
if (foundSrc === undefined) {
|
|
||||||
if (isIframeEventWrapper(payload)) {
|
|
||||||
console.warn('It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. ' +
|
|
||||||
'If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the ' +
|
|
||||||
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
|
|
||||||
'parameter to WA.nav.openCoWebSite())');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foundSrc = this.getBaseUrl(foundSrc, message.source);
|
|
||||||
|
|
||||||
if (isIframeEventWrapper(payload)) {
|
|
||||||
if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
|
|
||||||
this._showLayerStream.next(payload.data);
|
|
||||||
} else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) {
|
|
||||||
this._hideLayerStream.next(payload.data);
|
|
||||||
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
|
|
||||||
this._setPropertyStream.next(payload.data);
|
|
||||||
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
|
|
||||||
this._chatStream.next(payload.data);
|
|
||||||
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
|
|
||||||
this._openPopupStream.next(payload.data);
|
|
||||||
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
|
|
||||||
this._closePopupStream.next(payload.data);
|
|
||||||
}
|
|
||||||
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
|
|
||||||
scriptUtils.openTab(payload.data.url);
|
|
||||||
}
|
|
||||||
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
|
|
||||||
scriptUtils.goToPage(payload.data.url);
|
|
||||||
}
|
|
||||||
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
|
|
||||||
this._loadPageStream.next(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)) {
|
|
||||||
scriptUtils.openCoWebsite(payload.data.url, foundSrc, payload.data.allowApi, payload.data.allowPolicy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (payload.type === 'closeCoWebSite') {
|
const payload = message.data;
|
||||||
scriptUtils.closeCoWebSite();
|
|
||||||
|
if (foundSrc === undefined || iframe === undefined) {
|
||||||
|
if (isIframeEventWrapper(payload)) {
|
||||||
|
console.warn(
|
||||||
|
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
|
||||||
|
"If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " +
|
||||||
|
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
|
||||||
|
"parameter to WA.nav.openCoWebSite())"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (payload.type === 'disablePlayerControls') {
|
foundSrc = this.getBaseUrl(foundSrc, message.source);
|
||||||
this._disablePlayerControlStream.next();
|
|
||||||
}
|
|
||||||
else if (payload.type === 'restorePlayerControls') {
|
|
||||||
this._enablePlayerControlStream.next();
|
|
||||||
} else if (payload.type === 'displayBubble') {
|
|
||||||
this._displayBubbleStream.next();
|
|
||||||
} else if (payload.type === 'removeBubble') {
|
|
||||||
this._removeBubbleStream.next();
|
|
||||||
} else if (payload.type == "getState") {
|
|
||||||
this._gameStateStream.next();
|
|
||||||
} else if (payload.type == "onPlayerMove") {
|
|
||||||
this.sendPlayerMove = true
|
|
||||||
} else if (payload.type == "getDataLayer") {
|
|
||||||
this._dataLayerChangeStream.next();
|
|
||||||
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
|
||||||
const data = payload.data.menutItem;
|
|
||||||
// @ts-ignore
|
|
||||||
this.iframeCloseCallbacks.get(iframe).push(() => {
|
|
||||||
this._unregisterMenuCommandStream.next(data);
|
|
||||||
})
|
|
||||||
handleMenuItemRegistrationEvent(payload.data)
|
|
||||||
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
|
||||||
this._setTilesStream.next(payload.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
|
if (isIframeQueryWrapper(payload)) {
|
||||||
|
const queryId = payload.id;
|
||||||
|
const query = payload.query;
|
||||||
|
|
||||||
|
const answerer = this.answerers[query.type];
|
||||||
|
if (answerer === undefined) {
|
||||||
|
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
|
||||||
|
console.error(errorMsg);
|
||||||
|
iframe.contentWindow?.postMessage({
|
||||||
|
id: queryId,
|
||||||
|
type: query.type,
|
||||||
|
error: errorMsg
|
||||||
|
} as IframeErrorAnswerEvent, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.resolve(answerer(query.data)).then((value) => {
|
||||||
|
iframe?.contentWindow?.postMessage({
|
||||||
|
id: queryId,
|
||||||
|
type: query.type,
|
||||||
|
data: value
|
||||||
|
}, '*');
|
||||||
|
}).catch(reason => {
|
||||||
|
console.error('An error occurred while responding to an iFrame query.', reason);
|
||||||
|
let reasonMsg: string;
|
||||||
|
if (reason instanceof Error) {
|
||||||
|
reasonMsg = reason.message;
|
||||||
|
} else {
|
||||||
|
reasonMsg = reason.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe?.contentWindow?.postMessage({
|
||||||
|
id: queryId,
|
||||||
|
type: query.type,
|
||||||
|
error: reasonMsg
|
||||||
|
} as IframeErrorAnswerEvent, '*');
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (isIframeEventWrapper(payload)) {
|
||||||
|
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
|
||||||
|
this._showLayerStream.next(payload.data);
|
||||||
|
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
|
||||||
|
this._hideLayerStream.next(payload.data);
|
||||||
|
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
|
||||||
|
this._setPropertyStream.next(payload.data);
|
||||||
|
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
|
||||||
|
this._chatStream.next(payload.data);
|
||||||
|
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
|
||||||
|
this._openPopupStream.next(payload.data);
|
||||||
|
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
|
||||||
|
this._closePopupStream.next(payload.data);
|
||||||
|
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
|
||||||
|
scriptUtils.openTab(payload.data.url);
|
||||||
|
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
|
||||||
|
scriptUtils.goToPage(payload.data.url);
|
||||||
|
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
|
||||||
|
this._loadPageStream.next(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)) {
|
||||||
|
scriptUtils.openCoWebsite(
|
||||||
|
payload.data.url,
|
||||||
|
foundSrc,
|
||||||
|
payload.data.allowApi,
|
||||||
|
payload.data.allowPolicy
|
||||||
|
);
|
||||||
|
} else if (payload.type === "closeCoWebSite") {
|
||||||
|
scriptUtils.closeCoWebSite();
|
||||||
|
} else if (payload.type === "disablePlayerControls") {
|
||||||
|
this._disablePlayerControlStream.next();
|
||||||
|
} else if (payload.type === "restorePlayerControls") {
|
||||||
|
this._enablePlayerControlStream.next();
|
||||||
|
} else if (payload.type === "displayBubble") {
|
||||||
|
this._displayBubbleStream.next();
|
||||||
|
} else if (payload.type === "removeBubble") {
|
||||||
|
this._removeBubbleStream.next();
|
||||||
|
} else if (payload.type == "onPlayerMove") {
|
||||||
|
this.sendPlayerMove = true;
|
||||||
|
} else if (payload.type == "getDataLayer") {
|
||||||
|
this._dataLayerChangeStream.next();
|
||||||
|
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
||||||
|
const data = payload.data.menutItem;
|
||||||
|
// @ts-ignore
|
||||||
|
this.iframeCloseCallbacks.get(iframe).push(() => {
|
||||||
|
this._unregisterMenuCommandStream.next(data);
|
||||||
|
});
|
||||||
|
handleMenuItemRegistrationEvent(payload.data);
|
||||||
|
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
||||||
|
this._setTilesStream.next(payload.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
||||||
this.postMessage({
|
this.postMessage({
|
||||||
'type' : 'dataLayer',
|
type: "dataLayer",
|
||||||
'data' : dataLayerEvent
|
data: dataLayerEvent,
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sendGameStateEvent(gameStateEvent: GameStateEvent) {
|
|
||||||
this.postMessage({
|
|
||||||
'type': 'gameState',
|
|
||||||
'data': gameStateEvent
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,25 +264,25 @@ class IframeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unregisterIframe(iframe: HTMLIFrameElement): void {
|
unregisterIframe(iframe: HTMLIFrameElement): void {
|
||||||
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => {
|
this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
this.iframes.delete(iframe);
|
this.iframes.delete(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerScript(scriptUrl: string): void {
|
registerScript(scriptUrl: string): void {
|
||||||
console.log('Loading map related script at ', scriptUrl)
|
console.log("Loading map related script at ", scriptUrl);
|
||||||
|
|
||||||
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
|
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
|
||||||
// Using external iframe mode (
|
// Using external iframe mode (
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement("iframe");
|
||||||
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
||||||
iframe.style.display = 'none';
|
iframe.style.display = "none";
|
||||||
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
|
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
|
||||||
|
|
||||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||||
iframe.sandbox.add('allow-scripts');
|
iframe.sandbox.add("allow-scripts");
|
||||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
||||||
|
|
||||||
document.body.prepend(iframe);
|
document.body.prepend(iframe);
|
||||||
|
|
||||||
@ -260,45 +290,50 @@ class IframeListener {
|
|||||||
this.registerIframe(iframe);
|
this.registerIframe(iframe);
|
||||||
} else {
|
} else {
|
||||||
// production code
|
// production code
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement("iframe");
|
||||||
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
||||||
iframe.style.display = 'none';
|
iframe.style.display = "none";
|
||||||
|
|
||||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||||
iframe.sandbox.add('allow-scripts');
|
iframe.sandbox.add("allow-scripts");
|
||||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
||||||
|
|
||||||
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
||||||
iframe.srcdoc = '<!doctype html>\n' +
|
iframe.srcdoc =
|
||||||
'\n' +
|
"<!doctype html>\n" +
|
||||||
|
"\n" +
|
||||||
'<html lang="en">\n' +
|
'<html lang="en">\n' +
|
||||||
'<head>\n' +
|
"<head>\n" +
|
||||||
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
|
'<script src="' +
|
||||||
'<script src="' + scriptUrl + '" ></script>\n' +
|
window.location.protocol +
|
||||||
'<title></title>\n' +
|
"//" +
|
||||||
'</head>\n' +
|
window.location.host +
|
||||||
'</html>\n';
|
'/iframe_api.js" ></script>\n' +
|
||||||
|
'<script src="' +
|
||||||
|
scriptUrl +
|
||||||
|
'" ></script>\n' +
|
||||||
|
"<title></title>\n" +
|
||||||
|
"</head>\n" +
|
||||||
|
"</html>\n";
|
||||||
|
|
||||||
document.body.prepend(iframe);
|
document.body.prepend(iframe);
|
||||||
|
|
||||||
this.scripts.set(scriptUrl, iframe);
|
this.scripts.set(scriptUrl, iframe);
|
||||||
this.registerIframe(iframe);
|
this.registerIframe(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBaseUrl(src: string, source: MessageEventSource | null): string{
|
private getBaseUrl(src: string, source: MessageEventSource | null): string {
|
||||||
for (const script of this.scripts) {
|
for (const script of this.scripts) {
|
||||||
if (script[1].contentWindow === source) {
|
if (script[1].contentWindow === source) {
|
||||||
return script[0];
|
return script[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getIFrameId(scriptUrl: string): string {
|
private static getIFrameId(scriptUrl: string): string {
|
||||||
return 'script' + btoa(scriptUrl);
|
return "script" + btoa(scriptUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterScript(scriptUrl: string): void {
|
unregisterScript(scriptUrl: string): void {
|
||||||
@ -315,47 +350,47 @@ class IframeListener {
|
|||||||
|
|
||||||
sendUserInputChat(message: string) {
|
sendUserInputChat(message: string) {
|
||||||
this.postMessage({
|
this.postMessage({
|
||||||
'type': 'userInputChat',
|
type: "userInputChat",
|
||||||
'data': {
|
data: {
|
||||||
'message': message,
|
message: message,
|
||||||
} as UserInputChatEvent
|
} as UserInputChatEvent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendEnterEvent(name: string) {
|
sendEnterEvent(name: string) {
|
||||||
this.postMessage({
|
this.postMessage({
|
||||||
'type': 'enterEvent',
|
type: "enterEvent",
|
||||||
'data': {
|
data: {
|
||||||
"name": name
|
name: name,
|
||||||
} as EnterLeaveEvent
|
} as EnterLeaveEvent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendLeaveEvent(name: string) {
|
sendLeaveEvent(name: string) {
|
||||||
this.postMessage({
|
this.postMessage({
|
||||||
'type': 'leaveEvent',
|
type: "leaveEvent",
|
||||||
'data': {
|
data: {
|
||||||
"name": name
|
name: name,
|
||||||
} as EnterLeaveEvent
|
} as EnterLeaveEvent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
||||||
if (this.sendPlayerMove) {
|
if (this.sendPlayerMove) {
|
||||||
this.postMessage({
|
this.postMessage({
|
||||||
'type': 'hasPlayerMoved',
|
type: "hasPlayerMoved",
|
||||||
'data': event
|
data: event,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
||||||
this.postMessage({
|
this.postMessage({
|
||||||
'type': 'buttonClickedEvent',
|
type: "buttonClickedEvent",
|
||||||
'data': {
|
data: {
|
||||||
popupId,
|
popupId,
|
||||||
buttonId
|
buttonId,
|
||||||
} as ButtonClickedEvent
|
} as ButtonClickedEvent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,10 +399,25 @@ class IframeListener {
|
|||||||
*/
|
*/
|
||||||
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
||||||
for (const iframe of this.iframes) {
|
for (const iframe of this.iframes) {
|
||||||
iframe.contentWindow?.postMessage(message, '*');
|
iframe.contentWindow?.postMessage(message, "*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type).
|
||||||
|
*
|
||||||
|
* Important! There can be only one "answerer" so registering a new one will unregister the old one.
|
||||||
|
*
|
||||||
|
* @param key The "type" of the query we are answering
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
|
||||||
|
this.answerers[key] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unregisterAnswerer(key: keyof IframeQueryMap): void {
|
||||||
|
delete this.answerers[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const iframeListener = new IframeListener();
|
export const iframeListener = new IframeListener();
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
|
import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
|
||||||
|
|
||||||
class ScriptUtils {
|
class ScriptUtils {
|
||||||
|
public openTab(url: string) {
|
||||||
public openTab(url : string){
|
|
||||||
window.open(url);
|
window.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToPage(url : string){
|
public goToPage(url: string) {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
|
public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
|
||||||
coWebsiteManager.loadCoWebsite(url, base, api, policy);
|
coWebsiteManager.loadCoWebsite(url, base, api, policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeCoWebSite(){
|
public closeCoWebSite() {
|
||||||
coWebsiteManager.closeCoWebsite();
|
coWebsiteManager.closeCoWebsite();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,40 @@
|
|||||||
import type * as tg from "generic-type-guard";
|
import type * as tg from "generic-type-guard";
|
||||||
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent';
|
import type {
|
||||||
|
IframeEvent,
|
||||||
|
IframeEventMap, IframeQuery,
|
||||||
|
IframeQueryMap,
|
||||||
|
IframeResponseEventMap
|
||||||
|
} from '../Events/IframeEvent';
|
||||||
|
import type {IframeQueryWrapper} from "../Events/IframeEvent";
|
||||||
|
|
||||||
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
|
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
|
||||||
window.parent.postMessage(content, "*")
|
window.parent.postMessage(content, "*")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let queryNumber = 0;
|
||||||
|
|
||||||
|
export const answerPromises = new Map<number, {
|
||||||
|
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
reject: (reason?: any) => void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
|
||||||
|
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
|
||||||
|
window.parent.postMessage({
|
||||||
|
id: queryNumber,
|
||||||
|
query: content
|
||||||
|
} as IframeQueryWrapper<T>, "*");
|
||||||
|
|
||||||
|
answerPromises.set(queryNumber, {
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
});
|
||||||
|
|
||||||
|
queryNumber++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
|
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
|
||||||
|
|
||||||
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
|
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent';
|
import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
|
||||||
import { iframeListener } from '../../IframeListener';
|
import { iframeListener } from "../../IframeListener";
|
||||||
|
|
||||||
export function sendMenuClickedEvent(menuItem: string) {
|
export function sendMenuClickedEvent(menuItem: string) {
|
||||||
iframeListener.postMessage({
|
iframeListener.postMessage({
|
||||||
'type': 'menuItemClicked',
|
type: "menuItemClicked",
|
||||||
'data': {
|
data: {
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
} as MenuItemClickedEvent
|
} as MenuItemClickedEvent,
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -2,8 +2,15 @@ import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribut
|
|||||||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
import { apiCallback } from "./registeredCallbacks";
|
import { apiCallback } from "./registeredCallbacks";
|
||||||
|
import { getGameState } from "./room";
|
||||||
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string | undefined;
|
||||||
|
nickName: string | null;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||||
|
|
||||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||||
@ -24,6 +31,11 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||||||
data: null,
|
data: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getCurrentUser(): Promise<User> {
|
||||||
|
return getGameState().then((gameState) => {
|
||||||
|
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WorkadventurePlayerCommands();
|
export default new WorkadventurePlayerCommands();
|
||||||
|
@ -4,7 +4,7 @@ import { isDataLayerEvent } from "../Events/DataLayerEvent";
|
|||||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||||
import { isGameStateEvent } from "../Events/GameStateEvent";
|
import { isGameStateEvent } from "../Events/GameStateEvent";
|
||||||
|
|
||||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||||
import { apiCallback } from "./registeredCallbacks";
|
import { apiCallback } from "./registeredCallbacks";
|
||||||
|
|
||||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||||
@ -14,7 +14,6 @@ import type { GameStateEvent } from "../Events/GameStateEvent";
|
|||||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||||
const dataLayerResolver = new Subject<DataLayerEvent>();
|
const dataLayerResolver = new Subject<DataLayerEvent>();
|
||||||
const stateResolvers = new Subject<GameStateEvent>();
|
|
||||||
|
|
||||||
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
||||||
|
|
||||||
@ -25,26 +24,16 @@ interface Room {
|
|||||||
startLayer: string | null;
|
startLayer: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string | undefined;
|
|
||||||
nickName: string | null;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TileDescriptor {
|
interface TileDescriptor {
|
||||||
x: number
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
tile: number | string
|
tile: number | string | null;
|
||||||
layer: string
|
layer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getGameState(): Promise<GameStateEvent> {
|
||||||
function getGameState(): Promise<GameStateEvent> {
|
|
||||||
if (immutableDataPromise === undefined) {
|
if (immutableDataPromise === undefined) {
|
||||||
immutableDataPromise = new Promise<GameStateEvent>((resolver, thrower) => {
|
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
||||||
stateResolvers.subscribe(resolver);
|
|
||||||
sendToWorkadventure({ type: "getState", data: null });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return immutableDataPromise;
|
return immutableDataPromise;
|
||||||
}
|
}
|
||||||
@ -72,13 +61,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
leaveStreams.get(payloadData.name)?.next();
|
leaveStreams.get(payloadData.name)?.next();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
apiCallback({
|
|
||||||
type: "gameState",
|
|
||||||
typeChecker: isGameStateEvent,
|
|
||||||
callback: (payloadData) => {
|
|
||||||
stateResolvers.next(payloadData);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
apiCallback({
|
apiCallback({
|
||||||
type: "dataLayer",
|
type: "dataLayer",
|
||||||
typeChecker: isDataLayerEvent,
|
typeChecker: isDataLayerEvent,
|
||||||
@ -132,18 +114,12 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
getCurrentUser(): Promise<User> {
|
|
||||||
return getGameState().then((gameState) => {
|
|
||||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setTiles(tiles: TileDescriptor[]) {
|
setTiles(tiles: TileDescriptor[]) {
|
||||||
sendToWorkadventure({
|
sendToWorkadventure({
|
||||||
type: 'setTiles',
|
type: "setTiles",
|
||||||
data: tiles
|
data: tiles,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WorkadventureRoomCommands();
|
export default new WorkadventureRoomCommands();
|
||||||
|
@ -10,12 +10,14 @@
|
|||||||
import {errorStore} from "../Stores/ErrorStore";
|
import {errorStore} from "../Stores/ErrorStore";
|
||||||
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
||||||
import LoginScene from "./Login/LoginScene.svelte";
|
import LoginScene from "./Login/LoginScene.svelte";
|
||||||
|
import Chat from "./Chat/Chat.svelte";
|
||||||
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
||||||
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
||||||
import VisitCard from "./VisitCard/VisitCard.svelte";
|
import VisitCard from "./VisitCard/VisitCard.svelte";
|
||||||
import {requestVisitCardsStore} from "../Stores/GameStore";
|
import {requestVisitCardsStore} from "../Stores/GameStore";
|
||||||
|
|
||||||
import type {Game} from "../Phaser/Game/Game";
|
import type {Game} from "../Phaser/Game/Game";
|
||||||
|
import {chatVisibilityStore} from "../Stores/ChatStore";
|
||||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||||
@ -61,14 +63,6 @@
|
|||||||
<AudioPlaying url={$soundPlayingStore} />
|
<AudioPlaying url={$soundPlayingStore} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!--
|
|
||||||
{#if $menuIconVisible}
|
|
||||||
<div>
|
|
||||||
<MenuIcon />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
-->
|
|
||||||
{#if $gameOverlayVisibilityStore}
|
{#if $gameOverlayVisibilityStore}
|
||||||
<div>
|
<div>
|
||||||
<VideoOverlay></VideoOverlay>
|
<VideoOverlay></VideoOverlay>
|
||||||
@ -94,4 +88,7 @@
|
|||||||
<ErrorDialog></ErrorDialog>
|
<ErrorDialog></ErrorDialog>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $chatVisibilityStore}
|
||||||
|
<Chat></Chat>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
101
front/src/Components/Chat/Chat.svelte
Normal file
101
front/src/Components/Chat/Chat.svelte
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
import ChatMessageForm from './ChatMessageForm.svelte';
|
||||||
|
import ChatElement from './ChatElement.svelte';
|
||||||
|
import { afterUpdate, beforeUpdate } from "svelte";
|
||||||
|
|
||||||
|
let listDom: HTMLElement;
|
||||||
|
let autoscroll: boolean;
|
||||||
|
|
||||||
|
beforeUpdate(() => {
|
||||||
|
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeChat() {
|
||||||
|
chatVisibilityStore.set(false);
|
||||||
|
}
|
||||||
|
function onKeyDown(e:KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeChat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown}/>
|
||||||
|
|
||||||
|
|
||||||
|
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||||
|
<p class="close-icon" on:click={closeChat}>×</p>
|
||||||
|
<section class="messagesList" bind:this={listDom}>
|
||||||
|
<ul>
|
||||||
|
<li><p class="system-text">Here is your chat history: </p></li>
|
||||||
|
{#each $chatMessagesStore as message, i}
|
||||||
|
<li><ChatElement message={message} line={i}></ChatElement></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="messageForm">
|
||||||
|
<ChatMessageForm></ChatMessageForm>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
p.close-icon {
|
||||||
|
position: absolute;
|
||||||
|
padding: 4px;
|
||||||
|
right: 12px;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.system-text {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding:6px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
background: gray;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.chatWindow {
|
||||||
|
z-index:100;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width:30vw;
|
||||||
|
min-width: 350px;
|
||||||
|
background: rgb(5, 31, 51, 0.9);
|
||||||
|
color: whitesmoke;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
|
||||||
|
.messagesList {
|
||||||
|
margin-top: 35px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.messageForm {
|
||||||
|
flex: 0 70px;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
83
front/src/Components/Chat/ChatElement.svelte
Normal file
83
front/src/Components/Chat/ChatElement.svelte
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {ChatMessageTypes} from "../../Stores/ChatStore";
|
||||||
|
import type {ChatMessage} from "../../Stores/ChatStore";
|
||||||
|
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||||
|
import ChatPlayerName from './ChatPlayerName.svelte';
|
||||||
|
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||||
|
|
||||||
|
export let message: ChatMessage;
|
||||||
|
export let line: number;
|
||||||
|
|
||||||
|
$: author = message.author as PlayerInterface;
|
||||||
|
$: targets = message.targets || [];
|
||||||
|
$: texts = message.text || [];
|
||||||
|
|
||||||
|
function urlifyText(text: string): string {
|
||||||
|
return HtmlUtils.urlify(text)
|
||||||
|
}
|
||||||
|
function renderDate(date: Date) {
|
||||||
|
return date.toLocaleTimeString(navigator.language, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute:'2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function isLastIteration(index: number) {
|
||||||
|
return targets.length -1 === index;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chatElement">
|
||||||
|
<div class="messagePart">
|
||||||
|
{#if message.type === ChatMessageTypes.userIncoming}
|
||||||
|
>> {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
|
||||||
|
{:else if message.type === ChatMessageTypes.userOutcoming}
|
||||||
|
<< {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
|
||||||
|
{:else if message.type === ChatMessageTypes.me}
|
||||||
|
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
|
||||||
|
{#each texts as text}
|
||||||
|
<div><p class="my-text">{@html urlifyText(text)}</p></div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
|
||||||
|
{#each texts as text}
|
||||||
|
<div><p class="other-text">{@html urlifyText(text)}</p></div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h4, p {
|
||||||
|
font-family: Lato;
|
||||||
|
}
|
||||||
|
div.chatElement {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.messagePart {
|
||||||
|
flex-grow:1;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
span.date {
|
||||||
|
font-size: 80%;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
div > p {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding:6px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
&.other-text {
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.my-text {
|
||||||
|
background: #6489ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
56
front/src/Components/Chat/ChatMessageForm.svelte
Normal file
56
front/src/Components/Chat/ChatMessageForm.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
|
||||||
|
|
||||||
|
let newMessageText = '';
|
||||||
|
|
||||||
|
function onFocus() {
|
||||||
|
chatInputFocusStore.set(true);
|
||||||
|
}
|
||||||
|
function onBlur() {
|
||||||
|
chatInputFocusStore.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMessage() {
|
||||||
|
if (!newMessageText) return;
|
||||||
|
chatMessagesStore.addPersonnalMessage(newMessageText);
|
||||||
|
newMessageText = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={saveMessage}>
|
||||||
|
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/static/images/send.png" alt="Send" width="20">
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: auto;
|
||||||
|
background-color: #254560;
|
||||||
|
color: white;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: Lato;
|
||||||
|
padding-left: 6px;
|
||||||
|
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #254560;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
border-left: solid white 1px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
51
front/src/Components/Chat/ChatPlayerName.svelte
Normal file
51
front/src/Components/Chat/ChatPlayerName.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||||
|
import {chatSubMenuVisbilityStore} from "../../Stores/ChatStore";
|
||||||
|
import {onDestroy, onMount} from "svelte";
|
||||||
|
import type {Unsubscriber} from "svelte/store";
|
||||||
|
import ChatSubMenu from "./ChatSubMenu.svelte";
|
||||||
|
|
||||||
|
export let player: PlayerInterface;
|
||||||
|
export let line: number;
|
||||||
|
|
||||||
|
let isSubMenuOpen: boolean;
|
||||||
|
let chatSubMenuVisivilytUnsubcribe: Unsubscriber;
|
||||||
|
|
||||||
|
function openSubMenu() {
|
||||||
|
chatSubMenuVisbilityStore.openSubMenu(player.name, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisbilityStore.subscribe((newValue) => {
|
||||||
|
isSubMenuOpen = (newValue === player.name + line);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
chatSubMenuVisivilytUnsubcribe();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="subMenu">
|
||||||
|
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
|
||||||
|
{player.name}
|
||||||
|
</span>
|
||||||
|
{#if isSubMenuOpen}
|
||||||
|
<ChatSubMenu player={player}/>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
span.subMenu {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
span.chatPlayerName {
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
.chatPlayerName:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
33
front/src/Components/Chat/ChatSubMenu.svelte
Normal file
33
front/src/Components/Chat/ChatSubMenu.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||||
|
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||||
|
|
||||||
|
export let player: PlayerInterface;
|
||||||
|
|
||||||
|
|
||||||
|
function openVisitCard() {
|
||||||
|
if (player.visitCardUrl) {
|
||||||
|
requestVisitCardsStore.set(player.visitCardUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
|
||||||
|
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li>
|
||||||
|
<li><button class="text-btn" disabled>Add friend</button></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
ul.selectMenu {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -37,9 +37,7 @@
|
|||||||
<img alt="Report this user" src={reportImg}>
|
<img alt="Report this user" src={reportImg}>
|
||||||
<span>Report/Block</span>
|
<span>Report/Block</span>
|
||||||
</button>
|
</button>
|
||||||
{#if $streamStore }
|
|
||||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||||
{/if}
|
|
||||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||||
{#if $constraintStore && $constraintStore.audio !== false}
|
{#if $constraintStore && $constraintStore.audio !== false}
|
||||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
export function getColorByString(str: string) : string|null {
|
import type { UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
|
||||||
|
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
export function getColorByString(str: string): string | null {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -7,21 +10,37 @@ export function getColorByString(str: string) : string|null {
|
|||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
hash = hash & hash;
|
hash = hash & hash;
|
||||||
}
|
}
|
||||||
let color = '#';
|
let color = "#";
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const value = (hash >> (i * 8)) & 255;
|
const value = (hash >> (i * 8)) & 255;
|
||||||
color += ('00' + value.toString(16)).substr(-2);
|
color += ("00" + value.toString(16)).substr(-2);
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
|
||||||
node.srcObject = stream;
|
node.srcObject = stream;
|
||||||
return {
|
return {
|
||||||
update(newStream: MediaStream) {
|
update(newStream: MediaStream) {
|
||||||
if (node.srcObject != newStream) {
|
if (node.srcObject != newStream) {
|
||||||
node.srcObject = newStream
|
node.srcObject = newStream;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIceServersConfig(user: UserSimplePeerInterface): RTCIceServer[] {
|
||||||
|
const config: RTCIceServer[] = [
|
||||||
|
{
|
||||||
|
urls: STUN_SERVER.split(","),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (TURN_SERVER !== "") {
|
||||||
|
config.push({
|
||||||
|
urls: TURN_SERVER.split(","),
|
||||||
|
username: user.webRtcUser || TURN_USER,
|
||||||
|
credential: user.webRtcPassword || TURN_PASSWORD,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,9 @@
|
|||||||
|
|
||||||
.visitCard {
|
.visitCard {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
margin-left: auto;
|
position: absolute;
|
||||||
margin-right: auto;
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
margin-top: 200px;
|
margin-top: 200px;
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
|
|
||||||
|
@ -38,11 +38,9 @@ class ConnectionManager {
|
|||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
|
|
||||||
const organizationSlug = data.organizationSlug;
|
const roomUrl = data.roomUrl;
|
||||||
const worldSlug = data.worldSlug;
|
|
||||||
const roomSlug = data.roomSlug;
|
|
||||||
|
|
||||||
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
|
const room = await Room.createRoom(new URL(window.location.protocol + '//' + window.location.host + roomUrl + window.location.search + window.location.hash));
|
||||||
urlManager.pushRoomIdToUrl(room);
|
urlManager.pushRoomIdToUrl(room);
|
||||||
return Promise.resolve(room);
|
return Promise.resolve(room);
|
||||||
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
||||||
@ -66,22 +64,21 @@ class ConnectionManager {
|
|||||||
throw "Error to store local user data";
|
throw "Error to store local user data";
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomId: string;
|
let roomPath: string;
|
||||||
if (connexionType === GameConnexionTypes.empty) {
|
if (connexionType === GameConnexionTypes.empty) {
|
||||||
roomId = START_ROOM_URL;
|
roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
|
||||||
} else {
|
} else {
|
||||||
roomId = window.location.pathname + window.location.search + window.location.hash;
|
roomPath = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + window.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
//get detail map for anonymous login and set texture in local storage
|
//get detail map for anonymous login and set texture in local storage
|
||||||
const room = new Room(roomId);
|
const room = await Room.createRoom(new URL(roomPath));
|
||||||
const mapDetail = await room.getMapDetail();
|
if(room.textures != undefined && room.textures.length > 0) {
|
||||||
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
|
|
||||||
//check if texture was changed
|
//check if texture was changed
|
||||||
if(localUser.textures.length === 0){
|
if(localUser.textures.length === 0){
|
||||||
localUser.textures = mapDetail.textures;
|
localUser.textures = room.textures;
|
||||||
}else{
|
}else{
|
||||||
mapDetail.textures.forEach((newTexture) => {
|
room.textures.forEach((newTexture) => {
|
||||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||||
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
||||||
return;
|
return;
|
||||||
@ -114,9 +111,9 @@ class ConnectionManager {
|
|||||||
this.localUser = new LocalUser('', 'test', []);
|
this.localUser = new LocalUser('', 'test', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
|
public connectToRoomSocket(roomUrl: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
|
||||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||||
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion);
|
const connection = new RoomConnection(this.localUser.jwtToken, roomUrl, name, characterLayers, position, viewport, companion);
|
||||||
connection.onConnectError((error: object) => {
|
connection.onConnectError((error: object) => {
|
||||||
console.log('An error occurred while connecting to socket server. Retrying');
|
console.log('An error occurred while connecting to socket server. Retrying');
|
||||||
reject(error);
|
reject(error);
|
||||||
@ -137,7 +134,7 @@ class ConnectionManager {
|
|||||||
this.reconnectingTimeout = setTimeout(() => {
|
this.reconnectingTimeout = setTimeout(() => {
|
||||||
//todo: allow a way to break recursion?
|
//todo: allow a way to break recursion?
|
||||||
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
||||||
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
||||||
}, 4000 + Math.floor(Math.random() * 2000) );
|
}, 4000 + Math.floor(Math.random() * 2000) );
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type {SignalData} from "simple-peer";
|
import type { SignalData } from "simple-peer";
|
||||||
import type {RoomConnection} from "./RoomConnection";
|
import type { RoomConnection } from "./RoomConnection";
|
||||||
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
|
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||||
|
|
||||||
export enum EventMessage{
|
export enum EventMessage {
|
||||||
CONNECT = "connect",
|
CONNECT = "connect",
|
||||||
WEBRTC_SIGNAL = "webrtc-signal",
|
WEBRTC_SIGNAL = "webrtc-signal",
|
||||||
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
||||||
@ -17,7 +17,7 @@ export enum EventMessage{
|
|||||||
GROUP_CREATE_UPDATE = "group-create-update",
|
GROUP_CREATE_UPDATE = "group-create-update",
|
||||||
GROUP_DELETE = "group-delete",
|
GROUP_DELETE = "group-delete",
|
||||||
SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
|
SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
|
||||||
ITEM_EVENT = 'item-event',
|
ITEM_EVENT = "item-event",
|
||||||
|
|
||||||
CONNECT_ERROR = "connect_error",
|
CONNECT_ERROR = "connect_error",
|
||||||
CONNECTING_ERROR = "connecting_error",
|
CONNECTING_ERROR = "connecting_error",
|
||||||
@ -36,7 +36,7 @@ export enum EventMessage{
|
|||||||
export interface PointInterface {
|
export interface PointInterface {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
direction : string;
|
direction: string;
|
||||||
moving: boolean;
|
moving: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +45,9 @@ export interface MessageUserPositionInterface {
|
|||||||
name: string;
|
name: string;
|
||||||
characterLayers: BodyResourceDescriptionInterface[];
|
characterLayers: BodyResourceDescriptionInterface[];
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
visitCardUrl: string|null;
|
visitCardUrl: string | null;
|
||||||
companion: string|null;
|
companion: string | null;
|
||||||
|
userUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageUserMovedInterface {
|
export interface MessageUserMovedInterface {
|
||||||
@ -60,58 +61,59 @@ export interface MessageUserJoined {
|
|||||||
characterLayers: BodyResourceDescriptionInterface[];
|
characterLayers: BodyResourceDescriptionInterface[];
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
visitCardUrl: string | null;
|
visitCardUrl: string | null;
|
||||||
companion: string|null;
|
companion: string | null;
|
||||||
|
userUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PositionInterface {
|
export interface PositionInterface {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupCreatedUpdatedMessageInterface {
|
export interface GroupCreatedUpdatedMessageInterface {
|
||||||
position: PositionInterface,
|
position: PositionInterface;
|
||||||
groupId: number,
|
groupId: number;
|
||||||
groupSize: number
|
groupSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebRtcDisconnectMessageInterface {
|
export interface WebRtcDisconnectMessageInterface {
|
||||||
userId: number
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebRtcSignalReceivedMessageInterface {
|
export interface WebRtcSignalReceivedMessageInterface {
|
||||||
userId: number,
|
userId: number;
|
||||||
signal: SignalData,
|
signal: SignalData;
|
||||||
webRtcUser: string | undefined,
|
webRtcUser: string | undefined;
|
||||||
webRtcPassword: string | undefined
|
webRtcPassword: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewportInterface {
|
export interface ViewportInterface {
|
||||||
left: number,
|
left: number;
|
||||||
top: number,
|
top: number;
|
||||||
right: number,
|
right: number;
|
||||||
bottom: number,
|
bottom: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemEventMessageInterface {
|
export interface ItemEventMessageInterface {
|
||||||
itemId: number,
|
itemId: number;
|
||||||
event: string,
|
event: string;
|
||||||
state: unknown,
|
state: unknown;
|
||||||
parameters: unknown
|
parameters: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomJoinedMessageInterface {
|
export interface RoomJoinedMessageInterface {
|
||||||
//users: MessageUserPositionInterface[],
|
//users: MessageUserPositionInterface[],
|
||||||
//groups: GroupCreatedUpdatedMessageInterface[],
|
//groups: GroupCreatedUpdatedMessageInterface[],
|
||||||
items: { [itemId: number] : unknown }
|
items: { [itemId: number]: unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayGlobalMessageInterface {
|
export interface PlayGlobalMessageInterface {
|
||||||
id: string
|
id: string;
|
||||||
type: string
|
type: string;
|
||||||
message: string
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnConnectInterface {
|
export interface OnConnectInterface {
|
||||||
connection: RoomConnection,
|
connection: RoomConnection;
|
||||||
room: RoomJoinedMessageInterface
|
room: RoomJoinedMessageInterface;
|
||||||
}
|
}
|
||||||
|
@ -1,90 +1,105 @@
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import {PUSHER_URL} from "../Enum/EnvironmentVariable";
|
import { PUSHER_URL } from "../Enum/EnvironmentVariable";
|
||||||
import type {CharacterTexture} from "./LocalUser";
|
import type { CharacterTexture } from "./LocalUser";
|
||||||
|
|
||||||
export class MapDetail{
|
export class MapDetail {
|
||||||
constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) {
|
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoomRedirect {
|
||||||
|
redirectUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Room {
|
export class Room {
|
||||||
public readonly id: string;
|
public readonly id: string;
|
||||||
public readonly isPublic: boolean;
|
public readonly isPublic: boolean;
|
||||||
private mapUrl: string|undefined;
|
private _mapUrl: string | undefined;
|
||||||
private textures: CharacterTexture[]|undefined;
|
private _textures: CharacterTexture[] | undefined;
|
||||||
private instance: string|undefined;
|
private instance: string | undefined;
|
||||||
private _search: URLSearchParams;
|
private readonly _search: URLSearchParams;
|
||||||
|
|
||||||
constructor(id: string) {
|
private constructor(private roomUrl: URL) {
|
||||||
const url = new URL(id, 'https://example.com');
|
this.id = roomUrl.pathname;
|
||||||
|
|
||||||
this.id = url.pathname;
|
if (this.id.startsWith("/")) {
|
||||||
|
|
||||||
if (this.id.startsWith('/')) {
|
|
||||||
this.id = this.id.substr(1);
|
this.id = this.id.substr(1);
|
||||||
}
|
}
|
||||||
if (this.id.startsWith('_/')) {
|
if (this.id.startsWith("_/")) {
|
||||||
this.isPublic = true;
|
this.isPublic = true;
|
||||||
} else if (this.id.startsWith('@/')) {
|
} else if (this.id.startsWith("@/")) {
|
||||||
this.isPublic = false;
|
this.isPublic = false;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid room ID');
|
throw new Error("Invalid room ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._search = new URLSearchParams(url.search);
|
this._search = new URLSearchParams(roomUrl.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} {
|
/**
|
||||||
let roomId = '';
|
* Creates a "Room" object representing the room.
|
||||||
let hash = '';
|
* This method will follow room redirects if necessary, so the instance returned is a "real" room.
|
||||||
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link
|
*/
|
||||||
//Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects.
|
public static async createRoom(roomUrl: URL): Promise<Room> {
|
||||||
//We instead use 'workadventure' as a dummy base value.
|
let redirectCount = 0;
|
||||||
const baseUrlObject = new URL(baseUrl);
|
while (redirectCount < 32) {
|
||||||
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/'+currentInstance+'/'+baseUrlObject.hostname+baseUrlObject.pathname);
|
const room = new Room(roomUrl);
|
||||||
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
|
const result = await room.getMapDetail();
|
||||||
roomId = roomId.substring(1); //remove the leading slash
|
if (result instanceof MapDetail) {
|
||||||
hash = absoluteExitSceneUrl.hash;
|
return room;
|
||||||
hash = hash.substring(1); //remove the leading diese
|
|
||||||
} else { //absolute room Id
|
|
||||||
const parts = identifier.split('#');
|
|
||||||
roomId = parts[0];
|
|
||||||
roomId = roomId.substring(1); //remove the leading slash
|
|
||||||
if (parts.length > 1) {
|
|
||||||
hash = parts[1]
|
|
||||||
}
|
}
|
||||||
|
redirectCount++;
|
||||||
|
roomUrl = new URL(result.redirectUrl);
|
||||||
}
|
}
|
||||||
return {roomId, hash}
|
throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMapDetail(): Promise<MapDetail> {
|
public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
|
||||||
return new Promise<MapDetail>((resolve, reject) => {
|
const url = new URL(exitUrl, currentRoomUrl);
|
||||||
if (this.mapUrl !== undefined && this.textures != undefined) {
|
return url;
|
||||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isPublic) {
|
/**
|
||||||
const match = /_\/[^/]+\/(.+)/.exec(this.id);
|
* @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
|
||||||
if (!match) throw new Error('Could not extract url from "'+this.id+'"');
|
*/
|
||||||
this.mapUrl = window.location.protocol+'//'+match[1];
|
public static getRoomPathFromExitSceneUrl(
|
||||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
exitSceneUrl: string,
|
||||||
return;
|
currentRoomUrl: string,
|
||||||
} else {
|
currentMapUrl: string
|
||||||
// We have a private ID, we need to query the map URL from the server.
|
): URL {
|
||||||
const urlParts = this.parsePrivateUrl(this.id);
|
const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
|
||||||
|
const baseUrl = new URL(currentRoomUrl);
|
||||||
|
|
||||||
Axios.get(`${PUSHER_URL}/map`, {
|
const currentRoom = new Room(baseUrl);
|
||||||
params: urlParts
|
let instance: string = "global";
|
||||||
}).then(({data}) => {
|
if (currentRoom.isPublic) {
|
||||||
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
|
instance = currentRoom.instance as string;
|
||||||
resolve(data);
|
}
|
||||||
return;
|
|
||||||
}).catch((reason) => {
|
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
|
||||||
reject(reason);
|
if (absoluteExitSceneUrl.hash) {
|
||||||
});
|
baseUrl.hash = absoluteExitSceneUrl.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
|
||||||
|
const result = await Axios.get(`${PUSHER_URL}/map`, {
|
||||||
|
params: {
|
||||||
|
playUri: this.roomUrl.toString(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = result.data;
|
||||||
|
if (data.redirectUrl) {
|
||||||
|
return {
|
||||||
|
redirectUrl: data.redirectUrl as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
||||||
|
this._mapUrl = data.mapUrl;
|
||||||
|
this._textures = data.textures;
|
||||||
|
return new MapDetail(data.mapUrl, data.textures);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,37 +114,39 @@ export class Room {
|
|||||||
|
|
||||||
if (this.isPublic) {
|
if (this.isPublic) {
|
||||||
const match = /_\/([^/]+)\/.+/.exec(this.id);
|
const match = /_\/([^/]+)\/.+/.exec(this.id);
|
||||||
if (!match) throw new Error('Could not extract instance from "'+this.id+'"');
|
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
||||||
this.instance = match[1];
|
this.instance = match[1];
|
||||||
return this.instance;
|
return this.instance;
|
||||||
} else {
|
} else {
|
||||||
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
|
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
|
||||||
if (!match) throw new Error('Could not extract instance from "'+this.id+'"');
|
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
||||||
this.instance = match[1]+'/'+match[2];
|
this.instance = match[1] + "/" + match[2];
|
||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parsePrivateUrl(url: string): { organizationSlug: string, worldSlug: string, roomSlug?: string } {
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
|
||||||
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
|
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
|
||||||
const match = regex.exec(url);
|
const match = regex.exec(url);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error('Invalid URL '+url);
|
throw new Error("Invalid URL " + url);
|
||||||
}
|
}
|
||||||
const results: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
|
const results: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
|
||||||
organizationSlug: match[1],
|
organizationSlug: match[1],
|
||||||
worldSlug: match[2],
|
worldSlug: match[2],
|
||||||
}
|
};
|
||||||
if (match[3] !== undefined) {
|
if (match[3] !== undefined) {
|
||||||
results.roomSlug = match[3];
|
results.roomSlug = match[3];
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isDisconnected(): boolean
|
public isDisconnected(): boolean {
|
||||||
{
|
const alone = this._search.get("alone");
|
||||||
const alone = this._search.get('alone');
|
if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
|
||||||
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -138,4 +155,33 @@ export class Room {
|
|||||||
public get search(): URLSearchParams {
|
public get search(): URLSearchParams {
|
||||||
return this._search;
|
return this._search;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2 rooms are equal if they share the same path (but not necessarily the same hash)
|
||||||
|
* @param room
|
||||||
|
*/
|
||||||
|
public isEqual(room: Room): boolean {
|
||||||
|
return room.key === this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key representing this room
|
||||||
|
*/
|
||||||
|
public get key(): string {
|
||||||
|
const newUrl = new URL(this.roomUrl.toString());
|
||||||
|
newUrl.search = "";
|
||||||
|
newUrl.hash = "";
|
||||||
|
return newUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get textures(): CharacterTexture[] | undefined {
|
||||||
|
return this._textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mapUrl(): string {
|
||||||
|
if (!this._mapUrl) {
|
||||||
|
throw new Error("Map URL not fetched yet");
|
||||||
|
}
|
||||||
|
return this._mapUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
RoomJoinedMessage,
|
RoomJoinedMessage,
|
||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
SetPlayerDetailsMessage,
|
SetPlayerDetailsMessage,
|
||||||
SilentMessage, StopGlobalMessage,
|
SilentMessage,
|
||||||
|
StopGlobalMessage,
|
||||||
UserJoinedMessage,
|
UserJoinedMessage,
|
||||||
UserLeftMessage,
|
UserLeftMessage,
|
||||||
UserMovedMessage,
|
UserMovedMessage,
|
||||||
@ -31,17 +32,22 @@ import {
|
|||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
SendUserMessage,
|
SendUserMessage,
|
||||||
BanUserMessage,
|
BanUserMessage,
|
||||||
} from "../Messages/generated/messages_pb"
|
} from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
|
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
|
||||||
import {
|
import {
|
||||||
EventMessage,
|
EventMessage,
|
||||||
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface,
|
GroupCreatedUpdatedMessageInterface,
|
||||||
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface,
|
ItemEventMessageInterface,
|
||||||
|
MessageUserJoined,
|
||||||
|
OnConnectInterface,
|
||||||
|
PlayGlobalMessageInterface,
|
||||||
|
PositionInterface,
|
||||||
RoomJoinedMessageInterface,
|
RoomJoinedMessageInterface,
|
||||||
ViewportInterface, WebRtcDisconnectMessageInterface,
|
ViewportInterface,
|
||||||
|
WebRtcDisconnectMessageInterface,
|
||||||
WebRtcSignalReceivedMessageInterface,
|
WebRtcSignalReceivedMessageInterface,
|
||||||
} from "./ConnexionModels";
|
} from "./ConnexionModels";
|
||||||
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||||
@ -61,36 +67,45 @@ export class RoomConnection implements RoomConnection {
|
|||||||
private closed: boolean = false;
|
private closed: boolean = false;
|
||||||
private tags: string[] = [];
|
private tags: string[] = [];
|
||||||
|
|
||||||
public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
|
||||||
RoomConnection.websocketFactory = websocketFactory;
|
RoomConnection.websocketFactory = websocketFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param token A JWT token containing the UUID of the user
|
* @param token A JWT token containing the UUID of the user
|
||||||
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
|
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
|
||||||
*/
|
*/
|
||||||
public constructor(token: string | null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string | null) {
|
public constructor(
|
||||||
|
token: string | null,
|
||||||
|
roomUrl: string,
|
||||||
|
name: string,
|
||||||
|
characterLayers: string[],
|
||||||
|
position: PositionInterface,
|
||||||
|
viewport: ViewportInterface,
|
||||||
|
companion: string | null
|
||||||
|
) {
|
||||||
let url = new URL(PUSHER_URL, window.location.toString()).toString();
|
let url = new URL(PUSHER_URL, window.location.toString()).toString();
|
||||||
url = url.replace('http://', 'ws://').replace('https://', 'wss://');
|
url = url.replace("http://", "ws://").replace("https://", "wss://");
|
||||||
if (!url.endsWith('/')) {
|
if (!url.endsWith("/")) {
|
||||||
url += '/';
|
url += "/";
|
||||||
}
|
}
|
||||||
url += 'room';
|
url += "room";
|
||||||
url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : '');
|
url += "?roomId=" + encodeURIComponent(roomUrl);
|
||||||
url += '&token=' + (token ? encodeURIComponent(token) : '');
|
url += "&token=" + (token ? encodeURIComponent(token) : "");
|
||||||
url += '&name=' + encodeURIComponent(name);
|
url += "&name=" + encodeURIComponent(name);
|
||||||
for (const layer of characterLayers) {
|
for (const layer of characterLayers) {
|
||||||
url += '&characterLayers=' + encodeURIComponent(layer);
|
url += "&characterLayers=" + encodeURIComponent(layer);
|
||||||
}
|
}
|
||||||
url += '&x=' + Math.floor(position.x);
|
url += "&x=" + Math.floor(position.x);
|
||||||
url += '&y=' + Math.floor(position.y);
|
url += "&y=" + Math.floor(position.y);
|
||||||
url += '&top=' + Math.floor(viewport.top);
|
url += "&top=" + Math.floor(viewport.top);
|
||||||
url += '&bottom=' + Math.floor(viewport.bottom);
|
url += "&bottom=" + Math.floor(viewport.bottom);
|
||||||
url += '&left=' + Math.floor(viewport.left);
|
url += "&left=" + Math.floor(viewport.left);
|
||||||
url += '&right=' + Math.floor(viewport.right);
|
url += "&right=" + Math.floor(viewport.right);
|
||||||
if (typeof companion === 'string') {
|
if (typeof companion === "string") {
|
||||||
url += '&companion=' + encodeURIComponent(companion);
|
url += "&companion=" + encodeURIComponent(companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RoomConnection.websocketFactory) {
|
if (RoomConnection.websocketFactory) {
|
||||||
@ -99,7 +114,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.socket = new WebSocket(url);
|
this.socket = new WebSocket(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.binaryType = 'arraybuffer';
|
this.socket.binaryType = "arraybuffer";
|
||||||
|
|
||||||
let interval: ReturnType<typeof setInterval> | undefined = undefined;
|
let interval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||||
|
|
||||||
@ -109,7 +124,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
|
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.socket.addEventListener('close', (event) => {
|
this.socket.addEventListener("close", (event) => {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
@ -126,7 +141,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|null = null;
|
let event: string | null = null;
|
||||||
let payload;
|
let payload;
|
||||||
if (subMessage.hasUsermovedmessage()) {
|
if (subMessage.hasUsermovedmessage()) {
|
||||||
event = EventMessage.USER_MOVED;
|
event = EventMessage.USER_MOVED;
|
||||||
@ -150,7 +165,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
||||||
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unexpected batch message type');
|
throw new Error("Unexpected batch message type");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
@ -171,8 +186,8 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.dispatch(EventMessage.CONNECT, {
|
this.dispatch(EventMessage.CONNECT, {
|
||||||
connection: this,
|
connection: this,
|
||||||
room: {
|
room: {
|
||||||
items
|
items,
|
||||||
} as RoomJoinedMessageInterface
|
} as RoomJoinedMessageInterface,
|
||||||
});
|
});
|
||||||
} else if (message.hasWorldfullmessage()) {
|
} else if (message.hasWorldfullmessage()) {
|
||||||
worldFullMessageStream.onMessage();
|
worldFullMessageStream.onMessage();
|
||||||
@ -183,7 +198,10 @@ export class RoomConnection implements RoomConnection {
|
|||||||
} else if (message.hasWebrtcsignaltoclientmessage()) {
|
} else if (message.hasWebrtcsignaltoclientmessage()) {
|
||||||
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
|
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
|
||||||
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
|
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
|
||||||
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage());
|
this.dispatch(
|
||||||
|
EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
|
||||||
|
message.getWebrtcscreensharingsignaltoclientmessage()
|
||||||
|
);
|
||||||
} else if (message.hasWebrtcstartmessage()) {
|
} else if (message.hasWebrtcstartmessage()) {
|
||||||
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
|
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
|
||||||
} else if (message.hasWebrtcdisconnectmessage()) {
|
} else if (message.hasWebrtcdisconnectmessage()) {
|
||||||
@ -205,10 +223,9 @@ export class RoomConnection implements RoomConnection {
|
|||||||
} else if (message.hasRefreshroommessage()) {
|
} else if (message.hasRefreshroommessage()) {
|
||||||
//todo: implement a way to notify the user the room was refreshed.
|
//todo: implement a way to notify the user the room was refreshed.
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unknown message received');
|
throw new Error("Unknown message received");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private dispatch(event: string, payload: unknown): void {
|
private dispatch(event: string, payload: unknown): void {
|
||||||
@ -243,16 +260,16 @@ export class RoomConnection implements RoomConnection {
|
|||||||
positionMessage.setY(Math.floor(y));
|
positionMessage.setY(Math.floor(y));
|
||||||
let directionEnum: Direction;
|
let directionEnum: Direction;
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case 'up':
|
case "up":
|
||||||
directionEnum = Direction.UP;
|
directionEnum = Direction.UP;
|
||||||
break;
|
break;
|
||||||
case 'down':
|
case "down":
|
||||||
directionEnum = Direction.DOWN;
|
directionEnum = Direction.DOWN;
|
||||||
break;
|
break;
|
||||||
case 'left':
|
case "left":
|
||||||
directionEnum = Direction.LEFT;
|
directionEnum = Direction.LEFT;
|
||||||
break;
|
break;
|
||||||
case 'right':
|
case "right":
|
||||||
directionEnum = Direction.RIGHT;
|
directionEnum = Direction.RIGHT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -327,15 +344,17 @@ export class RoomConnection implements RoomConnection {
|
|||||||
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
|
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
|
||||||
const position = message.getPosition();
|
const position = message.getPosition();
|
||||||
if (position === undefined) {
|
if (position === undefined) {
|
||||||
throw new Error('Invalid JOIN_ROOM message');
|
throw new Error("Invalid JOIN_ROOM message");
|
||||||
}
|
}
|
||||||
|
|
||||||
const characterLayers = message.getCharacterlayersList().map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
|
const characterLayers = message
|
||||||
return {
|
.getCharacterlayersList()
|
||||||
name: characterLayer.getName(),
|
.map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
|
||||||
img: characterLayer.getUrl()
|
return {
|
||||||
}
|
name: characterLayer.getName(),
|
||||||
})
|
img: characterLayer.getUrl(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const companion = message.getCompanion();
|
const companion = message.getCompanion();
|
||||||
|
|
||||||
@ -345,8 +364,9 @@ export class RoomConnection implements RoomConnection {
|
|||||||
characterLayers,
|
characterLayers,
|
||||||
visitCardUrl: message.getVisitcardurl(),
|
visitCardUrl: message.getVisitcardurl(),
|
||||||
position: ProtobufClientUtils.toPointInterface(position),
|
position: ProtobufClientUtils.toPointInterface(position),
|
||||||
companion: companion ? companion.getName() : null
|
companion: companion ? companion.getName() : null,
|
||||||
}
|
userUuid: message.getUseruuid(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUserMoved(callback: (message: UserMovedMessage) => void): void {
|
public onUserMoved(callback: (message: UserMovedMessage) => void): void {
|
||||||
@ -372,7 +392,9 @@ export class RoomConnection implements RoomConnection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void {
|
public onGroupUpdatedOrCreated(
|
||||||
|
callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
|
||||||
|
): void {
|
||||||
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
|
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
|
||||||
callback(this.toGroupCreatedUpdatedMessage(message));
|
callback(this.toGroupCreatedUpdatedMessage(message));
|
||||||
});
|
});
|
||||||
@ -381,14 +403,14 @@ export class RoomConnection implements RoomConnection {
|
|||||||
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
|
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
|
||||||
const position = message.getPosition();
|
const position = message.getPosition();
|
||||||
if (position === undefined) {
|
if (position === undefined) {
|
||||||
throw new Error('Missing position in GROUP_CREATE_UPDATE');
|
throw new Error("Missing position in GROUP_CREATE_UPDATE");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: message.getGroupid(),
|
groupId: message.getGroupid(),
|
||||||
position: position.toObject(),
|
position: position.toObject(),
|
||||||
groupSize: message.getGroupsize()
|
groupSize: message.getGroupsize(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGroupDeleted(callback: (groupId: number) => void): void {
|
public onGroupDeleted(callback: (groupId: number) => void): void {
|
||||||
@ -404,7 +426,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onConnectError(callback: (error: Event) => void): void {
|
public onConnectError(callback: (error: Event) => void): void {
|
||||||
this.socket.addEventListener('error', callback)
|
this.socket.addEventListener("error", callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
|
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
|
||||||
@ -445,7 +467,6 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
|
this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
|
||||||
callback({
|
callback({
|
||||||
userId: message.getUserid(),
|
userId: message.getUserid(),
|
||||||
name: message.getName(),
|
|
||||||
initiator: message.getInitiator(),
|
initiator: message.getInitiator(),
|
||||||
webRtcUser: message.getWebrtcusername() ?? undefined,
|
webRtcUser: message.getWebrtcusername() ?? undefined,
|
||||||
webRtcPassword: message.getWebrtcpassword() ?? undefined,
|
webRtcPassword: message.getWebrtcpassword() ?? undefined,
|
||||||
@ -476,11 +497,11 @@ export class RoomConnection implements RoomConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onServerDisconnected(callback: () => void): void {
|
public onServerDisconnected(callback: () => void): void {
|
||||||
this.socket.addEventListener('close', (event) => {
|
this.socket.addEventListener("close", (event) => {
|
||||||
if (this.closed === true || connectionManager.unloading) {
|
if (this.closed === true || connectionManager.unloading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Socket closed with code ' + event.code + ". Reason: " + event.reason);
|
console.log("Socket closed with code " + event.code + ". Reason: " + event.reason);
|
||||||
if (event.code === 1000) {
|
if (event.code === 1000) {
|
||||||
// Normal closure case
|
// Normal closure case
|
||||||
return;
|
return;
|
||||||
@ -490,14 +511,14 @@ export class RoomConnection implements RoomConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getUserId(): number {
|
public getUserId(): number {
|
||||||
if (this.userId === null) throw 'UserId cannot be null!'
|
if (this.userId === null) throw "UserId cannot be null!";
|
||||||
return this.userId;
|
return this.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
|
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
|
||||||
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
|
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
|
||||||
callback({
|
callback({
|
||||||
userId: message.getUserid()
|
userId: message.getUserid(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -521,21 +542,22 @@ export class RoomConnection implements RoomConnection {
|
|||||||
itemId: message.getItemid(),
|
itemId: message.getItemid(),
|
||||||
event: message.getEvent(),
|
event: message.getEvent(),
|
||||||
parameters: JSON.parse(message.getParametersjson()),
|
parameters: JSON.parse(message.getParametersjson()),
|
||||||
state: JSON.parse(message.getStatejson())
|
state: JSON.parse(message.getStatejson()),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadAudio(file: FormData) {
|
public uploadAudio(file: FormData) {
|
||||||
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: { data: {} }) => {
|
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
|
||||||
return res.data;
|
.then((res: { data: {} }) => {
|
||||||
}).catch((err) => {
|
return res.data;
|
||||||
console.error(err);
|
})
|
||||||
throw err;
|
.catch((err) => {
|
||||||
});
|
console.error(err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
|
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
|
||||||
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
|
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
|
||||||
callback({
|
callback({
|
||||||
@ -570,9 +592,9 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void {
|
public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void {
|
||||||
const reportPlayerMessage = new ReportPlayerMessage();
|
const reportPlayerMessage = new ReportPlayerMessage();
|
||||||
reportPlayerMessage.setReporteduserid(reportedUserId);
|
reportPlayerMessage.setReporteduseruuid(reportedUserUuid);
|
||||||
reportPlayerMessage.setReportcomment(reportComment);
|
reportPlayerMessage.setReportcomment(reportComment);
|
||||||
|
|
||||||
const clientToServerMessage = new ClientToServerMessage();
|
const clientToServerMessage = new ClientToServerMessage();
|
||||||
@ -605,12 +627,12 @@ export class RoomConnection implements RoomConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isAdmin(): boolean {
|
public isAdmin(): boolean {
|
||||||
return this.hasTag('admin');
|
return this.hasTag("admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitEmoteEvent(emoteName: string): void {
|
public emitEmoteEvent(emoteName: string): void {
|
||||||
const emoteMessage = new EmotePromptMessage();
|
const emoteMessage = new EmotePromptMessage();
|
||||||
emoteMessage.setEmote(emoteName)
|
emoteMessage.setEmote(emoteName);
|
||||||
|
|
||||||
const clientToServerMessage = new ClientToServerMessage();
|
const clientToServerMessage = new ClientToServerMessage();
|
||||||
clientToServerMessage.setEmotepromptmessage(emoteMessage);
|
clientToServerMessage.setEmotepromptmessage(emoteMessage);
|
||||||
@ -618,7 +640,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllTags() : string[] {
|
public getAllTags(): string[] {
|
||||||
return this.tags;
|
return this.tags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {discussionManager} from "../../WebRtc/DiscussionManager";
|
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
||||||
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
export const openChatIconName = 'openChatIcon';
|
export const openChatIconName = "openChatIcon";
|
||||||
export class OpenChatIcon extends Phaser.GameObjects.Image {
|
export class OpenChatIcon extends Phaser.GameObjects.Image {
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
super(scene, x, y, openChatIconName, 3);
|
super(scene, x, y, openChatIconName, 3);
|
||||||
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
|
|||||||
this.setScrollFactor(0, 0);
|
this.setScrollFactor(0, 0);
|
||||||
this.setOrigin(0, 1);
|
this.setOrigin(0, 1);
|
||||||
this.setInteractive();
|
this.setInteractive();
|
||||||
this.setVisible(false);
|
//this.setVisible(false);
|
||||||
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||||
|
|
||||||
this.on("pointerup", () => discussionManager.showDiscussionPart());
|
this.on("pointerup", () => chatVisibilityStore.set(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -44,7 +44,6 @@ export class TextUtils {
|
|||||||
options.align = object.text.halign;
|
options.align = object.text.halign;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(options);
|
|
||||||
const textElem = scene.add.text(object.x, object.y, object.text.text, options);
|
const textElem = scene.add.text(object.x, object.y, object.text.text, options);
|
||||||
textElem.setAngle(object.rotation);
|
textElem.setAngle(object.rotation);
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,6 @@ export const createLoadingPromise = (
|
|||||||
frameConfig: FrameConfig
|
frameConfig: FrameConfig
|
||||||
) => {
|
) => {
|
||||||
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
|
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
|
||||||
console.log("count", loadPlugin.listenerCount("loaderror"));
|
|
||||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||||
return res(playerResourceDescriptor);
|
return res(playerResourceDescriptor);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
||||||
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
import type {PlayerInterface} from "./PlayerInterface";
|
||||||
|
|
||||||
export interface AddPlayerInterface {
|
export interface AddPlayerInterface extends PlayerInterface {
|
||||||
userId: number;
|
|
||||||
name: string;
|
|
||||||
characterLayers: BodyResourceDescriptionInterface[];
|
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
visitCardUrl: string|null;
|
|
||||||
companion: string|null;
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import {ResizableScene} from "../Login/ResizableScene";
|
import { ResizableScene } from "../Login/ResizableScene";
|
||||||
import GameObject = Phaser.GameObjects.GameObject;
|
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";
|
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.
|
||||||
*/
|
*/
|
||||||
export abstract class DirtyScene extends ResizableScene {
|
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;
|
private physicsEnabled: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +59,6 @@ export abstract class DirtyScene extends ResizableScene {
|
|||||||
this.physicsEnabled = false;
|
this.physicsEnabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private trackAnimation(): void {
|
private trackAnimation(): void {
|
||||||
@ -71,7 +70,7 @@ export abstract class DirtyScene extends ResizableScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public markDirty(): void {
|
public markDirty(): void {
|
||||||
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true);
|
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => (this.dirty = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public onResize(): void {
|
public onResize(): void {
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
import {GameScene} from "./GameScene";
|
import { GameScene } from "./GameScene";
|
||||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||||
import type {Room} from "../../Connexion/Room";
|
import type { Room } from "../../Connexion/Room";
|
||||||
import {MenuScene, MenuSceneName} from "../Menu/MenuScene";
|
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
||||||
import {LoginSceneName} from "../Login/LoginScene";
|
import { LoginSceneName } from "../Login/LoginScene";
|
||||||
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene";
|
import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||||
import {EnableCameraSceneName} from "../Login/EnableCameraScene";
|
import { EnableCameraSceneName } from "../Login/EnableCameraScene";
|
||||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
import {get} from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
|
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
|
||||||
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
|
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class should be responsible for any scene starting/stopping
|
* This class should be responsible for any scene starting/stopping
|
||||||
*/
|
*/
|
||||||
export class GameManager {
|
export class GameManager {
|
||||||
private playerName: string|null;
|
private playerName: string | null;
|
||||||
private characterLayers: string[]|null;
|
private characterLayers: string[] | null;
|
||||||
private companion: string|null;
|
private companion: string | null;
|
||||||
private startRoom!:Room;
|
private startRoom!: Room;
|
||||||
currentGameSceneName: string|null = null;
|
currentGameSceneName: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.playerName = localUserStore.getName();
|
this.playerName = localUserStore.getName();
|
||||||
@ -30,7 +28,7 @@ export class GameManager {
|
|||||||
|
|
||||||
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
||||||
this.startRoom = await connectionManager.initGameConnexion();
|
this.startRoom = await connectionManager.initGameConnexion();
|
||||||
await this.loadMap(this.startRoom, scenePlugin);
|
this.loadMap(this.startRoom, scenePlugin);
|
||||||
|
|
||||||
if (!this.playerName) {
|
if (!this.playerName) {
|
||||||
return LoginSceneName;
|
return LoginSceneName;
|
||||||
@ -51,43 +49,44 @@ export class GameManager {
|
|||||||
localUserStore.setCharacterLayers(layers);
|
localUserStore.setCharacterLayers(layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerName(): string|null {
|
getPlayerName(): string | null {
|
||||||
return this.playerName;
|
return this.playerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCharacterLayers(): string[] {
|
getCharacterLayers(): string[] {
|
||||||
if (!this.characterLayers) {
|
if (!this.characterLayers) {
|
||||||
throw 'characterLayers are not set';
|
throw "characterLayers are not set";
|
||||||
}
|
}
|
||||||
return this.characterLayers;
|
return this.characterLayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCompanion(companion: string | null): void {
|
||||||
setCompanion(companion: string|null): void {
|
|
||||||
this.companion = companion;
|
this.companion = companion;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompanion(): string|null {
|
getCompanion(): string | null {
|
||||||
return this.companion;
|
return this.companion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
|
public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
|
||||||
const roomID = room.id;
|
const roomID = room.key;
|
||||||
const mapDetail = await room.getMapDetail();
|
|
||||||
|
|
||||||
const gameIndex = scenePlugin.getIndex(roomID);
|
const gameIndex = scenePlugin.getIndex(roomID);
|
||||||
if(gameIndex === -1){
|
if (gameIndex === -1) {
|
||||||
const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
|
const game: Phaser.Scene = new GameScene(room, room.mapUrl);
|
||||||
scenePlugin.add(roomID, game, false);
|
scenePlugin.add(roomID, game, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
|
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
|
||||||
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
|
console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
|
||||||
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
|
scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
|
||||||
scenePlugin.launch(MenuSceneName);
|
scenePlugin.launch(MenuSceneName);
|
||||||
|
|
||||||
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
|
if (
|
||||||
|
!localUserStore.getHelpCameraSettingsShown() &&
|
||||||
|
(!get(requestedMicrophoneState) || !get(requestedCameraState))
|
||||||
|
) {
|
||||||
helpCameraSettingsVisibleStore.set(true);
|
helpCameraSettingsVisibleStore.set(true);
|
||||||
localUserStore.setHelpCameraSettingsShown();
|
localUserStore.setHelpCameraSettingsShown();
|
||||||
}
|
}
|
||||||
@ -104,7 +103,7 @@ export class GameManager {
|
|||||||
* This will close the socket connections and stop the gameScene, but won't remove it.
|
* This will close the socket connections and stop the gameScene, but won't remove it.
|
||||||
*/
|
*/
|
||||||
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void {
|
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void {
|
||||||
if (this.currentGameSceneName === null) throw 'No current scene id set!';
|
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||||
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
|
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
|
||||||
gameScene.cleanupClosingScene();
|
gameScene.cleanupClosingScene();
|
||||||
scene.scene.stop(this.currentGameSceneName);
|
scene.scene.stop(this.currentGameSceneName);
|
||||||
@ -123,13 +122,13 @@ export class GameManager {
|
|||||||
scene.scene.start(this.currentGameSceneName);
|
scene.scene.start(this.currentGameSceneName);
|
||||||
scene.scene.wake(MenuSceneName);
|
scene.scene.wake(MenuSceneName);
|
||||||
} else {
|
} else {
|
||||||
scene.scene.run(fallbackSceneName)
|
scene.scene.run(fallbackSceneName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentGameScene(scene: Phaser.Scene): GameScene {
|
public getCurrentGameScene(scene: Phaser.Scene): GameScene {
|
||||||
if (this.currentGameSceneName === null) throw 'No current scene id set!';
|
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||||
return scene.scene.get(this.currentGameSceneName) as GameScene
|
return scene.scene.get(this.currentGameSceneName) as GameScene;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap";
|
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
|
||||||
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||||
|
|
||||||
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void;
|
export type PropertyChangeCallback = (
|
||||||
|
newValue: string | number | boolean | undefined,
|
||||||
|
oldValue: string | number | boolean | undefined,
|
||||||
|
allProps: Map<string, string | boolean | number>
|
||||||
|
) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
||||||
@ -19,37 +23,50 @@ export class GameMap {
|
|||||||
public readonly flatLayers: ITiledMapLayer[];
|
public readonly flatLayers: ITiledMapLayer[];
|
||||||
public readonly phaserLayers: TilemapLayer[] = [];
|
public readonly phaserLayers: TilemapLayer[] = [];
|
||||||
|
|
||||||
public exitUrls: Array<string> = []
|
public exitUrls: Array<string> = [];
|
||||||
|
|
||||||
public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array<Phaser.Tilemaps.Tileset>) {
|
public hasStartTile = false;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private map: ITiledMap,
|
||||||
|
phaserMap: Phaser.Tilemaps.Tilemap,
|
||||||
|
terrains: Array<Phaser.Tilemaps.Tileset>
|
||||||
|
) {
|
||||||
this.flatLayers = flattenGroupLayersMap(map);
|
this.flatLayers = flattenGroupLayersMap(map);
|
||||||
let depth = -2;
|
let depth = -2;
|
||||||
for (const layer of this.flatLayers) {
|
for (const layer of this.flatLayers) {
|
||||||
if(layer.type === 'tilelayer'){
|
if (layer.type === "tilelayer") {
|
||||||
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
|
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
|
||||||
}
|
}
|
||||||
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
|
if (layer.type === "objectgroup" && layer.name === "floorLayer") {
|
||||||
depth = DEPTH_OVERLAY_INDEX;
|
depth = DEPTH_OVERLAY_INDEX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const tileset of map.tilesets) {
|
for (const tileset of map.tilesets) {
|
||||||
tileset?.tiles?.forEach(tile => {
|
tileset?.tiles?.forEach((tile) => {
|
||||||
if (tile.properties) {
|
if (tile.properties) {
|
||||||
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties
|
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties;
|
||||||
tile.properties.forEach(prop => {
|
tile.properties.forEach((prop) => {
|
||||||
if (prop.name == 'name' && typeof prop.value == "string") {
|
if (prop.name == "name" && typeof prop.value == "string") {
|
||||||
this.tileNameMap.set(prop.value, tileset.firstgid + tile.id);
|
this.tileNameMap.set(prop.value, tileset.firstgid + tile.id);
|
||||||
}
|
}
|
||||||
if (prop.name == "exitUrl" && typeof prop.value == "string") {
|
if (prop.name == "exitUrl" && typeof prop.value == "string") {
|
||||||
this.exitUrls.push(prop.value);
|
this.exitUrls.push(prop.value);
|
||||||
|
} else if (prop.name == "start") {
|
||||||
|
this.hasStartTile = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
|
||||||
|
if (this.tileSetPropertyMap[index]) {
|
||||||
|
return this.tileSetPropertyMap[index];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the position of the current player (in pixels)
|
* Sets the position of the current player (in pixels)
|
||||||
@ -93,7 +110,7 @@ export class GameMap {
|
|||||||
const properties = new Map<string, string | boolean | number>();
|
const properties = new Map<string, string | boolean | number>();
|
||||||
|
|
||||||
for (const layer of this.flatLayers) {
|
for (const layer of this.flatLayers) {
|
||||||
if (layer.type !== 'tilelayer') {
|
if (layer.type !== "tilelayer") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +120,7 @@ export class GameMap {
|
|||||||
if (tiles[key] == 0) {
|
if (tiles[key] == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tileIndex = tiles[key]
|
tileIndex = tiles[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is a tile in this layer, let's embed the properties
|
// There is a tile in this layer, let's embed the properties
|
||||||
@ -117,28 +134,36 @@ export class GameMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tileIndex) {
|
if (tileIndex) {
|
||||||
this.tileSetPropertyMap[tileIndex]?.forEach(property => {
|
this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
|
||||||
if (property.value) {
|
if (property.value) {
|
||||||
properties.set(property.name, property.value)
|
properties.set(property.name, property.value);
|
||||||
} else if (properties.has(property.name)) {
|
} else if (properties.has(property.name)) {
|
||||||
properties.delete(property.name)
|
properties.delete(property.name);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMap(): ITiledMap{
|
public getMap(): ITiledMap {
|
||||||
return this.map;
|
return this.map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
|
private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
|
||||||
return this.tileSetPropertyMap[index];
|
if (this.tileSetPropertyMap[index]) {
|
||||||
|
return this.tileSetPropertyMap[index];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) {
|
private trigger(
|
||||||
|
propName: string,
|
||||||
|
oldValue: string | number | boolean | undefined,
|
||||||
|
newValue: string | number | boolean | undefined,
|
||||||
|
allProps: Map<string, string | boolean | number>
|
||||||
|
) {
|
||||||
const callbacksArray = this.callbacks.get(propName);
|
const callbacksArray = this.callbacks.get(propName);
|
||||||
if (callbacksArray !== undefined) {
|
if (callbacksArray !== undefined) {
|
||||||
for (const callback of callbacksArray) {
|
for (const callback of callbacksArray) {
|
||||||
@ -167,7 +192,11 @@ export class GameMap {
|
|||||||
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
|
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addTerrain(terrain : Phaser.Tilemaps.Tileset): void {
|
public findPhaserLayers(groupName: string): TilemapLayer[] {
|
||||||
|
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
|
||||||
for (const phaserLayer of this.phaserLayers) {
|
for (const phaserLayer of this.phaserLayers) {
|
||||||
phaserLayer.tileset.push(terrain);
|
phaserLayer.tileset.push(terrain);
|
||||||
}
|
}
|
||||||
@ -175,40 +204,46 @@ export class GameMap {
|
|||||||
|
|
||||||
private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void {
|
private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void {
|
||||||
const fLayer = this.findLayer(layer);
|
const fLayer = this.findLayer(layer);
|
||||||
if ( fLayer == undefined ) {
|
if (fLayer == undefined) {
|
||||||
console.error("The layer that you want to change doesn't exist.");
|
console.error("The layer '" + layer + "' that you want to change doesn't exist.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fLayer.type !== 'tilelayer') {
|
if (fLayer.type !== "tilelayer") {
|
||||||
console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
|
console.error(
|
||||||
|
"The layer '" +
|
||||||
|
layer +
|
||||||
|
"' that you want to change is not a tilelayer. Tile can only be put in tilelayer."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof fLayer.data === "string") {
|
if (typeof fLayer.data === "string") {
|
||||||
console.error("Data of the layer that you want to change is only readable.");
|
console.error("Data of the layer '" + layer + "' that you want to change is only readable.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fLayer.data[x+y*fLayer.height] = index;
|
fLayer.data[x + y * fLayer.width] = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
public putTile(tile: string | number, x: number, y: number, layer: string): void {
|
public putTile(tile: string | number | null, x: number, y: number, layer: string): void {
|
||||||
const phaserLayer = this.findPhaserLayer(layer);
|
const phaserLayer = this.findPhaserLayer(layer);
|
||||||
if ( phaserLayer ) {
|
if (phaserLayer) {
|
||||||
|
if (tile === null) {
|
||||||
|
phaserLayer.putTileAt(-1, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tileIndex = this.getIndexForTileType(tile);
|
const tileIndex = this.getIndexForTileType(tile);
|
||||||
if ( tileIndex !== undefined ) {
|
if (tileIndex !== undefined) {
|
||||||
this.putTileInFlatLayer(tileIndex, x, y, layer);
|
this.putTileInFlatLayer(tileIndex, x, y, layer);
|
||||||
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
|
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
|
||||||
for (const property of this.getTileProperty(tileIndex)) {
|
for (const property of this.getTileProperty(tileIndex)) {
|
||||||
if ( property.name === "collides" && property.value === "true") {
|
if (property.name === "collides" && property.value) {
|
||||||
phaserTile.setCollision(true);
|
phaserTile.setCollision(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
|
||||||
}
|
}
|
||||||
else {
|
} else {
|
||||||
console.error("The tile that you want to place doesn't exist.");
|
console.error("The layer '" + layer + "' does not exist (or is not a tilelaye).");
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,5 +253,4 @@ export class GameMap {
|
|||||||
}
|
}
|
||||||
return this.tileNameMap.get(tile);
|
return this.tileNameMap.get(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Queue } from "queue-typescript";
|
|
||||||
import type { Subscription } from "rxjs";
|
import type { Subscription } from "rxjs";
|
||||||
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
|
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
|
||||||
import { userMessageManager } from "../../Administration/UserMessageManager";
|
import { userMessageManager } from "../../Administration/UserMessageManager";
|
||||||
@ -14,20 +13,9 @@ import type {
|
|||||||
PositionInterface,
|
PositionInterface,
|
||||||
RoomJoinedMessageInterface,
|
RoomJoinedMessageInterface,
|
||||||
} from "../../Connexion/ConnexionModels";
|
} from "../../Connexion/ConnexionModels";
|
||||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
|
||||||
import { Room } from "../../Connexion/Room";
|
|
||||||
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
|
||||||
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
|
|
||||||
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
|
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
|
||||||
import { TextureError } from "../../Exception/TextureError";
|
|
||||||
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
|
import { Queue } from "queue-typescript";
|
||||||
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
|
||||||
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
|
||||||
import { urlManager } from "../../Url/UrlManager";
|
|
||||||
import { audioManager } from "../../WebRtc/AudioManager";
|
|
||||||
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
|
|
||||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
|
||||||
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
|
||||||
import {
|
import {
|
||||||
AUDIO_LOOP_PROPERTY,
|
AUDIO_LOOP_PROPERTY,
|
||||||
AUDIO_VOLUME_PROPERTY,
|
AUDIO_VOLUME_PROPERTY,
|
||||||
@ -39,15 +27,21 @@ import {
|
|||||||
TRIGGER_WEBSITE_PROPERTIES,
|
TRIGGER_WEBSITE_PROPERTIES,
|
||||||
WEBSITE_MESSAGE_PROPERTIES,
|
WEBSITE_MESSAGE_PROPERTIES,
|
||||||
} from "../../WebRtc/LayoutManager";
|
} from "../../WebRtc/LayoutManager";
|
||||||
|
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
|
||||||
|
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
|
||||||
|
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
||||||
|
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
||||||
|
import { Room } from "../../Connexion/Room";
|
||||||
|
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
||||||
|
import { urlManager } from "../../Url/UrlManager";
|
||||||
|
import { audioManager } from "../../WebRtc/AudioManager";
|
||||||
|
import { TextureError } from "../../Exception/TextureError";
|
||||||
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
|
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||||
import { mediaManager } from "../../WebRtc/MediaManager";
|
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||||
import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
|
import { SimplePeer } from "../../WebRtc/SimplePeer";
|
||||||
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
|
|
||||||
import { ChatModeIcon } from "../Components/ChatModeIcon";
|
|
||||||
import { addLoader } from "../Components/Loader";
|
import { addLoader } from "../Components/Loader";
|
||||||
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
|
|
||||||
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
|
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
|
||||||
import { PresentationModeIcon } from "../Components/PresentationModeIcon";
|
|
||||||
import { TextUtils } from "../Components/TextUtils";
|
|
||||||
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
|
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||||
import { RemotePlayer } from "../Entity/RemotePlayer";
|
import { RemotePlayer } from "../Entity/RemotePlayer";
|
||||||
import type { ActionableItem } from "../Items/ActionableItem";
|
import type { ActionableItem } from "../Items/ActionableItem";
|
||||||
@ -58,7 +52,6 @@ import type {
|
|||||||
ITiledMapLayer,
|
ITiledMapLayer,
|
||||||
ITiledMapLayerProperty,
|
ITiledMapLayerProperty,
|
||||||
ITiledMapObject,
|
ITiledMapObject,
|
||||||
ITiledMapTileLayer,
|
|
||||||
ITiledTileSet,
|
ITiledTileSet,
|
||||||
} from "../Map/ITiledMap";
|
} from "../Map/ITiledMap";
|
||||||
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
||||||
@ -66,13 +59,8 @@ import { PlayerAnimationDirections } from "../Player/Animation";
|
|||||||
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
||||||
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
|
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
|
||||||
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
|
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
|
||||||
import { waScaleManager } from "../Services/WaScaleManager";
|
|
||||||
import { PinchManager } from "../UserInput/PinchManager";
|
|
||||||
import { UserInputManager } from "../UserInput/UserInputManager";
|
import { UserInputManager } from "../UserInput/UserInputManager";
|
||||||
import type { AddPlayerInterface } from "./AddPlayerInterface";
|
import type { AddPlayerInterface } from "./AddPlayerInterface";
|
||||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
|
||||||
import { DirtyScene } from "./DirtyScene";
|
|
||||||
import { EmoteManager } from "./EmoteManager";
|
|
||||||
import { gameManager } from "./GameManager";
|
import { gameManager } from "./GameManager";
|
||||||
import { GameMap } from "./GameMap";
|
import { GameMap } from "./GameMap";
|
||||||
import { PlayerMovement } from "./PlayerMovement";
|
import { PlayerMovement } from "./PlayerMovement";
|
||||||
@ -83,16 +71,28 @@ import CanvasTexture = Phaser.Textures.CanvasTexture;
|
|||||||
import GameObject = Phaser.GameObjects.GameObject;
|
import GameObject = Phaser.GameObjects.GameObject;
|
||||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||||
import DOMElement = Phaser.GameObjects.DOMElement;
|
import DOMElement = Phaser.GameObjects.DOMElement;
|
||||||
|
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
|
||||||
|
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
|
||||||
|
import { DirtyScene } from "./DirtyScene";
|
||||||
|
import { TextUtils } from "../Components/TextUtils";
|
||||||
|
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||||
|
import { PinchManager } from "../UserInput/PinchManager";
|
||||||
|
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
|
||||||
|
import { waScaleManager } from "../Services/WaScaleManager";
|
||||||
|
import { EmoteManager } from "./EmoteManager";
|
||||||
import EVENT_TYPE = Phaser.Scenes.Events;
|
import EVENT_TYPE = Phaser.Scenes.Events;
|
||||||
import RenderTexture = Phaser.GameObjects.RenderTexture;
|
import RenderTexture = Phaser.GameObjects.RenderTexture;
|
||||||
import Tilemap = Phaser.Tilemaps.Tilemap;
|
import Tilemap = Phaser.Tilemaps.Tilemap;
|
||||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||||
|
|
||||||
import AnimatedTiles from "phaser-animated-tiles";
|
import AnimatedTiles from "phaser-animated-tiles";
|
||||||
|
import { StartPositionCalculator } from "./StartPositionCalculator";
|
||||||
import { soundManager } from "./SoundManager";
|
import { soundManager } from "./SoundManager";
|
||||||
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
||||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||||
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
@ -129,8 +129,6 @@ interface DeleteGroupEventInterface {
|
|||||||
groupId: number;
|
groupId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStartLayerName = "start";
|
|
||||||
|
|
||||||
export class GameScene extends DirtyScene {
|
export class GameScene extends DirtyScene {
|
||||||
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
||||||
CurrentPlayer!: Player;
|
CurrentPlayer!: Player;
|
||||||
@ -141,8 +139,6 @@ export class GameScene extends DirtyScene {
|
|||||||
mapFile!: ITiledMap;
|
mapFile!: ITiledMap;
|
||||||
animatedTiles!: AnimatedTiles;
|
animatedTiles!: AnimatedTiles;
|
||||||
groups: Map<number, Sprite>;
|
groups: Map<number, Sprite>;
|
||||||
startX!: number;
|
|
||||||
startY!: number;
|
|
||||||
circleTexture!: CanvasTexture;
|
circleTexture!: CanvasTexture;
|
||||||
circleRedTexture!: CanvasTexture;
|
circleRedTexture!: CanvasTexture;
|
||||||
pendingEvents: Queue<
|
pendingEvents: Queue<
|
||||||
@ -174,9 +170,10 @@ export class GameScene extends DirtyScene {
|
|||||||
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
|
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
|
||||||
private iframeSubscriptionList!: Array<Subscription>;
|
private iframeSubscriptionList!: Array<Subscription>;
|
||||||
private peerStoreUnsubscribe!: () => void;
|
private peerStoreUnsubscribe!: () => void;
|
||||||
|
private chatVisibilityUnsubscribe!: () => void;
|
||||||
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
||||||
MapUrlFile: string;
|
MapUrlFile: string;
|
||||||
RoomId: string;
|
roomUrl: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
|
|
||||||
currentTick!: number;
|
currentTick!: number;
|
||||||
@ -194,7 +191,6 @@ export class GameScene extends DirtyScene {
|
|||||||
private outlinedItem: ActionableItem | null = null;
|
private outlinedItem: ActionableItem | null = null;
|
||||||
public userInputManager!: UserInputManager;
|
public userInputManager!: UserInputManager;
|
||||||
private isReconnecting: boolean | undefined = undefined;
|
private isReconnecting: boolean | undefined = undefined;
|
||||||
private startLayerName!: string | null;
|
|
||||||
private openChatIcon!: OpenChatIcon;
|
private openChatIcon!: OpenChatIcon;
|
||||||
private playerName!: string;
|
private playerName!: string;
|
||||||
private characterLayers!: string[];
|
private characterLayers!: string[];
|
||||||
@ -206,17 +202,18 @@ export class GameScene extends DirtyScene {
|
|||||||
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 emoteManager!: EmoteManager;
|
private emoteManager!: EmoteManager;
|
||||||
private preloading: boolean = true;
|
private preloading: boolean = true;
|
||||||
|
startPositionCalculator!: StartPositionCalculator;
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||||
super({
|
super({
|
||||||
key: customKey ?? room.id,
|
key: customKey ?? room.key,
|
||||||
});
|
});
|
||||||
this.Terrains = [];
|
this.Terrains = [];
|
||||||
this.groups = new Map<number, Sprite>();
|
this.groups = new Map<number, Sprite>();
|
||||||
this.instance = room.getInstance();
|
this.instance = room.getInstance();
|
||||||
|
|
||||||
this.MapUrlFile = MapUrlFile;
|
this.MapUrlFile = MapUrlFile;
|
||||||
this.RoomId = room.id;
|
this.roomUrl = room.key;
|
||||||
|
|
||||||
this.createPromise = new Promise<void>((resolve, reject): void => {
|
this.createPromise = new Promise<void>((resolve, reject): void => {
|
||||||
this.createPromiseResolve = resolve;
|
this.createPromiseResolve = resolve;
|
||||||
@ -426,7 +423,6 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
gameManager.gameSceneIsCreated(this);
|
gameManager.gameSceneIsCreated(this);
|
||||||
urlManager.pushRoomIdToUrl(this.room);
|
urlManager.pushRoomIdToUrl(this.room);
|
||||||
this.startLayerName = urlManager.getStartLayerNameFromUrl();
|
|
||||||
|
|
||||||
if (touchScreenManager.supportTouchScreen) {
|
if (touchScreenManager.supportTouchScreen) {
|
||||||
this.pinchManager = new PinchManager(this);
|
this.pinchManager = new PinchManager(this);
|
||||||
@ -469,11 +465,13 @@ export class GameScene extends DirtyScene {
|
|||||||
if (layer.type === "tilelayer") {
|
if (layer.type === "tilelayer") {
|
||||||
const exitSceneUrl = this.getExitSceneUrl(layer);
|
const exitSceneUrl = this.getExitSceneUrl(layer);
|
||||||
if (exitSceneUrl !== undefined) {
|
if (exitSceneUrl !== undefined) {
|
||||||
this.loadNextGame(exitSceneUrl);
|
this.loadNextGame(
|
||||||
|
Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const exitUrl = this.getExitUrl(layer);
|
const exitUrl = this.getExitUrl(layer);
|
||||||
if (exitUrl !== undefined) {
|
if (exitUrl !== undefined) {
|
||||||
this.loadNextGame(exitUrl);
|
this.loadNextGameFromExitUrl(exitUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (layer.type === "objectgroup") {
|
if (layer.type === "objectgroup") {
|
||||||
@ -486,10 +484,15 @@ export class GameScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.gameMap.exitUrls.forEach((exitUrl) => {
|
this.gameMap.exitUrls.forEach((exitUrl) => {
|
||||||
this.loadNextGame(exitUrl);
|
this.loadNextGameFromExitUrl(exitUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initStartXAndStartY();
|
this.startPositionCalculator = new StartPositionCalculator(
|
||||||
|
this.gameMap,
|
||||||
|
this.mapFile,
|
||||||
|
this.initPosition,
|
||||||
|
urlManager.getStartLayerNameFromUrl()
|
||||||
|
);
|
||||||
|
|
||||||
//add entities
|
//add entities
|
||||||
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
|
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
|
||||||
@ -572,6 +575,10 @@ export class GameScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
oldPeerNumber = newPeerNumber;
|
oldPeerNumber = newPeerNumber;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => {
|
||||||
|
this.openChatIcon.setVisible(!v);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -582,12 +589,11 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
connectionManager
|
connectionManager
|
||||||
.connectToRoomSocket(
|
.connectToRoomSocket(
|
||||||
this.RoomId,
|
this.roomUrl,
|
||||||
this.playerName,
|
this.playerName,
|
||||||
this.characterLayers,
|
this.characterLayers,
|
||||||
{
|
{
|
||||||
x: this.startX,
|
...this.startPositionCalculator.startPosition,
|
||||||
y: this.startY,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: camera.scrollX,
|
left: camera.scrollX,
|
||||||
@ -600,6 +606,8 @@ export class GameScene extends DirtyScene {
|
|||||||
.then((onConnect: OnConnectInterface) => {
|
.then((onConnect: OnConnectInterface) => {
|
||||||
this.connection = onConnect.connection;
|
this.connection = onConnect.connection;
|
||||||
|
|
||||||
|
playersStore.connectToRoomConnection(this.connection);
|
||||||
|
|
||||||
this.connection.onUserJoins((message: MessageUserJoined) => {
|
this.connection.onUserJoins((message: MessageUserJoined) => {
|
||||||
const userMessage: AddPlayerInterface = {
|
const userMessage: AddPlayerInterface = {
|
||||||
userId: message.userId,
|
userId: message.userId,
|
||||||
@ -608,6 +616,7 @@ export class GameScene extends DirtyScene {
|
|||||||
position: message.position,
|
position: message.position,
|
||||||
visitCardUrl: message.visitCardUrl,
|
visitCardUrl: message.visitCardUrl,
|
||||||
companion: message.companion,
|
companion: message.companion,
|
||||||
|
userUuid: message.userUuid,
|
||||||
};
|
};
|
||||||
this.addPlayer(userMessage);
|
this.addPlayer(userMessage);
|
||||||
});
|
});
|
||||||
@ -691,12 +700,12 @@ export class GameScene extends DirtyScene {
|
|||||||
const self = this;
|
const self = this;
|
||||||
this.simplePeer.registerPeerConnectionListener({
|
this.simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(peer) {
|
onConnect(peer) {
|
||||||
self.openChatIcon.setVisible(true);
|
//self.openChatIcon.setVisible(true);
|
||||||
audioManager.decreaseVolume();
|
audioManager.decreaseVolume();
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
if (self.simplePeer.getNbConnections() === 0) {
|
if (self.simplePeer.getNbConnections() === 0) {
|
||||||
self.openChatIcon.setVisible(false);
|
//self.openChatIcon.setVisible(false);
|
||||||
audioManager.restoreVolume();
|
audioManager.restoreVolume();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -768,10 +777,13 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
private triggerOnMapLayerPropertyChange() {
|
private triggerOnMapLayerPropertyChange() {
|
||||||
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
|
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
|
||||||
if (newValue) this.onMapExit(newValue as string);
|
if (newValue)
|
||||||
|
this.onMapExit(
|
||||||
|
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
|
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
|
||||||
if (newValue) this.onMapExit(newValue as string);
|
if (newValue) this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()));
|
||||||
});
|
});
|
||||||
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
||||||
if (newValue === undefined) {
|
if (newValue === undefined) {
|
||||||
@ -989,16 +1001,16 @@ ${escapedMessage}
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.iframeSubscriptionList.push(
|
this.iframeSubscriptionList.push(
|
||||||
iframeListener.enablePlayerControlStream.subscribe(()=>{
|
iframeListener.enablePlayerControlStream.subscribe(() => {
|
||||||
this.userInputManager.restoreControls();
|
this.userInputManager.restoreControls();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.iframeSubscriptionList.push(
|
this.iframeSubscriptionList.push(
|
||||||
iframeListener.loadPageStream.subscribe((url: string) => {
|
iframeListener.loadPageStream.subscribe((url: string) => {
|
||||||
this.loadNextGame(url).then(() => {
|
this.loadNextGameFromExitUrl(url).then(() => {
|
||||||
this.events.once(EVENT_TYPE.POST_UPDATE, () => {
|
this.events.once(EVENT_TYPE.POST_UPDATE, () => {
|
||||||
this.onMapExit(url);
|
this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -1047,24 +1059,23 @@ ${escapedMessage}
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
iframeListener.registerAnswerer("getState", () => {
|
||||||
|
return {
|
||||||
|
mapUrl: this.MapUrlFile,
|
||||||
|
startLayerName: this.startPositionCalculator.startLayerName,
|
||||||
|
uuid: localUserStore.getLocalUser()?.uuid,
|
||||||
|
nickname: localUserStore.getName(),
|
||||||
|
roomId: this.roomUrl,
|
||||||
|
tags: this.connection ? this.connection.getAllTags() : [],
|
||||||
|
};
|
||||||
|
});
|
||||||
this.iframeSubscriptionList.push(
|
this.iframeSubscriptionList.push(
|
||||||
iframeListener.gameStateStream.subscribe(() => {
|
iframeListener.setTilesStream.subscribe((eventTiles) => {
|
||||||
iframeListener.sendGameStateEvent({
|
for (const eventTile of eventTiles) {
|
||||||
mapUrl: this.MapUrlFile,
|
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
|
||||||
startLayerName: this.startLayerName,
|
}
|
||||||
uuid: localUserStore.getLocalUser()?.uuid,
|
|
||||||
nickname: localUserStore.getName(),
|
|
||||||
roomId: this.RoomId,
|
|
||||||
tags: this.connection ? this.connection.getAllTags() : [],
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
this.iframeSubscriptionList.push(iframeListener.setTilesStream.subscribe((eventTiles) => {
|
|
||||||
for (const eventTile of eventTiles) {
|
|
||||||
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setPropertyLayer(
|
private setPropertyLayer(
|
||||||
@ -1077,53 +1088,88 @@ ${escapedMessage}
|
|||||||
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
|
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const property = (layer.properties as ITiledMapLayerProperty[])?.find(
|
if (propertyName === "exitUrl" && typeof propertyValue === "string") {
|
||||||
(property) => property.name === propertyName
|
this.loadNextGameFromExitUrl(propertyValue);
|
||||||
);
|
}
|
||||||
if (property === undefined) {
|
if (layer.properties === undefined) {
|
||||||
layer.properties = [];
|
layer.properties = [];
|
||||||
|
}
|
||||||
|
const property = layer.properties.find((property) => property.name === propertyName);
|
||||||
|
if (property === undefined) {
|
||||||
|
if (propertyValue === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
|
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (propertyValue === undefined) {
|
||||||
|
const index = layer.properties.indexOf(property);
|
||||||
|
layer.properties.splice(index, 1);
|
||||||
|
}
|
||||||
property.value = propertyValue;
|
property.value = propertyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setLayerVisibility(layerName: string, visible: boolean): void {
|
private setLayerVisibility(layerName: string, visible: boolean): void {
|
||||||
const phaserLayer = this.gameMap.findPhaserLayer(layerName);
|
const phaserLayer = this.gameMap.findPhaserLayer(layerName);
|
||||||
if (phaserLayer === undefined) {
|
if (phaserLayer != undefined) {
|
||||||
console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer');
|
phaserLayer.setVisible(visible);
|
||||||
return;
|
phaserLayer.setCollisionByProperty({ collides: true }, visible);
|
||||||
|
} else {
|
||||||
|
const phaserLayers = this.gameMap.findPhaserLayers(layerName + "/");
|
||||||
|
if (phaserLayers === []) {
|
||||||
|
console.warn(
|
||||||
|
'Could not find layer with name that contains "' +
|
||||||
|
layerName +
|
||||||
|
'" when calling WA.hideLayer / WA.showLayer'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < phaserLayers.length; i++) {
|
||||||
|
phaserLayers[i].setVisible(visible);
|
||||||
|
phaserLayers[i].setCollisionByProperty({ collides: true }, visible);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
phaserLayer.setVisible(visible);
|
this.markDirty();
|
||||||
this.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMapDirUrl(): string {
|
private getMapDirUrl(): string {
|
||||||
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
|
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMapExit(exitKey: string) {
|
private async onMapExit(roomUrl: URL) {
|
||||||
if (this.mapTransitioning) return;
|
if (this.mapTransitioning) return;
|
||||||
this.mapTransitioning = true;
|
this.mapTransitioning = true;
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
|
|
||||||
if (!roomId) throw new Error("Could not find the room from its exit key: " + exitKey);
|
let targetRoom: Room;
|
||||||
urlManager.pushStartLayerNameToUrl(hash);
|
try {
|
||||||
|
targetRoom = await Room.createRoom(roomUrl);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
|
||||||
|
this.mapTransitioning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomUrl.hash) {
|
||||||
|
urlManager.pushStartLayerNameToUrl(roomUrl.hash);
|
||||||
|
}
|
||||||
|
|
||||||
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
|
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
|
||||||
menuScene.reset();
|
menuScene.reset();
|
||||||
if (roomId !== this.scene.key) {
|
|
||||||
if (this.scene.get(roomId) === null) {
|
if (!targetRoom.isEqual(this.room)) {
|
||||||
console.error("next room not loaded", exitKey);
|
if (this.scene.get(targetRoom.key) === null) {
|
||||||
|
console.error("next room not loaded", targetRoom.key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cleanupClosingScene();
|
this.cleanupClosingScene();
|
||||||
this.scene.stop();
|
this.scene.stop();
|
||||||
|
this.scene.start(targetRoom.key);
|
||||||
this.scene.remove(this.scene.key);
|
this.scene.remove(this.scene.key);
|
||||||
this.scene.start(roomId);
|
|
||||||
} else {
|
} else {
|
||||||
//if the exit points to the current map, we simply teleport the user back to the startLayer
|
//if the exit points to the current map, we simply teleport the user back to the startLayer
|
||||||
this.initPositionFromLayerName(hash || defaultStartLayerName);
|
this.startPositionCalculator.initPositionFromLayerName(roomUrl.hash, roomUrl.hash);
|
||||||
this.CurrentPlayer.x = this.startX;
|
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
|
||||||
this.CurrentPlayer.y = this.startY;
|
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
|
||||||
setTimeout(() => (this.mapTransitioning = false), 500);
|
setTimeout(() => (this.mapTransitioning = false), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1148,7 +1194,9 @@ ${escapedMessage}
|
|||||||
this.pinchManager?.destroy();
|
this.pinchManager?.destroy();
|
||||||
this.emoteManager.destroy();
|
this.emoteManager.destroy();
|
||||||
this.peerStoreUnsubscribe();
|
this.peerStoreUnsubscribe();
|
||||||
|
this.chatVisibilityUnsubscribe();
|
||||||
this.biggestAvailableAreaStoreUnsubscribe();
|
this.biggestAvailableAreaStoreUnsubscribe();
|
||||||
|
iframeListener.unregisterAnswerer("getState");
|
||||||
|
|
||||||
mediaManager.hideGameOverlay();
|
mediaManager.hideGameOverlay();
|
||||||
|
|
||||||
@ -1170,46 +1218,6 @@ ${escapedMessage}
|
|||||||
this.MapPlayersByKey = new Map<number, RemotePlayer>();
|
this.MapPlayersByKey = new Map<number, RemotePlayer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initStartXAndStartY() {
|
|
||||||
// If there is an init position passed
|
|
||||||
if (this.initPosition !== null) {
|
|
||||||
this.startX = this.initPosition.x;
|
|
||||||
this.startY = this.initPosition.y;
|
|
||||||
} else {
|
|
||||||
// Now, let's find the start layer
|
|
||||||
if (this.startLayerName) {
|
|
||||||
this.initPositionFromLayerName(this.startLayerName);
|
|
||||||
}
|
|
||||||
if (this.startX === undefined) {
|
|
||||||
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
|
|
||||||
this.initPositionFromLayerName(defaultStartLayerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Still no start position? Something is wrong with the map, we need a "start" layer.
|
|
||||||
if (this.startX === undefined) {
|
|
||||||
console.warn(
|
|
||||||
'This map is missing a layer named "start" that contains the available default start positions.'
|
|
||||||
);
|
|
||||||
// Let's start in the middle of the map
|
|
||||||
this.startX = this.mapFile.width * 16;
|
|
||||||
this.startY = this.mapFile.height * 16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initPositionFromLayerName(layerName: string) {
|
|
||||||
for (const layer of this.gameMap.flatLayers) {
|
|
||||||
if (
|
|
||||||
(layerName === layer.name || layer.name.endsWith("/" + layerName)) &&
|
|
||||||
layer.type === "tilelayer" &&
|
|
||||||
(layerName === defaultStartLayerName || this.isStartLayer(layer))
|
|
||||||
) {
|
|
||||||
const startPosition = this.startUser(layer);
|
|
||||||
this.startX = startPosition.x + this.mapFile.tilewidth / 2;
|
|
||||||
this.startY = startPosition.y + this.mapFile.tileheight / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getExitUrl(layer: ITiledMapLayer): string | undefined {
|
private getExitUrl(layer: ITiledMapLayer): string | undefined {
|
||||||
return this.getProperty(layer, "exitUrl") as string | undefined;
|
return this.getProperty(layer, "exitUrl") as string | undefined;
|
||||||
}
|
}
|
||||||
@ -1221,10 +1229,6 @@ ${escapedMessage}
|
|||||||
return this.getProperty(layer, "exitSceneUrl") as string | undefined;
|
return this.getProperty(layer, "exitSceneUrl") as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isStartLayer(layer: ITiledMapLayer): boolean {
|
|
||||||
return this.getProperty(layer, "startLayer") == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getScriptUrls(map: ITiledMap): string[] {
|
private getScriptUrls(map: ITiledMap): string[] {
|
||||||
return (this.getProperties(map, "script") as string[]).map((script) =>
|
return (this.getProperties(map, "script") as string[]).map((script) =>
|
||||||
new URL(script, this.MapUrlFile).toString()
|
new URL(script, this.MapUrlFile).toString()
|
||||||
@ -1255,38 +1259,18 @@ ${escapedMessage}
|
|||||||
.map((property) => property.value);
|
.map((property) => property.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo: push that into the gameManager
|
private loadNextGameFromExitUrl(exitUrl: string): Promise<void> {
|
||||||
private loadNextGame(exitSceneIdentifier: string): Promise<void> {
|
return this.loadNextGame(Room.getRoomPathFromExitUrl(exitUrl, window.location.toString()));
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
|
|
||||||
const room = new Room(roomId);
|
|
||||||
return gameManager.loadMap(room, this.scene).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private startUser(layer: ITiledMapTileLayer): PositionInterface {
|
//todo: push that into the gameManager
|
||||||
const tiles = layer.data;
|
private async loadNextGame(exitRoomPath: URL): Promise<void> {
|
||||||
if (typeof tiles === "string") {
|
try {
|
||||||
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
|
const room = await Room.createRoom(exitRoomPath);
|
||||||
|
return gameManager.loadMap(room, this.scene);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
||||||
}
|
}
|
||||||
const possibleStartPositions: PositionInterface[] = [];
|
|
||||||
tiles.forEach((objectKey: number, key: number) => {
|
|
||||||
if (objectKey === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const y = Math.floor(key / layer.width);
|
|
||||||
const x = key % layer.width;
|
|
||||||
|
|
||||||
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
|
|
||||||
});
|
|
||||||
// Get a value at random amongst allowed values
|
|
||||||
if (possibleStartPositions.length === 0) {
|
|
||||||
console.warn('The start layer "' + layer.name + '" for this map is empty.');
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Choose one of the available start positions at random amongst the list of available start positions.
|
|
||||||
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo: in a dedicated class/function?
|
//todo: in a dedicated class/function?
|
||||||
@ -1321,8 +1305,8 @@ ${escapedMessage}
|
|||||||
try {
|
try {
|
||||||
this.CurrentPlayer = new Player(
|
this.CurrentPlayer = new Player(
|
||||||
this,
|
this,
|
||||||
this.startX,
|
this.startPositionCalculator.startPosition.x,
|
||||||
this.startY,
|
this.startPositionCalculator.startPosition.y,
|
||||||
this.playerName,
|
this.playerName,
|
||||||
texturesPromise,
|
texturesPromise,
|
||||||
PlayerAnimationDirections.Down,
|
PlayerAnimationDirections.Down,
|
||||||
@ -1729,7 +1713,7 @@ ${escapedMessage}
|
|||||||
this.scene.start(ErrorSceneName, {
|
this.scene.start(ErrorSceneName, {
|
||||||
title: "Banned",
|
title: "Banned",
|
||||||
subTitle: "You were banned from WorkAdventure",
|
subTitle: "You were banned from WorkAdventure",
|
||||||
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com",
|
message: "If you want more information, you may contact us at: hello@workadventu.re",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1744,14 +1728,14 @@ ${escapedMessage}
|
|||||||
this.scene.start(ErrorSceneName, {
|
this.scene.start(ErrorSceneName, {
|
||||||
title: "Connection rejected",
|
title: "Connection rejected",
|
||||||
subTitle: "The world you are trying to join is full. Try again later.",
|
subTitle: "The world you are trying to join is full. Try again later.",
|
||||||
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com",
|
message: "If you want more information, you may contact us at: hello@workadventu.re",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.scene.start(ErrorSceneName, {
|
this.scene.start(ErrorSceneName, {
|
||||||
title: "Connection rejected",
|
title: "Connection rejected",
|
||||||
subTitle: "You cannot join the World. Try again later. \n\r \n\r Error: " + message + ".",
|
subTitle: "You cannot join the World. Try again later. \n\r \n\r Error: " + message + ".",
|
||||||
message:
|
message:
|
||||||
"If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com",
|
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
front/src/Phaser/Game/PlayerInterface.ts
Normal file
11
front/src/Phaser/Game/PlayerInterface.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
|
||||||
|
|
||||||
|
export interface PlayerInterface {
|
||||||
|
userId: number;
|
||||||
|
name: string;
|
||||||
|
characterLayers: BodyResourceDescriptionInterface[];
|
||||||
|
visitCardUrl: string | null;
|
||||||
|
companion: string | null;
|
||||||
|
userUuid: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
|
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
|
||||||
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||||
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
|
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||||
|
|
||||||
export class PlayerMovement {
|
export class PlayerMovement {
|
||||||
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) {
|
public constructor(
|
||||||
}
|
private startPosition: PositionInterface,
|
||||||
|
private startTick: number,
|
||||||
|
private endPosition: HasPlayerMovedEvent,
|
||||||
|
private endTick: number
|
||||||
|
) {}
|
||||||
|
|
||||||
public isOutdated(tick: number): boolean {
|
public isOutdated(tick: number): boolean {
|
||||||
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
|
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
|
||||||
@ -24,14 +28,18 @@ export class PlayerMovement {
|
|||||||
return this.endPosition;
|
return this.endPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x;
|
const x =
|
||||||
const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y;
|
(this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
|
||||||
|
this.startPosition.x;
|
||||||
|
const y =
|
||||||
|
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
|
||||||
|
this.startPosition.y;
|
||||||
//console.log('Computed position ', x, y)
|
//console.log('Computed position ', x, y)
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
direction: this.endPosition.direction,
|
direction: this.endPosition.direction,
|
||||||
moving: true
|
moving: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* This class is in charge of computing the position of all players.
|
* This class is in charge of computing the position of all players.
|
||||||
* Player movement is delayed by 200ms so position depends on ticks.
|
* Player movement is delayed by 200ms so position depends on ticks.
|
||||||
*/
|
*/
|
||||||
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
|
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||||
import type { PlayerMovement } from "./PlayerMovement";
|
import type { PlayerMovement } from "./PlayerMovement";
|
||||||
|
|
||||||
export class PlayersPositionInterpolator {
|
export class PlayersPositionInterpolator {
|
||||||
@ -24,7 +24,7 @@ export class PlayersPositionInterpolator {
|
|||||||
this.playerMovements.delete(userId);
|
this.playerMovements.delete(userId);
|
||||||
}
|
}
|
||||||
//console.log("moving")
|
//console.log("moving")
|
||||||
positions.set(userId, playerMovement.getPosition(tick))
|
positions.set(userId, playerMovement.getPosition(tick));
|
||||||
});
|
});
|
||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
127
front/src/Phaser/Game/StartPositionCalculator.ts
Normal file
127
front/src/Phaser/Game/StartPositionCalculator.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||||
|
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
|
||||||
|
import type { GameMap } from "./GameMap";
|
||||||
|
|
||||||
|
const defaultStartLayerName = "start";
|
||||||
|
|
||||||
|
export class StartPositionCalculator {
|
||||||
|
public startPosition!: PositionInterface;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly gameMap: GameMap,
|
||||||
|
private readonly mapFile: ITiledMap,
|
||||||
|
private readonly initPosition: PositionInterface | null,
|
||||||
|
public readonly startLayerName: string | null
|
||||||
|
) {
|
||||||
|
this.initStartXAndStartY();
|
||||||
|
}
|
||||||
|
private initStartXAndStartY() {
|
||||||
|
// If there is an init position passed
|
||||||
|
if (this.initPosition !== null) {
|
||||||
|
this.startPosition = this.initPosition;
|
||||||
|
} else {
|
||||||
|
// Now, let's find the start layer
|
||||||
|
if (this.startLayerName) {
|
||||||
|
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
|
||||||
|
}
|
||||||
|
if (this.startPosition === undefined) {
|
||||||
|
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
|
||||||
|
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Still no start position? Something is wrong with the map, we need a "start" layer.
|
||||||
|
if (this.startPosition === undefined) {
|
||||||
|
console.warn(
|
||||||
|
'This map is missing a layer named "start" that contains the available default start positions.'
|
||||||
|
);
|
||||||
|
// Let's start in the middle of the map
|
||||||
|
this.startPosition = {
|
||||||
|
x: this.mapFile.width * 16,
|
||||||
|
y: this.mapFile.height * 16,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||||
|
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
|
||||||
|
*/
|
||||||
|
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
|
||||||
|
if (!selectedOrDefaultLayer) {
|
||||||
|
selectedOrDefaultLayer = defaultStartLayerName;
|
||||||
|
}
|
||||||
|
for (const layer of this.gameMap.flatLayers) {
|
||||||
|
if (
|
||||||
|
(selectedOrDefaultLayer === layer.name || layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
|
||||||
|
layer.type === "tilelayer" &&
|
||||||
|
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))
|
||||||
|
) {
|
||||||
|
const startPosition = this.startUser(layer, selectedLayer);
|
||||||
|
this.startPosition = {
|
||||||
|
x: startPosition.x + this.mapFile.tilewidth / 2,
|
||||||
|
y: startPosition.y + this.mapFile.tileheight / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isStartLayer(layer: ITiledMapLayer): boolean {
|
||||||
|
return this.getProperty(layer, "startLayer") == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||||
|
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
|
||||||
|
*/
|
||||||
|
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
|
||||||
|
const tiles = selectedOrDefaultLayer.data;
|
||||||
|
if (typeof tiles === "string") {
|
||||||
|
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
|
||||||
|
}
|
||||||
|
const possibleStartPositions: PositionInterface[] = [];
|
||||||
|
tiles.forEach((objectKey: number, key: number) => {
|
||||||
|
if (objectKey === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const y = Math.floor(key / selectedOrDefaultLayer.width);
|
||||||
|
const x = key % selectedOrDefaultLayer.width;
|
||||||
|
|
||||||
|
if (selectedLayer && this.gameMap.hasStartTile) {
|
||||||
|
const properties = this.gameMap.getPropertiesForIndex(objectKey);
|
||||||
|
if (
|
||||||
|
!properties.length ||
|
||||||
|
!properties.some((property) => property.name == "start" && property.value == selectedLayer)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
|
||||||
|
});
|
||||||
|
// Get a value at random amongst allowed values
|
||||||
|
if (possibleStartPositions.length === 0) {
|
||||||
|
console.warn('The start layer "' + selectedOrDefaultLayer.name + '" for this map is empty.');
|
||||||
|
return {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Choose one of the available start positions at random amongst the list of available start positions.
|
||||||
|
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
||||||
|
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
||||||
|
if (!properties) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const obj = properties.find(
|
||||||
|
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (obj === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return obj.value;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import {gameManager} from "../Game/GameManager";
|
import { gameManager } from "../Game/GameManager";
|
||||||
import {Scene} from "phaser";
|
import { Scene } from "phaser";
|
||||||
import {ErrorScene} from "../Reconnecting/ErrorScene";
|
import { ErrorScene } from "../Reconnecting/ErrorScene";
|
||||||
import {WAError} from "../Reconnecting/WAError";
|
import { WAError } from "../Reconnecting/WAError";
|
||||||
import {waScaleManager} from "../Services/WaScaleManager";
|
import { waScaleManager } from "../Services/WaScaleManager";
|
||||||
|
|
||||||
export const EntrySceneName = "EntryScene";
|
export const EntrySceneName = "EntryScene";
|
||||||
|
|
||||||
@ -13,26 +13,32 @@ export const EntrySceneName = "EntryScene";
|
|||||||
export class EntryScene extends Scene {
|
export class EntryScene extends Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
key: EntrySceneName
|
key: EntrySceneName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
gameManager
|
||||||
gameManager.init(this.scene).then((nextSceneName) => {
|
.init(this.scene)
|
||||||
// Let's rescale before starting the game
|
.then((nextSceneName) => {
|
||||||
// We can do it at this stage.
|
// Let's rescale before starting the game
|
||||||
waScaleManager.applyNewSize();
|
// We can do it at this stage.
|
||||||
this.scene.start(nextSceneName);
|
waScaleManager.applyNewSize();
|
||||||
}).catch((err) => {
|
this.scene.start(nextSceneName);
|
||||||
if (err.response && err.response.status == 404) {
|
})
|
||||||
ErrorScene.showError(new WAError(
|
.catch((err) => {
|
||||||
'Access link incorrect',
|
if (err.response && err.response.status == 404) {
|
||||||
'Could not find map. Please check your access link.',
|
ErrorScene.showError(
|
||||||
'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com'), this.scene);
|
new WAError(
|
||||||
} else {
|
"Access link incorrect",
|
||||||
ErrorScene.showError(err, this.scene);
|
"Could not find map. Please check your access link.",
|
||||||
}
|
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
|
||||||
});
|
),
|
||||||
|
this.scene
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ErrorScene.showError(err, this.scene);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import {gameManager} from "../Game/GameManager";
|
import { gameManager } from "../Game/GameManager";
|
||||||
import Rectangle = Phaser.GameObjects.Rectangle;
|
import Rectangle = Phaser.GameObjects.Rectangle;
|
||||||
import {EnableCameraSceneName} from "./EnableCameraScene";
|
import { EnableCameraSceneName } from "./EnableCameraScene";
|
||||||
import {CustomizeSceneName} from "./CustomizeScene";
|
import { CustomizeSceneName } from "./CustomizeScene";
|
||||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager";
|
import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
|
||||||
import {addLoader} from "../Components/Loader";
|
import { addLoader } from "../Components/Loader";
|
||||||
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
|
||||||
import {AbstractCharacterScene} from "./AbstractCharacterScene";
|
import { AbstractCharacterScene } from "./AbstractCharacterScene";
|
||||||
import {areCharacterLayersValid} from "../../Connexion/LocalUser";
|
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
|
||||||
import {touchScreenManager} from "../../Touch/TouchScreenManager";
|
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||||
import {PinchManager} from "../UserInput/PinchManager";
|
import { PinchManager } from "../UserInput/PinchManager";
|
||||||
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
|
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
|
||||||
import {waScaleManager} from "../Services/WaScaleManager";
|
import { waScaleManager } from "../Services/WaScaleManager";
|
||||||
import {isMobile} from "../../Enum/EnvironmentVariable";
|
import { isMobile } from "../../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
//todo: put this constants in a dedicated file
|
//todo: put this constants in a dedicated file
|
||||||
export const SelectCharacterSceneName = "SelectCharacterScene";
|
export const SelectCharacterSceneName = "SelectCharacterScene";
|
||||||
|
|
||||||
export class SelectCharacterScene extends AbstractCharacterScene {
|
export class SelectCharacterScene extends AbstractCharacterScene {
|
||||||
protected readonly nbCharactersPerRow = 6;
|
protected readonly nbCharactersPerRow = 6;
|
||||||
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite|null; // null if we are selecting the "customize" option
|
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite | null; // null if we are selecting the "customize" option
|
||||||
protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
|
protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
|
||||||
protected playerModels!: BodyResourceDescriptionInterface[];
|
protected playerModels!: BodyResourceDescriptionInterface[];
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
|
|
||||||
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
|
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
|
||||||
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
||||||
this.playerModels.push(bodyResourceDescription);
|
this.playerModels.push(bodyResourceDescription);
|
||||||
@ -54,7 +53,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
|
|
||||||
create() {
|
create() {
|
||||||
selectCharacterSceneVisibleStore.set(true);
|
selectCharacterSceneVisibleStore.set(true);
|
||||||
this.events.addListener('wake', () => {
|
this.events.addListener("wake", () => {
|
||||||
waScaleManager.saveZoom();
|
waScaleManager.saveZoom();
|
||||||
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
|
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
|
||||||
selectCharacterSceneVisibleStore.set(true);
|
selectCharacterSceneVisibleStore.set(true);
|
||||||
@ -68,26 +67,26 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
|
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
|
||||||
|
|
||||||
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
|
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
|
||||||
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF);
|
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);
|
||||||
this.selectedRectangle.setDepth(2);
|
this.selectedRectangle.setDepth(2);
|
||||||
|
|
||||||
/*create user*/
|
/*create user*/
|
||||||
this.createCurrentPlayer();
|
this.createCurrentPlayer();
|
||||||
|
|
||||||
this.input.keyboard.on('keyup-ENTER', () => {
|
this.input.keyboard.on("keyup-ENTER", () => {
|
||||||
return this.nextSceneToCameraScene();
|
return this.nextSceneToCameraScene();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.keyboard.on('keydown-RIGHT', () => {
|
this.input.keyboard.on("keydown-RIGHT", () => {
|
||||||
this.moveToRight();
|
this.moveToRight();
|
||||||
});
|
});
|
||||||
this.input.keyboard.on('keydown-LEFT', () => {
|
this.input.keyboard.on("keydown-LEFT", () => {
|
||||||
this.moveToLeft();
|
this.moveToLeft();
|
||||||
});
|
});
|
||||||
this.input.keyboard.on('keydown-UP', () => {
|
this.input.keyboard.on("keydown-UP", () => {
|
||||||
this.moveToUp();
|
this.moveToUp();
|
||||||
});
|
});
|
||||||
this.input.keyboard.on('keydown-DOWN', () => {
|
this.input.keyboard.on("keydown-DOWN", () => {
|
||||||
this.moveToDown();
|
this.moveToDown();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -96,7 +95,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
|
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!this.selectedPlayer){
|
if (!this.selectedPlayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.scene.stop(SelectCharacterSceneName);
|
this.scene.stop(SelectCharacterSceneName);
|
||||||
@ -105,7 +104,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
gameManager.tryResumingGame(this, EnableCameraSceneName);
|
gameManager.tryResumingGame(this, EnableCameraSceneName);
|
||||||
this.players = [];
|
this.players = [];
|
||||||
selectCharacterSceneVisibleStore.set(false);
|
selectCharacterSceneVisibleStore.set(false);
|
||||||
this.events.removeListener('wake');
|
this.events.removeListener("wake");
|
||||||
}
|
}
|
||||||
|
|
||||||
public nextSceneToCustomizeScene(): void {
|
public nextSceneToCustomizeScene(): void {
|
||||||
@ -119,11 +118,11 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createCurrentPlayer(): void {
|
createCurrentPlayer(): void {
|
||||||
for (let i = 0; i <this.playerModels.length; i++) {
|
for (let i = 0; i < this.playerModels.length; i++) {
|
||||||
const playerResource = this.playerModels[i];
|
const playerResource = this.playerModels[i];
|
||||||
|
|
||||||
//check already exist texture
|
//check already exist texture
|
||||||
if(this.players.find((c) => c.texture.key === playerResource.name)){
|
if (this.players.find((c) => c.texture.key === playerResource.name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,9 +131,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
this.setUpPlayer(player, i);
|
this.setUpPlayer(player, i);
|
||||||
this.anims.create({
|
this.anims.create({
|
||||||
key: playerResource.name,
|
key: playerResource.name,
|
||||||
frames: this.anims.generateFrameNumbers(playerResource.name, {start: 0, end: 11}),
|
frames: this.anims.generateFrameNumbers(playerResource.name, { start: 0, end: 11 }),
|
||||||
frameRate: 8,
|
frameRate: 8,
|
||||||
repeat: -1
|
repeat: -1,
|
||||||
});
|
});
|
||||||
player.setInteractive().on("pointerdown", () => {
|
player.setInteractive().on("pointerdown", () => {
|
||||||
if (this.pointerClicked) {
|
if (this.pointerClicked) {
|
||||||
@ -153,77 +152,79 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
});
|
});
|
||||||
this.players.push(player);
|
this.players.push(player);
|
||||||
}
|
}
|
||||||
|
if (this.currentSelectUser >= this.players.length) {
|
||||||
|
this.currentSelectUser = 0;
|
||||||
|
}
|
||||||
this.selectedPlayer = this.players[this.currentSelectUser];
|
this.selectedPlayer = this.players[this.currentSelectUser];
|
||||||
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
|
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected moveUser(){
|
protected moveUser() {
|
||||||
for(let i = 0; i < this.players.length; i++){
|
for (let i = 0; i < this.players.length; i++) {
|
||||||
const player = this.players[i];
|
const player = this.players[i];
|
||||||
this.setUpPlayer(player, i);
|
this.setUpPlayer(player, i);
|
||||||
}
|
}
|
||||||
this.updateSelectedPlayer();
|
this.updateSelectedPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveToLeft(){
|
public moveToLeft() {
|
||||||
if(this.currentSelectUser === 0){
|
if (this.currentSelectUser === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentSelectUser -= 1;
|
this.currentSelectUser -= 1;
|
||||||
this.moveUser();
|
this.moveUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveToRight(){
|
public moveToRight() {
|
||||||
if(this.currentSelectUser === (this.players.length - 1)){
|
if (this.currentSelectUser === this.players.length - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentSelectUser += 1;
|
this.currentSelectUser += 1;
|
||||||
this.moveUser();
|
this.moveUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected moveToUp(){
|
protected moveToUp() {
|
||||||
if(this.currentSelectUser < this.nbCharactersPerRow){
|
if (this.currentSelectUser < this.nbCharactersPerRow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentSelectUser -= this.nbCharactersPerRow;
|
this.currentSelectUser -= this.nbCharactersPerRow;
|
||||||
this.moveUser();
|
this.moveUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected moveToDown(){
|
protected moveToDown() {
|
||||||
if((this.currentSelectUser + this.nbCharactersPerRow) > (this.players.length - 1)){
|
if (this.currentSelectUser + this.nbCharactersPerRow > this.players.length - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentSelectUser += this.nbCharactersPerRow;
|
this.currentSelectUser += this.nbCharactersPerRow;
|
||||||
this.moveUser();
|
this.moveUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected defineSetupPlayer(num: number){
|
protected defineSetupPlayer(num: number) {
|
||||||
const deltaX = 32;
|
const deltaX = 32;
|
||||||
const deltaY = 32;
|
const deltaY = 32;
|
||||||
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
|
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
|
||||||
|
|
||||||
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users
|
playerX = playerX - deltaX * 2.5 + deltaX * (num % this.nbCharactersPerRow); // calcul position on line users
|
||||||
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users
|
playerY = playerY - deltaY * 2 + deltaY * Math.floor(num / this.nbCharactersPerRow); // calcul position on column users
|
||||||
|
|
||||||
const playerVisible = true;
|
const playerVisible = true;
|
||||||
const playerScale = 1;
|
const playerScale = 1;
|
||||||
const playerOpacity = 1;
|
const playerOpacity = 1;
|
||||||
|
|
||||||
// if selected
|
// if selected
|
||||||
if( num === this.currentSelectUser ){
|
if (num === this.currentSelectUser) {
|
||||||
this.selectedRectangle.setX(playerX);
|
this.selectedRectangle.setX(playerX);
|
||||||
this.selectedRectangle.setY(playerY);
|
this.selectedRectangle.setY(playerY);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {playerX, playerY, playerScale, playerOpacity, playerVisible}
|
return { playerX, playerY, playerScale, playerOpacity, playerVisible };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){
|
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number) {
|
||||||
|
const { playerX, playerY, playerScale, playerOpacity, playerVisible } = this.defineSetupPlayer(num);
|
||||||
const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
|
|
||||||
player.setBounce(0.2);
|
player.setBounce(0.2);
|
||||||
player.setCollideWorldBounds(false);
|
player.setCollideWorldBounds(false);
|
||||||
player.setVisible( playerVisible );
|
player.setVisible(playerVisible);
|
||||||
player.setScale(playerScale, playerScale);
|
player.setScale(playerScale, playerScale);
|
||||||
player.setAlpha(playerOpacity);
|
player.setAlpha(playerOpacity);
|
||||||
player.setX(playerX);
|
player.setX(playerX);
|
||||||
@ -234,10 +235,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
* Returns pixel position by on column and row number
|
* Returns pixel position by on column and row number
|
||||||
*/
|
*/
|
||||||
protected getCharacterPosition(): [number, number] {
|
protected getCharacterPosition(): [number, number] {
|
||||||
return [
|
return [this.game.renderer.width / 2, this.game.renderer.height / 2.5];
|
||||||
this.game.renderer.width / 2,
|
|
||||||
this.game.renderer.height / 2.5
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateSelectedPlayer(): void {
|
protected updateSelectedPlayer(): void {
|
||||||
@ -256,7 +254,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
this.pointerClicked = false;
|
this.pointerClicked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.lazyloadingAttempt){
|
if (this.lazyloadingAttempt) {
|
||||||
//re-render players list
|
//re-render players list
|
||||||
this.createCurrentPlayer();
|
this.createCurrentPlayer();
|
||||||
this.moveUser();
|
this.moveUser();
|
||||||
|
@ -36,7 +36,7 @@ export interface ITiledMap {
|
|||||||
export interface ITiledMapLayerProperty {
|
export interface ITiledMapLayerProperty {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: string|boolean|number|undefined;
|
value: string | boolean | number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*export interface ITiledMapLayerBooleanProperty {
|
/*export interface ITiledMapLayerBooleanProperty {
|
||||||
@ -48,7 +48,7 @@ export interface ITiledMapLayerProperty {
|
|||||||
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
|
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
|
||||||
|
|
||||||
export interface ITiledMapGroupLayer {
|
export interface ITiledMapGroupLayer {
|
||||||
id?: number,
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
properties?: ITiledMapLayerProperty[];
|
properties?: ITiledMapLayerProperty[];
|
||||||
@ -64,8 +64,8 @@ export interface ITiledMapGroupLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledMapTileLayer {
|
export interface ITiledMapTileLayer {
|
||||||
id?: number,
|
id?: number;
|
||||||
data: number[]|string;
|
data: number[] | string;
|
||||||
height: number;
|
height: number;
|
||||||
name: string;
|
name: string;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
@ -87,7 +87,7 @@ export interface ITiledMapTileLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledMapObjectLayer {
|
export interface ITiledMapObjectLayer {
|
||||||
id?: number,
|
id?: number;
|
||||||
height: number;
|
height: number;
|
||||||
name: string;
|
name: string;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
@ -117,7 +117,7 @@ export interface ITiledMapObject {
|
|||||||
gid: number;
|
gid: number;
|
||||||
height: number;
|
height: number;
|
||||||
name: string;
|
name: string;
|
||||||
properties: {[key: string]: string};
|
properties: { [key: string]: string };
|
||||||
rotation: number;
|
rotation: number;
|
||||||
type: string;
|
type: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -133,26 +133,26 @@ export interface ITiledMapObject {
|
|||||||
/**
|
/**
|
||||||
* Polygon points
|
* Polygon points
|
||||||
*/
|
*/
|
||||||
polygon: {x: number, y: number}[];
|
polygon: { x: number; y: number }[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Polyline points
|
* Polyline points
|
||||||
*/
|
*/
|
||||||
polyline: {x: number, y: number}[];
|
polyline: { x: number; y: number }[];
|
||||||
|
|
||||||
text?: ITiledText
|
text?: ITiledText;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledText {
|
export interface ITiledText {
|
||||||
text: string,
|
text: string;
|
||||||
wrap?: boolean,
|
wrap?: boolean;
|
||||||
fontfamily?: string,
|
fontfamily?: string;
|
||||||
pixelsize?: number,
|
pixelsize?: number;
|
||||||
color?: string,
|
color?: string;
|
||||||
underline?: boolean,
|
underline?: boolean;
|
||||||
italic?: boolean,
|
italic?: boolean;
|
||||||
strikeout?: boolean,
|
strikeout?: boolean;
|
||||||
halign?: "center"|"right"|"justify"|"left"
|
halign?: "center" | "right" | "justify" | "left";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledTileSet {
|
export interface ITiledTileSet {
|
||||||
@ -163,7 +163,7 @@ export interface ITiledTileSet {
|
|||||||
imagewidth: number;
|
imagewidth: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
name: string;
|
name: string;
|
||||||
properties: {[key: string]: string};
|
properties: { [key: string]: string };
|
||||||
spacing: number;
|
spacing: number;
|
||||||
tilecount: number;
|
tilecount: number;
|
||||||
tileheight: number;
|
tileheight: number;
|
||||||
@ -179,10 +179,10 @@ export interface ITiledTileSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITile {
|
export interface ITile {
|
||||||
id: number,
|
id: number;
|
||||||
type?: string
|
type?: string;
|
||||||
|
|
||||||
properties?: Array<ITiledMapLayerProperty>
|
properties?: Array<ITiledMapLayerProperty>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledMapTerrain {
|
export interface ITiledMapTerrain {
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
|
import type { ITiledMap, ITiledMapLayer } from "./ITiledMap";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flatten the grouped layers
|
* Flatten the grouped layers
|
||||||
*/
|
*/
|
||||||
export function flattenGroupLayersMap(map: ITiledMap) {
|
export function flattenGroupLayersMap(map: ITiledMap) {
|
||||||
const flatLayers: ITiledMapLayer[] = [];
|
const flatLayers: ITiledMapLayer[] = [];
|
||||||
flattenGroupLayers(map.layers, '', flatLayers);
|
flattenGroupLayers(map.layers, "", flatLayers);
|
||||||
return flatLayers;
|
return flatLayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) {
|
function flattenGroupLayers(layers: ITiledMapLayer[], prefix: string, flatLayers: ITiledMapLayer[]) {
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
if (layer.type === 'group') {
|
if (layer.type === "group") {
|
||||||
flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers);
|
flattenGroupLayers(layer.layers, prefix + layer.name + "/", flatLayers);
|
||||||
} else {
|
} else {
|
||||||
layer.name = prefix+layer.name
|
layer.name = prefix + layer.name;
|
||||||
flatLayers.push(layer);
|
flatLayers.push(layer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
import {LoginScene, LoginSceneName} from "../Login/LoginScene";
|
import { LoginScene, LoginSceneName } from "../Login/LoginScene";
|
||||||
import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene";
|
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||||
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene";
|
import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene";
|
||||||
import {gameManager} from "../Game/GameManager";
|
import { gameManager } from "../Game/GameManager";
|
||||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
|
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
|
||||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
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 { menuIconVisible } from "../../Stores/MenuStore";
|
||||||
import {videoConstraintStore} from "../../Stores/MediaStore";
|
import { videoConstraintStore } from "../../Stores/MediaStore";
|
||||||
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
|
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
|
||||||
import { HtmlUtils } from '../../WebRtc/HtmlUtils';
|
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||||
import { iframeListener } from '../../Api/IframeListener';
|
import { iframeListener } from "../../Api/IframeListener";
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from "rxjs";
|
||||||
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent";
|
import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterEvent";
|
||||||
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
|
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
|
||||||
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
|
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||||
import {get} from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||||
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
export const MenuSceneName = 'MenuScene';
|
export const MenuSceneName = "MenuScene";
|
||||||
const gameMenuKey = 'gameMenu';
|
const gameMenuKey = "gameMenu";
|
||||||
const gameMenuIconKey = 'gameMenuIcon';
|
const gameMenuIconKey = "gameMenuIcon";
|
||||||
const gameSettingsMenuKey = 'gameSettingsMenu';
|
const gameSettingsMenuKey = "gameSettingsMenu";
|
||||||
const gameShare = 'gameShare';
|
const gameShare = "gameShare";
|
||||||
|
|
||||||
const closedSideMenuX = -1000;
|
const closedSideMenuX = -1000;
|
||||||
const openedSideMenuX = 0;
|
const openedSideMenuX = 0;
|
||||||
@ -44,45 +47,49 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
private menuButton!: Phaser.GameObjects.DOMElement;
|
private menuButton!: Phaser.GameObjects.DOMElement;
|
||||||
private warningContainer: WarningContainer | null = null;
|
private warningContainer: WarningContainer | null = null;
|
||||||
private warningContainerTimeout: NodeJS.Timeout | null = null;
|
private warningContainerTimeout: NodeJS.Timeout | null = null;
|
||||||
private subscriptions = new Subscription()
|
private subscriptions = new Subscription();
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: MenuSceneName });
|
super({ key: MenuSceneName });
|
||||||
|
|
||||||
this.gameQualityValue = localUserStore.getGameQualityValue();
|
this.gameQualityValue = localUserStore.getGameQualityValue();
|
||||||
this.videoQualityValue = localUserStore.getVideoQualityValue();
|
this.videoQualityValue = localUserStore.getVideoQualityValue();
|
||||||
|
|
||||||
this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => {
|
this.subscriptions.add(
|
||||||
this.addMenuOption(menuCommand);
|
registerMenuCommandStream.subscribe((menuCommand) => {
|
||||||
}))
|
this.addMenuOption(menuCommand);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => {
|
this.subscriptions.add(
|
||||||
this.destroyMenu(menuCommand);
|
iframeListener.unregisterMenuCommandStream.subscribe((menuCommand) => {
|
||||||
}))
|
this.destroyMenu(menuCommand);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
|
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
|
||||||
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
|
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
|
||||||
addedMenuItems[index].remove()
|
addedMenuItems[index].remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addMenuOption(menuText: string) {
|
public addMenuOption(menuText: string) {
|
||||||
const wrappingSection = document.createElement("section")
|
const wrappingSection = document.createElement("section");
|
||||||
const escapedHtml = HtmlUtils.escapeHtml(menuText);
|
const escapedHtml = HtmlUtils.escapeHtml(menuText);
|
||||||
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`
|
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`;
|
||||||
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
|
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
|
||||||
if (menuItemContainer) {
|
if (menuItemContainer) {
|
||||||
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove()
|
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove();
|
||||||
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"))
|
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.html(gameMenuKey, 'resources/html/gameMenu.html');
|
this.load.html(gameMenuKey, "resources/html/gameMenu.html");
|
||||||
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
|
this.load.html(gameMenuIconKey, "resources/html/gameMenuIcon.html");
|
||||||
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
|
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
|
||||||
this.load.html(gameShare, 'resources/html/gameShare.html');
|
this.load.html(gameShare, "resources/html/gameShare.html");
|
||||||
this.load.html(gameReportKey, gameReportRessource);
|
this.load.html(gameReportKey, gameReportRessource);
|
||||||
this.load.html(warningContainerKey, warningContainerHtml);
|
this.load.html(warningContainerKey, warningContainerHtml);
|
||||||
}
|
}
|
||||||
@ -91,46 +98,59 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
menuIconVisible.set(true);
|
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");
|
||||||
|
|
||||||
const middleX = (window.innerWidth / 3) - 298;
|
if (mediaManager.hasNotification()) {
|
||||||
|
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const middleX = window.innerWidth / 3 - 298;
|
||||||
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
|
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
|
||||||
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality');
|
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
|
||||||
|
|
||||||
|
|
||||||
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
|
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
|
||||||
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
|
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
|
||||||
this.gameShareElement.addListener('click');
|
this.gameShareElement.addListener("click");
|
||||||
this.gameShareElement.on('click', (event: MouseEvent) => {
|
this.gameShareElement.on("click", (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') {
|
if ((event?.target as HTMLInputElement).id === "gameShareFormSubmit") {
|
||||||
this.copyLink();
|
this.copyLink();
|
||||||
} else if ((event?.target as HTMLInputElement).id === 'gameShareFormCancel') {
|
} else if ((event?.target as HTMLInputElement).id === "gameShareFormCancel") {
|
||||||
this.closeGameShare();
|
this.closeGameShare();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
|
this.gameReportElement = new ReportMenu(
|
||||||
|
this,
|
||||||
|
connectionManager.getConnexionType === GameConnexionTypes.anonymous
|
||||||
|
);
|
||||||
showReportScreenStore.subscribe((user) => {
|
showReportScreenStore.subscribe((user) => {
|
||||||
if (user !== null) {
|
if (user !== null) {
|
||||||
this.closeAll();
|
this.closeAll();
|
||||||
this.gameReportElement.open(user.userId, user.userName);
|
const uuid = playersStore.getPlayerById(user.userId)?.userUuid;
|
||||||
|
if (uuid === undefined) {
|
||||||
|
throw new Error("Could not find UUID for user with ID " + user.userId);
|
||||||
|
}
|
||||||
|
this.gameReportElement.open(uuid, user.userName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.keyboard.on('keyup-TAB', () => {
|
this.input.keyboard.on("keyup-TAB", () => {
|
||||||
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
||||||
});
|
});
|
||||||
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
|
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
|
||||||
this.menuButton.addListener('click');
|
this.menuButton.addListener("click");
|
||||||
this.menuButton.on('click', () => {
|
this.menuButton.on("click", () => {
|
||||||
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.menuElement.addListener('click');
|
this.menuElement.addListener("click");
|
||||||
this.menuElement.on('click', this.onMenuClick.bind(this));
|
this.menuElement.on("click", this.onMenuClick.bind(this));
|
||||||
|
|
||||||
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
|
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
|
||||||
|
chatVisibilityStore.subscribe((v) => {
|
||||||
|
this.menuButton.setVisible(!v);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo put this method in a parent menuElement class
|
//todo put this method in a parent menuElement class
|
||||||
@ -145,7 +165,7 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
public revealMenuIcon(): void {
|
public revealMenuIcon(): void {
|
||||||
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
|
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
|
||||||
try {
|
try {
|
||||||
(this.menuButton.getChildByID('menuIcon') as HTMLElement).hidden = false;
|
(this.menuButton.getChildByID("menuIcon") as HTMLElement).hidden = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
@ -155,22 +175,22 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
if (this.sideMenuOpened) return;
|
if (this.sideMenuOpened) return;
|
||||||
this.closeAll();
|
this.closeAll();
|
||||||
this.sideMenuOpened = true;
|
this.sideMenuOpened = true;
|
||||||
this.menuButton.getChildByID('openMenuButton').innerHTML = 'X';
|
this.menuButton.getChildByID("openMenuButton").innerHTML = "X";
|
||||||
const connection = gameManager.getCurrentGameScene(this).connection;
|
const connection = gameManager.getCurrentGameScene(this).connection;
|
||||||
if (connection && connection.isAdmin()) {
|
if (connection && connection.isAdmin()) {
|
||||||
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement;
|
const adminSection = this.menuElement.getChildByID("adminConsoleSection") as HTMLElement;
|
||||||
adminSection.hidden = false;
|
adminSection.hidden = false;
|
||||||
}
|
}
|
||||||
//TODO bind with future metadata of card
|
//TODO bind with future metadata of card
|
||||||
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
|
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
|
||||||
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement;
|
const adminSection = this.menuElement.getChildByID("socialLinks") as HTMLElement;
|
||||||
adminSection.hidden = false;
|
adminSection.hidden = false;
|
||||||
//}
|
//}
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: this.menuElement,
|
targets: this.menuElement,
|
||||||
x: openedSideMenuX,
|
x: openedSideMenuX,
|
||||||
duration: 500,
|
duration: 500,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,23 +203,22 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
this.warningContainerTimeout = setTimeout(() => {
|
this.warningContainerTimeout = setTimeout(() => {
|
||||||
this.warningContainer?.destroy();
|
this.warningContainer?.destroy();
|
||||||
this.warningContainer = null
|
this.warningContainer = null;
|
||||||
this.warningContainerTimeout = null
|
this.warningContainerTimeout = null;
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeSideMenu(): void {
|
private closeSideMenu(): void {
|
||||||
if (!this.sideMenuOpened) return;
|
if (!this.sideMenuOpened) return;
|
||||||
this.sideMenuOpened = false;
|
this.sideMenuOpened = false;
|
||||||
this.closeAll();
|
this.closeAll();
|
||||||
this.menuButton.getChildByID('openMenuButton').innerHTML = `<img src="/static/images/menu.svg">`;
|
this.menuButton.getChildByID("openMenuButton").innerHTML = `<img src="/static/images/menu.svg">`;
|
||||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: this.menuElement,
|
targets: this.menuElement,
|
||||||
x: closedSideMenuX,
|
x: closedSideMenuX,
|
||||||
duration: 500,
|
duration: 500,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,19 +232,23 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.settingsMenuOpened = true;
|
this.settingsMenuOpened = true;
|
||||||
|
|
||||||
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
|
const gameQualitySelect = this.gameQualityMenuElement.getChildByID("select-game-quality") as HTMLInputElement;
|
||||||
gameQualitySelect.value = '' + this.gameQualityValue;
|
gameQualitySelect.value = "" + this.gameQualityValue;
|
||||||
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
|
const videoQualitySelect = this.gameQualityMenuElement.getChildByID("select-video-quality") as HTMLInputElement;
|
||||||
videoQualitySelect.value = '' + this.videoQualityValue;
|
videoQualitySelect.value = "" + this.videoQualityValue;
|
||||||
|
|
||||||
this.gameQualityMenuElement.addListener('click');
|
this.gameQualityMenuElement.addListener("click");
|
||||||
this.gameQualityMenuElement.on('click', (event: MouseEvent) => {
|
this.gameQualityMenuElement.on("click", (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') {
|
if ((event?.target as HTMLInputElement).id === "gameQualityFormSubmit") {
|
||||||
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
|
const gameQualitySelect = this.gameQualityMenuElement.getChildByID(
|
||||||
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
|
"select-game-quality"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const videoQualitySelect = this.gameQualityMenuElement.getChildByID(
|
||||||
|
"select-video-quality"
|
||||||
|
) as HTMLInputElement;
|
||||||
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
|
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
|
||||||
} else if ((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') {
|
} else if ((event?.target as HTMLInputElement).id === "gameQualityFormCancel") {
|
||||||
this.closeGameQualityMenu();
|
this.closeGameQualityMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -243,7 +266,7 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
y: middleY,
|
y: middleY,
|
||||||
x: middleX,
|
x: middleX,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,16 +274,15 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
if (!this.settingsMenuOpened) return;
|
if (!this.settingsMenuOpened) return;
|
||||||
this.settingsMenuOpened = false;
|
this.settingsMenuOpened = false;
|
||||||
|
|
||||||
this.gameQualityMenuElement.removeListener('click');
|
this.gameQualityMenuElement.removeListener("click");
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: this.gameQualityMenuElement,
|
targets: this.gameQualityMenuElement,
|
||||||
y: -400,
|
y: -400,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private openGameShare(): void {
|
private openGameShare(): void {
|
||||||
if (this.gameShareOpened) {
|
if (this.gameShareOpened) {
|
||||||
this.closeGameShare();
|
this.closeGameShare();
|
||||||
@ -269,7 +291,7 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
//close all
|
//close all
|
||||||
this.closeAll();
|
this.closeAll();
|
||||||
|
|
||||||
const gameShareLink = this.gameShareElement.getChildByID('gameShareLink') as HTMLInputElement;
|
const gameShareLink = this.gameShareElement.getChildByID("gameShareLink") as HTMLInputElement;
|
||||||
gameShareLink.value = location.toString();
|
gameShareLink.value = location.toString();
|
||||||
|
|
||||||
this.gameShareOpened = true;
|
this.gameShareOpened = true;
|
||||||
@ -287,64 +309,67 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
y: middleY,
|
y: middleY,
|
||||||
x: middleX,
|
x: middleX,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeGameShare(): void {
|
private closeGameShare(): void {
|
||||||
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
|
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
|
||||||
gameShareInfo.innerText = '';
|
gameShareInfo.innerText = "";
|
||||||
gameShareInfo.style.display = 'none';
|
gameShareInfo.style.display = "none";
|
||||||
this.gameShareOpened = false;
|
this.gameShareOpened = false;
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: this.gameShareElement,
|
targets: this.gameShareElement,
|
||||||
y: -400,
|
y: -400,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMenuClick(event: MouseEvent) {
|
private onMenuClick(event: MouseEvent) {
|
||||||
const htmlMenuItem = (event?.target as HTMLInputElement);
|
const htmlMenuItem = event?.target as HTMLInputElement;
|
||||||
if (htmlMenuItem.classList.contains('not-button')) {
|
if (htmlMenuItem.classList.contains("not-button")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (htmlMenuItem.classList.contains("fromApi")) {
|
if (htmlMenuItem.classList.contains("fromApi")) {
|
||||||
sendMenuClickedEvent(htmlMenuItem.id)
|
sendMenuClickedEvent(htmlMenuItem.id);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ((event?.target as HTMLInputElement).id) {
|
switch ((event?.target as HTMLInputElement).id) {
|
||||||
case 'changeNameButton':
|
case "changeNameButton":
|
||||||
this.closeSideMenu();
|
this.closeSideMenu();
|
||||||
gameManager.leaveGame(this, LoginSceneName, new LoginScene());
|
gameManager.leaveGame(this, LoginSceneName, new LoginScene());
|
||||||
break;
|
break;
|
||||||
case 'sparkButton':
|
case "sparkButton":
|
||||||
this.gotToCreateMapPage();
|
this.gotToCreateMapPage();
|
||||||
break;
|
break;
|
||||||
case 'changeSkinButton':
|
case "changeSkinButton":
|
||||||
this.closeSideMenu();
|
this.closeSideMenu();
|
||||||
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
|
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
|
||||||
break;
|
break;
|
||||||
case 'changeCompanionButton':
|
case "changeCompanionButton":
|
||||||
this.closeSideMenu();
|
this.closeSideMenu();
|
||||||
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
|
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
|
||||||
break;
|
break;
|
||||||
case 'closeButton':
|
case "closeButton":
|
||||||
this.closeSideMenu();
|
this.closeSideMenu();
|
||||||
break;
|
break;
|
||||||
case 'shareButton':
|
case "shareButton":
|
||||||
this.openGameShare();
|
this.openGameShare();
|
||||||
break;
|
break;
|
||||||
case 'editGameSettingsButton':
|
case "editGameSettingsButton":
|
||||||
this.openGameSettingsMenu();
|
this.openGameSettingsMenu();
|
||||||
break;
|
break;
|
||||||
case 'toggleFullscreen':
|
case "toggleFullscreen":
|
||||||
this.toggleFullscreen();
|
this.toggleFullscreen();
|
||||||
break;
|
break;
|
||||||
case 'adminConsoleButton':
|
case "enableNotification":
|
||||||
|
this.enableNotification();
|
||||||
|
break;
|
||||||
|
case "adminConsoleButton":
|
||||||
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
||||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||||
} else {
|
} else {
|
||||||
@ -356,9 +381,9 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private async copyLink() {
|
private async copyLink() {
|
||||||
await navigator.clipboard.writeText(location.toString());
|
await navigator.clipboard.writeText(location.toString());
|
||||||
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
|
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
|
||||||
gameShareInfo.innerText = 'Link copied, you can share it now!';
|
gameShareInfo.innerText = "Link copied, you can share it now!";
|
||||||
gameShareInfo.style.display = 'block';
|
gameShareInfo.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveSetting(valueGame: number, valueVideo: number) {
|
private saveSetting(valueGame: number, valueVideo: number) {
|
||||||
@ -378,8 +403,8 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
private gotToCreateMapPage() {
|
private gotToCreateMapPage() {
|
||||||
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
|
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
|
||||||
//TODO fix me: this button can to send us on WorkAdventure BO.
|
//TODO fix me: this button can to send us on WorkAdventure BO.
|
||||||
const sparkHost = 'https://workadventu.re/getting-started';
|
const sparkHost = "https://workadventu.re/getting-started";
|
||||||
window.open(sparkHost, '_blank');
|
window.open(sparkHost, "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeAll() {
|
private closeAll() {
|
||||||
@ -389,10 +414,10 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toggleFullscreen() {
|
private toggleFullscreen() {
|
||||||
const body = document.querySelector('body')
|
const body = document.querySelector("body");
|
||||||
if (body) {
|
if (body) {
|
||||||
if (document.fullscreenElement ?? document.fullscreen) {
|
if (document.fullscreenElement ?? document.fullscreen) {
|
||||||
document.exitFullscreen()
|
document.exitFullscreen();
|
||||||
} else {
|
} else {
|
||||||
body.requestFullscreen();
|
body.requestFullscreen();
|
||||||
}
|
}
|
||||||
@ -406,4 +431,12 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
public isDirty(): boolean {
|
public isDirty(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enableNotification() {
|
||||||
|
mediaManager.requestNotification().then(() => {
|
||||||
|
if (mediaManager.hasNotification()) {
|
||||||
|
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import {MenuScene} from "./MenuScene";
|
import { MenuScene } from "./MenuScene";
|
||||||
import {gameManager} from "../Game/GameManager";
|
import { gameManager } from "../Game/GameManager";
|
||||||
import {blackListManager} from "../../WebRtc/BlackListManager";
|
import { blackListManager } from "../../WebRtc/BlackListManager";
|
||||||
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
|
||||||
export const gameReportKey = 'gameReport';
|
export const gameReportKey = "gameReport";
|
||||||
export const gameReportRessource = 'resources/html/gameReport.html';
|
export const gameReportRessource = "resources/html/gameReport.html";
|
||||||
|
|
||||||
export class ReportMenu extends Phaser.GameObjects.DOMElement {
|
export class ReportMenu extends Phaser.GameObjects.DOMElement {
|
||||||
private opened: boolean = false;
|
private opened: boolean = false;
|
||||||
|
|
||||||
private userId!: number;
|
private userUuid!: string;
|
||||||
private userName!: string|undefined;
|
private userName!: string | undefined;
|
||||||
private anonymous: boolean;
|
private anonymous: boolean;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, anonymous: boolean) {
|
constructor(scene: Phaser.Scene, anonymous: boolean) {
|
||||||
@ -18,46 +19,46 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
|
|||||||
this.createFromCache(gameReportKey);
|
this.createFromCache(gameReportKey);
|
||||||
|
|
||||||
if (this.anonymous) {
|
if (this.anonymous) {
|
||||||
const divToHide = this.getChildByID('reportSection') as HTMLElement;
|
const divToHide = this.getChildByID("reportSection") as HTMLElement;
|
||||||
divToHide.hidden = true;
|
divToHide.hidden = true;
|
||||||
const textToHide = this.getChildByID('askActionP') as HTMLElement;
|
const textToHide = this.getChildByID("askActionP") as HTMLElement;
|
||||||
textToHide.hidden = true;
|
textToHide.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.add.existing(this);
|
scene.add.existing(this);
|
||||||
MenuScene.revealMenusAfterInit(this, gameReportKey);
|
MenuScene.revealMenusAfterInit(this, gameReportKey);
|
||||||
|
|
||||||
this.addListener('click');
|
this.addListener("click");
|
||||||
this.on('click', (event:MouseEvent) => {
|
this.on("click", (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') {
|
if ((event?.target as HTMLInputElement).id === "gameReportFormSubmit") {
|
||||||
this.submitReport();
|
this.submitReport();
|
||||||
} else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') {
|
} else if ((event?.target as HTMLInputElement).id === "gameReportFormCancel") {
|
||||||
this.close();
|
this.close();
|
||||||
} else if((event?.target as HTMLInputElement).id === 'toggleBlockButton') {
|
} else if ((event?.target as HTMLInputElement).id === "toggleBlockButton") {
|
||||||
this.toggleBlock();
|
this.toggleBlock();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public open(userId: number, userName: string|undefined): void {
|
public open(userUuid: string, userName: string | undefined): void {
|
||||||
if (this.opened) {
|
if (this.opened) {
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userId = userId;
|
this.userUuid = userUuid;
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
|
|
||||||
const mainEl = this.getChildByID('gameReport') as HTMLElement;
|
const mainEl = this.getChildByID("gameReport") as HTMLElement;
|
||||||
this.x = this.getCenteredX(mainEl);
|
this.x = this.getCenteredX(mainEl);
|
||||||
this.y = this.getHiddenY(mainEl);
|
this.y = this.getHiddenY(mainEl);
|
||||||
|
|
||||||
const gameTitleReport = this.getChildByID('nameReported') as HTMLElement;
|
const gameTitleReport = this.getChildByID("nameReported") as HTMLElement;
|
||||||
gameTitleReport.innerText = userName || '';
|
gameTitleReport.innerText = userName || "";
|
||||||
|
|
||||||
const blockButton = this.getChildByID('toggleBlockButton') as HTMLElement;
|
const blockButton = this.getChildByID("toggleBlockButton") as HTMLElement;
|
||||||
blockButton.innerText = blackListManager.isBlackListed(this.userId) ? 'Unblock this user' : 'Block this user';
|
blockButton.innerText = blackListManager.isBlackListed(this.userUuid) ? "Unblock this user" : "Block this user";
|
||||||
|
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
|
|
||||||
@ -67,19 +68,19 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
|
|||||||
targets: this,
|
targets: this,
|
||||||
y: this.getCenteredY(mainEl),
|
y: this.getCenteredY(mainEl),
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public close(): void {
|
public close(): void {
|
||||||
gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls();
|
gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls();
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
const mainEl = this.getChildByID('gameReport') as HTMLElement;
|
const mainEl = this.getChildByID("gameReport") as HTMLElement;
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: this,
|
targets: this,
|
||||||
y: this.getHiddenY(mainEl),
|
y: this.getHiddenY(mainEl),
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
ease: 'Power3'
|
ease: "Power3",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,31 +89,32 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
|
|||||||
return window.innerWidth / 4 - mainEl.clientWidth / 2;
|
return window.innerWidth / 4 - mainEl.clientWidth / 2;
|
||||||
}
|
}
|
||||||
private getHiddenY(mainEl: HTMLElement): number {
|
private getHiddenY(mainEl: HTMLElement): number {
|
||||||
return - mainEl.clientHeight - 50;
|
return -mainEl.clientHeight - 50;
|
||||||
}
|
}
|
||||||
private getCenteredY(mainEl: HTMLElement): number {
|
private getCenteredY(mainEl: HTMLElement): number {
|
||||||
return window.innerHeight / 4 - mainEl.clientHeight / 2;
|
return window.innerHeight / 4 - mainEl.clientHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleBlock(): void {
|
private toggleBlock(): void {
|
||||||
!blackListManager.isBlackListed(this.userId) ? blackListManager.blackList(this.userId) : blackListManager.cancelBlackList(this.userId);
|
!blackListManager.isBlackListed(this.userUuid)
|
||||||
|
? blackListManager.blackList(this.userUuid)
|
||||||
|
: blackListManager.cancelBlackList(this.userUuid);
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private submitReport(): void{
|
private submitReport(): void {
|
||||||
const gamePError = this.getChildByID('gameReportErr') as HTMLParagraphElement;
|
const gamePError = this.getChildByID("gameReportErr") as HTMLParagraphElement;
|
||||||
gamePError.innerText = '';
|
gamePError.innerText = "";
|
||||||
gamePError.style.display = 'none';
|
gamePError.style.display = "none";
|
||||||
const gameTextArea = this.getChildByID('gameReportInput') as HTMLInputElement;
|
const gameTextArea = this.getChildByID("gameReportInput") as HTMLInputElement;
|
||||||
if(!gameTextArea || !gameTextArea.value){
|
if (!gameTextArea || !gameTextArea.value) {
|
||||||
gamePError.innerText = 'Report message cannot to be empty.';
|
gamePError.innerText = "Report message cannot to be empty.";
|
||||||
gamePError.style.display = 'block';
|
gamePError.style.display = "block";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gameManager.getCurrentGameScene(this.scene).connection?.emitReportPlayerMessage(
|
gameManager
|
||||||
this.userId,
|
.getCurrentGameScene(this.scene)
|
||||||
gameTextArea.value
|
.connection?.emitReportPlayerMessage(this.userUuid, gameTextArea.value);
|
||||||
);
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export class ErrorScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.image(Textures.icon, "resources/logos/tcm_full.png");
|
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
|
||||||
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
||||||
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
||||||
this.load.spritesheet(
|
this.load.spritesheet(
|
||||||
|
@ -19,7 +19,7 @@ export class ReconnectingScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.image(ReconnectingTextures.icon, "resources/logos/tcm_full.png");
|
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
|
||||||
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
||||||
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
||||||
this.load.spritesheet(
|
this.load.spritesheet(
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import {get, writable} from "svelte/store";
|
import { get, writable } from "svelte/store";
|
||||||
import type {Box} from "../WebRtc/LayoutManager";
|
import type { Box } from "../WebRtc/LayoutManager";
|
||||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||||
import {LayoutMode} from "../WebRtc/LayoutManager";
|
import { LayoutMode } from "../WebRtc/LayoutManager";
|
||||||
import {layoutModeStore} from "./StreamableCollectionStore";
|
import { layoutModeStore } from "./StreamableCollectionStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
||||||
*/
|
*/
|
||||||
function findBiggestAvailableArea(): Box {
|
function findBiggestAvailableArea(): Box {
|
||||||
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
|
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
|
||||||
if (get(layoutModeStore) === LayoutMode.VideoChat) {
|
if (get(layoutModeStore) === LayoutMode.VideoChat) {
|
||||||
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
|
||||||
const htmlChildren = Array.from(children.values());
|
const htmlChildren = Array.from(children.values());
|
||||||
|
|
||||||
// No chat? Let's go full center
|
// No chat? Let's go full center
|
||||||
@ -19,18 +19,17 @@ function findBiggestAvailableArea(): Box {
|
|||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: 0,
|
yStart: 0,
|
||||||
xEnd: game.offsetWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
||||||
// Compute area between top right of the last div and bottom right of window
|
// Compute area between top right of the last div and bottom right of window
|
||||||
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
const area1 =
|
||||||
* (game.offsetHeight - lastDiv.offsetTop);
|
(game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) * (game.offsetHeight - lastDiv.offsetTop);
|
||||||
|
|
||||||
// Compute area between bottom of last div and bottom of the screen on whole width
|
// Compute area between bottom of last div and bottom of the screen on whole width
|
||||||
const area2 = game.offsetWidth
|
const area2 = game.offsetWidth * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
||||||
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
|
||||||
|
|
||||||
if (area1 < 0 && area2 < 0) {
|
if (area1 < 0 && area2 < 0) {
|
||||||
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
||||||
@ -38,28 +37,30 @@ function findBiggestAvailableArea(): Box {
|
|||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: 0,
|
yStart: 0,
|
||||||
xEnd: game.offsetWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
if (area1 <= area2) {
|
if (area1 <= area2) {
|
||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
||||||
xEnd: game.offsetWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
||||||
yStart: lastDiv.offsetTop,
|
yStart: lastDiv.offsetTop,
|
||||||
xEnd: game.offsetWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Possible destinations: at the center bottom or at the right bottom.
|
// Possible destinations: at the center bottom or at the right bottom.
|
||||||
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values());
|
const mainSectionChildren = Array.from(
|
||||||
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values());
|
document.querySelectorAll<HTMLDivElement>("div.main-section > div").values()
|
||||||
|
);
|
||||||
|
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>("aside.sidebar > div").values());
|
||||||
|
|
||||||
// No presentation? Let's center on the screen
|
// No presentation? Let's center on the screen
|
||||||
if (mainSectionChildren.length === 0) {
|
if (mainSectionChildren.length === 0) {
|
||||||
@ -67,60 +68,58 @@ function findBiggestAvailableArea(): Box {
|
|||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: 0,
|
yStart: 0,
|
||||||
xEnd: game.offsetWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, we know we have at least one element in the main section.
|
// At this point, we know we have at least one element in the main section.
|
||||||
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
|
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length - 1];
|
||||||
|
|
||||||
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
const presentationArea =
|
||||||
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
(game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) *
|
||||||
|
(lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
||||||
|
|
||||||
let leftSideBar: number;
|
let leftSideBar: number;
|
||||||
let bottomSideBar: number;
|
let bottomSideBar: number;
|
||||||
if (sidebarChildren.length === 0) {
|
if (sidebarChildren.length === 0) {
|
||||||
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft;
|
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("sidebar").offsetLeft;
|
||||||
bottomSideBar = 0;
|
bottomSideBar = 0;
|
||||||
} else {
|
} else {
|
||||||
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
|
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
|
||||||
leftSideBar = lastSideBarChildren.offsetLeft;
|
leftSideBar = lastSideBarChildren.offsetLeft;
|
||||||
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
||||||
}
|
}
|
||||||
const sideBarArea = (game.offsetWidth - leftSideBar)
|
const sideBarArea = (game.offsetWidth - leftSideBar) * (game.offsetHeight - bottomSideBar);
|
||||||
* (game.offsetHeight - bottomSideBar);
|
|
||||||
|
|
||||||
if (presentationArea <= sideBarArea) {
|
if (presentationArea <= sideBarArea) {
|
||||||
return {
|
return {
|
||||||
xStart: leftSideBar,
|
xStart: leftSideBar,
|
||||||
yStart: bottomSideBar,
|
yStart: bottomSideBar,
|
||||||
xEnd: game.offsetWidth,
|
xEnd: game.offsetWidth,
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
xStart: 0,
|
xStart: 0,
|
||||||
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
||||||
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth, // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
||||||
yEnd: game.offsetHeight
|
yEnd: game.offsetHeight,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the list of (video) peers we are connected to.
|
* A store that contains the list of (video) peers we are connected to.
|
||||||
*/
|
*/
|
||||||
function createBiggestAvailableAreaStore() {
|
function createBiggestAvailableAreaStore() {
|
||||||
|
const { subscribe, set } = writable<Box>({ xStart: 0, yStart: 0, xEnd: 1, yEnd: 1 });
|
||||||
const { subscribe, set } = writable<Box>({xStart:0, yStart: 0, xEnd: 1, yEnd: 1});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
recompute: () => {
|
recompute: () => {
|
||||||
set(findBiggestAvailableArea());
|
set(findBiggestAvailableArea());
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
front/src/Stores/ChatStore.ts
Normal file
119
front/src/Stores/ChatStore.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { playersStore } from "./PlayersStore";
|
||||||
|
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
||||||
|
|
||||||
|
export const chatVisibilityStore = writable(false);
|
||||||
|
export const chatInputFocusStore = writable(false);
|
||||||
|
|
||||||
|
export const newChatMessageStore = writable<string | null>(null);
|
||||||
|
|
||||||
|
export enum ChatMessageTypes {
|
||||||
|
text = 1,
|
||||||
|
me,
|
||||||
|
userIncoming,
|
||||||
|
userOutcoming,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
type: ChatMessageTypes;
|
||||||
|
date: Date;
|
||||||
|
author?: PlayerInterface;
|
||||||
|
targets?: PlayerInterface[];
|
||||||
|
text?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthor(authorId: number): PlayerInterface {
|
||||||
|
const author = playersStore.getPlayerById(authorId);
|
||||||
|
if (!author) {
|
||||||
|
throw "Could not find data for author " + authorId;
|
||||||
|
}
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChatMessagesStore() {
|
||||||
|
const { subscribe, update } = writable<ChatMessage[]>([]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
addIncomingUser(authorId: number) {
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.userIncoming && lastMessage.targets) {
|
||||||
|
lastMessage.targets.push(getAuthor(authorId));
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.userIncoming,
|
||||||
|
targets: [getAuthor(authorId)],
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addOutcomingUser(authorId: number) {
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.userOutcoming && lastMessage.targets) {
|
||||||
|
lastMessage.targets.push(getAuthor(authorId));
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.userOutcoming,
|
||||||
|
targets: [getAuthor(authorId)],
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addPersonnalMessage(text: string) {
|
||||||
|
newChatMessageStore.set(text);
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.me && lastMessage.text) {
|
||||||
|
lastMessage.text.push(text);
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.me,
|
||||||
|
text: [text],
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addExternalMessage(authorId: number, text: string) {
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.text && lastMessage.text) {
|
||||||
|
lastMessage.text.push(text);
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.text,
|
||||||
|
text: [text],
|
||||||
|
author: getAuthor(authorId),
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
chatVisibilityStore.set(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const chatMessagesStore = createChatMessagesStore();
|
||||||
|
|
||||||
|
function createChatSubMenuVisibilityStore() {
|
||||||
|
const { subscribe, update } = writable<string>("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
openSubMenu(playerName: string, index: number) {
|
||||||
|
const id = playerName + index;
|
||||||
|
update((oldValue) => {
|
||||||
|
return oldValue === id ? "" : id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatSubMenuVisbilityStore = createChatSubMenuVisibilityStore();
|
@ -1,4 +1,4 @@
|
|||||||
import {writable} from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains whether the game overlay is shown or not.
|
* A store that contains whether the game overlay is shown or not.
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
|
||||||
import {localUserStore} from "../Connexion/LocalUserStore";
|
import { localUserStore } from "../Connexion/LocalUserStore";
|
||||||
import {userMovingStore} from "./GameStore";
|
import { userMovingStore } from "./GameStore";
|
||||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||||
import {BrowserTooOldError} from "./Errors/BrowserTooOldError";
|
import { BrowserTooOldError } from "./Errors/BrowserTooOldError";
|
||||||
import {errorStore} from "./ErrorStore";
|
import { errorStore } from "./ErrorStore";
|
||||||
import {isIOS} from "../WebRtc/DeviceUtils";
|
import { isIOS } from "../WebRtc/DeviceUtils";
|
||||||
import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS";
|
import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
|
||||||
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility";
|
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
|
||||||
import {peerStore} from "./PeerStore";
|
import { peerStore } from "./PeerStore";
|
||||||
import {privacyShutdownStore} from "./PrivacyShutdownStore";
|
import { privacyShutdownStore } from "./PrivacyShutdownStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the camera state requested by the user (on or off).
|
* A store that contains the camera state requested by the user (on or off).
|
||||||
@ -57,7 +57,7 @@ export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilit
|
|||||||
* A store containing whether the webcam was enabled in the last 10 seconds
|
* A store containing whether the webcam was enabled in the last 10 seconds
|
||||||
*/
|
*/
|
||||||
const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
||||||
let timeout: NodeJS.Timeout|null = null;
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const unsubscribe = requestedCameraState.subscribe((enabled) => {
|
const unsubscribe = requestedCameraState.subscribe((enabled) => {
|
||||||
if (enabled === true) {
|
if (enabled === true) {
|
||||||
@ -71,7 +71,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
|||||||
} else {
|
} else {
|
||||||
set(false);
|
set(false);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return function stop() {
|
return function stop() {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
@ -82,7 +82,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
|||||||
* A store containing whether the webcam was enabled in the last 5 seconds
|
* A store containing whether the webcam was enabled in the last 5 seconds
|
||||||
*/
|
*/
|
||||||
const userMoved5SecondsAgoStore = readable(false, function start(set) {
|
const userMoved5SecondsAgoStore = readable(false, function start(set) {
|
||||||
let timeout: NodeJS.Timeout|null = null;
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const unsubscribe = userMovingStore.subscribe((moving) => {
|
const unsubscribe = userMovingStore.subscribe((moving) => {
|
||||||
if (moving === true) {
|
if (moving === true) {
|
||||||
@ -94,45 +94,51 @@ const userMoved5SecondsAgoStore = readable(false, function start(set) {
|
|||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
set(false);
|
set(false);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return function stop() {
|
return function stop() {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing whether the mouse is getting close the bottom right corner.
|
* A store containing whether the mouse is getting close the bottom right corner.
|
||||||
*/
|
*/
|
||||||
const mouseInBottomRight = readable(false, function start(set) {
|
const mouseInBottomRight = readable(false, function start(set) {
|
||||||
let lastInBottomRight = false;
|
let lastInBottomRight = false;
|
||||||
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
|
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game");
|
||||||
|
|
||||||
const detectInBottomRight = (event: MouseEvent) => {
|
const detectInBottomRight = (event: MouseEvent) => {
|
||||||
const rect = gameDiv.getBoundingClientRect();
|
const rect = gameDiv.getBoundingClientRect();
|
||||||
const inBottomRight = event.x - rect.left > rect.width * 3 / 4 && event.y - rect.top > rect.height * 3 / 4;
|
const inBottomRight = event.x - rect.left > (rect.width * 3) / 4 && event.y - rect.top > (rect.height * 3) / 4;
|
||||||
if (inBottomRight !== lastInBottomRight) {
|
if (inBottomRight !== lastInBottomRight) {
|
||||||
lastInBottomRight = inBottomRight;
|
lastInBottomRight = inBottomRight;
|
||||||
set(inBottomRight);
|
set(inBottomRight);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', detectInBottomRight);
|
document.addEventListener("mousemove", detectInBottomRight);
|
||||||
|
|
||||||
return function stop() {
|
return function stop() {
|
||||||
document.removeEventListener('mousemove', detectInBottomRight);
|
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.
|
* 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]) => {
|
export const cameraEnergySavingStore = derived(
|
||||||
return !$mouseInBottomRight && !$userMoved5SecondsAgoStore && $peerStore.size === 0 && !$enabledWebCam10secondsAgoStore;
|
[userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight],
|
||||||
});
|
([$userMoved5SecondsAgoStore, $peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
|
||||||
|
return (
|
||||||
|
!$mouseInBottomRight &&
|
||||||
|
!$userMoved5SecondsAgoStore &&
|
||||||
|
$peerStore.size === 0 &&
|
||||||
|
!$enabledWebCam10secondsAgoStore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains video constraints.
|
* A store that contains video constraints.
|
||||||
@ -143,28 +149,30 @@ function createVideoConstraintStore() {
|
|||||||
height: { min: 400, ideal: 720 },
|
height: { min: 400, ideal: 720 },
|
||||||
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
||||||
facingMode: "user",
|
facingMode: "user",
|
||||||
resizeMode: 'crop-and-scale',
|
resizeMode: "crop-and-scale",
|
||||||
aspectRatio: 1.777777778
|
aspectRatio: 1.777777778,
|
||||||
} as MediaTrackConstraints);
|
} as MediaTrackConstraints);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
setDeviceId: (deviceId: string|undefined) => update((constraints) => {
|
setDeviceId: (deviceId: string | undefined) =>
|
||||||
if (deviceId !== undefined) {
|
update((constraints) => {
|
||||||
constraints.deviceId = {
|
if (deviceId !== undefined) {
|
||||||
exact: deviceId
|
constraints.deviceId = {
|
||||||
};
|
exact: deviceId,
|
||||||
} else {
|
};
|
||||||
delete constraints.deviceId;
|
} else {
|
||||||
}
|
delete constraints.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
return constraints;
|
return constraints;
|
||||||
}),
|
}),
|
||||||
setFrameRate: (frameRate: number) => update((constraints) => {
|
setFrameRate: (frameRate: number) =>
|
||||||
constraints.frameRate = { ideal: frameRate };
|
update((constraints) => {
|
||||||
|
constraints.frameRate = { ideal: frameRate };
|
||||||
|
|
||||||
return constraints;
|
return constraints;
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,39 +186,39 @@ function createAudioConstraintStore() {
|
|||||||
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
||||||
autoGainControl: false,
|
autoGainControl: false,
|
||||||
echoCancellation: true,
|
echoCancellation: true,
|
||||||
noiseSuppression: true
|
noiseSuppression: true,
|
||||||
} as boolean|MediaTrackConstraints);
|
} as boolean | MediaTrackConstraints);
|
||||||
|
|
||||||
let selectedDeviceId = null;
|
let selectedDeviceId = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
setDeviceId: (deviceId: string|undefined) => update((constraints) => {
|
setDeviceId: (deviceId: string | undefined) =>
|
||||||
selectedDeviceId = deviceId;
|
update((constraints) => {
|
||||||
|
selectedDeviceId = deviceId;
|
||||||
|
|
||||||
if (typeof(constraints) === 'boolean') {
|
if (typeof constraints === "boolean") {
|
||||||
constraints = {}
|
constraints = {};
|
||||||
}
|
}
|
||||||
if (deviceId !== undefined) {
|
if (deviceId !== undefined) {
|
||||||
constraints.deviceId = {
|
constraints.deviceId = {
|
||||||
exact: selectedDeviceId
|
exact: selectedDeviceId,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
delete constraints.deviceId;
|
delete constraints.deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return constraints;
|
return constraints;
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const audioConstraintStore = createAudioConstraintStore();
|
export const audioConstraintStore = createAudioConstraintStore();
|
||||||
|
|
||||||
|
|
||||||
let timeout: NodeJS.Timeout;
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
|
let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
|
||||||
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
|
let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing the media constraints we want to apply.
|
* A store containing the media constraints we want to apply.
|
||||||
@ -225,7 +233,8 @@ export const mediaStreamConstraintsStore = derived(
|
|||||||
audioConstraintStore,
|
audioConstraintStore,
|
||||||
privacyShutdownStore,
|
privacyShutdownStore,
|
||||||
cameraEnergySavingStore,
|
cameraEnergySavingStore,
|
||||||
], (
|
],
|
||||||
|
(
|
||||||
[
|
[
|
||||||
$requestedCameraState,
|
$requestedCameraState,
|
||||||
$requestedMicrophoneState,
|
$requestedMicrophoneState,
|
||||||
@ -235,92 +244,97 @@ export const mediaStreamConstraintsStore = derived(
|
|||||||
$audioConstraintStore,
|
$audioConstraintStore,
|
||||||
$privacyShutdownStore,
|
$privacyShutdownStore,
|
||||||
$cameraEnergySavingStore,
|
$cameraEnergySavingStore,
|
||||||
], set
|
],
|
||||||
|
set
|
||||||
) => {
|
) => {
|
||||||
|
let currentVideoConstraint: boolean | MediaTrackConstraints = $videoConstraintStore;
|
||||||
|
let currentAudioConstraint: boolean | MediaTrackConstraints = $audioConstraintStore;
|
||||||
|
|
||||||
let currentVideoConstraint: boolean|MediaTrackConstraints = $videoConstraintStore;
|
if ($enableCameraSceneVisibilityStore) {
|
||||||
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({
|
set({
|
||||||
video: currentVideoConstraint,
|
video: currentVideoConstraint,
|
||||||
audio: currentAudioConstraint,
|
audio: currentAudioConstraint,
|
||||||
});
|
});
|
||||||
}, 100);
|
return;
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
video: false,
|
// Disable webcam if the user requested so
|
||||||
audio: false
|
if ($requestedCameraState === false) {
|
||||||
} as MediaStreamConstraints);
|
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;
|
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
|
||||||
|
|
||||||
interface StreamSuccessValue {
|
interface StreamSuccessValue {
|
||||||
type: "success",
|
type: "success";
|
||||||
stream: MediaStream|null,
|
stream: MediaStream | null;
|
||||||
// The constraints that we got (and not the one that have been requested)
|
// The constraints that we got (and not the one that have been requested)
|
||||||
constraints: MediaStreamConstraints
|
constraints: MediaStreamConstraints;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamErrorValue {
|
interface StreamErrorValue {
|
||||||
type: "error",
|
type: "error";
|
||||||
error: Error,
|
error: Error;
|
||||||
constraints: MediaStreamConstraints
|
constraints: MediaStreamConstraints;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentStream : MediaStream|null = null;
|
let currentStream: MediaStream | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the camera from filming
|
* Stops the camera from filming
|
||||||
@ -347,84 +361,94 @@ function stopMicrophone(): void {
|
|||||||
/**
|
/**
|
||||||
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
|
* 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) => {
|
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
|
||||||
const constraints = { ...$mediaStreamConstraintsStore };
|
mediaStreamConstraintsStore,
|
||||||
|
($mediaStreamConstraintsStore, set) => {
|
||||||
|
const constraints = { ...$mediaStreamConstraintsStore };
|
||||||
|
|
||||||
if (navigator.mediaDevices === undefined) {
|
if (navigator.mediaDevices === undefined) {
|
||||||
if (window.location.protocol === 'http:') {
|
if (window.location.protocol === "http:") {
|
||||||
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
|
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
|
||||||
|
set({
|
||||||
|
type: "error",
|
||||||
|
error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."),
|
||||||
|
constraints,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (isIOS()) {
|
||||||
|
set({
|
||||||
|
type: "error",
|
||||||
|
error: new WebviewOnOldIOS(),
|
||||||
|
constraints,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
type: "error",
|
||||||
|
error: new BrowserTooOldError(),
|
||||||
|
constraints,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints.audio === false) {
|
||||||
|
stopMicrophone();
|
||||||
|
}
|
||||||
|
if (constraints.video === false) {
|
||||||
|
stopCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints.audio === false && constraints.video === false) {
|
||||||
|
currentStream = null;
|
||||||
set({
|
set({
|
||||||
type: 'error',
|
type: "success",
|
||||||
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'),
|
stream: null,
|
||||||
constraints
|
constraints,
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else if (isIOS()) {
|
|
||||||
set({
|
|
||||||
type: 'error',
|
|
||||||
error: new WebviewOnOldIOS(),
|
|
||||||
constraints
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
set({
|
|
||||||
type: 'error',
|
|
||||||
error: new BrowserTooOldError(),
|
|
||||||
constraints
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (constraints.audio === false) {
|
(async () => {
|
||||||
stopMicrophone();
|
try {
|
||||||
}
|
stopMicrophone();
|
||||||
if (constraints.video === false) {
|
stopCamera();
|
||||||
stopCamera();
|
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
set({
|
||||||
type: 'error',
|
type: "success",
|
||||||
error: e,
|
stream: currentStream,
|
||||||
constraints
|
constraints,
|
||||||
});
|
});
|
||||||
// Let's try without video constraints
|
return;
|
||||||
requestedCameraState.disableWebcam();
|
} catch (e) {
|
||||||
} else {
|
if (constraints.video !== false) {
|
||||||
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
console.info(
|
||||||
set({
|
"Error. Unable to get microphone and/or camera access. Trying audio only.",
|
||||||
type: 'error',
|
$mediaStreamConstraintsStore,
|
||||||
error: e,
|
e
|
||||||
constraints
|
);
|
||||||
});
|
// 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;
|
/*constraints.video = false;
|
||||||
if (constraints.audio === false) {
|
if (constraints.audio === false) {
|
||||||
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
||||||
set({
|
set({
|
||||||
@ -453,9 +477,10 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
|
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
|
||||||
@ -472,12 +497,15 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
|
|||||||
|
|
||||||
const queryDeviceList = () => {
|
const queryDeviceList = () => {
|
||||||
// Note: so far, we are ignoring any failures.
|
// Note: so far, we are ignoring any failures.
|
||||||
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => {
|
navigator.mediaDevices
|
||||||
set(mediaDeviceInfos);
|
.enumerateDevices()
|
||||||
}).catch((e) => {
|
.then((mediaDeviceInfos) => {
|
||||||
console.error(e);
|
set(mediaDeviceInfos);
|
||||||
throw e;
|
})
|
||||||
});
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsubscribe = localStreamStore.subscribe((streamResult) => {
|
const unsubscribe = localStreamStore.subscribe((streamResult) => {
|
||||||
@ -490,23 +518,23 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (navigator.mediaDevices) {
|
if (navigator.mediaDevices) {
|
||||||
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList);
|
navigator.mediaDevices.addEventListener("devicechange", queryDeviceList);
|
||||||
}
|
}
|
||||||
|
|
||||||
return function stop() {
|
return function stop() {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
if (navigator.mediaDevices) {
|
if (navigator.mediaDevices) {
|
||||||
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList);
|
navigator.mediaDevices.removeEventListener("devicechange", queryDeviceList);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
|
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
|
||||||
return $deviceListStore.filter(device => device.kind === 'videoinput');
|
return $deviceListStore.filter((device) => device.kind === "videoinput");
|
||||||
});
|
});
|
||||||
|
|
||||||
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
|
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
|
||||||
return $deviceListStore.filter(device => device.kind === 'audioinput');
|
return $deviceListStore.filter((device) => device.kind === "audioinput");
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: detect the new webcam and automatically switch on it.
|
// TODO: detect the new webcam and automatically switch on it.
|
||||||
@ -519,7 +547,7 @@ cameraListStore.subscribe((devices) => {
|
|||||||
|
|
||||||
// If we cannot find the device ID, let's remove it.
|
// If we cannot find the device ID, let's remove it.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
|
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
|
||||||
videoConstraintStore.setDeviceId(undefined);
|
videoConstraintStore.setDeviceId(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -527,7 +555,7 @@ cameraListStore.subscribe((devices) => {
|
|||||||
microphoneListStore.subscribe((devices) => {
|
microphoneListStore.subscribe((devices) => {
|
||||||
// If the selected camera is unplugged, let's remove the constraint on deviceId
|
// If the selected camera is unplugged, let's remove the constraint on deviceId
|
||||||
const constraints = get(audioConstraintStore);
|
const constraints = get(audioConstraintStore);
|
||||||
if (typeof constraints === 'boolean') {
|
if (typeof constraints === "boolean") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!constraints.deviceId) {
|
if (!constraints.deviceId) {
|
||||||
@ -536,13 +564,13 @@ microphoneListStore.subscribe((devices) => {
|
|||||||
|
|
||||||
// If we cannot find the device ID, let's remove it.
|
// If we cannot find the device ID, let's remove it.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
|
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
|
||||||
audioConstraintStore.setDeviceId(undefined);
|
audioConstraintStore.setDeviceId(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
localStreamStore.subscribe(streamResult => {
|
localStreamStore.subscribe((streamResult) => {
|
||||||
if (streamResult.type === 'error') {
|
if (streamResult.type === "error") {
|
||||||
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
|
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
|
||||||
errorStore.addErrorMessage(streamResult.error);
|
errorStore.addErrorMessage(streamResult.error);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {readable, writable} from "svelte/store";
|
import { readable, writable } from "svelte/store";
|
||||||
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer";
|
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
|
||||||
import {VideoPeer} from "../WebRtc/VideoPeer";
|
import { VideoPeer } from "../WebRtc/VideoPeer";
|
||||||
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer";
|
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the list of (video) peers we are connected to.
|
* A store that contains the list of (video) peers we are connected to.
|
||||||
@ -19,20 +19,20 @@ function createPeerStore() {
|
|||||||
simplePeer.registerPeerConnectionListener({
|
simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(peer: RemotePeer) {
|
onConnect(peer: RemotePeer) {
|
||||||
if (peer instanceof VideoPeer) {
|
if (peer instanceof VideoPeer) {
|
||||||
update(users => {
|
update((users) => {
|
||||||
users.set(peer.userId, peer);
|
users.set(peer.userId, peer);
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
update(users => {
|
update((users) => {
|
||||||
users.delete(userId);
|
users.delete(userId);
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,20 +52,20 @@ function createScreenSharingPeerStore() {
|
|||||||
simplePeer.registerPeerConnectionListener({
|
simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(peer: RemotePeer) {
|
onConnect(peer: RemotePeer) {
|
||||||
if (peer instanceof ScreenSharingPeer) {
|
if (peer instanceof ScreenSharingPeer) {
|
||||||
update(users => {
|
update((users) => {
|
||||||
users.set(peer.userId, peer);
|
users.set(peer.userId, peer);
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
update(users => {
|
update((users) => {
|
||||||
users.delete(userId);
|
users.delete(userId);
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +79,7 @@ function createScreenSharingStreamStore() {
|
|||||||
let peers = new Map<number, ScreenSharingPeer>();
|
let peers = new Map<number, ScreenSharingPeer>();
|
||||||
|
|
||||||
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
|
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
|
||||||
|
let unsubscribes: (() => void)[] = [];
|
||||||
let unsubscribes: (()=>void)[] = [];
|
|
||||||
|
|
||||||
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
|
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
|
||||||
for (const unsubscribe of unsubscribes) {
|
for (const unsubscribe of unsubscribes) {
|
||||||
@ -91,24 +90,23 @@ function createScreenSharingStreamStore() {
|
|||||||
peers = new Map<number, ScreenSharingPeer>();
|
peers = new Map<number, ScreenSharingPeer>();
|
||||||
|
|
||||||
screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => {
|
screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => {
|
||||||
|
|
||||||
if (screenSharingPeer.isReceivingScreenSharingStream()) {
|
if (screenSharingPeer.isReceivingScreenSharingStream()) {
|
||||||
peers.set(key, screenSharingPeer);
|
peers.set(key, screenSharingPeer);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => {
|
unsubscribes.push(
|
||||||
if (stream) {
|
screenSharingPeer.streamStore.subscribe((stream) => {
|
||||||
peers.set(key, screenSharingPeer);
|
if (stream) {
|
||||||
} else {
|
peers.set(key, screenSharingPeer);
|
||||||
peers.delete(key);
|
} else {
|
||||||
}
|
peers.delete(key);
|
||||||
set(peers);
|
}
|
||||||
}));
|
set(peers);
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
set(peers);
|
set(peers);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return function stop() {
|
return function stop() {
|
||||||
@ -117,9 +115,7 @@ function createScreenSharingStreamStore() {
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const screenSharingStreamStore = createScreenSharingStreamStore();
|
export const screenSharingStreamStore = createScreenSharingStreamStore();
|
||||||
|
|
||||||
|
|
||||||
|
69
front/src/Stores/PlayersStore.ts
Normal file
69
front/src/Stores/PlayersStore.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
||||||
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
|
import { getRandomColor } from "../WebRtc/ColorGenerator";
|
||||||
|
|
||||||
|
let idCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the list of players currently known.
|
||||||
|
*/
|
||||||
|
function createPlayersStore() {
|
||||||
|
let players = new Map<number, PlayerInterface>();
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(players);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
connectToRoomConnection: (roomConnection: RoomConnection) => {
|
||||||
|
players = new Map<number, PlayerInterface>();
|
||||||
|
set(players);
|
||||||
|
roomConnection.onUserJoins((message) => {
|
||||||
|
update((users) => {
|
||||||
|
users.set(message.userId, {
|
||||||
|
userId: message.userId,
|
||||||
|
name: message.name,
|
||||||
|
characterLayers: message.characterLayers,
|
||||||
|
visitCardUrl: message.visitCardUrl,
|
||||||
|
companion: message.companion,
|
||||||
|
userUuid: message.userUuid,
|
||||||
|
color: getRandomColor(),
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
roomConnection.onUserLeft((userId) => {
|
||||||
|
update((users) => {
|
||||||
|
users.delete(userId);
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPlayerById(userId: number): PlayerInterface | undefined {
|
||||||
|
return players.get(userId);
|
||||||
|
},
|
||||||
|
addFacticePlayer(name: string): number {
|
||||||
|
let userId: number | null = null;
|
||||||
|
players.forEach((p) => {
|
||||||
|
if (p.name === name) userId = p.userId;
|
||||||
|
});
|
||||||
|
if (userId) return userId;
|
||||||
|
const newUserId = idCount--;
|
||||||
|
update((users) => {
|
||||||
|
users.set(newUserId, {
|
||||||
|
userId: newUserId,
|
||||||
|
name,
|
||||||
|
characterLayers: [],
|
||||||
|
visitCardUrl: null,
|
||||||
|
companion: null,
|
||||||
|
userUuid: "dummy",
|
||||||
|
color: getRandomColor(),
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
return newUserId;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const playersStore = createPlayersStore();
|
@ -1,6 +1,6 @@
|
|||||||
import {get, writable} from "svelte/store";
|
import { get, writable } from "svelte/store";
|
||||||
import {peerStore} from "./PeerStore";
|
import { peerStore } from "./PeerStore";
|
||||||
import {visibilityStore} from "./VisibilityStore";
|
import { visibilityStore } from "./VisibilityStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* 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.
|
||||||
@ -28,7 +28,6 @@ function createPrivacyShutdownStore() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
|
||||||
import {peerStore} from "./PeerStore";
|
import { peerStore } from "./PeerStore";
|
||||||
import type {
|
import type { LocalStreamStoreValue } from "./MediaStore";
|
||||||
LocalStreamStoreValue,
|
import { DivImportance } from "../WebRtc/LayoutManager";
|
||||||
} from "./MediaStore";
|
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
|
||||||
import {DivImportance} from "../WebRtc/LayoutManager";
|
|
||||||
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility";
|
|
||||||
|
|
||||||
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
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).
|
* A store that contains the camera state requested by the user (on or off).
|
||||||
@ -23,7 +21,7 @@ function createRequestedScreenSharingState() {
|
|||||||
|
|
||||||
export const requestedScreenSharingState = createRequestedScreenSharingState();
|
export const requestedScreenSharingState = createRequestedScreenSharingState();
|
||||||
|
|
||||||
let currentStream : MediaStream|null = null;
|
let currentStream: MediaStream | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the camera from filming
|
* Stops the camera from filming
|
||||||
@ -37,27 +35,17 @@ function stopScreenSharing(): void {
|
|||||||
currentStream = null;
|
currentStream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
|
let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
|
||||||
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
|
let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing the media constraints we want to apply.
|
* A store containing the media constraints we want to apply.
|
||||||
*/
|
*/
|
||||||
export const screenSharingConstraintsStore = derived(
|
export const screenSharingConstraintsStore = derived(
|
||||||
[
|
[requestedScreenSharingState, gameOverlayVisibilityStore, peerStore],
|
||||||
requestedScreenSharingState,
|
([$requestedScreenSharingState, $gameOverlayVisibilityStore, $peerStore], set) => {
|
||||||
gameOverlayVisibilityStore,
|
let currentVideoConstraint: boolean | MediaTrackConstraints = true;
|
||||||
peerStore,
|
let currentAudioConstraint: boolean | MediaTrackConstraints = false;
|
||||||
], (
|
|
||||||
[
|
|
||||||
$requestedScreenSharingState,
|
|
||||||
$gameOverlayVisibilityStore,
|
|
||||||
$peerStore,
|
|
||||||
], set
|
|
||||||
) => {
|
|
||||||
|
|
||||||
let currentVideoConstraint: boolean|MediaTrackConstraints = true;
|
|
||||||
let currentAudioConstraint: boolean|MediaTrackConstraints = false;
|
|
||||||
|
|
||||||
// Disable screen sharing if the user requested so
|
// Disable screen sharing if the user requested so
|
||||||
if (!$requestedScreenSharingState) {
|
if (!$requestedScreenSharingState) {
|
||||||
@ -78,7 +66,10 @@ export const screenSharingConstraintsStore = derived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Let's make the changes only if the new value is different from the old one.
|
// Let's make the changes only if the new value is different from the old one.
|
||||||
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
|
if (
|
||||||
|
previousComputedVideoConstraint != currentVideoConstraint ||
|
||||||
|
previousComputedAudioConstraint != currentAudioConstraint
|
||||||
|
) {
|
||||||
previousComputedVideoConstraint = currentVideoConstraint;
|
previousComputedVideoConstraint = currentVideoConstraint;
|
||||||
previousComputedAudioConstraint = currentAudioConstraint;
|
previousComputedAudioConstraint = currentAudioConstraint;
|
||||||
// Let's copy the objects.
|
// Let's copy the objects.
|
||||||
@ -94,85 +85,89 @@ export const screenSharingConstraintsStore = derived(
|
|||||||
audio: currentAudioConstraint,
|
audio: currentAudioConstraint,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
video: false,
|
video: false,
|
||||||
audio: false
|
audio: false,
|
||||||
} as MediaStreamConstraints);
|
} as MediaStreamConstraints
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
|
* 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) => {
|
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
|
||||||
const constraints = $screenSharingConstraintsStore;
|
screenSharingConstraintsStore,
|
||||||
|
($screenSharingConstraintsStore, set) => {
|
||||||
|
const constraints = $screenSharingConstraintsStore;
|
||||||
|
|
||||||
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
|
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 && 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();
|
stopScreenSharing();
|
||||||
currentStream = await currentStreamPromise;
|
requestedScreenSharingState.disableScreenSharing();
|
||||||
|
|
||||||
// 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({
|
set({
|
||||||
type: 'success',
|
type: "success",
|
||||||
stream: currentStream,
|
stream: null,
|
||||||
constraints
|
constraints,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
|
||||||
currentStream = null;
|
|
||||||
requestedScreenSharingState.disableScreenSharing();
|
|
||||||
console.info("Error. Unable to share screen.", e);
|
|
||||||
set({
|
|
||||||
type: 'error',
|
|
||||||
error: e,
|
|
||||||
constraints
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
});
|
let currentStreamPromise: Promise<MediaStream>;
|
||||||
|
if (navigator.getDisplayMedia) {
|
||||||
|
currentStreamPromise = navigator.getDisplayMedia({ constraints });
|
||||||
|
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
|
||||||
|
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints });
|
||||||
|
} else {
|
||||||
|
stopScreenSharing();
|
||||||
|
set({
|
||||||
|
type: "error",
|
||||||
|
error: new Error("Your browser does not support sharing screen"),
|
||||||
|
constraints,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
stopScreenSharing();
|
||||||
|
currentStream = await currentStreamPromise;
|
||||||
|
|
||||||
|
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
|
||||||
|
for (const track of currentStream.getTracks()) {
|
||||||
|
track.onended = () => {
|
||||||
|
stopScreenSharing();
|
||||||
|
requestedScreenSharingState.disableScreenSharing();
|
||||||
|
previousComputedVideoConstraint = false;
|
||||||
|
previousComputedAudioConstraint = false;
|
||||||
|
set({
|
||||||
|
type: "success",
|
||||||
|
stream: null,
|
||||||
|
constraints: {
|
||||||
|
video: false,
|
||||||
|
audio: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
type: "success",
|
||||||
|
stream: currentStream,
|
||||||
|
constraints,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
currentStream = null;
|
||||||
|
requestedScreenSharingState.disableScreenSharing();
|
||||||
|
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.
|
* A store containing whether the screen sharing button should be displayed or hidden.
|
||||||
@ -188,19 +183,18 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
|
|||||||
|
|
||||||
export interface ScreenSharingLocalMedia {
|
export interface ScreenSharingLocalMedia {
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
stream: MediaStream|null;
|
stream: MediaStream | null;
|
||||||
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
|
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The representation of the screen sharing stream.
|
* The representation of the screen sharing stream.
|
||||||
*/
|
*/
|
||||||
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(null, function start(set) {
|
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia | null>(null, function start(set) {
|
||||||
|
|
||||||
const localMedia: ScreenSharingLocalMedia = {
|
const localMedia: ScreenSharingLocalMedia = {
|
||||||
uniqueId: "localScreenSharingStream",
|
uniqueId: "localScreenSharingStream",
|
||||||
stream: null
|
stream: null,
|
||||||
}
|
};
|
||||||
|
|
||||||
const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => {
|
const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => {
|
||||||
if (screenSharingLocalStream.type === "success") {
|
if (screenSharingLocalStream.type === "success") {
|
||||||
@ -214,4 +208,4 @@ export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(nu
|
|||||||
return function stop() {
|
return function stop() {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import {writable} from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null);
|
export const showReportScreenStore = writable<{ userId: number; userName: string } | null>(null);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {derived, get, Readable, writable} from "svelte/store";
|
import { derived, get, Readable, writable } from "svelte/store";
|
||||||
import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore";
|
import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore";
|
||||||
import { peerStore, screenSharingStreamStore} from "./PeerStore";
|
import { peerStore, screenSharingStreamStore } from "./PeerStore";
|
||||||
import type {RemotePeer} from "../WebRtc/SimplePeer";
|
import type { RemotePeer } from "../WebRtc/SimplePeer";
|
||||||
import {LayoutMode} from "../WebRtc/LayoutManager";
|
import { LayoutMode } from "../WebRtc/LayoutManager";
|
||||||
|
|
||||||
export type Streamable = RemotePeer | ScreenSharingLocalMedia;
|
export type Streamable = RemotePeer | ScreenSharingLocalMedia;
|
||||||
|
|
||||||
@ -12,32 +12,25 @@ export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
|
|||||||
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
|
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
|
||||||
*/
|
*/
|
||||||
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
|
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
|
||||||
|
return derived(
|
||||||
|
[screenSharingStreamStore, peerStore, screenSharingLocalMedia],
|
||||||
|
([$screenSharingStreamStore, $peerStore, $screenSharingLocalMedia], set) => {
|
||||||
|
const peers = new Map<string, Streamable>();
|
||||||
|
|
||||||
return derived([
|
const addPeer = (peer: Streamable) => {
|
||||||
screenSharingStreamStore,
|
peers.set(peer.uniqueId, peer);
|
||||||
peerStore,
|
};
|
||||||
screenSharingLocalMedia,
|
|
||||||
], ([
|
|
||||||
$screenSharingStreamStore,
|
|
||||||
$peerStore,
|
|
||||||
$screenSharingLocalMedia,
|
|
||||||
], set) => {
|
|
||||||
|
|
||||||
const peers = new Map<string, Streamable>();
|
$screenSharingStreamStore.forEach(addPeer);
|
||||||
|
$peerStore.forEach(addPeer);
|
||||||
|
|
||||||
const addPeer = (peer: Streamable) => {
|
if ($screenSharingLocalMedia?.stream) {
|
||||||
peers.set(peer.uniqueId, peer);
|
addPeer($screenSharingLocalMedia);
|
||||||
};
|
}
|
||||||
|
|
||||||
$screenSharingStreamStore.forEach(addPeer);
|
set(peers);
|
||||||
$peerStore.forEach(addPeer);
|
|
||||||
|
|
||||||
if ($screenSharingLocalMedia?.stream) {
|
|
||||||
addPeer($screenSharingLocalMedia);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
set(peers);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const streamableCollectionStore = createStreamableCollectionStore();
|
export const streamableCollectionStore = createStreamableCollectionStore();
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {derived} from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
import {consoleGlobalMessageManagerFocusStore} from "./ConsoleGlobalMessageManagerStore";
|
import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
|
||||||
|
import { chatInputFocusStore } from "./ChatStore";
|
||||||
|
|
||||||
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
|
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
|
||||||
export const enableUserInputsStore = derived(
|
export const enableUserInputsStore = derived(
|
||||||
consoleGlobalMessageManagerFocusStore,
|
[consoleGlobalMessageManagerFocusStore, chatInputFocusStore],
|
||||||
($consoleGlobalMessageManagerFocusStore) => {
|
([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => {
|
||||||
return !$consoleGlobalMessageManagerFocusStore;
|
return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore;
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -1,8 +1,8 @@
|
|||||||
import {writable} from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer";
|
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
|
||||||
import {VideoPeer} from "../WebRtc/VideoPeer";
|
import { VideoPeer } from "../WebRtc/VideoPeer";
|
||||||
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer";
|
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
|
||||||
import type {Streamable} from "./StreamableCollectionStore";
|
import type { Streamable } from "./StreamableCollectionStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the peer / media that has currently the "importance" focus.
|
* A store that contains the peer / media that has currently the "importance" focus.
|
||||||
@ -32,15 +32,17 @@ function createVideoFocusStore() {
|
|||||||
},
|
},
|
||||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||||
simplePeer.registerPeerConnectionListener({
|
simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(peer: RemotePeer) {
|
onConnect(peer: RemotePeer) {},
|
||||||
},
|
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) {
|
if (
|
||||||
|
(focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) &&
|
||||||
|
focusedMedia.userId === userId
|
||||||
|
) {
|
||||||
set(null);
|
set(null);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import {readable} from "svelte/store";
|
import { readable } from "svelte/store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing whether the current page is visible or not.
|
* A store containing whether the current page is visible or not.
|
||||||
*/
|
*/
|
||||||
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) {
|
export const visibilityStore = readable(document.visibilityState === "visible", function start(set) {
|
||||||
const onVisibilityChange = () => {
|
const onVisibilityChange = () => {
|
||||||
set(document.visibilityState === 'visible');
|
set(document.visibilityState === "visible");
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||||
|
|
||||||
return function stop() {
|
return function stop() {
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
import {Subject} from 'rxjs';
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
class BlackListManager {
|
class BlackListManager {
|
||||||
private list: number[] = [];
|
private list: string[] = [];
|
||||||
public onBlockStream: Subject<number> = new Subject();
|
public onBlockStream: Subject<string> = new Subject();
|
||||||
public onUnBlockStream: Subject<number> = new Subject();
|
public onUnBlockStream: Subject<string> = new Subject();
|
||||||
|
|
||||||
isBlackListed(userId: number): boolean {
|
isBlackListed(userUuid: string): boolean {
|
||||||
return this.list.find((data) => data === userId) !== undefined;
|
return this.list.find((data) => data === userUuid) !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
blackList(userId: number): void {
|
blackList(userUuid: string): void {
|
||||||
if (this.isBlackListed(userId)) return;
|
if (this.isBlackListed(userUuid)) return;
|
||||||
this.list.push(userId);
|
this.list.push(userUuid);
|
||||||
this.onBlockStream.next(userId);
|
this.onBlockStream.next(userUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelBlackList(userId: number): void {
|
cancelBlackList(userUuid: string): void {
|
||||||
this.list.splice(this.list.findIndex(data => data === userId), 1);
|
this.list.splice(
|
||||||
this.onUnBlockStream.next(userId);
|
this.list.findIndex((data) => data === userUuid),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
this.onUnBlockStream.next(userUuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
52
front/src/WebRtc/ColorGenerator.ts
Normal file
52
front/src/WebRtc/ColorGenerator.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export function getRandomColor(): string {
|
||||||
|
const golden_ratio_conjugate = 0.618033988749895;
|
||||||
|
let hue = Math.random();
|
||||||
|
hue += golden_ratio_conjugate;
|
||||||
|
hue %= 1;
|
||||||
|
return hsv_to_rgb(hue, 0.5, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo: test this.
|
||||||
|
function hsv_to_rgb(hue: number, saturation: number, brightness: number): string {
|
||||||
|
const h_i = Math.floor(hue * 6);
|
||||||
|
const f = hue * 6 - h_i;
|
||||||
|
const p = brightness * (1 - saturation);
|
||||||
|
const q = brightness * (1 - f * saturation);
|
||||||
|
const t = brightness * (1 - (1 - f) * saturation);
|
||||||
|
let r: number, g: number, b: number;
|
||||||
|
switch (h_i) {
|
||||||
|
case 0:
|
||||||
|
r = brightness;
|
||||||
|
g = t;
|
||||||
|
b = p;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
r = q;
|
||||||
|
g = brightness;
|
||||||
|
b = p;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
r = p;
|
||||||
|
g = brightness;
|
||||||
|
b = t;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
r = p;
|
||||||
|
g = q;
|
||||||
|
b = brightness;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
r = t;
|
||||||
|
g = p;
|
||||||
|
b = brightness;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
r = brightness;
|
||||||
|
g = p;
|
||||||
|
b = q;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "h_i cannot be " + h_i;
|
||||||
|
}
|
||||||
|
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
|
||||||
|
}
|
@ -1,231 +1,13 @@
|
|||||||
import {HtmlUtils} from "./HtmlUtils";
|
import { iframeListener } from "../Api/IframeListener";
|
||||||
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
import { chatMessagesStore } from "../Stores/ChatStore";
|
||||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
|
||||||
import {iframeListener} from "../Api/IframeListener";
|
|
||||||
import {showReportScreenStore} from "../Stores/ShowReportScreenStore";
|
|
||||||
|
|
||||||
export type SendMessageCallback = (message:string) => void;
|
|
||||||
|
|
||||||
export class DiscussionManager {
|
export class DiscussionManager {
|
||||||
private mainContainer: HTMLDivElement;
|
|
||||||
|
|
||||||
private divDiscuss?: HTMLDivElement;
|
|
||||||
private divParticipants?: HTMLDivElement;
|
|
||||||
private nbpParticipants?: HTMLParagraphElement;
|
|
||||||
private divMessages?: HTMLParagraphElement;
|
|
||||||
|
|
||||||
private participants: Map<number|string, HTMLDivElement> = new Map<number|string, HTMLDivElement>();
|
|
||||||
|
|
||||||
private activeDiscussion: boolean = false;
|
|
||||||
|
|
||||||
private sendMessageCallBack : Map<number|string, SendMessageCallback> = new Map<number|string, SendMessageCallback>();
|
|
||||||
|
|
||||||
private userInputManager?: UserInputManager;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
|
||||||
this.createDiscussPart(''); //todo: why do we always use empty string?
|
|
||||||
|
|
||||||
iframeListener.chatStream.subscribe((chatEvent) => {
|
iframeListener.chatStream.subscribe((chatEvent) => {
|
||||||
this.addMessage(chatEvent.author, chatEvent.message, false);
|
const userId = playersStore.addFacticePlayer(chatEvent.author);
|
||||||
this.showDiscussion();
|
chatMessagesStore.addExternalMessage(userId, chatEvent.message);
|
||||||
});
|
});
|
||||||
this.onSendMessageCallback('iframe_listener', (message) => {
|
|
||||||
iframeListener.sendUserInputChat(message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private createDiscussPart(name: string) {
|
|
||||||
this.divDiscuss = document.createElement('div');
|
|
||||||
this.divDiscuss.classList.add('discussion');
|
|
||||||
|
|
||||||
const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button');
|
|
||||||
buttonCloseDiscussion.classList.add('close-btn');
|
|
||||||
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
|
|
||||||
buttonCloseDiscussion.addEventListener('click', () => {
|
|
||||||
this.hideDiscussion();
|
|
||||||
});
|
|
||||||
this.divDiscuss.appendChild(buttonCloseDiscussion);
|
|
||||||
|
|
||||||
const myName: HTMLParagraphElement = document.createElement('p');
|
|
||||||
myName.innerText = name.toUpperCase();
|
|
||||||
this.nbpParticipants = document.createElement('p');
|
|
||||||
this.nbpParticipants.innerText = 'PARTICIPANTS (1)';
|
|
||||||
|
|
||||||
this.divParticipants = document.createElement('div');
|
|
||||||
this.divParticipants.classList.add('participants');
|
|
||||||
|
|
||||||
this.divMessages = document.createElement('div');
|
|
||||||
this.divMessages.classList.add('messages');
|
|
||||||
this.divMessages.innerHTML = "<h2>Local messages</h2>"
|
|
||||||
|
|
||||||
this.divDiscuss.appendChild(myName);
|
|
||||||
this.divDiscuss.appendChild(this.nbpParticipants);
|
|
||||||
this.divDiscuss.appendChild(this.divParticipants);
|
|
||||||
this.divDiscuss.appendChild(this.divMessages);
|
|
||||||
|
|
||||||
const sendDivMessage: HTMLDivElement = document.createElement('div');
|
|
||||||
sendDivMessage.classList.add('send-message');
|
|
||||||
const inputMessage: HTMLInputElement = document.createElement('input');
|
|
||||||
inputMessage.onfocus = () => {
|
|
||||||
if(this.userInputManager) {
|
|
||||||
this.userInputManager.disableControls();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inputMessage.onblur = () => {
|
|
||||||
if(this.userInputManager) {
|
|
||||||
this.userInputManager.restoreControls();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inputMessage.type = "text";
|
|
||||||
inputMessage.addEventListener('keyup', (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
if(inputMessage.value === null
|
|
||||||
|| inputMessage.value === ''
|
|
||||||
|| inputMessage.value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.addMessage(name, inputMessage.value, true);
|
|
||||||
for(const callback of this.sendMessageCallBack.values()) {
|
|
||||||
callback(inputMessage.value);
|
|
||||||
}
|
|
||||||
inputMessage.value = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sendDivMessage.appendChild(inputMessage);
|
|
||||||
this.divDiscuss.appendChild(sendDivMessage);
|
|
||||||
|
|
||||||
//append in main container
|
|
||||||
this.mainContainer.appendChild(this.divDiscuss);
|
|
||||||
|
|
||||||
this.addParticipant('me', 'Moi', undefined, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addParticipant(
|
|
||||||
userId: number|'me',
|
|
||||||
name: string|undefined,
|
|
||||||
img?: string|undefined,
|
|
||||||
isMe: boolean = false,
|
|
||||||
) {
|
|
||||||
const divParticipant: HTMLDivElement = document.createElement('div');
|
|
||||||
divParticipant.classList.add('participant');
|
|
||||||
divParticipant.id = `participant-${userId}`;
|
|
||||||
|
|
||||||
const divImgParticipant: HTMLImageElement = document.createElement('img');
|
|
||||||
divImgParticipant.src = 'resources/logos/boy.svg';
|
|
||||||
if (img !== undefined) {
|
|
||||||
divImgParticipant.src = img;
|
|
||||||
}
|
|
||||||
const divPParticipant: HTMLParagraphElement = document.createElement('p');
|
|
||||||
if(!name){
|
|
||||||
name = 'Anonymous';
|
|
||||||
}
|
|
||||||
divPParticipant.innerText = name;
|
|
||||||
|
|
||||||
divParticipant.appendChild(divImgParticipant);
|
|
||||||
divParticipant.appendChild(divPParticipant);
|
|
||||||
|
|
||||||
if(
|
|
||||||
!isMe
|
|
||||||
&& connectionManager.getConnexionType
|
|
||||||
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
|
|
||||||
&& userId !== 'me'
|
|
||||||
) {
|
|
||||||
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
|
|
||||||
reportBanUserAction.classList.add('report-btn')
|
|
||||||
reportBanUserAction.innerText = 'Report';
|
|
||||||
reportBanUserAction.addEventListener('click', () => {
|
|
||||||
showReportScreenStore.set({ userId: userId, userName: name ? name : ''});
|
|
||||||
});
|
|
||||||
divParticipant.appendChild(reportBanUserAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.divParticipants?.appendChild(divParticipant);
|
|
||||||
|
|
||||||
this.participants.set(userId, divParticipant);
|
|
||||||
|
|
||||||
this.updateParticipant(this.participants.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateParticipant(nb: number) {
|
|
||||||
if (!this.nbpParticipants) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addMessage(name: string, message: string, isMe: boolean = false) {
|
|
||||||
const divMessage: HTMLDivElement = document.createElement('div');
|
|
||||||
divMessage.classList.add('message');
|
|
||||||
if(isMe){
|
|
||||||
divMessage.classList.add('me');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pMessage: HTMLParagraphElement = document.createElement('p');
|
|
||||||
const date = new Date();
|
|
||||||
if(isMe){
|
|
||||||
name = 'Me';
|
|
||||||
} else {
|
|
||||||
name = HtmlUtils.escapeHtml(name);
|
|
||||||
}
|
|
||||||
pMessage.innerHTML = `<span style="font-weight: bold">${name}</span>
|
|
||||||
<span style="color:#bac2cc;display:inline-block;font-size:12px;">
|
|
||||||
${date.getHours()}:${date.getMinutes()}
|
|
||||||
</span>`;
|
|
||||||
divMessage.appendChild(pMessage);
|
|
||||||
|
|
||||||
const userMessage: HTMLParagraphElement = document.createElement('p');
|
|
||||||
userMessage.innerHTML = HtmlUtils.urlify(message);
|
|
||||||
userMessage.classList.add('body');
|
|
||||||
divMessage.appendChild(userMessage);
|
|
||||||
this.divMessages?.appendChild(divMessage);
|
|
||||||
|
|
||||||
//automatic scroll when there are new message
|
|
||||||
setTimeout(() => {
|
|
||||||
this.divMessages?.scroll({
|
|
||||||
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeParticipant(userId: number|string){
|
|
||||||
const element = this.participants.get(userId);
|
|
||||||
if(element){
|
|
||||||
element.remove();
|
|
||||||
this.participants.delete(userId);
|
|
||||||
}
|
|
||||||
//if all participant leave, hide discussion button
|
|
||||||
|
|
||||||
this.sendMessageCallBack.delete(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void {
|
|
||||||
this.sendMessageCallBack.set(userId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
get activatedDiscussion(){
|
|
||||||
return this.activeDiscussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
private showDiscussion(){
|
|
||||||
this.activeDiscussion = true;
|
|
||||||
this.divDiscuss?.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
private hideDiscussion(){
|
|
||||||
this.activeDiscussion = false;
|
|
||||||
this.divDiscuss?.classList.remove('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
public setUserInputManager(userInputManager : UserInputManager){
|
|
||||||
this.userInputManager = userInputManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public showDiscussionPart(){
|
|
||||||
this.showDiscussion();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export class HtmlUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static escapeHtml(html: string): string {
|
public static escapeHtml(html: string): string {
|
||||||
const text = document.createTextNode(html);
|
const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g,'<br/>'));
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.appendChild(text);
|
p.appendChild(text);
|
||||||
return p.innerHTML;
|
return p.innerHTML;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||||
import {HtmlUtils} from "./HtmlUtils";
|
import { HtmlUtils } from "./HtmlUtils";
|
||||||
|
|
||||||
export enum LayoutMode {
|
export enum LayoutMode {
|
||||||
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
|
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
|
||||||
@ -15,40 +15,40 @@ export enum DivImportance {
|
|||||||
Normal = "Normal",
|
Normal = "Normal",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
|
export const ON_ACTION_TRIGGER_BUTTON = "onaction";
|
||||||
|
|
||||||
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
|
export const TRIGGER_WEBSITE_PROPERTIES = "openWebsiteTrigger";
|
||||||
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger';
|
export const TRIGGER_JITSI_PROPERTIES = "jitsiTrigger";
|
||||||
|
|
||||||
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage';
|
export const WEBSITE_MESSAGE_PROPERTIES = "openWebsiteTriggerMessage";
|
||||||
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage';
|
export const JITSI_MESSAGE_PROPERTIES = "jitsiTriggerMessage";
|
||||||
|
|
||||||
export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
|
export const AUDIO_VOLUME_PROPERTY = "audioVolume";
|
||||||
export const AUDIO_LOOP_PROPERTY = 'audioLoop';
|
export const AUDIO_LOOP_PROPERTY = "audioLoop";
|
||||||
|
|
||||||
export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number};
|
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
|
||||||
|
|
||||||
class LayoutManager {
|
class LayoutManager {
|
||||||
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
||||||
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||||
|
|
||||||
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
|
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
|
||||||
//delete previous element
|
//delete previous element
|
||||||
this.removeActionButton(id, userInputManager);
|
this.removeActionButton(id, userInputManager);
|
||||||
|
|
||||||
//create div and text html component
|
//create div and text html component
|
||||||
const p = document.createElement('p');
|
const p = document.createElement("p");
|
||||||
p.classList.add('action-body');
|
p.classList.add("action-body");
|
||||||
p.innerText = text;
|
p.innerText = text;
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement("div");
|
||||||
div.classList.add('action');
|
div.classList.add("action");
|
||||||
div.id = id;
|
div.id = id;
|
||||||
div.appendChild(p);
|
div.appendChild(p);
|
||||||
|
|
||||||
this.actionButtonInformation.set(id, div);
|
this.actionButtonInformation.set(id, div);
|
||||||
|
|
||||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||||
mainContainer.appendChild(div);
|
mainContainer.appendChild(div);
|
||||||
|
|
||||||
//add trigger action
|
//add trigger action
|
||||||
@ -57,42 +57,42 @@ class LayoutManager {
|
|||||||
userInputManager.addSpaceEventListner(callBack);
|
userInputManager.addSpaceEventListner(callBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeActionButton(id: string, userInputManager?: UserInputManager){
|
public removeActionButton(id: string, userInputManager?: UserInputManager) {
|
||||||
//delete previous element
|
//delete previous element
|
||||||
const previousDiv = this.actionButtonInformation.get(id);
|
const previousDiv = this.actionButtonInformation.get(id);
|
||||||
if(previousDiv){
|
if (previousDiv) {
|
||||||
previousDiv.remove();
|
previousDiv.remove();
|
||||||
this.actionButtonInformation.delete(id);
|
this.actionButtonInformation.delete(id);
|
||||||
}
|
}
|
||||||
const previousEventCallback = this.actionButtonTrigger.get(id);
|
const previousEventCallback = this.actionButtonTrigger.get(id);
|
||||||
if(previousEventCallback && userInputManager){
|
if (previousEventCallback && userInputManager) {
|
||||||
userInputManager.removeSpaceEventListner(previousEventCallback);
|
userInputManager.removeSpaceEventListner(previousEventCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager){
|
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
|
||||||
//delete previous element
|
//delete previous element
|
||||||
for ( const [key, value] of this.actionButtonInformation ) {
|
for (const [key, value] of this.actionButtonInformation) {
|
||||||
this.removeActionButton(key, userInputManager);
|
this.removeActionButton(key, userInputManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
//create div and text html component
|
//create div and text html component
|
||||||
const p = document.createElement('p');
|
const p = document.createElement("p");
|
||||||
p.classList.add('action-body');
|
p.classList.add("action-body");
|
||||||
p.innerText = text;
|
p.innerText = text;
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement("div");
|
||||||
div.classList.add('action');
|
div.classList.add("action");
|
||||||
div.classList.add(id);
|
div.classList.add(id);
|
||||||
div.id = id;
|
div.id = id;
|
||||||
div.appendChild(p);
|
div.appendChild(p);
|
||||||
|
|
||||||
this.actionButtonInformation.set(id, div);
|
this.actionButtonInformation.set(id, div);
|
||||||
|
|
||||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||||
mainContainer.appendChild(div);
|
mainContainer.appendChild(div);
|
||||||
//add trigger action
|
//add trigger action
|
||||||
if(callBack){
|
if (callBack) {
|
||||||
div.onpointerdown = () => {
|
div.onpointerdown = () => {
|
||||||
callBack();
|
callBack();
|
||||||
this.removeActionButton(id, userInputManager);
|
this.removeActionButton(id, userInputManager);
|
||||||
@ -102,7 +102,7 @@ class LayoutManager {
|
|||||||
//remove it after 10 sec
|
//remove it after 10 sec
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.removeActionButton(id, userInputManager);
|
this.removeActionButton(id, userInputManager);
|
||||||
}, 10000)
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,73 +1,65 @@
|
|||||||
import { DivImportance, layoutManager } from "./LayoutManager";
|
import { layoutManager } from "./LayoutManager";
|
||||||
import { HtmlUtils } from "./HtmlUtils";
|
import { HtmlUtils } from "./HtmlUtils";
|
||||||
import { discussionManager, SendMessageCallback } from "./DiscussionManager";
|
|
||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||||
import { localUserStore } from "../Connexion/LocalUserStore";
|
import { localStreamStore } from "../Stores/MediaStore";
|
||||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
||||||
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
|
|
||||||
import {
|
|
||||||
localStreamStore,
|
|
||||||
} from "../Stores/MediaStore";
|
|
||||||
import {
|
|
||||||
screenSharingLocalStreamStore
|
|
||||||
} from "../Stores/ScreenSharingStore";
|
|
||||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
|
||||||
|
|
||||||
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
|
|
||||||
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
||||||
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||||
|
|
||||||
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
|
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
|
||||||
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
||||||
|
|
||||||
export class MediaManager {
|
export class MediaManager {
|
||||||
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||||
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private focused: boolean = 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;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
||||||
//Check of ask notification navigator permission
|
|
||||||
this.getNotification();
|
|
||||||
|
|
||||||
localStreamStore.subscribe((result) => {
|
localStreamStore.subscribe((result) => {
|
||||||
if (result.type === 'error') {
|
if (result.type === "error") {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => {
|
layoutManager.addInformation(
|
||||||
helpCameraSettingsVisibleStore.set(true);
|
"warning",
|
||||||
}, this.userInputManager);
|
"Camera access denied. Click here and check your browser permissions.",
|
||||||
|
() => {
|
||||||
|
helpCameraSettingsVisibleStore.set(true);
|
||||||
|
},
|
||||||
|
this.userInputManager
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
screenSharingLocalStreamStore.subscribe((result) => {
|
screenSharingLocalStreamStore.subscribe((result) => {
|
||||||
if (result.type === 'error') {
|
if (result.type === "error") {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => {
|
layoutManager.addInformation(
|
||||||
helpCameraSettingsVisibleStore.set(true);
|
"warning",
|
||||||
}, this.userInputManager);
|
"Screen sharing denied. Click here and check your browser permissions.",
|
||||||
|
() => {
|
||||||
|
helpCameraSettingsVisibleStore.set(true);
|
||||||
|
},
|
||||||
|
this.userInputManager
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public showGameOverlay(): void {
|
public showGameOverlay(): void {
|
||||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
|
||||||
gameOverlay.classList.add('active');
|
gameOverlay.classList.add("active");
|
||||||
|
|
||||||
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
||||||
const functionTrigger = () => {
|
const functionTrigger = () => {
|
||||||
this.triggerCloseJitsiFrameButton();
|
this.triggerCloseJitsiFrameButton();
|
||||||
}
|
};
|
||||||
buttonCloseFrame.removeEventListener('click', () => {
|
buttonCloseFrame.removeEventListener("click", () => {
|
||||||
buttonCloseFrame.blur();
|
buttonCloseFrame.blur();
|
||||||
functionTrigger();
|
functionTrigger();
|
||||||
});
|
});
|
||||||
@ -76,14 +68,14 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public hideGameOverlay(): void {
|
public hideGameOverlay(): void {
|
||||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
|
||||||
gameOverlay.classList.remove('active');
|
gameOverlay.classList.remove("active");
|
||||||
|
|
||||||
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
||||||
const functionTrigger = () => {
|
const functionTrigger = () => {
|
||||||
this.triggerCloseJitsiFrameButton();
|
this.triggerCloseJitsiFrameButton();
|
||||||
}
|
};
|
||||||
buttonCloseFrame.addEventListener('click', () => {
|
buttonCloseFrame.addEventListener("click", () => {
|
||||||
buttonCloseFrame.blur();
|
buttonCloseFrame.blur();
|
||||||
functionTrigger();
|
functionTrigger();
|
||||||
});
|
});
|
||||||
@ -100,7 +92,7 @@ export class MediaManager {
|
|||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
|
element.classList.add("active"); //todo: why does a method 'disable' add a class 'active'?
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledMicrophoneByUserId(userId: number) {
|
enabledMicrophoneByUserId(userId: number) {
|
||||||
@ -108,7 +100,7 @@ export class MediaManager {
|
|||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
|
element.classList.remove("active"); //todo: why does a method 'enable' remove a class 'active'?
|
||||||
}
|
}
|
||||||
|
|
||||||
disabledVideoByUserId(userId: number) {
|
disabledVideoByUserId(userId: number) {
|
||||||
@ -134,8 +126,8 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleBlockLogo(userId: number, show: boolean): void {
|
toggleBlockLogo(userId: number, show: boolean): void {
|
||||||
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId);
|
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>("blocking-" + userId);
|
||||||
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
|
show ? blockLogoElement.classList.add("active") : blockLogoElement.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
isError(userId: string): void {
|
isError(userId: string): void {
|
||||||
@ -144,27 +136,28 @@ export class MediaManager {
|
|||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement | null;
|
const errorDiv = element.getElementsByClassName("rtc-error").item(0) as HTMLDivElement | null;
|
||||||
if (errorDiv === null) {
|
if (errorDiv === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
errorDiv.style.display = 'block';
|
errorDiv.style.display = "block";
|
||||||
}
|
}
|
||||||
isErrorScreenSharing(userId: string): void {
|
isErrorScreenSharing(userId: string): void {
|
||||||
this.isError(this.getScreenSharingId(userId));
|
this.isError(this.getScreenSharingId(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private getSpinner(userId: string): HTMLDivElement | null {
|
private getSpinner(userId: string): HTMLDivElement | null {
|
||||||
const element = document.getElementById(`div-${userId}`);
|
const element = document.getElementById(`div-${userId}`);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
|
const connectingSpinnerDiv = element
|
||||||
|
.getElementsByClassName("connecting-spinner")
|
||||||
|
.item(0) as HTMLDivElement | null;
|
||||||
return connectingSpinnerDiv;
|
return connectingSpinnerDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){
|
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
|
||||||
this.triggerCloseJistiFrame.set(id, Function);
|
this.triggerCloseJistiFrame.set(id, Function);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,67 +171,35 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addNewMessage(name: string, message: string, isMe: boolean = false) {
|
public setUserInputManager(userInputManager: UserInputManager) {
|
||||||
discussionManager.addMessage(name, message, isMe);
|
|
||||||
|
|
||||||
//when there are new message, show discussion
|
|
||||||
if (!discussionManager.activatedDiscussion) {
|
|
||||||
discussionManager.showDiscussionPart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
|
|
||||||
discussionManager.onSendMessageCallback(userId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setUserInputManager(userInputManager : UserInputManager){
|
|
||||||
this.userInputManager = userInputManager;
|
this.userInputManager = userInputManager;
|
||||||
discussionManager.setUserInputManager(userInputManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNotification(){
|
public hasNotification(): boolean {
|
||||||
//Get notification
|
return Notification.permission === "granted";
|
||||||
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
|
}
|
||||||
if (this.checkNotificationPromise()) {
|
|
||||||
Notification.requestPermission().catch((err) => {
|
public requestNotification() {
|
||||||
console.error(`Notification permission error`, err);
|
if (window.Notification && Notification.permission !== "granted") {
|
||||||
});
|
return Notification.requestPermission();
|
||||||
} else {
|
} else {
|
||||||
Notification.requestPermission();
|
return Promise.reject();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public createNotification(userName: string) {
|
||||||
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
|
if (document.hasFocus()) {
|
||||||
* if we are on Safari...
|
|
||||||
*
|
|
||||||
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
|
||||||
*/
|
|
||||||
private checkNotificationPromise(): boolean {
|
|
||||||
try {
|
|
||||||
Notification.requestPermission().then();
|
|
||||||
} catch(e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public createNotification(userName: string){
|
|
||||||
if(this.focused){
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (window.Notification && Notification.permission === "granted") {
|
|
||||||
const title = 'WorkAdventure';
|
if (this.hasNotification()) {
|
||||||
|
const title = `${userName} wants to discuss with you`;
|
||||||
const options = {
|
const options = {
|
||||||
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`,
|
icon: "/resources/logos/logo-WA-min.png",
|
||||||
icon: '/resources/logos/logo-WA-min.png',
|
image: "/resources/logos/logo-WA-min.png",
|
||||||
image: '/resources/logos/logo-WA-min.png',
|
badge: "/resources/logos/logo-WA-min.png",
|
||||||
badge: '/resources/logos/logo-WA-min.png',
|
|
||||||
};
|
};
|
||||||
new Notification(title, options);
|
new Notification(title, options);
|
||||||
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import type * as SimplePeerNamespace from "simple-peer";
|
import type * as SimplePeerNamespace from "simple-peer";
|
||||||
import {mediaManager} from "./MediaManager";
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
|
||||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||||
import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer";
|
import { Readable, readable } from "svelte/store";
|
||||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
import { videoFocusStore } from "../Stores/VideoFocusStore";
|
||||||
import {Readable, readable, writable, Writable} from "svelte/store";
|
import { getIceServersConfig } from "../Components/Video/utils";
|
||||||
import {videoFocusStore} from "../Stores/VideoFocusStore";
|
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A peer connection used to transmit video / audio signals between 2 peers.
|
* A peer connection used to transmit video / audio signals between 2 peers.
|
||||||
@ -16,7 +15,7 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
/**
|
/**
|
||||||
* Whether this connection is currently receiving a video stream from a remote user.
|
* Whether this connection is currently receiving a video stream from a remote user.
|
||||||
*/
|
*/
|
||||||
private isReceivingStream:boolean = false;
|
private isReceivingStream: boolean = false;
|
||||||
public toClose: boolean = false;
|
public toClose: boolean = false;
|
||||||
public _connected: boolean = false;
|
public _connected: boolean = false;
|
||||||
public readonly userId: number;
|
public readonly userId: number;
|
||||||
@ -24,29 +23,25 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
public readonly streamStore: Readable<MediaStream | null>;
|
public readonly streamStore: Readable<MediaStream | null>;
|
||||||
public readonly statusStore: Readable<PeerStatus>;
|
public readonly statusStore: Readable<PeerStatus>;
|
||||||
|
|
||||||
constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) {
|
constructor(
|
||||||
|
user: UserSimplePeerInterface,
|
||||||
|
initiator: boolean,
|
||||||
|
public readonly userName: string,
|
||||||
|
private connection: RoomConnection,
|
||||||
|
stream: MediaStream | null
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator,
|
||||||
//reconnectTimer: 10000,
|
|
||||||
config: {
|
config: {
|
||||||
iceServers: [
|
iceServers: getIceServersConfig(user),
|
||||||
{
|
},
|
||||||
urls: STUN_SERVER.split(',')
|
|
||||||
},
|
|
||||||
TURN_SERVER !== '' ? {
|
|
||||||
urls: TURN_SERVER.split(','),
|
|
||||||
username: user.webRtcUser || TURN_USER,
|
|
||||||
credential: user.webRtcPassword || TURN_PASSWORD
|
|
||||||
} : undefined,
|
|
||||||
].filter((value) => value !== undefined)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userId = user.userId;
|
this.userId = user.userId;
|
||||||
this.uniqueId = 'screensharing_'+this.userId;
|
this.uniqueId = "screensharing_" + this.userId;
|
||||||
|
|
||||||
this.streamStore = readable<MediaStream|null>(null, (set) => {
|
this.streamStore = readable<MediaStream | null>(null, (set) => {
|
||||||
const onStream = (stream: MediaStream|null) => {
|
const onStream = (stream: MediaStream | null) => {
|
||||||
videoFocusStore.focus(this);
|
videoFocusStore.focus(this);
|
||||||
set(stream);
|
set(stream);
|
||||||
};
|
};
|
||||||
@ -54,71 +49,71 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
|
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
|
||||||
// It seems there is no native way to detect that.
|
// It seems there is no native way to detect that.
|
||||||
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
|
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
|
||||||
const message = JSON.parse(chunk.toString('utf8'));
|
const message = JSON.parse(chunk.toString("utf8"));
|
||||||
if (message.streamEnded !== true) {
|
if (message.streamEnded !== true) {
|
||||||
console.error('Unexpected message on screen sharing peer connection');
|
console.error("Unexpected message on screen sharing peer connection");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
set(null);
|
set(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
this.on('stream', onStream);
|
this.on("stream", onStream);
|
||||||
this.on('data', onData);
|
this.on("data", onData);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.off('stream', onStream);
|
this.off("stream", onStream);
|
||||||
this.off('data', onData);
|
this.off("data", onData);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
||||||
const onConnect = () => {
|
const onConnect = () => {
|
||||||
set('connected');
|
set("connected");
|
||||||
};
|
};
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
set('error');
|
set("error");
|
||||||
};
|
};
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
set('closed');
|
set("closed");
|
||||||
};
|
};
|
||||||
|
|
||||||
this.on('connect', onConnect);
|
this.on("connect", onConnect);
|
||||||
this.on('error', onError);
|
this.on("error", onError);
|
||||||
this.on('close', onClose);
|
this.on("close", onClose);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.off('connect', onConnect);
|
this.off("connect", onConnect);
|
||||||
this.off('error', onError);
|
this.off("error", onError);
|
||||||
this.off('close', onClose);
|
this.off("close", onClose);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
//start listen signal for the peer connection
|
//start listen signal for the peer connection
|
||||||
this.on('signal', (data: unknown) => {
|
this.on("signal", (data: unknown) => {
|
||||||
this.sendWebrtcScreenSharingSignal(data);
|
this.sendWebrtcScreenSharingSignal(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('stream', (stream: MediaStream) => {
|
this.on("stream", (stream: MediaStream) => {
|
||||||
this.stream(stream);
|
this.stream(stream);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('close', () => {
|
this.on("close", () => {
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
this.toClose = true;
|
this.toClose = true;
|
||||||
this.destroy();
|
this.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
this.on('error', (err: any) => {
|
this.on("error", (err: any) => {
|
||||||
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
|
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('connect', () => {
|
this.on("connect", () => {
|
||||||
this._connected = true;
|
this._connected = true;
|
||||||
console.info(`connect => ${this.userId}`);
|
console.info(`connect => ${this.userId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.once('finish', () => {
|
this.once("finish", () => {
|
||||||
this._onFinish();
|
this._onFinish();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -130,7 +125,7 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
private sendWebrtcScreenSharingSignal(data: unknown) {
|
private sendWebrtcScreenSharingSignal(data: unknown) {
|
||||||
try {
|
try {
|
||||||
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
|
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +134,7 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
* Sends received stream to screen.
|
* Sends received stream to screen.
|
||||||
*/
|
*/
|
||||||
private stream(stream?: MediaStream) {
|
private stream(stream?: MediaStream) {
|
||||||
if(!stream){
|
if (!stream) {
|
||||||
this.isReceivingStream = false;
|
this.isReceivingStream = false;
|
||||||
} else {
|
} else {
|
||||||
this.isReceivingStream = true;
|
this.isReceivingStream = true;
|
||||||
@ -152,8 +147,8 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
|
|
||||||
public destroy(error?: Error): void {
|
public destroy(error?: Error): void {
|
||||||
try {
|
try {
|
||||||
this._connected = false
|
this._connected = false;
|
||||||
if(!this.toClose){
|
if (!this.toClose) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
||||||
@ -162,24 +157,24 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
super.destroy(error);
|
super.destroy(error);
|
||||||
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
|
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("ScreenSharingPeer::destroy", err)
|
console.error("ScreenSharingPeer::destroy", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFinish () {
|
_onFinish() {
|
||||||
if (this.destroyed) return
|
if (this.destroyed) return;
|
||||||
const destroySoon = () => {
|
const destroySoon = () => {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
};
|
||||||
if (this._connected) {
|
if (this._connected) {
|
||||||
destroySoon();
|
destroySoon();
|
||||||
} else {
|
} else {
|
||||||
this.once('connect', destroySoon);
|
this.once("connect", destroySoon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
|
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
|
||||||
this.removeStream(stream);
|
this.removeStream(stream);
|
||||||
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true})));
|
this.write(new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,23 @@ import type {
|
|||||||
WebRtcDisconnectMessageInterface,
|
WebRtcDisconnectMessageInterface,
|
||||||
WebRtcSignalReceivedMessageInterface,
|
WebRtcSignalReceivedMessageInterface,
|
||||||
} from "../Connexion/ConnexionModels";
|
} from "../Connexion/ConnexionModels";
|
||||||
import {
|
import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
|
||||||
mediaManager,
|
import { ScreenSharingPeer } from "./ScreenSharingPeer";
|
||||||
StartScreenSharingCallback,
|
import { MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer } from "./VideoPeer";
|
||||||
StopScreenSharingCallback,
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
} from "./MediaManager";
|
import { blackListManager } from "./BlackListManager";
|
||||||
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
import { get } from "svelte/store";
|
||||||
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
|
import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||||
import {blackListManager} from "./BlackListManager";
|
import { discussionManager } from "./DiscussionManager";
|
||||||
import {get} from "svelte/store";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
import { newChatMessageStore } from "../Stores/ChatStore";
|
||||||
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
|
|
||||||
import {discussionManager} from "./DiscussionManager";
|
|
||||||
|
|
||||||
export interface UserSimplePeerInterface{
|
export interface UserSimplePeerInterface {
|
||||||
userId: number;
|
userId: number;
|
||||||
name?: string;
|
|
||||||
initiator?: boolean;
|
initiator?: boolean;
|
||||||
webRtcUser?: string|undefined;
|
webRtcUser?: string | undefined;
|
||||||
webRtcPassword?: string|undefined;
|
webRtcPassword?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RemotePeer = VideoPeer | ScreenSharingPeer;
|
export type RemotePeer = VideoPeer | ScreenSharingPeer;
|
||||||
@ -45,36 +42,40 @@ export class SimplePeer {
|
|||||||
private readonly unsubscribers: (() => void)[] = [];
|
private readonly unsubscribers: (() => void)[] = [];
|
||||||
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
||||||
private readonly userId: number;
|
private readonly userId: number;
|
||||||
private lastWebrtcUserName: string|undefined;
|
private lastWebrtcUserName: string | undefined;
|
||||||
private lastWebrtcPassword: string|undefined;
|
private lastWebrtcPassword: string | undefined;
|
||||||
|
|
||||||
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
|
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
|
||||||
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
|
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
|
||||||
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
|
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
|
||||||
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
|
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
|
||||||
|
|
||||||
this.unsubscribers.push(localStreamStore.subscribe((streamResult) => {
|
this.unsubscribers.push(
|
||||||
this.sendLocalVideoStream(streamResult);
|
localStreamStore.subscribe((streamResult) => {
|
||||||
}));
|
this.sendLocalVideoStream(streamResult);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
let localScreenCapture: MediaStream|null = null;
|
let localScreenCapture: MediaStream | null = null;
|
||||||
|
|
||||||
this.unsubscribers.push(screenSharingLocalStreamStore.subscribe((streamResult) => {
|
this.unsubscribers.push(
|
||||||
if (streamResult.type === 'error') {
|
screenSharingLocalStreamStore.subscribe((streamResult) => {
|
||||||
// Let's ignore screen sharing errors, we will deal with those in a different way.
|
if (streamResult.type === "error") {
|
||||||
return;
|
// Let's ignore screen sharing errors, we will deal with those in a different way.
|
||||||
}
|
return;
|
||||||
|
|
||||||
if (streamResult.stream !== null) {
|
|
||||||
localScreenCapture = streamResult.stream;
|
|
||||||
this.sendLocalScreenSharingStream(localScreenCapture);
|
|
||||||
} else {
|
|
||||||
if (localScreenCapture) {
|
|
||||||
this.stopLocalScreenSharingStream(localScreenCapture);
|
|
||||||
localScreenCapture = null;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}));
|
if (streamResult.stream !== null) {
|
||||||
|
localScreenCapture = streamResult.stream;
|
||||||
|
this.sendLocalScreenSharingStream(localScreenCapture);
|
||||||
|
} else {
|
||||||
|
if (localScreenCapture) {
|
||||||
|
this.stopLocalScreenSharingStream(localScreenCapture);
|
||||||
|
localScreenCapture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.userId = Connection.getUserId();
|
this.userId = Connection.getUserId();
|
||||||
this.initialise();
|
this.initialise();
|
||||||
@ -92,7 +93,6 @@ export class SimplePeer {
|
|||||||
* permit to listen when user could start visio
|
* permit to listen when user could start visio
|
||||||
*/
|
*/
|
||||||
private initialise() {
|
private initialise() {
|
||||||
|
|
||||||
//receive signal by gemer
|
//receive signal by gemer
|
||||||
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
|
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
|
||||||
this.receiveWebrtcSignal(message);
|
this.receiveWebrtcSignal(message);
|
||||||
@ -122,12 +122,12 @@ export class SimplePeer {
|
|||||||
// This would be symmetrical to the way we handle disconnection.
|
// This would be symmetrical to the way we handle disconnection.
|
||||||
|
|
||||||
//start connection
|
//start connection
|
||||||
if(!user.initiator){
|
if (!user.initiator) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const streamResult = get(localStreamStore);
|
const streamResult = get(localStreamStore);
|
||||||
let stream : MediaStream | null = null;
|
let stream: MediaStream | null = null;
|
||||||
if (streamResult.type === 'success' && streamResult.stream) {
|
if (streamResult.type === "success" && streamResult.stream) {
|
||||||
stream = streamResult.stream;
|
stream = streamResult.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,15 +137,15 @@ export class SimplePeer {
|
|||||||
/**
|
/**
|
||||||
* create peer connection to bind users
|
* create peer connection to bind users
|
||||||
*/
|
*/
|
||||||
private createPeerConnection(user : UserSimplePeerInterface, localStream: MediaStream | null) : VideoPeer | null {
|
private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null {
|
||||||
const peerConnection = this.PeerConnectionArray.get(user.userId)
|
const peerConnection = this.PeerConnectionArray.get(user.userId);
|
||||||
if (peerConnection) {
|
if (peerConnection) {
|
||||||
if (peerConnection.destroyed) {
|
if (peerConnection.destroyed) {
|
||||||
peerConnection.toClose = true;
|
peerConnection.toClose = true;
|
||||||
peerConnection.destroy();
|
peerConnection.destroy();
|
||||||
const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId);
|
const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId);
|
||||||
if (!peerConnexionDeleted) {
|
if (!peerConnexionDeleted) {
|
||||||
throw 'Error to delete peer connection';
|
throw "Error to delete peer connection";
|
||||||
}
|
}
|
||||||
//return this.createPeerConnection(user, localStream);
|
//return this.createPeerConnection(user, localStream);
|
||||||
} else {
|
} else {
|
||||||
@ -154,36 +154,26 @@ export class SimplePeer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = user.name;
|
const name = this.getName(user.userId);
|
||||||
if (!name) {
|
|
||||||
name = this.getName(user.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
discussionManager.removeParticipant(user.userId);
|
|
||||||
|
|
||||||
this.lastWebrtcUserName = user.webRtcUser;
|
this.lastWebrtcUserName = user.webRtcUser;
|
||||||
this.lastWebrtcPassword = user.webRtcPassword;
|
this.lastWebrtcPassword = user.webRtcPassword;
|
||||||
|
|
||||||
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
|
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
|
||||||
|
|
||||||
//permit to send message
|
|
||||||
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
|
|
||||||
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), userId: this.userId, message: message})));
|
|
||||||
});
|
|
||||||
|
|
||||||
peer.toClose = false;
|
peer.toClose = false;
|
||||||
// When a connection is established to a video stream, and if a screen sharing is taking place,
|
// When a connection is established to a video stream, and if a screen sharing is taking place,
|
||||||
// the user sharing screen should also initiate a connection to the remote user!
|
// the user sharing screen should also initiate a connection to the remote user!
|
||||||
peer.on('connect', () => {
|
peer.on("connect", () => {
|
||||||
const streamResult = get(screenSharingLocalStreamStore);
|
const streamResult = get(screenSharingLocalStreamStore);
|
||||||
if (streamResult.type === 'success' && streamResult.stream !== null) {
|
if (streamResult.type === "success" && streamResult.stream !== null) {
|
||||||
this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
|
this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Create a notification for first user in circle discussion
|
//Create a notification for first user in circle discussion
|
||||||
if(this.PeerConnectionArray.size === 0){
|
if (this.PeerConnectionArray.size === 0) {
|
||||||
mediaManager.createNotification(user.name??'');
|
mediaManager.createNotification(name);
|
||||||
}
|
}
|
||||||
this.PeerConnectionArray.set(user.userId, peer);
|
this.PeerConnectionArray.set(user.userId, peer);
|
||||||
|
|
||||||
@ -194,29 +184,27 @@ export class SimplePeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getName(userId: number): string {
|
private getName(userId: number): string {
|
||||||
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
|
return playersStore.getPlayerById(userId)?.name || "";
|
||||||
if (userSearch) {
|
|
||||||
return userSearch.name || '';
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create peer connection to bind users
|
* create peer connection to bind users
|
||||||
*/
|
*/
|
||||||
private createPeerScreenSharingConnection(user : UserSimplePeerInterface, stream: MediaStream | null) : ScreenSharingPeer | null{
|
private createPeerScreenSharingConnection(
|
||||||
|
user: UserSimplePeerInterface,
|
||||||
|
stream: MediaStream | null
|
||||||
|
): ScreenSharingPeer | null {
|
||||||
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
|
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
|
||||||
if(peerConnection){
|
if (peerConnection) {
|
||||||
if(peerConnection.destroyed){
|
if (peerConnection.destroyed) {
|
||||||
peerConnection.toClose = true;
|
peerConnection.toClose = true;
|
||||||
peerConnection.destroy();
|
peerConnection.destroy();
|
||||||
const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId);
|
const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId);
|
||||||
if(!peerConnexionDeleted){
|
if (!peerConnexionDeleted) {
|
||||||
throw 'Error to delete peer connection';
|
throw "Error to delete peer connection";
|
||||||
}
|
}
|
||||||
this.createPeerConnection(user, stream);
|
this.createPeerConnection(user, stream);
|
||||||
}else {
|
} else {
|
||||||
peerConnection.toClose = false;
|
peerConnection.toClose = false;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -230,7 +218,13 @@ export class SimplePeer {
|
|||||||
|
|
||||||
const name = this.getName(user.userId);
|
const name = this.getName(user.userId);
|
||||||
|
|
||||||
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream);
|
const peer = new ScreenSharingPeer(
|
||||||
|
user,
|
||||||
|
user.initiator ? user.initiator : false,
|
||||||
|
name,
|
||||||
|
this.Connection,
|
||||||
|
stream
|
||||||
|
);
|
||||||
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
|
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
|
||||||
|
|
||||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||||
@ -242,11 +236,13 @@ export class SimplePeer {
|
|||||||
/**
|
/**
|
||||||
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
||||||
*/
|
*/
|
||||||
private closeConnection(userId : number) {
|
private closeConnection(userId: number) {
|
||||||
try {
|
try {
|
||||||
const peer = this.PeerConnectionArray.get(userId);
|
const peer = this.PeerConnectionArray.get(userId);
|
||||||
if (peer === undefined) {
|
if (peer === undefined) {
|
||||||
console.warn("closeConnection => Tried to close connection for user "+userId+" but could not find user");
|
console.warn(
|
||||||
|
"closeConnection => Tried to close connection for user " + userId + " but could not find user"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//create temp perr to close
|
//create temp perr to close
|
||||||
@ -257,18 +253,18 @@ export class SimplePeer {
|
|||||||
|
|
||||||
this.closeScreenSharingConnection(userId);
|
this.closeScreenSharingConnection(userId);
|
||||||
|
|
||||||
const userIndex = this.Users.findIndex(user => user.userId === userId);
|
const userIndex = this.Users.findIndex((user) => user.userId === userId);
|
||||||
if(userIndex < 0){
|
if (userIndex < 0) {
|
||||||
throw 'Couldn\'t delete user';
|
throw "Couldn't delete user";
|
||||||
} else {
|
} else {
|
||||||
this.Users.splice(userIndex, 1);
|
this.Users.splice(userIndex, 1);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("closeConnection", err)
|
console.error("closeConnection", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
//if user left discussion, clear array peer connection of sharing
|
//if user left discussion, clear array peer connection of sharing
|
||||||
if(this.Users.length === 0) {
|
if (this.Users.length === 0) {
|
||||||
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
|
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
|
||||||
this.closeScreenSharingConnection(userId);
|
this.closeScreenSharingConnection(userId);
|
||||||
this.PeerScreenSharingConnectionArray.delete(userId);
|
this.PeerScreenSharingConnectionArray.delete(userId);
|
||||||
@ -283,12 +279,16 @@ export class SimplePeer {
|
|||||||
/**
|
/**
|
||||||
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
||||||
*/
|
*/
|
||||||
private closeScreenSharingConnection(userId : number) {
|
private closeScreenSharingConnection(userId: number) {
|
||||||
try {
|
try {
|
||||||
//mediaManager.removeActiveScreenSharingVideo("" + userId);
|
//mediaManager.removeActiveScreenSharingVideo("" + userId);
|
||||||
const peer = this.PeerScreenSharingConnectionArray.get(userId);
|
const peer = this.PeerScreenSharingConnectionArray.get(userId);
|
||||||
if (peer === undefined) {
|
if (peer === undefined) {
|
||||||
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user")
|
console.warn(
|
||||||
|
"closeScreenSharingConnection => Tried to close connection for user " +
|
||||||
|
userId +
|
||||||
|
" but could not find user"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
||||||
@ -301,7 +301,7 @@ export class SimplePeer {
|
|||||||
}*/
|
}*/
|
||||||
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
|
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("closeConnection", err)
|
console.error("closeConnection", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,10 +328,10 @@ export class SimplePeer {
|
|||||||
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
|
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
|
||||||
try {
|
try {
|
||||||
//if offer type, create peer connection
|
//if offer type, create peer connection
|
||||||
if(data.signal.type === "offer"){
|
if (data.signal.type === "offer") {
|
||||||
const streamResult = get(localStreamStore);
|
const streamResult = get(localStreamStore);
|
||||||
let stream : MediaStream | null = null;
|
let stream: MediaStream | null = null;
|
||||||
if (streamResult.type === 'success' && streamResult.stream) {
|
if (streamResult.type === "success" && streamResult.stream) {
|
||||||
stream = streamResult.stream;
|
stream = streamResult.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +341,7 @@ export class SimplePeer {
|
|||||||
if (peer !== undefined) {
|
if (peer !== undefined) {
|
||||||
peer.signal(data.signal);
|
peer.signal(data.signal);
|
||||||
} else {
|
} else {
|
||||||
console.error('Could not find peer whose ID is "'+data.userId+'" in PeerConnectionArray');
|
console.error('Could not find peer whose ID is "' + data.userId + '" in PeerConnectionArray');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
|
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
|
||||||
@ -349,25 +349,28 @@ export class SimplePeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
|
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
|
||||||
if (blackListManager.isBlackListed(data.userId)) return;
|
const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
|
||||||
|
if (blackListManager.isBlackListed(uuid)) return;
|
||||||
console.log("receiveWebrtcScreenSharingSignal", data);
|
console.log("receiveWebrtcScreenSharingSignal", data);
|
||||||
const streamResult = get(screenSharingLocalStreamStore);
|
const streamResult = get(screenSharingLocalStreamStore);
|
||||||
let stream : MediaStream | null = null;
|
let stream: MediaStream | null = null;
|
||||||
if (streamResult.type === 'success' && streamResult.stream !== null) {
|
if (streamResult.type === "success" && streamResult.stream !== null) {
|
||||||
stream = streamResult.stream;
|
stream = streamResult.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//if offer type, create peer connection
|
//if offer type, create peer connection
|
||||||
if(data.signal.type === "offer"){
|
if (data.signal.type === "offer") {
|
||||||
this.createPeerScreenSharingConnection(data, stream);
|
this.createPeerScreenSharingConnection(data, stream);
|
||||||
}
|
}
|
||||||
const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
|
const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
|
||||||
if (peer !== undefined) {
|
if (peer !== undefined) {
|
||||||
peer.signal(data.signal);
|
peer.signal(data.signal);
|
||||||
} else {
|
} else {
|
||||||
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal');
|
console.error(
|
||||||
console.info('Attempt to create new peer connexion');
|
'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
|
||||||
|
);
|
||||||
|
console.info("Attempt to create new peer connexion");
|
||||||
if (stream) {
|
if (stream) {
|
||||||
this.sendLocalScreenSharingStreamToUser(data.userId, stream);
|
this.sendLocalScreenSharingStreamToUser(data.userId, stream);
|
||||||
}
|
}
|
||||||
@ -384,17 +387,19 @@ export class SimplePeer {
|
|||||||
try {
|
try {
|
||||||
const PeerConnection = this.PeerConnectionArray.get(userId);
|
const PeerConnection = this.PeerConnectionArray.get(userId);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints})));
|
PeerConnection.write(
|
||||||
|
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints }))
|
||||||
|
);
|
||||||
|
|
||||||
if (streamResult.type === 'error') {
|
if (streamResult.type === "error") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const localStream: MediaStream | null = streamResult.stream;
|
const localStream: MediaStream | null = streamResult.stream;
|
||||||
|
|
||||||
if(!localStream){
|
if (!localStream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +409,7 @@ export class SimplePeer {
|
|||||||
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
|
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
PeerConnection.addTrack(track, localStream);
|
PeerConnection.addTrack(track, localStream);
|
||||||
}
|
}
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
console.error(`pushVideoToRemoteUser => ${userId}`, e);
|
console.error(`pushVideoToRemoteUser => ${userId}`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,7 +417,7 @@ export class SimplePeer {
|
|||||||
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
|
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
|
||||||
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
|
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
|
||||||
if (!PeerConnection) {
|
if (!PeerConnection) {
|
||||||
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId);
|
throw new Error("While pushing screen sharing, cannot find user with ID " + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const track of localScreenCapture.getTracks()) {
|
for (const track of localScreenCapture.getTracks()) {
|
||||||
@ -421,7 +426,7 @@ export class SimplePeer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendLocalVideoStream(streamResult: LocalStreamStoreValue){
|
public sendLocalVideoStream(streamResult: LocalStreamStoreValue) {
|
||||||
for (const user of this.Users) {
|
for (const user of this.Users) {
|
||||||
this.pushVideoToRemoteUser(user.userId, streamResult);
|
this.pushVideoToRemoteUser(user.userId, streamResult);
|
||||||
}
|
}
|
||||||
@ -446,7 +451,8 @@ export class SimplePeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void {
|
private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void {
|
||||||
if (blackListManager.isBlackListed(userId)) return;
|
const uuid = playersStore.getPlayerById(userId)?.userUuid || "";
|
||||||
|
if (blackListManager.isBlackListed(uuid)) return;
|
||||||
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
|
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
|
||||||
if (this.PeerScreenSharingConnectionArray.has(userId)) {
|
if (this.PeerScreenSharingConnectionArray.has(userId)) {
|
||||||
this.pushScreenSharingToRemoteUser(userId, localScreenCapture);
|
this.pushScreenSharingToRemoteUser(userId, localScreenCapture);
|
||||||
@ -455,9 +461,12 @@ export class SimplePeer {
|
|||||||
|
|
||||||
const screenSharingUser: UserSimplePeerInterface = {
|
const screenSharingUser: UserSimplePeerInterface = {
|
||||||
userId,
|
userId,
|
||||||
initiator: true
|
initiator: true,
|
||||||
};
|
};
|
||||||
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser, localScreenCapture);
|
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(
|
||||||
|
screenSharingUser,
|
||||||
|
localScreenCapture
|
||||||
|
);
|
||||||
if (!PeerConnectionScreenSharing) {
|
if (!PeerConnectionScreenSharing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -466,7 +475,7 @@ export class SimplePeer {
|
|||||||
private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void {
|
private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void {
|
||||||
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
|
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
|
||||||
if (!PeerConnectionScreenSharing) {
|
if (!PeerConnectionScreenSharing) {
|
||||||
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found')
|
throw new Error("Weird, screen sharing connection to user " + userId + "not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);
|
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import type * as SimplePeerNamespace from "simple-peer";
|
import type * as SimplePeerNamespace from "simple-peer";
|
||||||
import {mediaManager} from "./MediaManager";
|
import { mediaManager } from "./MediaManager";
|
||||||
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
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, readable, Readable, Unsubscriber } from "svelte/store";
|
||||||
import {get, readable, Readable} from "svelte/store";
|
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||||
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
import {discussionManager} from "./DiscussionManager";
|
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
|
||||||
|
import { getIceServersConfig } from "../Components/Video/utils";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||||
|
|
||||||
export type PeerStatus = "connecting" | "connected" | "error" | "closed";
|
export type PeerStatus = "connecting" | "connected" | "error" | "closed";
|
||||||
|
|
||||||
export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
|
export const MESSAGE_TYPE_CONSTRAINT = "constraint";
|
||||||
export const MESSAGE_TYPE_MESSAGE = 'message';
|
export const MESSAGE_TYPE_MESSAGE = "message";
|
||||||
export const MESSAGE_TYPE_BLOCKED = 'blocked';
|
export const MESSAGE_TYPE_BLOCKED = "blocked";
|
||||||
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked';
|
export const MESSAGE_TYPE_UNBLOCKED = "unblocked";
|
||||||
/**
|
/**
|
||||||
* A peer connection used to transmit video / audio signals between 2 peers.
|
* A peer connection used to transmit video / audio signals between 2 peers.
|
||||||
*/
|
*/
|
||||||
@ -26,121 +27,135 @@ export class VideoPeer extends Peer {
|
|||||||
private remoteStream!: MediaStream;
|
private remoteStream!: MediaStream;
|
||||||
private blocked: boolean = false;
|
private blocked: boolean = false;
|
||||||
public readonly userId: number;
|
public readonly userId: number;
|
||||||
|
public readonly userUuid: string;
|
||||||
public readonly uniqueId: string;
|
public readonly uniqueId: string;
|
||||||
private onBlockSubscribe: Subscription;
|
private onBlockSubscribe: Subscription;
|
||||||
private onUnBlockSubscribe: Subscription;
|
private onUnBlockSubscribe: Subscription;
|
||||||
public readonly streamStore: Readable<MediaStream | null>;
|
public readonly streamStore: Readable<MediaStream | null>;
|
||||||
public readonly statusStore: Readable<PeerStatus>;
|
public readonly statusStore: Readable<PeerStatus>;
|
||||||
public readonly constraintsStore: Readable<MediaStreamConstraints|null>;
|
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
|
||||||
|
private newMessageunsubscriber: Unsubscriber | null = null;
|
||||||
|
private closing: Boolean = false; //this is used to prevent destroy() from being called twice
|
||||||
|
|
||||||
constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) {
|
constructor(
|
||||||
|
public user: UserSimplePeerInterface,
|
||||||
|
initiator: boolean,
|
||||||
|
public readonly userName: string,
|
||||||
|
private connection: RoomConnection,
|
||||||
|
localStream: MediaStream | null
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator,
|
||||||
//reconnectTimer: 10000,
|
|
||||||
config: {
|
config: {
|
||||||
iceServers: [
|
iceServers: getIceServersConfig(user),
|
||||||
{
|
},
|
||||||
urls: STUN_SERVER.split(',')
|
|
||||||
},
|
|
||||||
TURN_SERVER !== '' ? {
|
|
||||||
urls: TURN_SERVER.split(','),
|
|
||||||
username: user.webRtcUser || TURN_USER,
|
|
||||||
credential: user.webRtcPassword || TURN_PASSWORD
|
|
||||||
} : undefined,
|
|
||||||
].filter((value) => value !== undefined)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userId = user.userId;
|
this.userId = user.userId;
|
||||||
this.uniqueId = 'video_'+this.userId;
|
this.userUuid = playersStore.getPlayerById(this.userId)?.userUuid || "";
|
||||||
|
this.uniqueId = "video_" + this.userId;
|
||||||
|
|
||||||
this.streamStore = readable<MediaStream|null>(null, (set) => {
|
this.streamStore = readable<MediaStream | null>(null, (set) => {
|
||||||
const onStream = (stream: MediaStream|null) => {
|
const onStream = (stream: MediaStream | null) => {
|
||||||
set(stream);
|
set(stream);
|
||||||
};
|
};
|
||||||
const onData = (chunk: Buffer) => {
|
const onData = (chunk: Buffer) => {
|
||||||
this.on('data', (chunk: Buffer) => {
|
this.on("data", (chunk: Buffer) => {
|
||||||
const message = JSON.parse(chunk.toString('utf8'));
|
const message = JSON.parse(chunk.toString("utf8"));
|
||||||
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||||
if (!message.video) {
|
if (!message.video) {
|
||||||
set(null);
|
set(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
this.on('stream', onStream);
|
this.on("stream", onStream);
|
||||||
this.on('data', onData);
|
this.on("data", onData);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.off('stream', onStream);
|
this.off("stream", onStream);
|
||||||
this.off('data', onData);
|
this.off("data", onData);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.constraintsStore = readable<MediaStreamConstraints|null>(null, (set) => {
|
this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => {
|
||||||
const onData = (chunk: Buffer) => {
|
const onData = (chunk: Buffer) => {
|
||||||
const message = JSON.parse(chunk.toString('utf8'));
|
const message = JSON.parse(chunk.toString("utf8"));
|
||||||
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||||
set(message);
|
set(message);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.on('data', onData);
|
this.on("data", onData);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.off('data', onData);
|
this.off("data", onData);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
||||||
const onConnect = () => {
|
const onConnect = () => {
|
||||||
set('connected');
|
set("connected");
|
||||||
};
|
};
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
set('error');
|
set("error");
|
||||||
};
|
};
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
set('closed');
|
set("closed");
|
||||||
};
|
};
|
||||||
|
|
||||||
this.on('connect', onConnect);
|
this.on("connect", onConnect);
|
||||||
this.on('error', onError);
|
this.on("error", onError);
|
||||||
this.on('close', onClose);
|
this.on("close", onClose);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.off('connect', onConnect);
|
this.off("connect", onConnect);
|
||||||
this.off('error', onError);
|
this.off("error", onError);
|
||||||
this.off('close', onClose);
|
this.off("close", onClose);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
//start listen signal for the peer connection
|
//start listen signal for the peer connection
|
||||||
this.on('signal', (data: unknown) => {
|
this.on("signal", (data: unknown) => {
|
||||||
this.sendWebrtcSignal(data);
|
this.sendWebrtcSignal(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('stream', (stream: MediaStream) => this.stream(stream));
|
this.on("stream", (stream: MediaStream) => this.stream(stream));
|
||||||
|
|
||||||
this.on('close', () => {
|
this.on("close", () => {
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
this.toClose = true;
|
this.toClose = true;
|
||||||
this.destroy();
|
this.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
this.on('error', (err: any) => {
|
this.on("error", (err: any) => {
|
||||||
console.error(`error => ${this.userId} => ${err.code}`, err);
|
console.error(`error => ${this.userId} => ${err.code}`, err);
|
||||||
mediaManager.isError("" + this.userId);
|
mediaManager.isError("" + this.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('connect', () => {
|
this.on("connect", () => {
|
||||||
this._connected = true;
|
this._connected = true;
|
||||||
|
chatMessagesStore.addIncomingUser(this.userId);
|
||||||
|
|
||||||
|
this.newMessageunsubscriber = newChatMessageStore.subscribe((newMessage) => {
|
||||||
|
if (!newMessage) return;
|
||||||
|
this.write(
|
||||||
|
new Buffer(
|
||||||
|
JSON.stringify({
|
||||||
|
type: MESSAGE_TYPE_MESSAGE,
|
||||||
|
message: newMessage,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
); //send more data
|
||||||
|
newChatMessageStore.set(null); //This is to prevent a newly created SimplePeer to send an old message a 2nd time. Is there a better way?
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('data', (chunk: Buffer) => {
|
this.on("data", (chunk: Buffer) => {
|
||||||
const message = JSON.parse(chunk.toString('utf8'));
|
const message = JSON.parse(chunk.toString("utf8"));
|
||||||
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||||
if (message.audio) {
|
if (message.audio) {
|
||||||
mediaManager.enabledMicrophoneByUserId(this.userId);
|
mediaManager.enabledMicrophoneByUserId(this.userId);
|
||||||
} else {
|
} else {
|
||||||
@ -152,58 +167,67 @@ export class VideoPeer extends Peer {
|
|||||||
} else {
|
} else {
|
||||||
mediaManager.disabledVideoByUserId(this.userId);
|
mediaManager.disabledVideoByUserId(this.userId);
|
||||||
}
|
}
|
||||||
} else if(message.type === MESSAGE_TYPE_MESSAGE) {
|
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
|
||||||
if (!blackListManager.isBlackListed(message.userId)) {
|
if (!blackListManager.isBlackListed(this.userUuid)) {
|
||||||
mediaManager.addNewMessage(message.name, message.message);
|
chatMessagesStore.addExternalMessage(this.userId, message.message);
|
||||||
}
|
}
|
||||||
} else if(message.type === MESSAGE_TYPE_BLOCKED) {
|
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
|
||||||
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
|
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
|
||||||
// Find a way to block A's output stream in A's js client
|
// Find a way to block A's output stream in A's js client
|
||||||
//However, the output stream stream B is correctly blocked in A client
|
//However, the output stream stream B is correctly blocked in A client
|
||||||
this.blocked = true;
|
this.blocked = true;
|
||||||
this.toggleRemoteStream(false);
|
this.toggleRemoteStream(false);
|
||||||
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) {
|
} else if (message.type === MESSAGE_TYPE_UNBLOCKED) {
|
||||||
this.blocked = false;
|
this.blocked = false;
|
||||||
this.toggleRemoteStream(true);
|
this.toggleRemoteStream(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.once('finish', () => {
|
this.once("finish", () => {
|
||||||
this._onFinish();
|
this._onFinish();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pushVideoToRemoteUser(localStream);
|
this.pushVideoToRemoteUser(localStream);
|
||||||
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => {
|
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => {
|
||||||
if (userId === this.userId) {
|
if (userUuid === this.userUuid) {
|
||||||
this.toggleRemoteStream(false);
|
this.toggleRemoteStream(false);
|
||||||
this.sendBlockMessage(true);
|
this.sendBlockMessage(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userId) => {
|
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userUuid) => {
|
||||||
if (userId === this.userId) {
|
if (userUuid === this.userUuid) {
|
||||||
this.toggleRemoteStream(true);
|
this.toggleRemoteStream(true);
|
||||||
this.sendBlockMessage(false);
|
this.sendBlockMessage(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blackListManager.isBlackListed(this.userId)) {
|
if (blackListManager.isBlackListed(this.userUuid)) {
|
||||||
this.sendBlockMessage(true)
|
this.sendBlockMessage(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendBlockMessage(blocking: boolean) {
|
private sendBlockMessage(blocking: boolean) {
|
||||||
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''})));
|
this.write(
|
||||||
|
new Buffer(
|
||||||
|
JSON.stringify({
|
||||||
|
type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED,
|
||||||
|
name: this.userName.toUpperCase(),
|
||||||
|
userId: this.userId,
|
||||||
|
message: "",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleRemoteStream(enable: boolean) {
|
private toggleRemoteStream(enable: boolean) {
|
||||||
this.remoteStream.getTracks().forEach(track => track.enabled = enable);
|
this.remoteStream.getTracks().forEach((track) => (track.enabled = enable));
|
||||||
mediaManager.toggleBlockLogo(this.userId, !enable);
|
mediaManager.toggleBlockLogo(this.userId, !enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendWebrtcSignal(data: unknown) {
|
private sendWebrtcSignal(data: unknown) {
|
||||||
try {
|
try {
|
||||||
this.connection.sendWebrtcSignal(data, this.userId);
|
this.connection.sendWebrtcSignal(data, this.userId);
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
console.error(`sendWebrtcSignal => ${this.userId}`, e);
|
console.error(`sendWebrtcSignal => ${this.userId}`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,10 +238,10 @@ export class VideoPeer extends Peer {
|
|||||||
private stream(stream: MediaStream) {
|
private stream(stream: MediaStream) {
|
||||||
try {
|
try {
|
||||||
this.remoteStream = stream;
|
this.remoteStream = stream;
|
||||||
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
|
if (blackListManager.isBlackListed(this.userUuid) || this.blocked) {
|
||||||
this.toggleRemoteStream(false);
|
this.toggleRemoteStream(false);
|
||||||
}
|
}
|
||||||
}catch (err){
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,47 +249,49 @@ export class VideoPeer extends Peer {
|
|||||||
/**
|
/**
|
||||||
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
||||||
*/
|
*/
|
||||||
public destroy(error?: Error): void {
|
public destroy(): void {
|
||||||
try {
|
try {
|
||||||
this._connected = false
|
this._connected = false;
|
||||||
if(!this.toClose){
|
if (!this.toClose || this.closing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.closing = true;
|
||||||
this.onBlockSubscribe.unsubscribe();
|
this.onBlockSubscribe.unsubscribe();
|
||||||
this.onUnBlockSubscribe.unsubscribe();
|
this.onUnBlockSubscribe.unsubscribe();
|
||||||
discussionManager.removeParticipant(this.userId);
|
if (this.newMessageunsubscriber) this.newMessageunsubscriber();
|
||||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
chatMessagesStore.addOutcomingUser(this.userId);
|
||||||
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
|
super.destroy();
|
||||||
super.destroy(error);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("VideoPeer::destroy", err)
|
console.error("VideoPeer::destroy", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFinish () {
|
_onFinish() {
|
||||||
if (this.destroyed) return
|
if (this.destroyed) return;
|
||||||
const destroySoon = () => {
|
const destroySoon = () => {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
};
|
||||||
if (this._connected) {
|
if (this._connected) {
|
||||||
destroySoon();
|
destroySoon();
|
||||||
} else {
|
} else {
|
||||||
this.once('connect', destroySoon);
|
this.once("connect", destroySoon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private pushVideoToRemoteUser(localStream: MediaStream | null) {
|
private pushVideoToRemoteUser(localStream: MediaStream | null) {
|
||||||
try {
|
try {
|
||||||
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)})));
|
this.write(
|
||||||
|
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore) }))
|
||||||
|
);
|
||||||
|
|
||||||
if(!localStream){
|
if (!localStream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const track of localStream.getTracks()) {
|
for (const track of localStream.getTracks()) {
|
||||||
this.addTrack(track, localStream);
|
this.addTrack(track, localStream);
|
||||||
}
|
}
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
|
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
|
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
|
||||||
import {
|
import {
|
||||||
IframeResponseEvent,
|
IframeResponseEvent,
|
||||||
IframeResponseEventMap,
|
IframeResponseEventMap, isIframeAnswerEvent, isIframeErrorAnswerEvent,
|
||||||
isIframeResponseEventWrapper,
|
isIframeResponseEventWrapper,
|
||||||
TypedMessageEvent
|
TypedMessageEvent,
|
||||||
} from "./Api/Events/IframeEvent";
|
} from "./Api/Events/IframeEvent";
|
||||||
import chat from "./Api/iframe/chat";
|
import chat from "./Api/iframe/chat";
|
||||||
import type { IframeCallback } from './Api/iframe/IframeApiContribution';
|
import type { IframeCallback } from "./Api/iframe/IframeApiContribution";
|
||||||
import nav from "./Api/iframe/nav";
|
import nav from "./Api/iframe/nav";
|
||||||
import controls from "./Api/iframe/controls";
|
import controls from "./Api/iframe/controls";
|
||||||
import ui from "./Api/iframe/ui";
|
import ui from "./Api/iframe/ui";
|
||||||
@ -16,7 +16,7 @@ import player from "./Api/iframe/player";
|
|||||||
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
|
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
|
||||||
import type { Popup } from "./Api/iframe/Ui/Popup";
|
import type { Popup } from "./Api/iframe/Ui/Popup";
|
||||||
import type { Sound } from "./Api/iframe/Sound/Sound";
|
import type { Sound } from "./Api/iframe/Sound/Sound";
|
||||||
import {sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
|
import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
|
||||||
|
|
||||||
const wa = {
|
const wa = {
|
||||||
ui,
|
ui,
|
||||||
@ -34,7 +34,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.chat.sendChatMessage instead
|
* @deprecated Use WA.chat.sendChatMessage instead
|
||||||
*/
|
*/
|
||||||
sendChatMessage(message: string, author: string): void {
|
sendChatMessage(message: string, author: string): void {
|
||||||
console.warn('Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead');
|
console.warn("Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead");
|
||||||
chat.sendChatMessage(message, author);
|
chat.sendChatMessage(message, author);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -42,7 +42,9 @@ const wa = {
|
|||||||
* @deprecated Use WA.chat.disablePlayerControls instead
|
* @deprecated Use WA.chat.disablePlayerControls instead
|
||||||
*/
|
*/
|
||||||
disablePlayerControls(): void {
|
disablePlayerControls(): void {
|
||||||
console.warn('Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead');
|
console.warn(
|
||||||
|
"Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead"
|
||||||
|
);
|
||||||
controls.disablePlayerControls();
|
controls.disablePlayerControls();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -50,7 +52,9 @@ const wa = {
|
|||||||
* @deprecated Use WA.controls.restorePlayerControls instead
|
* @deprecated Use WA.controls.restorePlayerControls instead
|
||||||
*/
|
*/
|
||||||
restorePlayerControls(): void {
|
restorePlayerControls(): void {
|
||||||
console.warn('Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead');
|
console.warn(
|
||||||
|
"Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead"
|
||||||
|
);
|
||||||
controls.restorePlayerControls();
|
controls.restorePlayerControls();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -58,7 +62,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.ui.displayBubble instead
|
* @deprecated Use WA.ui.displayBubble instead
|
||||||
*/
|
*/
|
||||||
displayBubble(): void {
|
displayBubble(): void {
|
||||||
console.warn('Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead');
|
console.warn("Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead");
|
||||||
ui.displayBubble();
|
ui.displayBubble();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -66,7 +70,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.ui.removeBubble instead
|
* @deprecated Use WA.ui.removeBubble instead
|
||||||
*/
|
*/
|
||||||
removeBubble(): void {
|
removeBubble(): void {
|
||||||
console.warn('Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead');
|
console.warn("Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead");
|
||||||
ui.removeBubble();
|
ui.removeBubble();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -74,7 +78,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.nav.openTab instead
|
* @deprecated Use WA.nav.openTab instead
|
||||||
*/
|
*/
|
||||||
openTab(url: string): void {
|
openTab(url: string): void {
|
||||||
console.warn('Method WA.openTab is deprecated. Please use WA.nav.openTab instead');
|
console.warn("Method WA.openTab is deprecated. Please use WA.nav.openTab instead");
|
||||||
nav.openTab(url);
|
nav.openTab(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -82,7 +86,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.sound.loadSound instead
|
* @deprecated Use WA.sound.loadSound instead
|
||||||
*/
|
*/
|
||||||
loadSound(url: string): Sound {
|
loadSound(url: string): Sound {
|
||||||
console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead');
|
console.warn("Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead");
|
||||||
return sound.loadSound(url);
|
return sound.loadSound(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -90,7 +94,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.nav.goToPage instead
|
* @deprecated Use WA.nav.goToPage instead
|
||||||
*/
|
*/
|
||||||
goToPage(url: string): void {
|
goToPage(url: string): void {
|
||||||
console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead');
|
console.warn("Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead");
|
||||||
nav.goToPage(url);
|
nav.goToPage(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -98,7 +102,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.nav.goToRoom instead
|
* @deprecated Use WA.nav.goToRoom instead
|
||||||
*/
|
*/
|
||||||
goToRoom(url: string): void {
|
goToRoom(url: string): void {
|
||||||
console.warn('Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead');
|
console.warn("Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead");
|
||||||
nav.goToRoom(url);
|
nav.goToRoom(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -106,7 +110,7 @@ const wa = {
|
|||||||
* @deprecated Use WA.nav.openCoWebSite instead
|
* @deprecated Use WA.nav.openCoWebSite instead
|
||||||
*/
|
*/
|
||||||
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
|
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
|
||||||
console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead');
|
console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead");
|
||||||
nav.openCoWebSite(url, allowApi, allowPolicy);
|
nav.openCoWebSite(url, allowApi, allowPolicy);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -114,36 +118,36 @@ const wa = {
|
|||||||
* @deprecated Use WA.nav.closeCoWebSite instead
|
* @deprecated Use WA.nav.closeCoWebSite instead
|
||||||
*/
|
*/
|
||||||
closeCoWebSite(): void {
|
closeCoWebSite(): void {
|
||||||
console.warn('Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead');
|
console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead");
|
||||||
nav.closeCoWebSite();
|
nav.closeCoWebSite();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use WA.controls.restorePlayerControls instead
|
* @deprecated Use WA.ui.openPopup instead
|
||||||
*/
|
*/
|
||||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||||
console.warn('Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead');
|
console.warn("Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead");
|
||||||
return ui.openPopup(targetObject, message, buttons);
|
return ui.openPopup(targetObject, message, buttons);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @deprecated Use WA.chat.onChatMessage instead
|
* @deprecated Use WA.chat.onChatMessage instead
|
||||||
*/
|
*/
|
||||||
onChatMessage(callback: (message: string) => void): void {
|
onChatMessage(callback: (message: string) => void): void {
|
||||||
console.warn('Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead');
|
console.warn("Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead");
|
||||||
chat.onChatMessage(callback);
|
chat.onChatMessage(callback);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @deprecated Use WA.room.onEnterZone instead
|
* @deprecated Use WA.room.onEnterZone instead
|
||||||
*/
|
*/
|
||||||
onEnterZone(name: string, callback: () => void): void {
|
onEnterZone(name: string, callback: () => void): void {
|
||||||
console.warn('Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead');
|
console.warn("Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead");
|
||||||
room.onEnterZone(name, callback);
|
room.onEnterZone(name, callback);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @deprecated Use WA.room.onLeaveZone instead
|
* @deprecated Use WA.room.onLeaveZone instead
|
||||||
*/
|
*/
|
||||||
onLeaveZone(name: string, callback: () => void): void {
|
onLeaveZone(name: string, callback: () => void): void {
|
||||||
console.warn('Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead');
|
console.warn("Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead");
|
||||||
room.onLeaveZone(name, callback);
|
room.onLeaveZone(name, callback);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -151,30 +155,54 @@ const wa = {
|
|||||||
export type WorkAdventureApi = typeof wa;
|
export type WorkAdventureApi = typeof wa;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
WA: WorkAdventureApi
|
WA: WorkAdventureApi;
|
||||||
}
|
}
|
||||||
let WA: WorkAdventureApi
|
let WA: WorkAdventureApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.WA = wa;
|
window.WA = wa;
|
||||||
|
|
||||||
window.addEventListener('message', <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
|
window.addEventListener(
|
||||||
|
"message", <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
|
||||||
if (message.source !== window.parent) {
|
if (message.source !== window.parent) {
|
||||||
return; // Skip message in this event listener
|
return; // Skip message in this event listener
|
||||||
}
|
}
|
||||||
const payload = message.data;
|
const payload = message.data;
|
||||||
|
|
||||||
console.debug(payload);
|
console.debug(payload);
|
||||||
|
|
||||||
if (isIframeResponseEventWrapper(payload)) {
|
if (isIframeAnswerEvent(payload)) {
|
||||||
|
const queryId = payload.id;
|
||||||
const payloadData = payload.data;
|
const payloadData = payload.data;
|
||||||
|
|
||||||
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined
|
const resolver = answerPromises.get(queryId);
|
||||||
if (callback?.typeChecker(payloadData)) {
|
if (resolver === undefined) {
|
||||||
callback?.callback(payloadData)
|
throw new Error('In Iframe API, got an answer for a question that we have no track of.');
|
||||||
}
|
}
|
||||||
}
|
resolver.resolve(payloadData);
|
||||||
|
|
||||||
// ...
|
answerPromises.delete(queryId);
|
||||||
});
|
} else if (isIframeErrorAnswerEvent(payload)) {
|
||||||
|
const queryId = payload.id;
|
||||||
|
const payloadError = payload.error;
|
||||||
|
|
||||||
|
const resolver = answerPromises.get(queryId);
|
||||||
|
if (resolver === undefined) {
|
||||||
|
throw new Error('In Iframe API, got an error answer for a question that we have no track of.');
|
||||||
|
}
|
||||||
|
resolver.reject(payloadError);
|
||||||
|
|
||||||
|
answerPromises.delete(queryId);
|
||||||
|
} else if (isIframeResponseEventWrapper(payload)) {
|
||||||
|
const payloadData = payload.data;
|
||||||
|
|
||||||
|
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined;
|
||||||
|
if (callback?.typeChecker(payloadData)) {
|
||||||
|
callback?.callback(payloadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -160,3 +160,15 @@ const app = new App({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
navigator.serviceWorker.register('/resources/service-worker.js')
|
||||||
|
.then(serviceWorker => {
|
||||||
|
console.log("Service Worker registered: ", serviceWorker);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error registering the Service Worker: ", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
@import "~@fontsource/press-start-2p/index.css";
|
@import "~@fontsource/press-start-2p/index.css";
|
||||||
|
|
||||||
*{
|
|
||||||
font-family: PixelFont-7,monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nes-btn {
|
.nes-btn {
|
||||||
font-family: "Press Start 2P";
|
font-family: "Press Start 2P";
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
*{
|
*{
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: Lato;
|
||||||
cursor: url('./images/cursor_normal.png'), auto;
|
cursor: url('./images/cursor_normal.png'), auto;
|
||||||
}
|
}
|
||||||
* a, button, select{
|
* a, button, select{
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import "jasmine";
|
|
||||||
import {Room} from "../../../src/Connexion/Room";
|
|
||||||
|
|
||||||
describe("Room getIdFromIdentifier()", () => {
|
|
||||||
it("should work with an absolute room id and no hash as parameter", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', '');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
it("should work with an absolute room id and a hash as parameters", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', '');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual("start");
|
|
||||||
});
|
|
||||||
it("should work with an absolute room id, regardless of baseUrl or instance", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', 'https://another.domain/_/global/test.json', 'lol');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("should work with a relative file link and no hash as parameters", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
it("should work with a relative file link with no dot", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
it("should work with a relative file link two levels deep", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('../floor1/Floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
it("should work with a relative file link that rewrite the map domain", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('../../maps.workadventure.localhost/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventure.localhost/Floor1/floor1.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
it("should work with a relative file link that rewrite the map instance", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('../../../notglobal/maps.workadventu.re/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/notglobal/maps.workadventu.re/Floor1/floor1.json');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
it("should work with a relative file link that change the map type", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('../../../../@/tcm/is/great', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('@/tcm/is/great');
|
|
||||||
expect(hash).toEqual('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with a relative file link and a hash as parameters", () => {
|
|
||||||
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual("start");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,150 +1,155 @@
|
|||||||
import "jasmine";
|
import "jasmine";
|
||||||
import {Room} from "../../../src/Connexion/Room";
|
import { Room } from "../../../src/Connexion/Room";
|
||||||
import {flattenGroupLayersMap} from "../../../src/Phaser/Map/LayersFlattener";
|
import { flattenGroupLayersMap } from "../../../src/Phaser/Map/LayersFlattener";
|
||||||
import type {ITiledMapLayer} from "../../../src/Phaser/Map/ITiledMap";
|
import type { ITiledMapLayer } from "../../../src/Phaser/Map/ITiledMap";
|
||||||
|
|
||||||
describe("Layers flattener", () => {
|
describe("Layers flattener", () => {
|
||||||
it("should iterate maps with no group", () => {
|
it("should iterate maps with no group", () => {
|
||||||
let flatLayers:ITiledMapLayer[] = [];
|
let flatLayers: ITiledMapLayer[] = [];
|
||||||
flatLayers = flattenGroupLayersMap({
|
flatLayers = flattenGroupLayersMap({
|
||||||
"compressionlevel": -1,
|
compressionlevel: -1,
|
||||||
"height": 2,
|
height: 2,
|
||||||
"infinite": false,
|
infinite: false,
|
||||||
"layers": [
|
layers: [
|
||||||
{
|
{
|
||||||
"data": [0, 0, 0, 0],
|
data: [0, 0, 0, 0],
|
||||||
"height": 2,
|
height: 2,
|
||||||
"id": 1,
|
id: 1,
|
||||||
"name": "Tile Layer 1",
|
name: "Tile Layer 1",
|
||||||
"opacity": 1,
|
opacity: 1,
|
||||||
"type": "tilelayer",
|
type: "tilelayer",
|
||||||
"visible": true,
|
visible: true,
|
||||||
"width": 2,
|
width: 2,
|
||||||
"x": 0,
|
x: 0,
|
||||||
"y": 0
|
y: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": [0, 0, 0, 0],
|
data: [0, 0, 0, 0],
|
||||||
"height": 2,
|
height: 2,
|
||||||
"id": 1,
|
id: 1,
|
||||||
"name": "Tile Layer 2",
|
name: "Tile Layer 2",
|
||||||
"opacity": 1,
|
opacity: 1,
|
||||||
"type": "tilelayer",
|
type: "tilelayer",
|
||||||
"visible": true,
|
visible: true,
|
||||||
"width": 2,
|
width: 2,
|
||||||
"x": 0,
|
x: 0,
|
||||||
"y": 0
|
y: 0,
|
||||||
}],
|
},
|
||||||
"nextlayerid": 2,
|
],
|
||||||
"nextobjectid": 1,
|
nextlayerid: 2,
|
||||||
"orientation": "orthogonal",
|
nextobjectid: 1,
|
||||||
"renderorder": "right-down",
|
orientation: "orthogonal",
|
||||||
"tiledversion": "2021.03.23",
|
renderorder: "right-down",
|
||||||
"tileheight": 32,
|
tiledversion: "2021.03.23",
|
||||||
"tilesets": [],
|
tileheight: 32,
|
||||||
"tilewidth": 32,
|
tilesets: [],
|
||||||
"type": "map",
|
tilewidth: 32,
|
||||||
"version": 1.5,
|
type: "map",
|
||||||
"width": 2
|
version: 1.5,
|
||||||
})
|
width: 2,
|
||||||
|
});
|
||||||
|
|
||||||
const layers = [];
|
const layers = [];
|
||||||
for (const layer of flatLayers) {
|
for (const layer of flatLayers) {
|
||||||
layers.push(layer.name);
|
layers.push(layer.name);
|
||||||
}
|
}
|
||||||
expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']);
|
expect(layers).toEqual(["Tile Layer 1", "Tile Layer 2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should iterate maps with recursive groups", () => {
|
it("should iterate maps with recursive groups", () => {
|
||||||
let flatLayers:ITiledMapLayer[] = [];
|
let flatLayers: ITiledMapLayer[] = [];
|
||||||
flatLayers = flattenGroupLayersMap({
|
flatLayers = flattenGroupLayersMap({
|
||||||
"compressionlevel": -1,
|
compressionlevel: -1,
|
||||||
"height": 2,
|
height: 2,
|
||||||
"infinite": false,
|
infinite: false,
|
||||||
"layers": [
|
layers: [
|
||||||
{
|
{
|
||||||
"id": 6,
|
id: 6,
|
||||||
"layers": [
|
layers: [
|
||||||
{
|
{
|
||||||
"id": 5,
|
id: 5,
|
||||||
"layers": [
|
layers: [
|
||||||
{
|
{
|
||||||
"data": [0, 0, 0, 0],
|
data: [0, 0, 0, 0],
|
||||||
"height": 2,
|
height: 2,
|
||||||
"id": 10,
|
id: 10,
|
||||||
"name": "Tile3",
|
name: "Tile3",
|
||||||
"opacity": 1,
|
opacity: 1,
|
||||||
"type": "tilelayer",
|
type: "tilelayer",
|
||||||
"visible": true,
|
visible: true,
|
||||||
"width": 2,
|
width: 2,
|
||||||
"x": 0,
|
x: 0,
|
||||||
"y": 0
|
y: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": [0, 0, 0, 0],
|
data: [0, 0, 0, 0],
|
||||||
"height": 2,
|
height: 2,
|
||||||
"id": 9,
|
id: 9,
|
||||||
"name": "Tile2",
|
name: "Tile2",
|
||||||
"opacity": 1,
|
opacity: 1,
|
||||||
"type": "tilelayer",
|
type: "tilelayer",
|
||||||
"visible": true,
|
visible: true,
|
||||||
"width": 2,
|
width: 2,
|
||||||
"x": 0,
|
x: 0,
|
||||||
"y": 0
|
y: 0,
|
||||||
}],
|
},
|
||||||
"name": "Group 3",
|
],
|
||||||
"opacity": 1,
|
name: "Group 3",
|
||||||
"type": "group",
|
opacity: 1,
|
||||||
"visible": true,
|
type: "group",
|
||||||
"x": 0,
|
visible: true,
|
||||||
"y": 0
|
x: 0,
|
||||||
|
y: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
id: 7,
|
||||||
"layers": [
|
layers: [
|
||||||
{
|
{
|
||||||
"data": [0, 0, 0, 0],
|
data: [0, 0, 0, 0],
|
||||||
"height": 2,
|
height: 2,
|
||||||
"id": 8,
|
id: 8,
|
||||||
"name": "Tile1",
|
name: "Tile1",
|
||||||
"opacity": 1,
|
opacity: 1,
|
||||||
"type": "tilelayer",
|
type: "tilelayer",
|
||||||
"visible": true,
|
visible: true,
|
||||||
"width": 2,
|
width: 2,
|
||||||
"x": 0,
|
x: 0,
|
||||||
"y": 0
|
y: 0,
|
||||||
}],
|
},
|
||||||
"name": "Group 2",
|
],
|
||||||
"opacity": 1,
|
name: "Group 2",
|
||||||
"type": "group",
|
opacity: 1,
|
||||||
"visible": true,
|
type: "group",
|
||||||
"x": 0,
|
visible: true,
|
||||||
"y": 0
|
x: 0,
|
||||||
}],
|
y: 0,
|
||||||
"name": "Group 1",
|
},
|
||||||
"opacity": 1,
|
],
|
||||||
"type": "group",
|
name: "Group 1",
|
||||||
"visible": true,
|
opacity: 1,
|
||||||
"x": 0,
|
type: "group",
|
||||||
"y": 0
|
visible: true,
|
||||||
}],
|
x: 0,
|
||||||
"nextlayerid": 11,
|
y: 0,
|
||||||
"nextobjectid": 1,
|
},
|
||||||
"orientation": "orthogonal",
|
],
|
||||||
"renderorder": "right-down",
|
nextlayerid: 11,
|
||||||
"tiledversion": "2021.03.23",
|
nextobjectid: 1,
|
||||||
"tileheight": 32,
|
orientation: "orthogonal",
|
||||||
"tilesets": [],
|
renderorder: "right-down",
|
||||||
"tilewidth": 32,
|
tiledversion: "2021.03.23",
|
||||||
"type": "map",
|
tileheight: 32,
|
||||||
"version": 1.5,
|
tilesets: [],
|
||||||
"width": 2
|
tilewidth: 32,
|
||||||
})
|
type: "map",
|
||||||
|
version: 1.5,
|
||||||
|
width: 2,
|
||||||
|
});
|
||||||
|
|
||||||
const layers = [];
|
const layers = [];
|
||||||
for (const layer of flatLayers) {
|
for (const layer of flatLayers) {
|
||||||
layers.push(layer.name);
|
layers.push(layer.name);
|
||||||
}
|
}
|
||||||
expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']);
|
expect(layers).toEqual(["Group 1/Group 3/Tile3", "Group 1/Group 3/Tile2", "Group 1/Group 2/Tile1"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|||||||
import sveltePreprocess from "svelte-preprocess";
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||||
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
|
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
|
||||||
import { DISPLAY_TERMS_OF_USE } from "./src/Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
const mode = process.env.NODE_ENV ?? "development";
|
const mode = process.env.NODE_ENV ?? "development";
|
||||||
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
|
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
|
||||||
|
@ -262,10 +262,10 @@
|
|||||||
"@types/mime" "^1"
|
"@types/mime" "^1"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/simple-peer@^9.6.0":
|
"@types/simple-peer@^9.11.1":
|
||||||
version "9.6.3"
|
version "9.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.6.3.tgz#aa118a57e036f4ce2059a7e25367526a4764206d"
|
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.1.tgz#bef6ff1e75178d83438e33aa6a4df2fd98fded1d"
|
||||||
integrity sha512-zrXEBch9tF4NgkZDsGR3c1D0kq99M1bBCjzEyL0PVfEWzCIXrK64TuxRz3XKOx1B0KoEQ9kTs+AhMDuQaHy5RQ==
|
integrity sha512-Pzqbau/WlivSXdRC0He2Wz/ANj2wbi4gzJrtysZz93jvOyI2jo/ibMjUe6AvPllFl/UO6QXT/A0Rcp44bDQB5A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
@ -5008,7 +5008,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||||
|
|
||||||
simple-peer@^9.6.2:
|
simple-peer@^9.11.0:
|
||||||
version "9.11.0"
|
version "9.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571"
|
resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571"
|
||||||
integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg==
|
integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg==
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
{ "compressionlevel":-1,
|
{ "compressionlevel":-1,
|
||||||
"editorsettings":
|
|
||||||
{
|
|
||||||
"export":
|
|
||||||
{
|
|
||||||
"target":"."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"height":26,
|
"height":26,
|
||||||
"infinite":false,
|
"infinite":false,
|
||||||
"layers":[
|
"layers":[
|
||||||
@ -101,7 +94,7 @@
|
|||||||
"opacity":1,
|
"opacity":1,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"exitSceneUrl",
|
"name":"exitUrl",
|
||||||
"type":"string",
|
"type":"string",
|
||||||
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs"
|
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs"
|
||||||
}],
|
}],
|
||||||
@ -119,7 +112,7 @@
|
|||||||
"opacity":1,
|
"opacity":1,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"exitSceneUrl",
|
"name":"exitUrl",
|
||||||
"type":"string",
|
"type":"string",
|
||||||
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs-secours"
|
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs-secours"
|
||||||
}],
|
}],
|
||||||
@ -264,7 +257,7 @@
|
|||||||
"nextobjectid":1,
|
"nextobjectid":1,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
"renderorder":"right-down",
|
"renderorder":"right-down",
|
||||||
"tiledversion":"1.3.3",
|
"tiledversion":"2021.03.23",
|
||||||
"tileheight":32,
|
"tileheight":32,
|
||||||
"tilesets":[
|
"tilesets":[
|
||||||
{
|
{
|
||||||
@ -1959,6 +1952,6 @@
|
|||||||
}],
|
}],
|
||||||
"tilewidth":32,
|
"tilewidth":32,
|
||||||
"type":"map",
|
"type":"map",
|
||||||
"version":1.2,
|
"version":1.5,
|
||||||
"width":46
|
"width":46
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user