Merge branch 'develop' of github.com:thecodingmachine/workadventure into scripting_api_room_metadata
This commit is contained in:
commit
5c7ea7b258
14
CHANGELOG.md
14
CHANGELOG.md
@ -11,7 +11,7 @@
|
|||||||
- Use `WA.onInit(): Promise<void>` to wait for scripting API initialization
|
- Use `WA.onInit(): Promise<void>` to wait for scripting API initialization
|
||||||
- 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.player.id: string|undefined` to get the ID of the current player
|
- Use `WA.player.id: string|undefined` to get the ID of the current player
|
||||||
- Use `WA.player.name: string` to get the name of the current player
|
- Use `WA.player.name: string` to get the name of the current player
|
||||||
@ -20,13 +20,23 @@
|
|||||||
- Use `WA.room.mapURL: string` to get the URL of the map
|
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||||
- Use `WA.room.mapURL: string` to get the URL of the map
|
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||||
- Use `WA.room.getMap(): Promise<ITiledMap>` to get the JSON map file
|
- Use `WA.room.getMap(): Promise<ITiledMap>` to get the JSON map file
|
||||||
- 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
|
||||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||||
- Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable
|
- Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable
|
||||||
- Use `WA.state.saveVariable(key: string, value: unknown): Promise<void>` to set a variable (across the room, for all users)
|
- Use `WA.state.saveVariable(key: string, value: unknown): Promise<void>` to set a variable (across the room, for all users)
|
||||||
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` to track a variable
|
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` to track a variable
|
||||||
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
|
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
|
||||||
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
- 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
|
||||||
|
|
||||||
|
@ -5,27 +5,21 @@ 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 {
|
import {
|
||||||
BatchToPusherMessage,
|
BatchToPusherMessage,
|
||||||
BatchToPusherRoomMessage,
|
BatchToPusherRoomMessage,
|
||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
JoinRoomMessage, SubToPusherRoomMessage, VariableMessage
|
JoinRoomMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
|
VariableMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import {RoomSocket, ZoneSocket} from "src/RoomManager";
|
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||||
import { Admin } from "../Model/Admin";
|
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;
|
||||||
@ -43,17 +37,14 @@ export class GameRoom {
|
|||||||
public readonly variables = new Map<string, string>();
|
public readonly variables = new Map<string, string>();
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
roomId: string,
|
roomUrl: string,
|
||||||
connectCallback: ConnectCallback,
|
connectCallback: ConnectCallback,
|
||||||
disconnectCallback: DisconnectCallback,
|
disconnectCallback: DisconnectCallback,
|
||||||
minDistance: number,
|
minDistance: number,
|
||||||
@ -63,16 +54,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>();
|
||||||
@ -191,7 +173,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 };
|
|
||||||
};
|
|
@ -30,7 +30,9 @@ import {
|
|||||||
BanUserMessage,
|
BanUserMessage,
|
||||||
RefreshRoomMessage,
|
RefreshRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
VariableMessage, BatchToPusherRoomMessage, SubToPusherRoomMessage,
|
VariableMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { User, UserSocket } from "../Model/User";
|
import { User, UserSocket } from "../Model/User";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
@ -49,7 +51,7 @@ import Jwt from "jsonwebtoken";
|
|||||||
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||||
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
||||||
import { gaugeManager } from "./GaugeManager";
|
import { gaugeManager } from "./GaugeManager";
|
||||||
import {RoomSocket, ZoneSocket} from "../RoomManager";
|
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import { Admin } from "_Model/Admin";
|
import { Admin } from "_Model/Admin";
|
||||||
@ -270,12 +272,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -467,10 +469,7 @@ 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);
|
||||||
@ -484,10 +483,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)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -731,9 +727,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -875,7 +871,6 @@ export class SocketManager {
|
|||||||
emoteEventMessage.setActoruserid(user.id);
|
emoteEventMessage.setActoruserid(user.id);
|
||||||
room.emitEmoteEvent(user, emoteEventMessage);
|
room.emitEmoteEvent(user, emoteEventMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socketManager = new SocketManager();
|
export const socketManager = new SocketManager();
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
})
|
|
@ -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,6 +71,9 @@ 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/');
|
||||||
@ -148,6 +152,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>
|
||||||
|
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"
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@ export const isSetTilesEvent = tg.isArray(
|
|||||||
.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()
|
||||||
|
@ -2,27 +2,34 @@ 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>();
|
||||||
|
|
||||||
let playerName: string|undefined;
|
let playerName: string | undefined;
|
||||||
|
|
||||||
export const setPlayerName = (name: string) => {
|
export const setPlayerName = (name: string) => {
|
||||||
playerName = name;
|
playerName = name;
|
||||||
}
|
};
|
||||||
|
|
||||||
let tags: string[]|undefined;
|
let tags: string[] | undefined;
|
||||||
|
|
||||||
export const setTags = (_tags: string[]) => {
|
export const setTags = (_tags: string[]) => {
|
||||||
tags = _tags;
|
tags = _tags;
|
||||||
}
|
};
|
||||||
|
|
||||||
let uuid: string|undefined;
|
let uuid: string | undefined;
|
||||||
|
|
||||||
export const setUuid = (_uuid: string|undefined) => {
|
export const setUuid = (_uuid: string | undefined) => {
|
||||||
uuid = _uuid;
|
uuid = _uuid;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||||
callbacks = [
|
callbacks = [
|
||||||
@ -43,25 +50,27 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() : string {
|
get name(): string {
|
||||||
if (playerName === undefined) {
|
if (playerName === undefined) {
|
||||||
throw new Error('Player name not initialized yet. You should call WA.player.name within a WA.onInit callback.');
|
throw new Error(
|
||||||
|
"Player name not initialized yet. You should call WA.player.name within a WA.onInit callback."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return playerName;
|
return playerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
get tags() : string[] {
|
get tags(): string[] {
|
||||||
if (tags === undefined) {
|
if (tags === undefined) {
|
||||||
throw new Error('Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.');
|
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
|
||||||
}
|
}
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() : string|undefined {
|
get id(): string | undefined {
|
||||||
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
|
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
|
||||||
// while uuid could.
|
// while uuid could.
|
||||||
if (playerName === undefined) {
|
if (playerName === undefined) {
|
||||||
throw new Error('Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.');
|
throw new Error("Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.");
|
||||||
}
|
}
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {Observable, Subject} from "rxjs";
|
import { Observable, Subject } from "rxjs";
|
||||||
|
|
||||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||||
|
|
||||||
import {IframeApiContribution, queryWorkadventure, 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";
|
||||||
@ -13,21 +13,21 @@ const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subj
|
|||||||
interface TileDescriptor {
|
interface TileDescriptor {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
tile: number | string;
|
tile: number | string | null;
|
||||||
layer: string;
|
layer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomId: string|undefined;
|
let roomId: string | undefined;
|
||||||
|
|
||||||
export const setRoomId = (id: string) => {
|
export const setRoomId = (id: string) => {
|
||||||
roomId = id;
|
roomId = id;
|
||||||
}
|
};
|
||||||
|
|
||||||
let mapURL: string|undefined;
|
let mapURL: string | undefined;
|
||||||
|
|
||||||
export const setMapURL = (url: string) => {
|
export const setMapURL = (url: string) => {
|
||||||
mapURL = url;
|
mapURL = url;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||||
callbacks = [
|
callbacks = [
|
||||||
@ -90,16 +90,18 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() : string {
|
get id(): string {
|
||||||
if (roomId === undefined) {
|
if (roomId === undefined) {
|
||||||
throw new Error('Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.');
|
throw new Error("Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.");
|
||||||
}
|
}
|
||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mapURL() : string {
|
get mapURL(): string {
|
||||||
if (mapURL === undefined) {
|
if (mapURL === undefined) {
|
||||||
throw new Error('mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback.');
|
throw new Error(
|
||||||
|
"mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return mapURL;
|
return mapURL;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
104
front/src/Components/Chat/Chat.svelte
Normal file
104
front/src/Components/Chat/Chat.svelte
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<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 }}">
|
||||||
|
<section class="chatWindowTitle">
|
||||||
|
<h1>Your chat history <span class="float-right" on:click={closeChat}>×</span></h1>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section class="messagesList" bind:this={listDom}>
|
||||||
|
<ul>
|
||||||
|
{#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">
|
||||||
|
h1 {
|
||||||
|
font-family: Lato;
|
||||||
|
|
||||||
|
span.float-right {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
background-color: #5f5f5f;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatWindowTitle {
|
||||||
|
flex: 0 100px;
|
||||||
|
}
|
||||||
|
.messagesList {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.messageForm {
|
||||||
|
flex: 0 70px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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: #42464d;
|
||||||
|
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: #42464d;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
border-left: solid black 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,3 +1,6 @@
|
|||||||
|
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 {
|
export function getColorByString(str: string): string | null {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
@ -15,7 +18,7 @@ export function getColorByString(str: string): string | null {
|
|||||||
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) {
|
||||||
@ -25,3 +28,19 @@ export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -1,92 +1,107 @@
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
|
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
|
||||||
import {RoomConnection} from "./RoomConnection";
|
import { RoomConnection } from "./RoomConnection";
|
||||||
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
|
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
|
||||||
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
|
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
|
||||||
import {localUserStore} from "./LocalUserStore";
|
import { localUserStore } from "./LocalUserStore";
|
||||||
import {CharacterTexture, LocalUser} from "./LocalUser";
|
import { CharacterTexture, LocalUser } from "./LocalUser";
|
||||||
import {Room} from "./Room";
|
import { Room } from "./Room";
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager {
|
class ConnectionManager {
|
||||||
private localUser!:LocalUser;
|
private localUser!: LocalUser;
|
||||||
|
|
||||||
private connexionType?: GameConnexionTypes
|
private connexionType?: GameConnexionTypes;
|
||||||
private reconnectingTimeout: NodeJS.Timeout|null = null;
|
private reconnectingTimeout: NodeJS.Timeout | null = null;
|
||||||
private _unloading:boolean = false;
|
private _unloading: boolean = false;
|
||||||
|
|
||||||
get unloading () {
|
get unloading() {
|
||||||
return this._unloading;
|
return this._unloading;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
this._unloading = true;
|
this._unloading = true;
|
||||||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
|
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Tries to login to the node server and return the starting map url to be loaded
|
* Tries to login to the node server and return the starting map url to be loaded
|
||||||
*/
|
*/
|
||||||
public async initGameConnexion(): Promise<Room> {
|
public async initGameConnexion(): Promise<Room> {
|
||||||
|
|
||||||
const connexionType = urlManager.getGameConnexionType();
|
const connexionType = urlManager.getGameConnexionType();
|
||||||
this.connexionType = connexionType;
|
this.connexionType = connexionType;
|
||||||
if(connexionType === GameConnexionTypes.register) {
|
if (connexionType === GameConnexionTypes.register) {
|
||||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||||
const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
|
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
||||||
|
(res) => res.data
|
||||||
|
);
|
||||||
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
|
||||||
|
) {
|
||||||
let localUser = localUserStore.getLocalUser();
|
let localUser = localUserStore.getLocalUser();
|
||||||
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
||||||
this.localUser = localUser;
|
this.localUser = localUser;
|
||||||
try {
|
try {
|
||||||
await this.verifyToken(localUser.jwtToken);
|
await this.verifyToken(localUser.jwtToken);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// If the token is invalid, let's generate an anonymous one.
|
// If the token is invalid, let's generate an anonymous one.
|
||||||
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
|
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
|
||||||
await this.anonymousLogin();
|
await this.anonymousLogin();
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
await this.anonymousLogin();
|
await this.anonymousLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
localUser = localUserStore.getLocalUser();
|
localUser = localUserStore.getLocalUser();
|
||||||
if(!localUser){
|
if (!localUser) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
localUser?.textures.push(newTexture)
|
localUser?.textures.push(newTexture);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.localUser = localUser;
|
this.localUser = localUser;
|
||||||
@ -95,55 +110,79 @@ class ConnectionManager {
|
|||||||
return Promise.resolve(room);
|
return Promise.resolve(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error('Invalid URL'));
|
return Promise.reject(new Error("Invalid URL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyToken(token: string): Promise<void> {
|
private async verifyToken(token: string): Promise<void> {
|
||||||
await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
|
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
||||||
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
|
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
||||||
if (!isBenchmark) { // In benchmark, we don't have a local storage.
|
if (!isBenchmark) {
|
||||||
|
// In benchmark, we don't have a local storage.
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public initBenchmark(): void {
|
public initBenchmark(): void {
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onConnectingError((event: CloseEvent) => {
|
connection.onConnectingError((event: CloseEvent) => {
|
||||||
console.log('An error occurred while connecting to socket server. Retrying');
|
console.log("An error occurred while connecting to socket server. Retrying");
|
||||||
reject(new Error('An error occurred while connecting to socket server. Retrying. Code: '+event.code+', Reason: '+event.reason));
|
reject(
|
||||||
|
new Error(
|
||||||
|
"An error occurred while connecting to socket server. Retrying. Code: " +
|
||||||
|
event.code +
|
||||||
|
", Reason: " +
|
||||||
|
event.reason
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onConnect((connect: OnConnectInterface) => {
|
connection.onConnect((connect: OnConnectInterface) => {
|
||||||
resolve(connect);
|
resolve(connect);
|
||||||
});
|
});
|
||||||
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
// Let's retry in 4-6 seconds
|
// Let's retry in 4-6 seconds
|
||||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||||
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(
|
||||||
}, 4000 + Math.floor(Math.random() * 2000) );
|
(connection) => resolve(connection)
|
||||||
|
);
|
||||||
|
}, 4000 + Math.floor(Math.random() * 2000));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get getConnexionType(){
|
get getConnexionType() {
|
||||||
return this.connexionType;
|
return this.connexionType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,91 +3,103 @@ 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 | null } {
|
/**
|
||||||
let roomId = '';
|
* Creates a "Room" object representing the room.
|
||||||
let hash = null;
|
* 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
|
|
||||||
if (!hash.length) {
|
|
||||||
hash = null
|
|
||||||
}
|
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,21 +120,24 @@ export class Room {
|
|||||||
} 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];
|
||||||
}
|
}
|
||||||
@ -130,8 +145,8 @@ export class Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -140,4 +155,32 @@ 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.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,11 +76,11 @@ export class RoomConnection implements RoomConnection {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @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(
|
public constructor(
|
||||||
token: string | null,
|
token: string | null,
|
||||||
roomId: string,
|
roomUrl: string,
|
||||||
name: string,
|
name: string,
|
||||||
characterLayers: string[],
|
characterLayers: string[],
|
||||||
position: PositionInterface,
|
position: PositionInterface,
|
||||||
@ -93,7 +93,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
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) {
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -28,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;
|
||||||
@ -68,20 +68,19 @@ export class GameManager {
|
|||||||
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 (
|
if (
|
||||||
|
@ -152,7 +152,10 @@ export class GameMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getTileProperty(index: number): Array<ITiledMapProperty> {
|
private getTileProperty(index: number): Array<ITiledMapProperty> {
|
||||||
return this.tileSetPropertyMap[index];
|
if (this.tileSetPropertyMap[index]) {
|
||||||
|
return this.tileSetPropertyMap[index];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private trigger(
|
private trigger(
|
||||||
@ -189,6 +192,10 @@ export class GameMap {
|
|||||||
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
|
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findPhaserLayers(groupName: string): TilemapLayer[] {
|
||||||
|
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
|
||||||
|
}
|
||||||
|
|
||||||
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
|
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);
|
||||||
@ -198,37 +205,45 @@ 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 {
|
} else {
|
||||||
console.error("The tile that you want to place doesn't exist.");
|
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
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 + "' does not exist (or is not a tilelaye).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,7 @@ import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
|||||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||||
import { playersStore } from "../../Stores/PlayersStore";
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
@ -164,9 +165,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;
|
||||||
@ -200,14 +202,14 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
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;
|
||||||
@ -459,11 +461,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") {
|
||||||
@ -476,7 +480,7 @@ export class GameScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.gameMap.exitUrls.forEach((exitUrl) => {
|
this.gameMap.exitUrls.forEach((exitUrl) => {
|
||||||
this.loadNextGame(exitUrl);
|
this.loadNextGameFromExitUrl(exitUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.startPositionCalculator = new StartPositionCalculator(
|
this.startPositionCalculator = new StartPositionCalculator(
|
||||||
@ -567,6 +571,10 @@ export class GameScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
oldPeerNumber = newPeerNumber;
|
oldPeerNumber = newPeerNumber;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => {
|
||||||
|
this.openChatIcon.setVisible(!v);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -577,7 +585,7 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
connectionManager
|
connectionManager
|
||||||
.connectToRoomSocket(
|
.connectToRoomSocket(
|
||||||
this.RoomId,
|
this.roomUrl,
|
||||||
this.playerName,
|
this.playerName,
|
||||||
this.characterLayers,
|
this.characterLayers,
|
||||||
{
|
{
|
||||||
@ -688,12 +696,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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -707,7 +715,11 @@ export class GameScene extends DirtyScene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set up variables manager
|
// Set up variables manager
|
||||||
this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap, onConnect.room.variables);
|
this.sharedVariablesManager = new SharedVariablesManager(
|
||||||
|
this.connection,
|
||||||
|
this.gameMap,
|
||||||
|
onConnect.room.variables
|
||||||
|
);
|
||||||
|
|
||||||
//this.initUsersPosition(roomJoinedMessage.users);
|
//this.initUsersPosition(roomJoinedMessage.users);
|
||||||
this.connectionAnswerPromiseResolve(onConnect.room);
|
this.connectionAnswerPromiseResolve(onConnect.room);
|
||||||
@ -768,10 +780,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) {
|
||||||
@ -996,9 +1011,9 @@ ${escapedMessage}
|
|||||||
);
|
);
|
||||||
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()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -1056,7 +1071,7 @@ ${escapedMessage}
|
|||||||
startLayerName: this.startPositionCalculator.startLayerName,
|
startLayerName: this.startPositionCalculator.startLayerName,
|
||||||
uuid: localUserStore.getLocalUser()?.uuid,
|
uuid: localUserStore.getLocalUser()?.uuid,
|
||||||
nickname: this.playerName,
|
nickname: this.playerName,
|
||||||
roomId: this.RoomId,
|
roomId: this.roomUrl,
|
||||||
tags: this.connection ? this.connection.getAllTags() : [],
|
tags: this.connection ? this.connection.getAllTags() : [],
|
||||||
variables: this.sharedVariablesManager.variables,
|
variables: this.sharedVariablesManager.variables,
|
||||||
};
|
};
|
||||||
@ -1080,53 +1095,86 @@ ${escapedMessage}
|
|||||||
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
|
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (propertyName === "exitUrl" && typeof propertyValue === "string") {
|
||||||
|
this.loadNextGameFromExitUrl(propertyValue);
|
||||||
|
}
|
||||||
if (layer.properties === undefined) {
|
if (layer.properties === undefined) {
|
||||||
layer.properties = [];
|
layer.properties = [];
|
||||||
}
|
}
|
||||||
const property = layer.properties.find((property) => property.name === propertyName);
|
const property = layer.properties.find((property) => property.name === propertyName);
|
||||||
if (property === undefined) {
|
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;
|
||||||
if (hash) {
|
try {
|
||||||
urlManager.pushStartLayerNameToUrl(hash);
|
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.startPositionCalculator.initPositionFromLayerName(hash, hash);
|
this.startPositionCalculator.initPositionFromLayerName(roomUrl.hash, roomUrl.hash);
|
||||||
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
|
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
|
||||||
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
|
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
|
||||||
setTimeout(() => (this.mapTransitioning = false), 500);
|
setTimeout(() => (this.mapTransitioning = false), 500);
|
||||||
@ -1153,6 +1201,7 @@ ${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");
|
iframeListener.unregisterAnswerer("getState");
|
||||||
this.sharedVariablesManager?.close();
|
this.sharedVariablesManager?.close();
|
||||||
@ -1218,11 +1267,18 @@ ${escapedMessage}
|
|||||||
.map((property) => property.value);
|
.map((property) => property.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadNextGameFromExitUrl(exitUrl: string): Promise<void> {
|
||||||
|
return this.loadNextGame(Room.getRoomPathFromExitUrl(exitUrl, window.location.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
//todo: push that into the gameManager
|
//todo: push that into the gameManager
|
||||||
private loadNextGame(exitSceneIdentifier: string): Promise<void> {
|
private async loadNextGame(exitRoomPath: URL): Promise<void> {
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
|
try {
|
||||||
const room = new Room(roomId);
|
const room = await Room.createRoom(exitRoomPath);
|
||||||
return gameManager.loadMap(room, this.scene).catch(() => {});
|
return gameManager.loadMap(room, this.scene);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo: in a dedicated class/function?
|
//todo: in a dedicated class/function?
|
||||||
|
@ -7,4 +7,5 @@ export interface PlayerInterface {
|
|||||||
visitCardUrl: string | null;
|
visitCardUrl: string | null;
|
||||||
companion: string | null;
|
companion: string | null;
|
||||||
userUuid: string;
|
userUuid: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ 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 { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||||
|
|
||||||
export const MenuSceneName = "MenuScene";
|
export const MenuSceneName = "MenuScene";
|
||||||
const gameMenuKey = "gameMenu";
|
const gameMenuKey = "gameMenu";
|
||||||
@ -98,6 +99,10 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
this.menuElement.setOrigin(0);
|
this.menuElement.setOrigin(0);
|
||||||
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
|
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
|
||||||
|
|
||||||
|
if (mediaManager.hasNotification()) {
|
||||||
|
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
const middleX = window.innerWidth / 3 - 298;
|
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");
|
||||||
@ -357,6 +362,9 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
case "toggleFullscreen":
|
case "toggleFullscreen":
|
||||||
this.toggleFullscreen();
|
this.toggleFullscreen();
|
||||||
break;
|
break;
|
||||||
|
case "enableNotification":
|
||||||
|
this.enableNotification();
|
||||||
|
break;
|
||||||
case "adminConsoleButton":
|
case "adminConsoleButton":
|
||||||
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
||||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||||
@ -419,4 +427,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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
118
front/src/Stores/ChatStore.ts
Normal file
118
front/src/Stores/ChatStore.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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,6 +1,7 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
||||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
|
import { getRandomColor } from "../WebRtc/ColorGenerator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the list of players currently known.
|
* A store that contains the list of players currently known.
|
||||||
@ -24,6 +25,7 @@ function createPlayersStore() {
|
|||||||
visitCardUrl: message.visitCardUrl,
|
visitCardUrl: message.visitCardUrl,
|
||||||
companion: message.companion,
|
companion: message.companion,
|
||||||
userUuid: message.userUuid,
|
userUuid: message.userUuid,
|
||||||
|
color: getRandomColor(),
|
||||||
});
|
});
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
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,232 +1,12 @@
|
|||||||
import { HtmlUtils } from "./HtmlUtils";
|
|
||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
|
||||||
import { connectionManager } from "../Connexion/ConnectionManager";
|
|
||||||
import { GameConnexionTypes } from "../Url/UrlManager";
|
|
||||||
import { iframeListener } from "../Api/IframeListener";
|
import { iframeListener } from "../Api/IframeListener";
|
||||||
import { showReportScreenStore } from "../Stores/ShowReportScreenStore";
|
import { chatMessagesStore, chatVisibilityStore } from "../Stores/ChatStore";
|
||||||
|
|
||||||
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);
|
chatMessagesStore.addExternalMessage(parseInt(chatEvent.author), chatEvent.message);
|
||||||
this.showDiscussion();
|
chatVisibilityStore.set(true);
|
||||||
});
|
});
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@ export class HtmlUtils {
|
|||||||
public static getElementByIdOrFail<T extends HTMLElement>(id: string): T {
|
public static getElementByIdOrFail<T extends HTMLElement>(id: string): T {
|
||||||
const elem = document.getElementById(id);
|
const elem = document.getElementById(id);
|
||||||
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot find HTML element with id '"+id+"'");
|
throw new Error("Cannot find HTML element with id '" + id + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
|
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
|
||||||
@ -12,7 +12,7 @@ export class HtmlUtils {
|
|||||||
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot find HTML element with selector '"+selector+"'");
|
throw new Error("Cannot find HTML element with selector '" + selector + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
|
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
|
||||||
@ -21,12 +21,12 @@ export class HtmlUtils {
|
|||||||
elem.remove();
|
elem.remove();
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot find HTML element with id '"+id+"'");
|
throw new Error("Cannot find HTML element with id '" + id + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export class HtmlUtils {
|
|||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
text = HtmlUtils.escapeHtml(text);
|
text = HtmlUtils.escapeHtml(text);
|
||||||
return text.replace(urlRegex, (url: string) => {
|
return text.replace(urlRegex, (url: string) => {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.target = "_blank";
|
link.target = "_blank";
|
||||||
const text = document.createTextNode(url);
|
const text = document.createTextNode(url);
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
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 type { UserSimplePeerInterface } from "./SimplePeer";
|
|
||||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
|
||||||
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
|
|
||||||
import { localStreamStore } from "../Stores/MediaStore";
|
import { localStreamStore } from "../Stores/MediaStore";
|
||||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||||
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
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;
|
||||||
|
|
||||||
@ -21,16 +15,11 @@ 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);
|
||||||
@ -182,67 +171,35 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addNewMessage(name: string, message: string, isMe: boolean = false) {
|
|
||||||
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) {
|
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) => {
|
|
||||||
console.error(`Notification permission error`, err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Notification.requestPermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public requestNotification() {
|
||||||
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
|
if (window.Notification && Notification.permission !== "granted") {
|
||||||
* if we are on Safari...
|
return Notification.requestPermission();
|
||||||
*
|
} else {
|
||||||
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
return Promise.reject();
|
||||||
*/
|
|
||||||
private checkNotificationPromise(): boolean {
|
|
||||||
try {
|
|
||||||
Notification.requestPermission().then();
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public createNotification(userName: string) {
|
public createNotification(userName: string) {
|
||||||
if (this.focused) {
|
if (document.hasFocus()) {
|
||||||
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,11 +1,10 @@
|
|||||||
import type * as SimplePeerNamespace from "simple-peer";
|
import type * as SimplePeerNamespace from "simple-peer";
|
||||||
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 { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
|
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
|
||||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||||
import { Readable, readable, writable, Writable } from "svelte/store";
|
import { Readable, readable } from "svelte/store";
|
||||||
import { videoFocusStore } from "../Stores/VideoFocusStore";
|
import { videoFocusStore } from "../Stores/VideoFocusStore";
|
||||||
|
import { getIceServersConfig } from "../Components/Video/utils";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||||
|
|
||||||
@ -32,21 +31,9 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
stream: MediaStream | null
|
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),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore }
|
|||||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||||
import { discussionManager } from "./DiscussionManager";
|
import { discussionManager } from "./DiscussionManager";
|
||||||
import { playersStore } from "../Stores/PlayersStore";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
|
import { newChatMessageStore } from "../Stores/ChatStore";
|
||||||
|
|
||||||
export interface UserSimplePeerInterface {
|
export interface UserSimplePeerInterface {
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -155,27 +156,11 @@ export class SimplePeer {
|
|||||||
|
|
||||||
const name = this.getName(user.userId);
|
const 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!
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
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 } from "svelte/store";
|
import { get, readable, Readable, Unsubscriber } from "svelte/store";
|
||||||
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||||
import { discussionManager } from "./DiscussionManager";
|
|
||||||
import { playersStore } from "../Stores/PlayersStore";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
|
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");
|
||||||
|
|
||||||
@ -34,6 +34,8 @@ export class VideoPeer extends Peer {
|
|||||||
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(
|
constructor(
|
||||||
public user: UserSimplePeerInterface,
|
public user: UserSimplePeerInterface,
|
||||||
@ -43,21 +45,9 @@ export class VideoPeer extends Peer {
|
|||||||
localStream: MediaStream | null
|
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),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -147,6 +137,20 @@ export class VideoPeer extends Peer {
|
|||||||
|
|
||||||
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) => {
|
||||||
@ -164,8 +168,9 @@ export class VideoPeer extends Peer {
|
|||||||
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);
|
||||||
|
chatVisibilityStore.set(true);
|
||||||
}
|
}
|
||||||
} 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.
|
||||||
@ -245,18 +250,18 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,34 @@
|
|||||||
import 'phaser';
|
import "phaser";
|
||||||
import GameConfig = Phaser.Types.Core.GameConfig;
|
import GameConfig = Phaser.Types.Core.GameConfig;
|
||||||
import "../style/index.scss";
|
import "../style/index.scss";
|
||||||
|
|
||||||
import {DEBUG_MODE, isMobile} from "./Enum/EnvironmentVariable";
|
import { DEBUG_MODE, isMobile } from "./Enum/EnvironmentVariable";
|
||||||
import {LoginScene} from "./Phaser/Login/LoginScene";
|
import { LoginScene } from "./Phaser/Login/LoginScene";
|
||||||
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
|
import { ReconnectingScene } from "./Phaser/Reconnecting/ReconnectingScene";
|
||||||
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
|
import { SelectCharacterScene } from "./Phaser/Login/SelectCharacterScene";
|
||||||
import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene";
|
import { SelectCompanionScene } from "./Phaser/Login/SelectCompanionScene";
|
||||||
import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene";
|
import { EnableCameraScene } from "./Phaser/Login/EnableCameraScene";
|
||||||
import {CustomizeScene} from "./Phaser/Login/CustomizeScene";
|
import { CustomizeScene } from "./Phaser/Login/CustomizeScene";
|
||||||
import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugin.js';
|
import WebFontLoaderPlugin from "phaser3-rex-plugins/plugins/webfontloader-plugin.js";
|
||||||
import OutlinePipelinePlugin from 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js';
|
import OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
|
||||||
import {EntryScene} from "./Phaser/Login/EntryScene";
|
import { EntryScene } from "./Phaser/Login/EntryScene";
|
||||||
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
|
import { coWebsiteManager } from "./WebRtc/CoWebsiteManager";
|
||||||
import {MenuScene} from "./Phaser/Menu/MenuScene";
|
import { MenuScene } from "./Phaser/Menu/MenuScene";
|
||||||
import {localUserStore} from "./Connexion/LocalUserStore";
|
import { localUserStore } from "./Connexion/LocalUserStore";
|
||||||
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
|
import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene";
|
||||||
import {iframeListener} from "./Api/IframeListener";
|
import { iframeListener } from "./Api/IframeListener";
|
||||||
import { SelectCharacterMobileScene } from './Phaser/Login/SelectCharacterMobileScene';
|
import { SelectCharacterMobileScene } from "./Phaser/Login/SelectCharacterMobileScene";
|
||||||
import {HdpiManager} from "./Phaser/Services/HdpiManager";
|
import { HdpiManager } from "./Phaser/Services/HdpiManager";
|
||||||
import {waScaleManager} from "./Phaser/Services/WaScaleManager";
|
import { waScaleManager } from "./Phaser/Services/WaScaleManager";
|
||||||
import {Game} from "./Phaser/Game/Game";
|
import { Game } from "./Phaser/Game/Game";
|
||||||
import App from './Components/App.svelte';
|
import App from "./Components/App.svelte";
|
||||||
import {HtmlUtils} from "./WebRtc/HtmlUtils";
|
import { HtmlUtils } from "./WebRtc/HtmlUtils";
|
||||||
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
|
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
|
||||||
|
|
||||||
|
const { width, height } = coWebsiteManager.getGameSize();
|
||||||
const {width, height} = coWebsiteManager.getGameSize();
|
|
||||||
|
|
||||||
const valueGameQuality = localUserStore.getGameQualityValue();
|
const valueGameQuality = localUserStore.getGameQualityValue();
|
||||||
const fps : Phaser.Types.Core.FPSConfig = {
|
const fps: Phaser.Types.Core.FPSConfig = {
|
||||||
/**
|
/**
|
||||||
* The minimum acceptable rendering rate, in frames per second.
|
* The minimum acceptable rendering rate, in frames per second.
|
||||||
*/
|
*/
|
||||||
@ -53,30 +52,30 @@ const fps : Phaser.Types.Core.FPSConfig = {
|
|||||||
/**
|
/**
|
||||||
* Apply delta smoothing during the game update to help avoid spikes?
|
* Apply delta smoothing during the game update to help avoid spikes?
|
||||||
*/
|
*/
|
||||||
smoothStep: false
|
smoothStep: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
// the ?phaserMode=canvas parameter can be used to force Canvas usage
|
// the ?phaserMode=canvas parameter can be used to force Canvas usage
|
||||||
const params = new URLSearchParams(document.location.search.substring(1));
|
const params = new URLSearchParams(document.location.search.substring(1));
|
||||||
const phaserMode = params.get("phaserMode");
|
const phaserMode = params.get("phaserMode");
|
||||||
let mode: number;
|
let mode: number;
|
||||||
switch (phaserMode) {
|
switch (phaserMode) {
|
||||||
case 'auto':
|
case "auto":
|
||||||
case null:
|
case null:
|
||||||
mode = Phaser.AUTO;
|
mode = Phaser.AUTO;
|
||||||
break;
|
break;
|
||||||
case 'canvas':
|
case "canvas":
|
||||||
mode = Phaser.CANVAS;
|
mode = Phaser.CANVAS;
|
||||||
break;
|
break;
|
||||||
case 'webgl':
|
case "webgl":
|
||||||
mode = Phaser.WEBGL;
|
mode = Phaser.WEBGL;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
|
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hdpiManager = new HdpiManager(640*480, 196*196);
|
const hdpiManager = new HdpiManager(640 * 480, 196 * 196);
|
||||||
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({width, height});
|
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({ width, height });
|
||||||
|
|
||||||
const config: GameConfig = {
|
const config: GameConfig = {
|
||||||
type: mode,
|
type: mode,
|
||||||
@ -87,9 +86,10 @@ const config: GameConfig = {
|
|||||||
height: gameSize.height,
|
height: gameSize.height,
|
||||||
zoom: realSize.width / gameSize.width,
|
zoom: realSize.width / gameSize.width,
|
||||||
autoRound: true,
|
autoRound: true,
|
||||||
resizeInterval: 999999999999
|
resizeInterval: 999999999999,
|
||||||
},
|
},
|
||||||
scene: [EntryScene,
|
scene: [
|
||||||
|
EntryScene,
|
||||||
LoginScene,
|
LoginScene,
|
||||||
isMobile() ? SelectCharacterMobileScene : SelectCharacterScene,
|
isMobile() ? SelectCharacterMobileScene : SelectCharacterScene,
|
||||||
SelectCompanionScene,
|
SelectCompanionScene,
|
||||||
@ -102,37 +102,39 @@ const config: GameConfig = {
|
|||||||
//resolution: window.devicePixelRatio / 2,
|
//resolution: window.devicePixelRatio / 2,
|
||||||
fps: fps,
|
fps: fps,
|
||||||
dom: {
|
dom: {
|
||||||
createContainer: true
|
createContainer: true,
|
||||||
},
|
},
|
||||||
render: {
|
render: {
|
||||||
pixelArt: true,
|
pixelArt: true,
|
||||||
roundPixels: true,
|
roundPixels: true,
|
||||||
antialias: false
|
antialias: false,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
global: [{
|
global: [
|
||||||
key: 'rexWebFontLoader',
|
{
|
||||||
plugin: WebFontLoaderPlugin,
|
key: "rexWebFontLoader",
|
||||||
start: true
|
plugin: WebFontLoaderPlugin,
|
||||||
}]
|
start: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
physics: {
|
physics: {
|
||||||
default: "arcade",
|
default: "arcade",
|
||||||
arcade: {
|
arcade: {
|
||||||
debug: DEBUG_MODE,
|
debug: DEBUG_MODE,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Instruct systems with 2 GPU to choose the low power one. We don't need that extra power and we want to save battery
|
// Instruct systems with 2 GPU to choose the low power one. We don't need that extra power and we want to save battery
|
||||||
powerPreference: "low-power",
|
powerPreference: "low-power",
|
||||||
callbacks: {
|
callbacks: {
|
||||||
postBoot: game => {
|
postBoot: (game) => {
|
||||||
// Install rexOutlinePipeline only if the renderer is WebGL.
|
// Install rexOutlinePipeline only if the renderer is WebGL.
|
||||||
const renderer = game.renderer;
|
const renderer = game.renderer;
|
||||||
if (renderer instanceof WebGLRenderer) {
|
if (renderer instanceof WebGLRenderer) {
|
||||||
game.plugins.install('rexOutlinePipeline', OutlinePipelinePlugin, true);
|
game.plugins.install("rexOutlinePipeline", OutlinePipelinePlugin, true);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
//const game = new Phaser.Game(config);
|
//const game = new Phaser.Game(config);
|
||||||
@ -140,7 +142,7 @@ const game = new Game(config);
|
|||||||
|
|
||||||
waScaleManager.setGame(game);
|
waScaleManager.setGame(game);
|
||||||
|
|
||||||
window.addEventListener('resize', function (event) {
|
window.addEventListener("resize", function (event) {
|
||||||
coWebsiteManager.resetStyle();
|
coWebsiteManager.resetStyle();
|
||||||
|
|
||||||
waScaleManager.applyNewSize();
|
waScaleManager.applyNewSize();
|
||||||
@ -153,10 +155,23 @@ coWebsiteManager.onResize.subscribe(() => {
|
|||||||
iframeListener.init();
|
iframeListener.init();
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
|
target: HtmlUtils.getElementByIdOrFail("svelte-overlay"),
|
||||||
props: {
|
props: {
|
||||||
game: game
|
game: game,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
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(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
@ -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
|
||||||
}
|
}
|
@ -39,9 +39,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
||||||
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
||||||
const userUuid = data.userUuid;
|
const userUuid = data.userUuid;
|
||||||
const organizationSlug = data.organizationSlug;
|
const roomUrl = data.roomUrl;
|
||||||
const worldSlug = data.worldSlug;
|
|
||||||
const roomSlug = data.roomSlug;
|
|
||||||
const mapUrlStart = data.mapUrlStart;
|
const mapUrlStart = data.mapUrlStart;
|
||||||
const textures = data.textures;
|
const textures = data.textures;
|
||||||
|
|
||||||
@ -52,9 +50,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
authToken,
|
authToken,
|
||||||
userUuid,
|
userUuid,
|
||||||
organizationSlug,
|
roomUrl,
|
||||||
worldSlug,
|
|
||||||
roomSlug,
|
|
||||||
mapUrlStart,
|
mapUrlStart,
|
||||||
organizationMemberToken,
|
organizationMemberToken,
|
||||||
textures,
|
textures,
|
||||||
|
@ -16,19 +16,21 @@ import {
|
|||||||
SendUserMessage,
|
SendUserMessage,
|
||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
CompanionMessage,
|
CompanionMessage,
|
||||||
EmotePromptMessage, VariableMessage,
|
EmotePromptMessage,
|
||||||
|
VariableMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
||||||
import { TemplatedApp } from "uWebSockets.js";
|
import { TemplatedApp } from "uWebSockets.js";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import { adminApi, CharacterTexture, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
||||||
import { SocketManager, socketManager } from "../Services/SocketManager";
|
import { SocketManager, socketManager } from "../Services/SocketManager";
|
||||||
import { emitInBatch } from "../Services/IoSocketHelpers";
|
import { emitInBatch } from "../Services/IoSocketHelpers";
|
||||||
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
import { CharacterTexture } from "../Services/AdminApi/CharacterTexture";
|
||||||
|
|
||||||
export class IoSocketController {
|
export class IoSocketController {
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
@ -221,14 +223,12 @@ export class IoSocketController {
|
|||||||
memberVisitCardUrl = userData.visitCardUrl;
|
memberVisitCardUrl = userData.visitCardUrl;
|
||||||
memberTextures = userData.textures;
|
memberTextures = userData.textures;
|
||||||
if (
|
if (
|
||||||
!room.public &&
|
|
||||||
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
|
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
|
||||||
(userData.anonymous === true || !room.canAccess(memberTags))
|
(userData.anonymous === true || !room.canAccess(memberTags))
|
||||||
) {
|
) {
|
||||||
throw new Error("Insufficient privileges to access this room");
|
throw new Error("Insufficient privileges to access this room");
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!room.public &&
|
|
||||||
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
|
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
|
||||||
userData.anonymous === true
|
userData.anonymous === true
|
||||||
) {
|
) {
|
||||||
|
@ -2,6 +2,9 @@ import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
|||||||
import { BaseController } from "./BaseController";
|
import { BaseController } from "./BaseController";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { adminApi } from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
|
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||||
|
import { MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||||
|
|
||||||
export class MapController extends BaseController {
|
export class MapController extends BaseController {
|
||||||
constructor(private App: TemplatedApp) {
|
constructor(private App: TemplatedApp) {
|
||||||
@ -25,35 +28,46 @@ export class MapController extends BaseController {
|
|||||||
|
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (typeof query.organizationSlug !== "string") {
|
if (typeof query.playUri !== "string") {
|
||||||
console.error("Expected organizationSlug parameter");
|
console.error("Expected playUri parameter in /map endpoint");
|
||||||
res.writeStatus("400 Bad request");
|
res.writeStatus("400 Bad request");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected organizationSlug parameter");
|
res.end("Expected playUri parameter");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof query.worldSlug !== "string") {
|
|
||||||
console.error("Expected worldSlug parameter");
|
// If no admin URL is set, let's react on '/_/[instance]/[map url]' URLs
|
||||||
res.writeStatus("400 Bad request");
|
if (!ADMIN_API_URL) {
|
||||||
|
const roomUrl = new URL(query.playUri);
|
||||||
|
|
||||||
|
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
|
||||||
|
if (!match) {
|
||||||
|
res.writeStatus("404 Not Found");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapUrl = roomUrl.protocol + "//" + match[1];
|
||||||
|
|
||||||
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected worldSlug parameter");
|
res.end(
|
||||||
return;
|
JSON.stringify({
|
||||||
}
|
mapUrl,
|
||||||
if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) {
|
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
|
||||||
console.error("Expected only one roomSlug parameter");
|
roomSlug: "", // Deprecated
|
||||||
res.writeStatus("400 Bad request");
|
tags: [],
|
||||||
this.addCorsHeaders(res);
|
textures: [],
|
||||||
res.end("Expected only one roomSlug parameter");
|
} as MapDetailsData)
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const mapDetails = await adminApi.fetchMapDetails(
|
const mapDetails = await adminApi.fetchMapDetails(query.playUri as string);
|
||||||
query.organizationSlug as string,
|
|
||||||
query.worldSlug as string,
|
|
||||||
query.roomSlug as string | undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||||
import { PositionDispatcher } from "./PositionDispatcher";
|
import { PositionDispatcher } from "./PositionDispatcher";
|
||||||
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
|
|
||||||
import { arrayIntersect } from "../Services/ArrayHelper";
|
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||||
import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone";
|
import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone";
|
||||||
import {apiClientRepository} from "../Services/ApiClientRepository";
|
import { apiClientRepository } from "../Services/ApiClientRepository";
|
||||||
import {
|
import {
|
||||||
BatchToPusherMessage, BatchToPusherRoomMessage, EmoteEventMessage, GroupLeftZoneMessage,
|
BatchToPusherMessage,
|
||||||
GroupUpdateZoneMessage, RoomMessage, SubMessage,
|
BatchToPusherRoomMessage,
|
||||||
UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, VariableMessage,
|
EmoteEventMessage,
|
||||||
ZoneMessage
|
GroupLeftZoneMessage,
|
||||||
|
GroupUpdateZoneMessage,
|
||||||
|
RoomMessage,
|
||||||
|
SubMessage,
|
||||||
|
UserJoinedZoneMessage,
|
||||||
|
UserLeftZoneMessage,
|
||||||
|
UserMovedMessage,
|
||||||
|
VariableMessage,
|
||||||
|
ZoneMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import {ClientReadableStream} from "grpc";
|
import { ClientReadableStream } from "grpc";
|
||||||
import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface";
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
|
|
||||||
const debug = Debug("room");
|
const debug = Debug("room");
|
||||||
|
|
||||||
@ -25,20 +32,15 @@ export enum GameRoomPolicyTypes {
|
|||||||
|
|
||||||
export class PusherRoom {
|
export class PusherRoom {
|
||||||
private readonly positionNotifier: PositionDispatcher;
|
private readonly positionNotifier: PositionDispatcher;
|
||||||
public readonly public: boolean;
|
|
||||||
public tags: string[];
|
public tags: string[];
|
||||||
public policyType: GameRoomPolicyTypes;
|
public policyType: GameRoomPolicyTypes;
|
||||||
public readonly roomSlug: string;
|
|
||||||
public readonly worldSlug: string = "";
|
|
||||||
public readonly organizationSlug: string = "";
|
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
private backConnection!: ClientReadableStream<BatchToPusherRoomMessage>;
|
private backConnection!: ClientReadableStream<BatchToPusherRoomMessage>;
|
||||||
private isClosing: boolean = false;
|
private isClosing: boolean = false;
|
||||||
private listeners: Set<ExSocketInterface> = new Set<ExSocketInterface>();
|
private listeners: Set<ExSocketInterface> = new Set<ExSocketInterface>();
|
||||||
public readonly variables = new Map<string, string>();
|
public readonly variables = new Map<string, string>();
|
||||||
|
|
||||||
constructor(public readonly roomId: string, private socketListener: ZoneEventListener) {
|
constructor(public readonly roomUrl: string, private socketListener: ZoneEventListener) {
|
||||||
this.public = isRoomAnonymous(roomId);
|
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
|
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ export class PusherRoom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A zone is 10 sprites wide.
|
// A zone is 10 sprites wide.
|
||||||
this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener);
|
this.positionNotifier = new PositionDispatcher(this.roomUrl, 320, 320, this.socketListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
||||||
@ -121,7 +123,7 @@ export class PusherRoom {
|
|||||||
// Let's close all connections linked to that room
|
// Let's close all connections linked to that room
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
listener.disconnecting = true;
|
listener.disconnecting = true;
|
||||||
listener.end(1011, "Connection error between pusher and back server")
|
listener.end(1011, "Connection error between pusher and back server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -132,7 +134,7 @@ export class PusherRoom {
|
|||||||
// Let's close all connections linked to that room
|
// Let's close all connections linked to that room
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
listener.disconnecting = true;
|
listener.disconnecting = true;
|
||||||
listener.end(1011, "Connection closed between pusher and back server")
|
listener.end(1011, "Connection closed between pusher and back server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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 };
|
|
||||||
};
|
|
@ -10,7 +10,6 @@ import {
|
|||||||
SubMessage,
|
SubMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import { WebSocket } from "uWebSockets.js";
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import { CharacterTexture } from "../../Services/AdminApi";
|
|
||||||
import { ClientDuplexStream } from "grpc";
|
import { ClientDuplexStream } from "grpc";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ import {
|
|||||||
SubMessage,
|
SubMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import { WebSocket } from "uWebSockets.js";
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import { CharacterTexture } from "../../Services/AdminApi";
|
|
||||||
import { ClientDuplexStream } from "grpc";
|
import { ClientDuplexStream } from "grpc";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
|
import { CharacterTexture } from "../../Services/AdminApi/CharacterTexture";
|
||||||
|
|
||||||
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
||||||
|
import { CharacterTexture } from "./AdminApi/CharacterTexture";
|
||||||
|
import { MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||||
|
import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||||
|
|
||||||
export interface AdminApiData {
|
export interface AdminApiData {
|
||||||
organizationSlug: string;
|
roomUrl: string;
|
||||||
worldSlug: string;
|
|
||||||
roomSlug: string;
|
|
||||||
mapUrlStart: string;
|
mapUrlStart: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
policy_type: number;
|
policy_type: number;
|
||||||
@ -14,25 +15,11 @@ export interface AdminApiData {
|
|||||||
textures: CharacterTexture[];
|
textures: CharacterTexture[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MapDetailsData {
|
|
||||||
roomSlug: string;
|
|
||||||
mapUrl: string;
|
|
||||||
policy_type: GameRoomPolicyTypes;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminBannedData {
|
export interface AdminBannedData {
|
||||||
is_banned: boolean;
|
is_banned: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CharacterTexture {
|
|
||||||
id: number;
|
|
||||||
level: number;
|
|
||||||
url: string;
|
|
||||||
rights: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FetchMemberDataByUuidResponse {
|
export interface FetchMemberDataByUuidResponse {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
@ -43,24 +30,15 @@ export interface FetchMemberDataByUuidResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AdminApi {
|
class AdminApi {
|
||||||
async fetchMapDetails(
|
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||||
organizationSlug: string,
|
|
||||||
worldSlug: string,
|
|
||||||
roomSlug: string | undefined
|
|
||||||
): Promise<MapDetailsData> {
|
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
|
const params: { playUri: string } = {
|
||||||
organizationSlug,
|
playUri,
|
||||||
worldSlug,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (roomSlug) {
|
|
||||||
params.roomSlug = roomSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
params,
|
params,
|
||||||
@ -121,26 +99,20 @@ class AdminApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyBanUser(
|
async verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
|
||||||
organizationMemberToken: string,
|
|
||||||
ipAddress: string,
|
|
||||||
organization: string,
|
|
||||||
world: string
|
|
||||||
): Promise<AdminBannedData> {
|
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
}
|
}
|
||||||
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||||
return Axios.get(
|
return Axios.get(
|
||||||
ADMIN_API_URL +
|
ADMIN_API_URL +
|
||||||
"/api/check-moderate-user/" +
|
"/api/ban" +
|
||||||
organization +
|
|
||||||
"/" +
|
|
||||||
world +
|
|
||||||
"?ipAddress=" +
|
"?ipAddress=" +
|
||||||
ipAddress +
|
encodeURIComponent(ipAddress) +
|
||||||
"&token=" +
|
"&token=" +
|
||||||
organizationMemberToken,
|
encodeURIComponent(userUuid) +
|
||||||
|
"&roomUrl=" +
|
||||||
|
encodeURIComponent(roomUrl),
|
||||||
{ headers: { Authorization: `${ADMIN_API_TOKEN}` } }
|
{ headers: { Authorization: `${ADMIN_API_TOKEN}` } }
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
|
11
pusher/src/Services/AdminApi/CharacterTexture.ts
Normal file
11
pusher/src/Services/AdminApi/CharacterTexture.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isCharacterTexture = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
id: tg.isNumber,
|
||||||
|
level: tg.isNumber,
|
||||||
|
url: tg.isString,
|
||||||
|
rights: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type CharacterTexture = tg.GuardedType<typeof isCharacterTexture>;
|
20
pusher/src/Services/AdminApi/MapDetailsData.ts
Normal file
20
pusher/src/Services/AdminApi/MapDetailsData.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
||||||
|
import { isCharacterTexture } from "./CharacterTexture";
|
||||||
|
import { isAny, isNumber } from "generic-type-guard";
|
||||||
|
|
||||||
|
/*const isNumericEnum =
|
||||||
|
<T extends { [n: number]: string }>(vs: T) =>
|
||||||
|
(v: any): v is T =>
|
||||||
|
typeof v === "number" && v in vs;*/
|
||||||
|
|
||||||
|
export const isMapDetailsData = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
roomSlug: tg.isOptional(tg.isString), // deprecated
|
||||||
|
mapUrl: tg.isString,
|
||||||
|
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
||||||
|
tags: tg.isArray(tg.isString),
|
||||||
|
textures: tg.isArray(isCharacterTexture),
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
8
pusher/src/Services/AdminApi/RoomRedirect.ts
Normal file
8
pusher/src/Services/AdminApi/RoomRedirect.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isRoomRedirect = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
redirectUrl: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type RoomRedirect = tg.GuardedType<typeof isRoomRedirect>;
|
@ -9,7 +9,7 @@ class JWTTokenManager {
|
|||||||
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token
|
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserUuidFromToken(token: unknown, ipAddress?: string, room?: string): Promise<string> {
|
public async getUserUuidFromToken(token: unknown, ipAddress?: string, roomUrl?: string): Promise<string> {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("An authentication error happened, a user tried to connect without a token.");
|
throw new Error("An authentication error happened, a user tried to connect without a token.");
|
||||||
}
|
}
|
||||||
@ -50,8 +50,8 @@ class JWTTokenManager {
|
|||||||
if (ADMIN_API_URL) {
|
if (ADMIN_API_URL) {
|
||||||
//verify user in admin
|
//verify user in admin
|
||||||
let promise = new Promise((resolve) => resolve());
|
let promise = new Promise((resolve) => resolve());
|
||||||
if (ipAddress && room) {
|
if (ipAddress && roomUrl) {
|
||||||
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room);
|
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, roomUrl);
|
||||||
}
|
}
|
||||||
promise
|
promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -79,19 +79,9 @@ class JWTTokenManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise<AdminBannedData> {
|
private verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
|
||||||
const parts = room.split("/");
|
|
||||||
if (parts.length < 3 || parts[0] !== "@") {
|
|
||||||
return Promise.resolve({
|
|
||||||
is_banned: false,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = parts[1];
|
|
||||||
const world = parts[2];
|
|
||||||
return adminApi
|
return adminApi
|
||||||
.verifyBanUser(userUuid, ipAddress, organization, world)
|
.verifyBanUser(userUuid, ipAddress, roomUrl)
|
||||||
.then((data: AdminBannedData) => {
|
.then((data: AdminBannedData) => {
|
||||||
if (data && data.is_banned) {
|
if (data && data.is_banned) {
|
||||||
throw new Error("User was banned");
|
throw new Error("User was banned");
|
||||||
|
@ -33,8 +33,8 @@ import {
|
|||||||
VariableMessage,
|
VariableMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||||
import { adminApi, CharacterTexture } from "./AdminApi";
|
import { adminApi } from "./AdminApi";
|
||||||
import { emitInBatch } from "./IoSocketHelpers";
|
import { emitInBatch } from "./IoSocketHelpers";
|
||||||
import Jwt from "jsonwebtoken";
|
import Jwt from "jsonwebtoken";
|
||||||
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||||
@ -45,6 +45,8 @@ import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone"
|
|||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
import { WebSocket } from "uWebSockets.js";
|
import { WebSocket } from "uWebSockets.js";
|
||||||
|
import { isRoomRedirect } from "./AdminApi/RoomRedirect";
|
||||||
|
import { CharacterTexture } from "./AdminApi/CharacterTexture";
|
||||||
|
|
||||||
const debug = Debug("socket");
|
const debug = Debug("socket");
|
||||||
|
|
||||||
@ -370,24 +372,30 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<PusherRoom> {
|
async getOrCreateRoom(roomUrl: string): Promise<PusherRoom> {
|
||||||
//check and create new world for a room
|
//check and create new world for a room
|
||||||
let world = this.rooms.get(roomId);
|
let room = this.rooms.get(roomUrl);
|
||||||
if (world === undefined) {
|
if (room === undefined) {
|
||||||
world = new PusherRoom(roomId, this);
|
room = new PusherRoom(roomUrl, this);
|
||||||
if (!world.public) {
|
if (ADMIN_API_URL) {
|
||||||
await this.updateRoomWithAdminData(world);
|
await this.updateRoomWithAdminData(room);
|
||||||
}
|
}
|
||||||
await world.init();
|
await world.init();
|
||||||
this.rooms.set(roomId, world);
|
this.rooms.set(roomUrl, room);
|
||||||
}
|
}
|
||||||
return world;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateRoomWithAdminData(world: PusherRoom): Promise<void> {
|
public async updateRoomWithAdminData(room: PusherRoom): Promise<void> {
|
||||||
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug);
|
const data = await adminApi.fetchMapDetails(room.roomUrl);
|
||||||
world.tags = data.tags;
|
|
||||||
world.policyType = Number(data.policy_type);
|
if (isRoomRedirect(data)) {
|
||||||
|
// TODO: if the updated room data is actually a redirect, we need to take everybody on the map
|
||||||
|
// and redirect everybody to the new location (so we need to close the connection for everybody)
|
||||||
|
} else {
|
||||||
|
room.tags = data.tags;
|
||||||
|
room.policyType = Number(data.policy_type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
|
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
})
|
|
Loading…
Reference in New Issue
Block a user