Merge branch 'develop' of github.com:thecodingmachine/workadventure into main

This commit is contained in:
_Bastler 2021-07-02 20:59:39 +02:00
commit 74823177f5
58 changed files with 2227 additions and 1746 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ docker-compose.override.yaml
maps/yarn.lock maps/yarn.lock
maps/dist/computer.js maps/dist/computer.js
maps/dist/computer.js.map maps/dist/computer.js.map
/node_modules/ node_modules
_

View File

@ -42,7 +42,7 @@ Before committing, be sure to install the "Prettier" precommit hook that will re
In order to enable the "Prettier" precommit hook, at the root of the project, run: In order to enable the "Prettier" precommit hook, at the root of the project, run:
```console ```console
$ yarn run install $ yarn install
$ yarn run prepare $ yarn run prepare
``` ```

View File

@ -81,7 +81,7 @@ WA.room.getCurrentRoom(): Promise<Room>
``` ```
Return a promise that resolves to a `Room` object with the following attributes : Return a promise that resolves to a `Room` object with the following attributes :
* **id (string) :** ID of the current room * **id (string) :** ID of the current room
* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. * **map (ITiledMap) :** contains the JSON map file with the properties that were set by the script if `setProperty` was called.
* **mapUrl (string) :** Url of the JSON map file * **mapUrl (string) :** Url of the JSON map file
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer * **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer

View File

@ -64,7 +64,7 @@
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack", "build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
"test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
"precommit": "lint-staged", "precommit": "lint-staged",

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isDataLayerEvent = new tg.IsInterface()
.withProperties({
export const isDataLayerEvent = data: tg.isObject,
new tg.IsInterface().withProperties({ })
data: tg.isObject .get();
}).get();
/** /**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers

View File

@ -1,14 +1,15 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isGameStateEvent = export const isGameStateEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
roomId: tg.isString, roomId: tg.isString,
mapUrl: tg.isString, mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull), nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined), uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull), startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags : tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when the gameState is received by the script * A message sent from the game to the iFrame when the gameState is received by the script
*/ */

View File

@ -1,19 +1,17 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent = new tg.IsInterface()
.withProperties({
export const isHasPlayerMovedEvent = direction: tg.isElementOf("right", "left", "up", "down"),
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
moving: tg.isBoolean, moving: tg.isBoolean,
x: tg.isNumber, x: tg.isNumber,
y: tg.isNumber y: tg.isNumber,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame to notify a movement from the current player. * A message sent from the game to the iFrame to notify a movement from the current player.
*/ */
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>; export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void

View File

@ -1,73 +1,73 @@
import type { GameStateEvent } from "./GameStateEvent";
import type { GameStateEvent } from './GameStateEvent'; import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ButtonClickedEvent } from './ButtonClickedEvent'; import type { ChatEvent } from "./ChatEvent";
import type { ChatEvent } from './ChatEvent'; import type { ClosePopupEvent } from "./ClosePopupEvent";
import type { ClosePopupEvent } from './ClosePopupEvent'; import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { GoToPageEvent } from "./GoToPageEvent";
import type { GoToPageEvent } from './GoToPageEvent'; import type { LoadPageEvent } from "./LoadPageEvent";
import type { LoadPageEvent } from './LoadPageEvent'; import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from "./OpenTabEvent";
import type { OpenTabEvent } from './OpenTabEvent'; import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { DataLayerEvent } from "./DataLayerEvent"; import type { DataLayerEvent } from "./DataLayerEvent";
import type { LayerEvent } from './LayerEvent'; import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent"; import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetTilesEvent } from "./SetTilesEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T data: T;
} }
/**
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = { export type IframeEventMap = {
//getState: GameStateEvent, loadPage: LoadPageEvent;
loadPage: LoadPageEvent chat: ChatEvent;
chat: ChatEvent, openPopup: OpenPopupEvent;
openPopup: OpenPopupEvent closePopup: ClosePopupEvent;
closePopup: ClosePopupEvent openTab: OpenTabEvent;
openTab: OpenTabEvent goToPage: GoToPageEvent;
goToPage: GoToPageEvent openCoWebSite: OpenCoWebSiteEvent;
openCoWebSite: OpenCoWebSiteEvent closeCoWebSite: null;
closeCoWebSite: null disablePlayerControls: null;
disablePlayerControls: null restorePlayerControls: null;
restorePlayerControls: null displayBubble: null;
displayBubble: null removeBubble: null;
removeBubble: null onPlayerMove: undefined;
onPlayerMove: undefined showLayer: LayerEvent;
showLayer: LayerEvent hideLayer: LayerEvent;
hideLayer: LayerEvent setProperty: SetPropertyEvent;
setProperty: SetPropertyEvent getDataLayer: undefined;
getDataLayer: undefined loadSound: LoadSoundEvent;
loadSound: LoadSoundEvent playSound: PlaySoundEvent;
playSound: PlaySoundEvent stopSound: null;
stopSound: null getState: undefined;
setTiles: SetTilesEvent registerMenuCommand: MenuItemRegisterEvent;
getState: undefined, setTiles: SetTilesEvent;
registerMenuCommand: MenuItemRegisterEvent };
}
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
data: IframeEventMap[T]; data: IframeEventMap[T];
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string'; export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export interface IframeResponseEventMap { export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent userInputChat: UserInputChatEvent;
enterEvent: EnterLeaveEvent enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent buttonClickedEvent: ButtonClickedEvent;
gameState: GameStateEvent hasPlayerMoved: HasPlayerMovedEvent;
hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent;
dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent;
menuItemClicked: MenuItemClickedEvent
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;
@ -75,4 +75,49 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string'; export const isIframeResponseEventWrapper = (event: {
type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
*/
export type IframeQueryMap = {
getState: {
query: undefined,
answer: GameStateEvent
},
}
export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T;
data: IframeQueryMap[T]['query'];
}
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
id: number;
query: IframeQuery<T>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === 'string';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
id: number;
type: T;
data: IframeQueryMap[T]['answer'];
}
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number';
export interface IframeErrorAnswerEvent {
id: number;
type: keyof IframeQueryMap;
error: string;
}
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string';

View File

@ -1,9 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isLayerEvent = export const isLayerEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
name: tg.isString, name: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to show/hide a layer. * A message sent from the iFrame to the game to show/hide a layer.
*/ */

View File

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

View File

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isOpenCoWebsite = new tg.IsInterface()
.withProperties({
export const isOpenCoWebsite =
new tg.IsInterface().withProperties({
url: tg.isString, url: tg.isString,
allowApi: tg.isBoolean, allowApi: tg.isBoolean,
allowPolicy: tg.isString, allowPolicy: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View File

@ -1,14 +1,15 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isSetTilesEvent = export const isSetTilesEvent = tg.isArray(
tg.isArray( new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
x: tg.isNumber, x: tg.isNumber,
y: tg.isNumber, y: tg.isNumber,
tile: tg.isUnion(tg.isNumber, tg.isString), tile: tg.isUnion(tg.isNumber, tg.isString),
layer: tg.isString layer: tg.isString,
}).get() })
); .get()
);
/** /**
* A message sent from the iFrame to the game to set one or many tiles. * A message sent from the iFrame to the game to set one or many tiles.
*/ */

View File

@ -1,11 +1,12 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isSetPropertyEvent = export const isSetPropertyEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
layerName: tg.isString, layerName: tg.isString,
propertyName: tg.isString, propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))) propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */

View File

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isMenuItemClickedEvent = export const isMenuItemClickedEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
menuItem: tg.isString menuItem: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when a menu item is clicked. * A message sent from the game to the iFrame when a menu item is clicked.
*/ */
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>; export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;

View File

@ -1,25 +1,26 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import { Subject } from 'rxjs'; import { Subject } from "rxjs";
export const isMenuItemRegisterEvent = export const isMenuItemRegisterEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
menutItem: tg.isString menutItem: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a new menu item. * A message sent from the iFrame to the game to add a new menu item.
*/ */
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>; export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent = export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
type: tg.isSingletonString("registerMenuCommand"), type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent data: isMenuItemRegisterEvent,
}).get(); })
.get();
const _registerMenuCommandStream: Subject<string> = new Subject(); const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem) _registerMenuCommandStream.next(event.menutItem);
} }

View File

@ -1,42 +1,45 @@
import {Subject} from "rxjs"; import { Subject } from "rxjs";
import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import {scriptUtils} from "./ScriptUtils"; import { scriptUtils } from "./ScriptUtils";
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import { import {
IframeErrorAnswerEvent,
IframeEvent, IframeEvent,
IframeEventMap, IframeEventMap, IframeQueryMap,
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
isIframeEventWrapper, isIframeEventWrapper,
TypedMessageEvent isIframeQueryWrapper,
TypedMessageEvent,
} from "./Events/IframeEvent"; } from "./Events/IframeEvent";
import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent"; import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent"; import type { DataLayerEvent } from "./Events/DataLayerEvent";
import type {GameStateEvent} from "./Events/GameStateEvent"; import type { GameStateEvent } from "./Events/GameStateEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import {SetTilesEvent, isSetTilesEvent} from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
*/ */
class IframeListener { class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject(); private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable(); public readonly chatStream = this._chatStream.asObservable();
@ -85,9 +88,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject(); private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable(); public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject(); private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
@ -114,117 +114,147 @@ class IframeListener {
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false; private sendPlayerMove: boolean = false;
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>
} = {};
init() { init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => { window.addEventListener(
// Do we trust the sender of this message? "message",
// Let's only accept messages from the iframe that are allowed. (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). // Do we trust the sender of this message?
let foundSrc: string | undefined; // Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
let iframe: HTMLIFrameElement; let iframe: HTMLIFrameElement | undefined;
for (iframe of this.iframes) { for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) { if (iframe.contentWindow === message.source) {
foundSrc = iframe.src; foundSrc = iframe.src;
break; break;
} }
}
const payload = message.data;
if (foundSrc === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn('It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. ' +
'If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the ' +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
'parameter to WA.nav.openCoWebSite())');
}
return;
}
foundSrc = this.getBaseUrl(foundSrc, message.source);
if (isIframeEventWrapper(payload)) {
if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
}
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc, payload.data.allowApi, payload.data.allowPolicy);
} }
else if (payload.type === 'closeCoWebSite') { const payload = message.data;
scriptUtils.closeCoWebSite();
if (foundSrc === undefined || iframe === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn(
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
"If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
"parameter to WA.nav.openCoWebSite())"
);
}
return;
} }
else if (payload.type === 'disablePlayerControls') { foundSrc = this.getBaseUrl(foundSrc, message.source);
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next();
} else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next();
} else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next();
} else if (payload.type == "getState") {
this._gameStateStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
})
handleMenuItemRegistrationEvent(payload.data)
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
}
}
}, false);
if (isIframeQueryWrapper(payload)) {
const queryId = payload.id;
const query = payload.query;
const answerer = this.answerers[query.type];
if (answerer === undefined) {
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
console.error(errorMsg);
iframe.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: errorMsg
} as IframeErrorAnswerEvent, '*');
return;
}
Promise.resolve(answerer(query.data)).then((value) => {
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
data: value
}, '*');
}).catch(reason => {
console.error('An error occurred while responding to an iFrame query.', reason);
let reasonMsg: string;
if (reason instanceof Error) {
reasonMsg = reason.message;
} else {
reasonMsg = reason.toString();
}
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: reasonMsg
} as IframeErrorAnswerEvent, '*');
});
} else if (isIframeEventWrapper(payload)) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(
payload.data.url,
foundSrc,
payload.data.allowApi,
payload.data.allowPolicy
);
} else if (payload.type === "closeCoWebSite") {
scriptUtils.closeCoWebSite();
} else if (payload.type === "disablePlayerControls") {
this._disablePlayerControlStream.next();
} else if (payload.type === "restorePlayerControls") {
this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") {
this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
});
handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
}
}
},
false
);
} }
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({ this.postMessage({
'type' : 'dataLayer', type: "dataLayer",
'data' : dataLayerEvent data: dataLayerEvent,
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
}); });
} }
@ -245,14 +275,14 @@ class IframeListener {
} }
registerScript(scriptUrl: string): void { registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl) console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode ( // Using external iframe mode (
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = "none";
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add('allow-scripts');
@ -265,45 +295,50 @@ class IframeListener {
this.registerIframe(iframe); this.registerIframe(iframe);
} else { } else {
// production code // production code
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = "none";
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add("allow-scripts");
iframe.sandbox.add('allow-top-navigation-by-user-activation'); iframe.sandbox.add("allow-top-navigation-by-user-activation");
//iframe.src = "data:text/html;charset=utf-8," + escape(html); //iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' + iframe.srcdoc =
'\n' + "<!doctype html>\n" +
"\n" +
'<html lang="en">\n' + '<html lang="en">\n' +
'<head>\n' + "<head>\n" +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' + '<script src="' +
'<script src="' + scriptUrl + '" ></script>\n' + window.location.protocol +
'<title></title>\n' + "//" +
'</head>\n' + window.location.host +
'</html>\n'; '/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
document.body.prepend(iframe); document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe); this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe); this.registerIframe(iframe);
} }
} }
private getBaseUrl(src: string, source: MessageEventSource | null): string{ private getBaseUrl(src: string, source: MessageEventSource | null): string {
for (const script of this.scripts) { for (const script of this.scripts) {
if (script[1].contentWindow === source) { if (script[1].contentWindow === source) {
return script[0]; return script[0];
} }
} }
return src; return src;
} }
private static getIFrameId(scriptUrl: string): string { private static getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl); return "script" + btoa(scriptUrl);
} }
unregisterScript(scriptUrl: string): void { unregisterScript(scriptUrl: string): void {
@ -320,44 +355,44 @@ class IframeListener {
sendUserInputChat(message: string) { sendUserInputChat(message: string) {
this.postMessage({ this.postMessage({
'type': 'userInputChat', type: "userInputChat",
'data': { data: {
'message': message, message: message,
} as UserInputChatEvent } as UserInputChatEvent,
}); });
} }
sendEnterEvent(name: string) { sendEnterEvent(name: string) {
this.postMessage({ this.postMessage({
'type': 'enterEvent', type: "enterEvent",
'data': { data: {
"name": name name: name,
} as EnterLeaveEvent } as EnterLeaveEvent,
}); });
} }
sendLeaveEvent(name: string) { sendLeaveEvent(name: string) {
this.postMessage({ this.postMessage({
'type': 'leaveEvent', type: "leaveEvent",
'data': { data: {
"name": name name: name,
} as EnterLeaveEvent } as EnterLeaveEvent,
}); });
} }
hasPlayerMoved(event: HasPlayerMovedEvent) { hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) { if (this.sendPlayerMove) {
this.postMessage({ this.postMessage({
'type': 'hasPlayerMoved', type: "hasPlayerMoved",
'data': event data: event,
}); });
} }
} }
sendButtonClickedEvent(popupId: number, buttonId: number, input : boolean, inputValue : string | null): void { sendButtonClickedEvent(popupId: number, buttonId: number, input : boolean, inputValue : string | null): void {
this.postMessage({ this.postMessage({
'type': 'buttonClickedEvent', type: "buttonClickedEvent",
'data': { data: {
popupId, popupId,
buttonId, buttonId,
input, input,
@ -371,10 +406,25 @@ class IframeListener {
*/ */
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) { public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*'); iframe.contentWindow?.postMessage(message, "*");
} }
} }
/**
* Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type).
*
* Important! There can be only one "answerer" so registering a new one will unregister the old one.
*
* @param key The "type" of the query we are answering
* @param callback
*/
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
} }
export const iframeListener = new IframeListener(); export const iframeListener = new IframeListener();

View File

@ -1,21 +1,19 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
class ScriptUtils { class ScriptUtils {
public openTab(url: string) {
public openTab(url : string){
window.open(url); window.open(url);
} }
public goToPage(url : string){ public goToPage(url: string) {
window.location.href = url; window.location.href = url;
} }
public openCoWebsite(url: string, base: string, api: boolean, policy: string) { public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
coWebsiteManager.loadCoWebsite(url, base, api, policy); coWebsiteManager.loadCoWebsite(url, base, api, policy);
} }
public closeCoWebSite(){ public closeCoWebSite() {
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
} }
} }

View File

@ -1,9 +1,40 @@
import type * as tg from "generic-type-guard"; import type * as tg from "generic-type-guard";
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; import type {
IframeEvent,
IframeEventMap, IframeQuery,
IframeQueryMap,
IframeResponseEventMap
} from '../Events/IframeEvent';
import type {IframeQueryWrapper} from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) { export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*") window.parent.postMessage(content, "*")
} }
let queryNumber = 0;
export const answerPromises = new Map<number, {
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void
}>();
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
window.parent.postMessage({
id: queryNumber,
query: content
} as IframeQueryWrapper<T>, "*");
answerPromises.set(queryNumber, {
resolve,
reject
});
queryNumber++;
});
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> { export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {

View File

@ -1,11 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent'; import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
import { iframeListener } from '../../IframeListener'; import { iframeListener } from "../../IframeListener";
export function sendMenuClickedEvent(menuItem: string) { export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({ iframeListener.postMessage({
'type': 'menuItemClicked', type: "menuItemClicked",
'data': { data: {
menuItem: menuItem, menuItem: menuItem,
} as MenuItemClickedEvent } as MenuItemClickedEvent,
}); });
} }

View File

@ -4,7 +4,7 @@ import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent"; import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
@ -32,19 +32,15 @@ interface User {
} }
interface TileDescriptor { interface TileDescriptor {
x: number x: number;
y: number y: number;
tile: number | string tile: number | string;
layer: string layer: string;
} }
function getGameState(): Promise<GameStateEvent> { function getGameState(): Promise<GameStateEvent> {
if (immutableDataPromise === undefined) { if (immutableDataPromise === undefined) {
immutableDataPromise = new Promise<GameStateEvent>((resolver, thrower) => { immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
stateResolvers.subscribe(resolver);
sendToWorkadventure({ type: "getState", data: null });
});
} }
return immutableDataPromise; return immutableDataPromise;
} }
@ -72,13 +68,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
leaveStreams.get(payloadData.name)?.next(); leaveStreams.get(payloadData.name)?.next();
}, },
}), }),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
},
}),
apiCallback({ apiCallback({
type: "dataLayer", type: "dataLayer",
typeChecker: isDataLayerEvent, typeChecker: isDataLayerEvent,
@ -139,11 +128,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
} }
setTiles(tiles: TileDescriptor[]) { setTiles(tiles: TileDescriptor[]) {
sendToWorkadventure({ sendToWorkadventure({
type: 'setTiles', type: "setTiles",
data: tiles data: tiles,
}) });
} }
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View File

@ -1,4 +1,4 @@
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) {
return null; return null;
@ -7,10 +7,10 @@ export function getColorByString(str: string) : string|null {
hash = str.charCodeAt(i) + ((hash << 5) - hash); hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; hash = hash & hash;
} }
let color = '#'; let color = "#";
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255; const value = (hash >> (i * 8)) & 255;
color += ('00' + value.toString(16)).substr(-2); color += ("00" + value.toString(16)).substr(-2);
} }
return color; return color;
} }
@ -20,8 +20,8 @@ export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
return { return {
update(newStream: MediaStream) { update(newStream: MediaStream) {
if (node.srcObject != newStream) { if (node.srcObject != newStream) {
node.srcObject = newStream node.srcObject = newStream;
} }
} },
} };
} }

View File

@ -1,18 +1,18 @@
import Axios from "axios"; import Axios from "axios";
import {PUSHER_URL} from "../Enum/EnvironmentVariable"; import { PUSHER_URL } from "../Enum/EnvironmentVariable";
import type {CharacterTexture} from "./LocalUser"; import type { CharacterTexture } from "./LocalUser";
export class MapDetail{ export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) { constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {
} }
} }
export 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 _search: URLSearchParams;
constructor(id: string) { constructor(id: string) {
@ -34,18 +34,21 @@ export class Room {
this._search = new URLSearchParams(url.search); this._search = new URLSearchParams(url.search);
} }
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} { public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): { roomId: string, hash: string | null } {
let roomId = ''; let roomId = '';
let hash = ''; let hash = null;
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link 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. //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.
//We instead use 'workadventure' as a dummy base value. //We instead use 'workadventure' as a dummy base value.
const baseUrlObject = new URL(baseUrl); const baseUrlObject = new URL(baseUrl);
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/'+currentInstance+'/'+baseUrlObject.hostname+baseUrlObject.pathname); const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/' + currentInstance + '/' + baseUrlObject.hostname + baseUrlObject.pathname);
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
roomId = roomId.substring(1); //remove the leading slash roomId = roomId.substring(1); //remove the leading slash
hash = absoluteExitSceneUrl.hash; hash = absoluteExitSceneUrl.hash;
hash = hash.substring(1); //remove the leading diese hash = hash.substring(1); //remove the leading diese
if (!hash.length) {
hash = null
}
} else { //absolute room Id } else { //absolute room Id
const parts = identifier.split('#'); const parts = identifier.split('#');
roomId = parts[0]; roomId = parts[0];
@ -54,7 +57,7 @@ export class Room {
hash = parts[1] hash = parts[1]
} }
} }
return {roomId, hash} return { roomId, hash }
} }
public async getMapDetail(): Promise<MapDetail> { public async getMapDetail(): Promise<MapDetail> {
@ -66,8 +69,8 @@ export class Room {
if (this.isPublic) { if (this.isPublic) {
const match = /_\/[^/]+\/(.+)/.exec(this.id); const match = /_\/[^/]+\/(.+)/.exec(this.id);
if (!match) throw new Error('Could not extract url from "'+this.id+'"'); if (!match) throw new Error('Could not extract url from "' + this.id + '"');
this.mapUrl = window.location.protocol+'//'+match[1]; this.mapUrl = window.location.protocol + '//' + match[1];
resolve(new MapDetail(this.mapUrl, this.textures)); resolve(new MapDetail(this.mapUrl, this.textures));
return; return;
} else { } else {
@ -76,7 +79,7 @@ export class Room {
Axios.get(`${PUSHER_URL}/map`, { Axios.get(`${PUSHER_URL}/map`, {
params: urlParts params: urlParts
}).then(({data}) => { }).then(({ data }) => {
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl); console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
resolve(data); resolve(data);
return; return;
@ -99,13 +102,13 @@ export class Room {
if (this.isPublic) { if (this.isPublic) {
const match = /_\/([^/]+)\/.+/.exec(this.id); const match = /_\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "'+this.id+'"'); if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1]; this.instance = match[1];
return this.instance; return this.instance;
} else { } else {
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id); const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "'+this.id+'"'); if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1]+'/'+match[2]; this.instance = match[1] + '/' + match[2];
return this.instance; return this.instance;
} }
} }
@ -114,7 +117,7 @@ export class Room {
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],
@ -126,8 +129,7 @@ export class Room {
return results; return results;
} }
public isDisconnected(): boolean public isDisconnected(): boolean {
{
const alone = this._search.get('alone'); const alone = this._search.get('alone');
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') { if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
return true; return true;

View File

@ -11,7 +11,8 @@ import {
RoomJoinedMessage, RoomJoinedMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SilentMessage, StopGlobalMessage, SilentMessage,
StopGlobalMessage,
UserJoinedMessage, UserJoinedMessage,
UserLeftMessage, UserLeftMessage,
UserMovedMessage, UserMovedMessage,
@ -31,17 +32,22 @@ import {
EmotePromptMessage, EmotePromptMessage,
SendUserMessage, SendUserMessage,
BanUserMessage, BanUserMessage,
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
import { import {
EventMessage, EventMessage,
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, GroupCreatedUpdatedMessageInterface,
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface, ItemEventMessageInterface,
MessageUserJoined,
OnConnectInterface,
PlayGlobalMessageInterface,
PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
ViewportInterface, WebRtcDisconnectMessageInterface, ViewportInterface,
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels"; } from "./ConnexionModels";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
@ -61,7 +67,8 @@ export class RoomConnection implements RoomConnection {
private closed: boolean = false; private closed: boolean = false;
private tags: string[] = []; private tags: string[] = [];
public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
} }
@ -70,27 +77,35 @@ 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 roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
*/ */
public constructor(token: string | null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string | null) { public constructor(
token: string | null,
roomId: string,
name: string,
characterLayers: string[],
position: PositionInterface,
viewport: ViewportInterface,
companion: string | null
) {
let url = new URL(PUSHER_URL, window.location.toString()).toString(); let url = new URL(PUSHER_URL, window.location.toString()).toString();
url = url.replace('http://', 'ws://').replace('https://', 'wss://'); url = url.replace("http://", "ws://").replace("https://", "wss://");
if (!url.endsWith('/')) { if (!url.endsWith("/")) {
url += '/'; url += "/";
} }
url += 'room'; url += "room";
url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : ''); url += "?roomId=" + (roomId ? encodeURIComponent(roomId) : "");
url += '&token=' + (token ? encodeURIComponent(token) : ''); url += "&token=" + (token ? encodeURIComponent(token) : "");
url += '&name=' + encodeURIComponent(name); url += "&name=" + encodeURIComponent(name);
for (const layer of characterLayers) { for (const layer of characterLayers) {
url += '&characterLayers=' + encodeURIComponent(layer); url += "&characterLayers=" + encodeURIComponent(layer);
} }
url += '&x=' + Math.floor(position.x); url += "&x=" + Math.floor(position.x);
url += '&y=' + Math.floor(position.y); url += "&y=" + Math.floor(position.y);
url += '&top=' + Math.floor(viewport.top); url += "&top=" + Math.floor(viewport.top);
url += '&bottom=' + Math.floor(viewport.bottom); url += "&bottom=" + Math.floor(viewport.bottom);
url += '&left=' + Math.floor(viewport.left); url += "&left=" + Math.floor(viewport.left);
url += '&right=' + Math.floor(viewport.right); url += "&right=" + Math.floor(viewport.right);
if (typeof companion === 'string') { if (typeof companion === "string") {
url += '&companion=' + encodeURIComponent(companion); url += "&companion=" + encodeURIComponent(companion);
} }
if (RoomConnection.websocketFactory) { if (RoomConnection.websocketFactory) {
@ -99,7 +114,7 @@ export class RoomConnection implements RoomConnection {
this.socket = new WebSocket(url); this.socket = new WebSocket(url);
} }
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = "arraybuffer";
let interval: ReturnType<typeof setInterval> | undefined = undefined; let interval: ReturnType<typeof setInterval> | undefined = undefined;
@ -109,7 +124,7 @@ export class RoomConnection implements RoomConnection {
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
}; };
this.socket.addEventListener('close', (event) => { this.socket.addEventListener("close", (event) => {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
} }
@ -126,7 +141,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) { if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) { for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
let event: string|null = null; let event: string | null = null;
let payload; let payload;
if (subMessage.hasUsermovedmessage()) { if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED; event = EventMessage.USER_MOVED;
@ -150,7 +165,7 @@ export class RoomConnection implements RoomConnection {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else { } else {
throw new Error('Unexpected batch message type'); throw new Error("Unexpected batch message type");
} }
if (event) { if (event) {
@ -171,8 +186,8 @@ export class RoomConnection implements RoomConnection {
this.dispatch(EventMessage.CONNECT, { this.dispatch(EventMessage.CONNECT, {
connection: this, connection: this,
room: { room: {
items items,
} as RoomJoinedMessageInterface } as RoomJoinedMessageInterface,
}); });
} else if (message.hasWorldfullmessage()) { } else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
@ -183,7 +198,10 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWebrtcsignaltoclientmessage()) { } else if (message.hasWebrtcsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage()); this.dispatch(
EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
message.getWebrtcscreensharingsignaltoclientmessage()
);
} else if (message.hasWebrtcstartmessage()) { } else if (message.hasWebrtcstartmessage()) {
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage()); this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
} else if (message.hasWebrtcdisconnectmessage()) { } else if (message.hasWebrtcdisconnectmessage()) {
@ -205,10 +223,9 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else { } else {
throw new Error('Unknown message received'); throw new Error("Unknown message received");
} }
};
}
} }
private dispatch(event: string, payload: unknown): void { private dispatch(event: string, payload: unknown): void {
@ -243,16 +260,16 @@ export class RoomConnection implements RoomConnection {
positionMessage.setY(Math.floor(y)); positionMessage.setY(Math.floor(y));
let directionEnum: Direction; let directionEnum: Direction;
switch (direction) { switch (direction) {
case 'up': case "up":
directionEnum = Direction.UP; directionEnum = Direction.UP;
break; break;
case 'down': case "down":
directionEnum = Direction.DOWN; directionEnum = Direction.DOWN;
break; break;
case 'left': case "left":
directionEnum = Direction.LEFT; directionEnum = Direction.LEFT;
break; break;
case 'right': case "right":
directionEnum = Direction.RIGHT; directionEnum = Direction.RIGHT;
break; break;
default: default:
@ -327,15 +344,17 @@ export class RoomConnection implements RoomConnection {
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined { private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Invalid JOIN_ROOM message'); throw new Error("Invalid JOIN_ROOM message");
} }
const characterLayers = message.getCharacterlayersList().map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => { const characterLayers = message
return { .getCharacterlayersList()
name: characterLayer.getName(), .map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
img: characterLayer.getUrl() return {
} name: characterLayer.getName(),
}) img: characterLayer.getUrl(),
};
});
const companion = message.getCompanion(); const companion = message.getCompanion();
@ -345,8 +364,8 @@ export class RoomConnection implements RoomConnection {
characterLayers, characterLayers,
visitCardUrl: message.getVisitcardurl(), visitCardUrl: message.getVisitcardurl(),
position: ProtobufClientUtils.toPointInterface(position), position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null companion: companion ? companion.getName() : null,
} };
} }
public onUserMoved(callback: (message: UserMovedMessage) => void): void { public onUserMoved(callback: (message: UserMovedMessage) => void): void {
@ -372,7 +391,9 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { public onGroupUpdatedOrCreated(
callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
): void {
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => { this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
callback(this.toGroupCreatedUpdatedMessage(message)); callback(this.toGroupCreatedUpdatedMessage(message));
}); });
@ -381,14 +402,14 @@ export class RoomConnection implements RoomConnection {
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface { private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Missing position in GROUP_CREATE_UPDATE'); throw new Error("Missing position in GROUP_CREATE_UPDATE");
} }
return { return {
groupId: message.getGroupid(), groupId: message.getGroupid(),
position: position.toObject(), position: position.toObject(),
groupSize: message.getGroupsize() groupSize: message.getGroupsize(),
} };
} }
public onGroupDeleted(callback: (groupId: number) => void): void { public onGroupDeleted(callback: (groupId: number) => void): void {
@ -404,7 +425,7 @@ export class RoomConnection implements RoomConnection {
} }
public onConnectError(callback: (error: Event) => void): void { public onConnectError(callback: (error: Event) => void): void {
this.socket.addEventListener('error', callback) this.socket.addEventListener("error", callback);
} }
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void { public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
@ -476,11 +497,11 @@ export class RoomConnection implements RoomConnection {
} }
public onServerDisconnected(callback: () => void): void { public onServerDisconnected(callback: () => void): void {
this.socket.addEventListener('close', (event) => { this.socket.addEventListener("close", (event) => {
if (this.closed === true || connectionManager.unloading) { if (this.closed === true || connectionManager.unloading) {
return; return;
} }
console.log('Socket closed with code ' + event.code + ". Reason: " + event.reason); console.log("Socket closed with code " + event.code + ". Reason: " + event.reason);
if (event.code === 1000) { if (event.code === 1000) {
// Normal closure case // Normal closure case
return; return;
@ -490,14 +511,14 @@ export class RoomConnection implements RoomConnection {
} }
public getUserId(): number { public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!' if (this.userId === null) throw "UserId cannot be null!";
return this.userId; return this.userId;
} }
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => { this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
callback({ callback({
userId: message.getUserid() userId: message.getUserid(),
}); });
}); });
} }
@ -521,21 +542,22 @@ export class RoomConnection implements RoomConnection {
itemId: message.getItemid(), itemId: message.getItemid(),
event: message.getEvent(), event: message.getEvent(),
parameters: JSON.parse(message.getParametersjson()), parameters: JSON.parse(message.getParametersjson()),
state: JSON.parse(message.getStatejson()) state: JSON.parse(message.getStatejson()),
}); });
}); });
} }
public uploadAudio(file: FormData) { public uploadAudio(file: FormData) {
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: { data: {} }) => { return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
return res.data; .then((res: { data: {} }) => {
}).catch((err) => { return res.data;
console.error(err); })
throw err; .catch((err) => {
}); console.error(err);
throw err;
});
} }
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) { public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => { return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({ callback({
@ -605,12 +627,12 @@ export class RoomConnection implements RoomConnection {
} }
public isAdmin(): boolean { public isAdmin(): boolean {
return this.hasTag('admin'); return this.hasTag("admin");
} }
public emitEmoteEvent(emoteName: string): void { public emitEmoteEvent(emoteName: string): void {
const emoteMessage = new EmotePromptMessage(); const emoteMessage = new EmotePromptMessage();
emoteMessage.setEmote(emoteName) emoteMessage.setEmote(emoteName);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setEmotepromptmessage(emoteMessage); clientToServerMessage.setEmotepromptmessage(emoteMessage);
@ -618,7 +640,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public getAllTags() : string[] { public getAllTags(): string[] {
return this.tags; return this.tags;
} }
} }

View File

@ -44,7 +44,6 @@ export class TextUtils {
options.align = object.text.halign; options.align = object.text.halign;
} }
console.warn(options);
const textElem = scene.add.text(object.x, object.y, object.text.text, options); const textElem = scene.add.text(object.x, object.y, object.text.text, options);
textElem.setAngle(object.rotation); textElem.setAngle(object.rotation);
} }

View File

@ -1,17 +1,17 @@
import {ResizableScene} from "../Login/ResizableScene"; import { ResizableScene } from "../Login/ResizableScene";
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import Events = Phaser.Scenes.Events; import Events = Phaser.Scenes.Events;
import AnimationEvents = Phaser.Animations.Events; import AnimationEvents = Phaser.Animations.Events;
import StructEvents = Phaser.Structs.Events; import StructEvents = Phaser.Structs.Events;
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
/** /**
* A scene that can track its dirty/pristine state. * A scene that can track its dirty/pristine state.
*/ */
export abstract class DirtyScene extends ResizableScene { export abstract class DirtyScene extends ResizableScene {
private isAlreadyTracking: boolean = false; private isAlreadyTracking: boolean = false;
protected dirty:boolean = true; protected dirty: boolean = true;
private objectListChanged:boolean = true; private objectListChanged: boolean = true;
private physicsEnabled: boolean = false; private physicsEnabled: boolean = false;
/** /**
@ -59,7 +59,6 @@ export abstract class DirtyScene extends ResizableScene {
this.physicsEnabled = false; this.physicsEnabled = false;
} }
}); });
} }
private trackAnimation(): void { private trackAnimation(): void {
@ -71,7 +70,7 @@ export abstract class DirtyScene extends ResizableScene {
} }
public markDirty(): void { public markDirty(): void {
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true); this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => (this.dirty = true));
} }
public onResize(): void { public onResize(): void {

View File

@ -1,27 +1,25 @@
import {GameScene} from "./GameScene"; import { GameScene } from "./GameScene";
import {connectionManager} from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import type {Room} from "../../Connexion/Room"; import type { Room } from "../../Connexion/Room";
import {MenuScene, MenuSceneName} from "../Menu/MenuScene"; import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import {LoginSceneName} from "../Login/LoginScene"; import { LoginSceneName } from "../Login/LoginScene";
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import { EnableCameraSceneName } from "../Login/EnableCameraScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store";
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import Axios from "axios"; import Axios from "axios";
import {get} from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
/** /**
* This class should be responsible for any scene starting/stopping * This class should be responsible for any scene starting/stopping
*/ */
export class GameManager { export class GameManager {
private playerName: string|null; private playerName: string | null;
private characterLayers: string[]|null; private characterLayers: string[] | null;
private companion: string|null; private companion: string | null;
private startRoom!:Room; private startRoom!: Room;
currentGameSceneName: string|null = null; currentGameSceneName: string | null = null;
constructor() { constructor() {
this.playerName = localUserStore.getName(); this.playerName = localUserStore.getName();
@ -57,23 +55,22 @@ export class GameManager {
localUserStore.setCharacterLayers(layers); localUserStore.setCharacterLayers(layers);
} }
getPlayerName(): string|null { getPlayerName(): string | null {
return this.playerName; return this.playerName;
} }
getCharacterLayers(): string[] { getCharacterLayers(): string[] {
if (!this.characterLayers) { if (!this.characterLayers) {
throw 'characterLayers are not set'; throw "characterLayers are not set";
} }
return this.characterLayers; return this.characterLayers;
} }
setCompanion(companion: string | null): void {
setCompanion(companion: string|null): void {
this.companion = companion; this.companion = companion;
} }
getCompanion(): string|null { getCompanion(): string | null {
return this.companion; return this.companion;
} }
@ -82,18 +79,21 @@ export class GameManager {
const mapDetail = await room.getMapDetail(); 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, mapDetail.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.id));
scenePlugin.start(this.currentGameSceneName || this.startRoom.id); scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
scenePlugin.launch(MenuSceneName); scenePlugin.launch(MenuSceneName);
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){ if (
!localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState))
) {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
localUserStore.setHelpCameraSettingsShown(); localUserStore.setHelpCameraSettingsShown();
} }
@ -110,7 +110,7 @@ export class GameManager {
* This will close the socket connections and stop the gameScene, but won't remove it. * This will close the socket connections and stop the gameScene, but won't remove it.
*/ */
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void { leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void {
if (this.currentGameSceneName === null) throw 'No current scene id set!'; if (this.currentGameSceneName === null) throw "No current scene id set!";
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene; const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
gameScene.cleanupClosingScene(); gameScene.cleanupClosingScene();
scene.scene.stop(this.currentGameSceneName); scene.scene.stop(this.currentGameSceneName);
@ -129,13 +129,13 @@ export class GameManager {
scene.scene.start(this.currentGameSceneName); scene.scene.start(this.currentGameSceneName);
scene.scene.wake(MenuSceneName); scene.scene.wake(MenuSceneName);
} else { } else {
scene.scene.run(fallbackSceneName) scene.scene.run(fallbackSceneName);
} }
} }
public getCurrentGameScene(scene: Phaser.Scene): GameScene { public getCurrentGameScene(scene: Phaser.Scene): GameScene {
if (this.currentGameSceneName === null) throw 'No current scene id set!'; if (this.currentGameSceneName === null) throw "No current scene id set!";
return scene.scene.get(this.currentGameSceneName) as GameScene return scene.scene.get(this.currentGameSceneName) as GameScene;
} }
} }

View File

@ -1,9 +1,13 @@
import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap"; import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void; export type PropertyChangeCallback = (
newValue: string | number | boolean | undefined,
oldValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) => void;
/** /**
* A wrapper around a ITiledMap interface to provide additional capabilities. * A wrapper around a ITiledMap interface to provide additional capabilities.
@ -19,37 +23,50 @@ export class GameMap {
public readonly flatLayers: ITiledMapLayer[]; public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = []; public readonly phaserLayers: TilemapLayer[] = [];
public exitUrls: Array<string> = [] public exitUrls: Array<string> = [];
public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array<Phaser.Tilemaps.Tileset>) { public hasStartTile = false;
public constructor(
private map: ITiledMap,
phaserMap: Phaser.Tilemaps.Tilemap,
terrains: Array<Phaser.Tilemaps.Tileset>
) {
this.flatLayers = flattenGroupLayersMap(map); this.flatLayers = flattenGroupLayersMap(map);
let depth = -2; let depth = -2;
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if(layer.type === 'tilelayer'){ if (layer.type === "tilelayer") {
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
} }
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { if (layer.type === "objectgroup" && layer.name === "floorLayer") {
depth = DEPTH_OVERLAY_INDEX; depth = DEPTH_OVERLAY_INDEX;
} }
} }
for (const tileset of map.tilesets) { for (const tileset of map.tilesets) {
tileset?.tiles?.forEach(tile => { tileset?.tiles?.forEach((tile) => {
if (tile.properties) { if (tile.properties) {
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties;
tile.properties.forEach(prop => { tile.properties.forEach((prop) => {
if (prop.name == 'name' && typeof prop.value == "string") { if (prop.name == "name" && typeof prop.value == "string") {
this.tileNameMap.set(prop.value, tileset.firstgid + tile.id); this.tileNameMap.set(prop.value, tileset.firstgid + tile.id);
} }
if (prop.name == "exitUrl" && typeof prop.value == "string") { if (prop.name == "exitUrl" && typeof prop.value == "string") {
this.exitUrls.push(prop.value); this.exitUrls.push(prop.value);
} else if (prop.name == "start") {
this.hasStartTile = true;
} }
}) });
} }
}) });
} }
} }
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
return [];
}
/** /**
* Sets the position of the current player (in pixels) * Sets the position of the current player (in pixels)
@ -93,7 +110,7 @@ export class GameMap {
const properties = new Map<string, string | boolean | number>(); const properties = new Map<string, string | boolean | number>();
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if (layer.type !== 'tilelayer') { if (layer.type !== "tilelayer") {
continue; continue;
} }
@ -103,7 +120,7 @@ export class GameMap {
if (tiles[key] == 0) { if (tiles[key] == 0) {
continue; continue;
} }
tileIndex = tiles[key] tileIndex = tiles[key];
} }
// There is a tile in this layer, let's embed the properties // There is a tile in this layer, let's embed the properties
@ -117,20 +134,20 @@ export class GameMap {
} }
if (tileIndex) { if (tileIndex) {
this.tileSetPropertyMap[tileIndex]?.forEach(property => { this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
if (property.value) { if (property.value) {
properties.set(property.name, property.value) properties.set(property.name, property.value);
} else if (properties.has(property.name)) { } else if (properties.has(property.name)) {
properties.delete(property.name) properties.delete(property.name);
} }
}) });
} }
} }
return properties; return properties;
} }
public getMap(): ITiledMap{ public getMap(): ITiledMap {
return this.map; return this.map;
} }
@ -138,7 +155,12 @@ export class GameMap {
return this.tileSetPropertyMap[index] || []; return this.tileSetPropertyMap[index] || [];
} }
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) { private trigger(
propName: string,
oldValue: string | number | boolean | undefined,
newValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) {
const callbacksArray = this.callbacks.get(propName); const callbacksArray = this.callbacks.get(propName);
if (callbacksArray !== undefined) { if (callbacksArray !== undefined) {
for (const callback of callbacksArray) { for (const callback of callbacksArray) {
@ -167,7 +189,7 @@ export class GameMap {
return this.phaserLayers.find((layer) => layer.layer.name === layerName); return this.phaserLayers.find((layer) => layer.layer.name === layerName);
} }
public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) { for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain); phaserLayer.tileset.push(terrain);
} }
@ -175,11 +197,11 @@ 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 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 that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
return; return;
} }
@ -187,27 +209,25 @@ export class GameMap {
console.error("Data of the layer that you want to change is only readable."); console.error("Data of the layer that you want to change is only readable.");
return; return;
} }
fLayer.data[x+y*fLayer.height] = index; fLayer.data[x + y * fLayer.height] = index;
} }
public putTile(tile: string | number, x: number, y: number, layer: string): void { public putTile(tile: string | number, x: number, y: number, layer: string): void {
const phaserLayer = this.findPhaserLayer(layer); const phaserLayer = this.findPhaserLayer(layer);
if ( phaserLayer ) { if (phaserLayer) {
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) { 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 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 that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
} }
} }
@ -218,5 +238,4 @@ export class GameMap {
} }
return this.tileNameMap.get(tile); return this.tileNameMap.get(tile);
} }
} }

View File

@ -1,4 +1,3 @@
import { Queue } from "queue-typescript";
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
@ -14,20 +13,9 @@ import type {
PositionInterface, PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { Room } from "../../Connexion/Room";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { TextureError } from "../../Exception/TextureError";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import { Queue } from "queue-typescript";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { urlManager } from "../../Url/UrlManager";
import { audioManager } from "../../WebRtc/AudioManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { import {
AUDIO_LOOP_PROPERTY, AUDIO_LOOP_PROPERTY,
AUDIO_VOLUME_PROPERTY, AUDIO_VOLUME_PROPERTY,
@ -39,15 +27,21 @@ import {
TRIGGER_WEBSITE_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES,
WEBSITE_MESSAGE_PROPERTIES, WEBSITE_MESSAGE_PROPERTIES,
} from "../../WebRtc/LayoutManager"; } from "../../WebRtc/LayoutManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { urlManager } from "../../Url/UrlManager";
import { audioManager } from "../../WebRtc/AudioManager";
import { TextureError } from "../../Exception/TextureError";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; import { SimplePeer } from "../../WebRtc/SimplePeer";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ChatModeIcon } from "../Components/ChatModeIcon";
import { addLoader } from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { PresentationModeIcon } from "../Components/PresentationModeIcon";
import { TextUtils } from "../Components/TextUtils";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer"; import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem"; import type { ActionableItem } from "../Items/ActionableItem";
@ -58,7 +52,6 @@ import type {
ITiledMapLayer, ITiledMapLayer,
ITiledMapLayerProperty, ITiledMapLayerProperty,
ITiledMapObject, ITiledMapObject,
ITiledMapTileLayer,
ITiledTileSet, ITiledTileSet,
} from "../Map/ITiledMap"; } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene"; import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
@ -66,13 +59,8 @@ import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene"; import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { waScaleManager } from "../Services/WaScaleManager";
import { PinchManager } from "../UserInput/PinchManager";
import { UserInputManager } from "../UserInput/UserInputManager"; import { UserInputManager } from "../UserInput/UserInputManager";
import type { AddPlayerInterface } from "./AddPlayerInterface"; import type { AddPlayerInterface } from "./AddPlayerInterface";
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
import { DirtyScene } from "./DirtyScene";
import { EmoteManager } from "./EmoteManager";
import { gameManager } from "./GameManager"; import { gameManager } from "./GameManager";
import { GameMap } from "./GameMap"; import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement"; import { PlayerMovement } from "./PlayerMovement";
@ -83,12 +71,22 @@ import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement; import DOMElement = Phaser.GameObjects.DOMElement;
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events; import EVENT_TYPE = Phaser.Scenes.Events;
import RenderTexture = Phaser.GameObjects.RenderTexture; import RenderTexture = Phaser.GameObjects.RenderTexture;
import Tilemap = Phaser.Tilemaps.Tilemap; import Tilemap = Phaser.Tilemaps.Tilemap;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles"; import AnimatedTiles from "phaser-animated-tiles";
import { StartPositionCalculator } from "./StartPositionCalculator";
import { soundManager } from "./SoundManager"; import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
@ -129,8 +127,6 @@ interface DeleteGroupEventInterface {
groupId: number; groupId: number;
} }
const defaultStartLayerName = "start";
export class GameScene extends DirtyScene { export class GameScene extends DirtyScene {
Terrains: Array<Phaser.Tilemaps.Tileset>; Terrains: Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player; CurrentPlayer!: Player;
@ -141,8 +137,6 @@ export class GameScene extends DirtyScene {
mapFile!: ITiledMap; mapFile!: ITiledMap;
animatedTiles!: AnimatedTiles; animatedTiles!: AnimatedTiles;
groups: Map<number, Sprite>; groups: Map<number, Sprite>;
startX!: number;
startY!: number;
circleTexture!: CanvasTexture; circleTexture!: CanvasTexture;
circleRedTexture!: CanvasTexture; circleRedTexture!: CanvasTexture;
pendingEvents: Queue< pendingEvents: Queue<
@ -194,7 +188,6 @@ export class GameScene extends DirtyScene {
private outlinedItem: ActionableItem | null = null; private outlinedItem: ActionableItem | null = null;
public userInputManager!: UserInputManager; public userInputManager!: UserInputManager;
private isReconnecting: boolean | undefined = undefined; private isReconnecting: boolean | undefined = undefined;
private startLayerName!: string | null;
private openChatIcon!: OpenChatIcon; private openChatIcon!: OpenChatIcon;
private playerName!: string; private playerName!: string;
private characterLayers!: string[]; private characterLayers!: string[];
@ -206,6 +199,7 @@ export class GameScene extends DirtyScene {
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private emoteManager!: EmoteManager; private emoteManager!: EmoteManager;
private preloading: boolean = true; private preloading: boolean = true;
startPositionCalculator!: StartPositionCalculator;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({ super({
@ -426,7 +420,6 @@ export class GameScene extends DirtyScene {
gameManager.gameSceneIsCreated(this); gameManager.gameSceneIsCreated(this);
urlManager.pushRoomIdToUrl(this.room); urlManager.pushRoomIdToUrl(this.room);
this.startLayerName = urlManager.getStartLayerNameFromUrl();
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
this.pinchManager = new PinchManager(this); this.pinchManager = new PinchManager(this);
@ -489,7 +482,12 @@ export class GameScene extends DirtyScene {
this.loadNextGame(exitUrl); this.loadNextGame(exitUrl);
}); });
this.initStartXAndStartY(); this.startPositionCalculator = new StartPositionCalculator(
this.gameMap,
this.mapFile,
this.initPosition,
urlManager.getStartLayerNameFromUrl()
);
//add entities //add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>(); this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@ -586,8 +584,7 @@ export class GameScene extends DirtyScene {
this.playerName, this.playerName,
this.characterLayers, this.characterLayers,
{ {
x: this.startX, ...this.startPositionCalculator.startPosition,
y: this.startY,
}, },
{ {
left: camera.scrollX, left: camera.scrollX,
@ -995,8 +992,8 @@ export class GameScene extends DirtyScene {
}) })
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.enablePlayerControlStream.subscribe(()=>{ iframeListener.enablePlayerControlStream.subscribe(() => {
this.userInputManager.restoreControls(); this.userInputManager.restoreControls();
}) })
); );
@ -1053,16 +1050,21 @@ export class GameScene extends DirtyScene {
}) })
); );
iframeListener.registerAnswerer('getState', () => {
return {
mapUrl: this.MapUrlFile,
startLayerName: this.startPositionCalculator.startLayerName,
uuid: localUserStore.getLocalUser()?.uuid,
nickname: localUserStore.getName(),
roomId: this.RoomId,
tags: this.connection ? this.connection.getAllTags() : [],
};
});
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.gameStateStream.subscribe(() => { iframeListener.setTilesStream.subscribe((eventTiles) => {
iframeListener.sendGameStateEvent({ for (const eventTile of eventTiles) {
mapUrl: this.MapUrlFile, this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
startLayerName: this.startLayerName, }
uuid: localUserStore.getLocalUser()?.uuid,
nickname: localUserStore.getName(),
roomId: this.RoomId,
tags: this.connection ? this.connection.getAllTags() : [],
});
}) })
); );
@ -1104,11 +1106,11 @@ export class GameScene extends DirtyScene {
console.warn('Could not find layer "' + layerName + '" when calling setProperty'); console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return; return;
} }
const property = (layer.properties as ITiledMapLayerProperty[])?.find( if (layer.properties === undefined) {
(property) => property.name === propertyName
);
if (property === undefined) {
layer.properties = []; layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return; return;
} }
@ -1134,7 +1136,9 @@ export class GameScene extends DirtyScene {
this.mapTransitioning = true; this.mapTransitioning = true;
const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); 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); if (!roomId) throw new Error("Could not find the room from its exit key: " + exitKey);
urlManager.pushStartLayerNameToUrl(hash); if (hash) {
urlManager.pushStartLayerNameToUrl(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 (roomId !== this.scene.key) {
@ -1148,9 +1152,9 @@ export class GameScene extends DirtyScene {
this.scene.start(roomId); this.scene.start(roomId);
} else { } else {
//if the exit points to the current map, we simply teleport the user back to the startLayer //if the exit points to the current map, we simply teleport the user back to the startLayer
this.initPositionFromLayerName(hash || defaultStartLayerName); this.startPositionCalculator.initPositionFromLayerName(hash, hash);
this.CurrentPlayer.x = this.startX; this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
this.CurrentPlayer.y = this.startY; this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
setTimeout(() => (this.mapTransitioning = false), 500); setTimeout(() => (this.mapTransitioning = false), 500);
} }
} }
@ -1176,6 +1180,7 @@ export class GameScene extends DirtyScene {
this.emoteManager.destroy(); this.emoteManager.destroy();
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer('getState');
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
@ -1197,46 +1202,6 @@ export class GameScene extends DirtyScene {
this.MapPlayersByKey = new Map<number, RemotePlayer>(); this.MapPlayersByKey = new Map<number, RemotePlayer>();
} }
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startX = this.initPosition.x;
this.startY = this.initPosition.y;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName);
}
if (this.startX === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startX === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startX = this.mapFile.width * 16;
this.startY = this.mapFile.height * 16;
}
}
private initPositionFromLayerName(layerName: string) {
for (const layer of this.gameMap.flatLayers) {
if (
(layerName === layer.name || layer.name.endsWith("/" + layerName)) &&
layer.type === "tilelayer" &&
(layerName === defaultStartLayerName || this.isStartLayer(layer))
) {
const startPosition = this.startUser(layer);
this.startX = startPosition.x + this.mapFile.tilewidth / 2;
this.startY = startPosition.y + this.mapFile.tileheight / 2;
}
}
}
private getExitUrl(layer: ITiledMapLayer): string | undefined { private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, "exitUrl") as string | undefined; return this.getProperty(layer, "exitUrl") as string | undefined;
} }
@ -1248,10 +1213,6 @@ export class GameScene extends DirtyScene {
return this.getProperty(layer, "exitSceneUrl") as string | undefined; return this.getProperty(layer, "exitSceneUrl") as string | undefined;
} }
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
private getScriptUrls(map: ITiledMap): string[] { private getScriptUrls(map: ITiledMap): string[] {
return (this.getProperties(map, "script") as string[]).map((script) => return (this.getProperties(map, "script") as string[]).map((script) =>
new URL(script, this.MapUrlFile).toString() new URL(script, this.MapUrlFile).toString()
@ -1289,33 +1250,6 @@ export class GameScene extends DirtyScene {
return gameManager.loadMap(room, this.scene).catch(() => {}); return gameManager.loadMap(room, this.scene).catch(() => {});
} }
private startUser(layer: ITiledMapTileLayer): PositionInterface {
const tiles = layer.data;
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possibleStartPositions: PositionInterface[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
const y = Math.floor(key / layer.width);
const x = key % layer.width;
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
});
// Get a value at random amongst allowed values
if (possibleStartPositions.length === 0) {
console.warn('The start layer "' + layer.name + '" for this map is empty.');
return {
x: 0,
y: 0,
};
}
// Choose one of the available start positions at random amongst the list of available start positions.
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
}
//todo: in a dedicated class/function? //todo: in a dedicated class/function?
initCamera() { initCamera() {
this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
@ -1348,8 +1282,8 @@ export class GameScene extends DirtyScene {
try { try {
this.CurrentPlayer = new Player( this.CurrentPlayer = new Player(
this, this,
this.startX, this.startPositionCalculator.startPosition.x,
this.startY, this.startPositionCalculator.startPosition.y,
this.playerName, this.playerName,
texturesPromise, texturesPromise,
PlayerAnimationDirections.Down, PlayerAnimationDirections.Down,

View File

@ -1,10 +1,14 @@
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
import type { PositionInterface } from "../../Connexion/ConnexionModels"; import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
export class PlayerMovement { export class PlayerMovement {
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) { public constructor(
} private startPosition: PositionInterface,
private startTick: number,
private endPosition: HasPlayerMovedEvent,
private endTick: number
) {}
public isOutdated(tick: number): boolean { public isOutdated(tick: number): boolean {
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME) //console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
@ -24,14 +28,18 @@ export class PlayerMovement {
return this.endPosition; return this.endPosition;
} }
const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; const x =
const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.x;
const y =
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.y;
//console.log('Computed position ', x, y) //console.log('Computed position ', x, y)
return { return {
x, x,
y, y,
direction: this.endPosition.direction, direction: this.endPosition.direction,
moving: true moving: true,
} };
} }
} }

View File

@ -2,7 +2,7 @@
* This class is in charge of computing the position of all players. * This class is in charge of computing the position of all players.
* Player movement is delayed by 200ms so position depends on ticks. * Player movement is delayed by 200ms so position depends on ticks.
*/ */
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import type { PlayerMovement } from "./PlayerMovement"; import type { PlayerMovement } from "./PlayerMovement";
export class PlayersPositionInterpolator { export class PlayersPositionInterpolator {
@ -24,7 +24,7 @@ export class PlayersPositionInterpolator {
this.playerMovements.delete(userId); this.playerMovements.delete(userId);
} }
//console.log("moving") //console.log("moving")
positions.set(userId, playerMovement.getPosition(tick)) positions.set(userId, playerMovement.getPosition(tick));
}); });
return positions; return positions;
} }

View File

@ -0,0 +1,127 @@
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { GameMap } from "./GameMap";
const defaultStartLayerName = "start";
export class StartPositionCalculator {
public startPosition!: PositionInterface;
constructor(
private readonly gameMap: GameMap,
private readonly mapFile: ITiledMap,
private readonly initPosition: PositionInterface | null,
public readonly startLayerName: string | null
) {
this.initStartXAndStartY();
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
/**
*
* @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
*/
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
if (!selectedOrDefaultLayer) {
selectedOrDefaultLayer = defaultStartLayerName;
}
for (const layer of this.gameMap.flatLayers) {
if (
(selectedOrDefaultLayer === layer.name || layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
layer.type === "tilelayer" &&
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))
) {
const startPosition = this.startUser(layer, selectedLayer);
this.startPosition = {
x: startPosition.x + this.mapFile.tilewidth / 2,
y: startPosition.y + this.mapFile.tileheight / 2,
};
}
}
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
/**
*
* @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
*/
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
const tiles = selectedOrDefaultLayer.data;
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possibleStartPositions: PositionInterface[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
const y = Math.floor(key / selectedOrDefaultLayer.width);
const x = key % selectedOrDefaultLayer.width;
if (selectedLayer && this.gameMap.hasStartTile) {
const properties = this.gameMap.getPropertiesForIndex(objectKey);
if (
!properties.length ||
!properties.some((property) => property.name == "start" && property.value == selectedLayer)
) {
return;
}
}
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
});
// Get a value at random amongst allowed values
if (possibleStartPositions.length === 0) {
console.warn('The start layer "' + selectedOrDefaultLayer.name + '" for this map is empty.');
return {
x: 0,
y: 0,
};
}
// Choose one of the available start positions at random amongst the list of available start positions.
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;
}
return obj.value;
}
}

View File

@ -36,7 +36,7 @@ export interface ITiledMap {
export interface ITiledMapLayerProperty { export interface ITiledMapLayerProperty {
name: string; name: string;
type: string; type: string;
value: string|boolean|number|undefined; value: string | boolean | number | undefined;
} }
/*export interface ITiledMapLayerBooleanProperty { /*export interface ITiledMapLayerBooleanProperty {
@ -48,7 +48,7 @@ export interface ITiledMapLayerProperty {
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer; export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
export interface ITiledMapGroupLayer { export interface ITiledMapGroupLayer {
id?: number, id?: number;
name: string; name: string;
opacity: number; opacity: number;
properties?: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
@ -64,8 +64,8 @@ export interface ITiledMapGroupLayer {
} }
export interface ITiledMapTileLayer { export interface ITiledMapTileLayer {
id?: number, id?: number;
data: number[]|string; data: number[] | string;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
@ -87,7 +87,7 @@ export interface ITiledMapTileLayer {
} }
export interface ITiledMapObjectLayer { export interface ITiledMapObjectLayer {
id?: number, id?: number;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
@ -117,7 +117,7 @@ export interface ITiledMapObject {
gid: number; gid: number;
height: number; height: number;
name: string; name: string;
properties: {[key: string]: string}; properties: { [key: string]: string };
rotation: number; rotation: number;
type: string; type: string;
visible: boolean; visible: boolean;
@ -133,26 +133,26 @@ export interface ITiledMapObject {
/** /**
* Polygon points * Polygon points
*/ */
polygon: {x: number, y: number}[]; polygon: { x: number; y: number }[];
/** /**
* Polyline points * Polyline points
*/ */
polyline: {x: number, y: number}[]; polyline: { x: number; y: number }[];
text?: ITiledText text?: ITiledText;
} }
export interface ITiledText { export interface ITiledText {
text: string, text: string;
wrap?: boolean, wrap?: boolean;
fontfamily?: string, fontfamily?: string;
pixelsize?: number, pixelsize?: number;
color?: string, color?: string;
underline?: boolean, underline?: boolean;
italic?: boolean, italic?: boolean;
strikeout?: boolean, strikeout?: boolean;
halign?: "center"|"right"|"justify"|"left" halign?: "center" | "right" | "justify" | "left";
} }
export interface ITiledTileSet { export interface ITiledTileSet {
@ -163,7 +163,7 @@ export interface ITiledTileSet {
imagewidth: number; imagewidth: number;
margin: number; margin: number;
name: string; name: string;
properties: {[key: string]: string}; properties: { [key: string]: string };
spacing: number; spacing: number;
tilecount: number; tilecount: number;
tileheight: number; tileheight: number;
@ -179,10 +179,10 @@ export interface ITiledTileSet {
} }
export interface ITile { export interface ITile {
id: number, id: number;
type?: string type?: string;
properties?: Array<ITiledMapLayerProperty> properties?: Array<ITiledMapLayerProperty>;
} }
export interface ITiledMapTerrain { export interface ITiledMapTerrain {

View File

@ -1,20 +1,20 @@
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap"; import type { ITiledMap, ITiledMapLayer } from "./ITiledMap";
/** /**
* Flatten the grouped layers * Flatten the grouped layers
*/ */
export function flattenGroupLayersMap(map: ITiledMap) { export function flattenGroupLayersMap(map: ITiledMap) {
const flatLayers: ITiledMapLayer[] = []; const flatLayers: ITiledMapLayer[] = [];
flattenGroupLayers(map.layers, '', flatLayers); flattenGroupLayers(map.layers, "", flatLayers);
return flatLayers; return flatLayers;
} }
function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) { function flattenGroupLayers(layers: ITiledMapLayer[], prefix: string, flatLayers: ITiledMapLayer[]) {
for (const layer of layers) { for (const layer of layers) {
if (layer.type === 'group') { if (layer.type === "group") {
flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); flattenGroupLayers(layer.layers, prefix + layer.name + "/", flatLayers);
} else { } else {
layer.name = prefix+layer.name layer.name = prefix + layer.name;
flatLayers.push(layer); flatLayers.push(layer);
} }
} }

View File

@ -1,29 +1,29 @@
import {LoginScene, LoginSceneName} from "../Login/LoginScene"; import { LoginScene, LoginSceneName } from "../Login/LoginScene";
import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene";
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import {connectionManager} from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager"; import { GameConnexionTypes } from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore"; import { menuIconVisible } from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore"; import { videoConstraintStore } from "../../Stores/MediaStore";
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { HtmlUtils } from '../../WebRtc/HtmlUtils'; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { iframeListener } from '../../Api/IframeListener'; import { iframeListener } from "../../Api/IframeListener";
import { Subscription } from 'rxjs'; import { Subscription } from "rxjs";
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent"; import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterEvent";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem"; import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {get} from "svelte/store"; import { get } from "svelte/store";
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = "MenuScene";
const gameMenuKey = 'gameMenu'; const gameMenuKey = "gameMenu";
const gameMenuIconKey = 'gameMenuIcon'; const gameMenuIconKey = "gameMenuIcon";
const gameSettingsMenuKey = 'gameSettingsMenu'; const gameSettingsMenuKey = "gameSettingsMenu";
const gameShare = 'gameShare'; const gameShare = "gameShare";
const closedSideMenuX = -1000; const closedSideMenuX = -1000;
const openedSideMenuX = 0; const openedSideMenuX = 0;
@ -44,45 +44,49 @@ export class MenuScene extends Phaser.Scene {
private menuButton!: Phaser.GameObjects.DOMElement; private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null; private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription() private subscriptions = new Subscription();
constructor() { constructor() {
super({ key: MenuSceneName }); super({ key: MenuSceneName });
this.gameQualityValue = localUserStore.getGameQualityValue(); this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => { this.subscriptions.add(
this.addMenuOption(menuCommand); registerMenuCommandStream.subscribe((menuCommand) => {
})) this.addMenuOption(menuCommand);
})
);
this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => { this.subscriptions.add(
this.destroyMenu(menuCommand); iframeListener.unregisterMenuCommandStream.subscribe((menuCommand) => {
})) this.destroyMenu(menuCommand);
})
);
} }
reset() { reset() {
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")]; const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
for (let index = addedMenuItems.length - 1; index >= 0; index--) { for (let index = addedMenuItems.length - 1; index >= 0; index--) {
addedMenuItems[index].remove() addedMenuItems[index].remove();
} }
} }
public addMenuOption(menuText: string) { public addMenuOption(menuText: string) {
const wrappingSection = document.createElement("section") const wrappingSection = document.createElement("section");
const escapedHtml = HtmlUtils.escapeHtml(menuText); const escapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>` wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`;
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
if (menuItemContainer) { if (menuItemContainer) {
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove() menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove();
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"));
} }
} }
preload() { preload() {
this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); this.load.html(gameMenuKey, "resources/html/gameMenu.html");
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameMenuIconKey, "resources/html/gameMenuIcon.html");
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, 'resources/html/gameShare.html'); this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource); this.load.html(gameReportKey, gameReportRessource);
this.load.html(warningContainerKey, warningContainerHtml); this.load.html(warningContainerKey, warningContainerHtml);
} }
@ -91,26 +95,28 @@ export class MenuScene extends Phaser.Scene {
menuIconVisible.set(true); menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0); this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu'); MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
const middleX = (window.innerWidth / 3) - 298; const middleX = window.innerWidth / 3 - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey); this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality'); MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare); this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare); MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener('click'); this.gameShareElement.addListener("click");
this.gameShareElement.on('click', (event: MouseEvent) => { this.gameShareElement.on("click", (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { if ((event?.target as HTMLInputElement).id === "gameShareFormSubmit") {
this.copyLink(); this.copyLink();
} else if ((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { } else if ((event?.target as HTMLInputElement).id === "gameShareFormCancel") {
this.closeGameShare(); this.closeGameShare();
} }
}); });
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous); this.gameReportElement = new ReportMenu(
this,
connectionManager.getConnexionType === GameConnexionTypes.anonymous
);
showReportScreenStore.subscribe((user) => { showReportScreenStore.subscribe((user) => {
if (user !== null) { if (user !== null) {
this.closeAll(); this.closeAll();
@ -118,17 +124,17 @@ export class MenuScene extends Phaser.Scene {
} }
}); });
this.input.keyboard.on('keyup-TAB', () => { this.input.keyboard.on("keyup-TAB", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
}); });
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey); this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
this.menuButton.addListener('click'); this.menuButton.addListener("click");
this.menuButton.on('click', () => { this.menuButton.on("click", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
}); });
this.menuElement.addListener('click'); this.menuElement.addListener("click");
this.menuElement.on('click', this.onMenuClick.bind(this)); this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning()); worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
} }
@ -145,7 +151,7 @@ export class MenuScene extends Phaser.Scene {
public revealMenuIcon(): void { public revealMenuIcon(): void {
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function //TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
try { try {
(this.menuButton.getChildByID('menuIcon') as HTMLElement).hidden = false; (this.menuButton.getChildByID("menuIcon") as HTMLElement).hidden = false;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -155,22 +161,22 @@ export class MenuScene extends Phaser.Scene {
if (this.sideMenuOpened) return; if (this.sideMenuOpened) return;
this.closeAll(); this.closeAll();
this.sideMenuOpened = true; this.sideMenuOpened = true;
this.menuButton.getChildByID('openMenuButton').innerHTML = 'X'; this.menuButton.getChildByID("openMenuButton").innerHTML = "X";
const connection = gameManager.getCurrentGameScene(this).connection; const connection = gameManager.getCurrentGameScene(this).connection;
if (connection && connection.isAdmin()) { if (connection && connection.isAdmin()) {
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement; const adminSection = this.menuElement.getChildByID("adminConsoleSection") as HTMLElement;
adminSection.hidden = false; adminSection.hidden = false;
} }
//TODO bind with future metadata of card //TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){ //if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; const adminSection = this.menuElement.getChildByID("socialLinks") as HTMLElement;
adminSection.hidden = false; adminSection.hidden = false;
//} //}
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
x: openedSideMenuX, x: openedSideMenuX,
duration: 500, duration: 500,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -183,23 +189,22 @@ export class MenuScene extends Phaser.Scene {
} }
this.warningContainerTimeout = setTimeout(() => { this.warningContainerTimeout = setTimeout(() => {
this.warningContainer?.destroy(); this.warningContainer?.destroy();
this.warningContainer = null this.warningContainer = null;
this.warningContainerTimeout = null this.warningContainerTimeout = null;
}, 120000); }, 120000);
} }
private closeSideMenu(): void { private closeSideMenu(): void {
if (!this.sideMenuOpened) return; if (!this.sideMenuOpened) return;
this.sideMenuOpened = false; this.sideMenuOpened = false;
this.closeAll(); this.closeAll();
this.menuButton.getChildByID('openMenuButton').innerHTML = `<img src="/static/images/menu.svg">`; this.menuButton.getChildByID("openMenuButton").innerHTML = `<img src="/static/images/menu.svg">`;
consoleGlobalMessageManagerVisibleStore.set(false); consoleGlobalMessageManagerVisibleStore.set(false);
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
x: closedSideMenuX, x: closedSideMenuX,
duration: 500, duration: 500,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -213,19 +218,23 @@ export class MenuScene extends Phaser.Scene {
this.settingsMenuOpened = true; this.settingsMenuOpened = true;
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const gameQualitySelect = this.gameQualityMenuElement.getChildByID("select-game-quality") as HTMLInputElement;
gameQualitySelect.value = '' + this.gameQualityValue; gameQualitySelect.value = "" + this.gameQualityValue;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; const videoQualitySelect = this.gameQualityMenuElement.getChildByID("select-video-quality") as HTMLInputElement;
videoQualitySelect.value = '' + this.videoQualityValue; videoQualitySelect.value = "" + this.videoQualityValue;
this.gameQualityMenuElement.addListener('click'); this.gameQualityMenuElement.addListener("click");
this.gameQualityMenuElement.on('click', (event: MouseEvent) => { this.gameQualityMenuElement.on("click", (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') { if ((event?.target as HTMLInputElement).id === "gameQualityFormSubmit") {
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const gameQualitySelect = this.gameQualityMenuElement.getChildByID(
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; "select-game-quality"
) as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID(
"select-video-quality"
) as HTMLInputElement;
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value)); this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
} else if ((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { } else if ((event?.target as HTMLInputElement).id === "gameQualityFormCancel") {
this.closeGameQualityMenu(); this.closeGameQualityMenu();
} }
}); });
@ -243,7 +252,7 @@ export class MenuScene extends Phaser.Scene {
y: middleY, y: middleY,
x: middleX, x: middleX,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -251,16 +260,15 @@ export class MenuScene extends Phaser.Scene {
if (!this.settingsMenuOpened) return; if (!this.settingsMenuOpened) return;
this.settingsMenuOpened = false; this.settingsMenuOpened = false;
this.gameQualityMenuElement.removeListener('click'); this.gameQualityMenuElement.removeListener("click");
this.tweens.add({ this.tweens.add({
targets: this.gameQualityMenuElement, targets: this.gameQualityMenuElement,
y: -400, y: -400,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
private openGameShare(): void { private openGameShare(): void {
if (this.gameShareOpened) { if (this.gameShareOpened) {
this.closeGameShare(); this.closeGameShare();
@ -269,7 +277,7 @@ export class MenuScene extends Phaser.Scene {
//close all //close all
this.closeAll(); this.closeAll();
const gameShareLink = this.gameShareElement.getChildByID('gameShareLink') as HTMLInputElement; const gameShareLink = this.gameShareElement.getChildByID("gameShareLink") as HTMLInputElement;
gameShareLink.value = location.toString(); gameShareLink.value = location.toString();
this.gameShareOpened = true; this.gameShareOpened = true;
@ -287,64 +295,64 @@ export class MenuScene extends Phaser.Scene {
y: middleY, y: middleY,
x: middleX, x: middleX,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
private closeGameShare(): void { private closeGameShare(): void {
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = ''; gameShareInfo.innerText = "";
gameShareInfo.style.display = 'none'; gameShareInfo.style.display = "none";
this.gameShareOpened = false; this.gameShareOpened = false;
this.tweens.add({ this.tweens.add({
targets: this.gameShareElement, targets: this.gameShareElement,
y: -400, y: -400,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
private onMenuClick(event: MouseEvent) { private onMenuClick(event: MouseEvent) {
const htmlMenuItem = (event?.target as HTMLInputElement); const htmlMenuItem = event?.target as HTMLInputElement;
if (htmlMenuItem.classList.contains('not-button')) { if (htmlMenuItem.classList.contains("not-button")) {
return; return;
} }
event.preventDefault(); event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) { if (htmlMenuItem.classList.contains("fromApi")) {
sendMenuClickedEvent(htmlMenuItem.id) sendMenuClickedEvent(htmlMenuItem.id);
return return;
} }
switch ((event?.target as HTMLInputElement).id) { switch ((event?.target as HTMLInputElement).id) {
case 'changeNameButton': case "changeNameButton":
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, LoginSceneName, new LoginScene()); gameManager.leaveGame(this, LoginSceneName, new LoginScene());
break; break;
case 'sparkButton': case "sparkButton":
this.gotToCreateMapPage(); this.gotToCreateMapPage();
break; break;
case 'changeSkinButton': case "changeSkinButton":
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
break; break;
case 'changeCompanionButton': case "changeCompanionButton":
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene()); gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
break; break;
case 'closeButton': case "closeButton":
this.closeSideMenu(); this.closeSideMenu();
break; break;
case 'shareButton': case "shareButton":
this.openGameShare(); this.openGameShare();
break; break;
case 'editGameSettingsButton': case "editGameSettingsButton":
this.openGameSettingsMenu(); this.openGameSettingsMenu();
break; break;
case 'toggleFullscreen': case "toggleFullscreen":
this.toggleFullscreen(); this.toggleFullscreen();
break; break;
case 'adminConsoleButton': case "adminConsoleButton":
if (get(consoleGlobalMessageManagerVisibleStore)) { if (get(consoleGlobalMessageManagerVisibleStore)) {
consoleGlobalMessageManagerVisibleStore.set(false); consoleGlobalMessageManagerVisibleStore.set(false);
} else { } else {
@ -356,9 +364,9 @@ export class MenuScene extends Phaser.Scene {
private async copyLink() { private async copyLink() {
await navigator.clipboard.writeText(location.toString()); await navigator.clipboard.writeText(location.toString());
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = 'Link copied, you can share it now!'; gameShareInfo.innerText = "Link copied, you can share it now!";
gameShareInfo.style.display = 'block'; gameShareInfo.style.display = "block";
} }
private saveSetting(valueGame: number, valueVideo: number) { private saveSetting(valueGame: number, valueVideo: number) {
@ -378,8 +386,8 @@ export class MenuScene extends Phaser.Scene {
private gotToCreateMapPage() { private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html'; //const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO. //TODO fix me: this button can to send us on WorkAdventure BO.
const sparkHost = 'https://workadventu.re/getting-started'; const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, '_blank'); window.open(sparkHost, "_blank");
} }
private closeAll() { private closeAll() {
@ -389,10 +397,10 @@ export class MenuScene extends Phaser.Scene {
} }
private toggleFullscreen() { private toggleFullscreen() {
const body = document.querySelector('body') const body = document.querySelector("body");
if (body) { if (body) {
if (document.fullscreenElement ?? document.fullscreen) { if (document.fullscreenElement ?? document.fullscreen) {
document.exitFullscreen() document.exitFullscreen();
} else { } else {
body.requestFullscreen(); body.requestFullscreen();
} }

View File

@ -1,16 +1,16 @@
import {get, writable} from "svelte/store"; import { get, writable } from "svelte/store";
import type {Box} from "../WebRtc/LayoutManager"; import type { Box } from "../WebRtc/LayoutManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import {LayoutMode} from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import {layoutModeStore} from "./StreamableCollectionStore"; import { layoutModeStore } from "./StreamableCollectionStore";
/** /**
* Tries to find the biggest available box of remaining space (this is a space where we can center the character) * Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/ */
function findBiggestAvailableArea(): Box { function findBiggestAvailableArea(): Box {
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas'); const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
if (get(layoutModeStore) === LayoutMode.VideoChat) { if (get(layoutModeStore) === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div'); const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
const htmlChildren = Array.from(children.values()); const htmlChildren = Array.from(children.values());
// No chat? Let's go full center // No chat? Let's go full center
@ -19,18 +19,17 @@ function findBiggestAvailableArea(): Box {
xStart: 0, xStart: 0,
yStart: 0, yStart: 0,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
const lastDiv = htmlChildren[htmlChildren.length - 1]; const lastDiv = htmlChildren[htmlChildren.length - 1];
// Compute area between top right of the last div and bottom right of window // Compute area between top right of the last div and bottom right of window
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) const area1 =
* (game.offsetHeight - lastDiv.offsetTop); (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) * (game.offsetHeight - lastDiv.offsetTop);
// Compute area between bottom of last div and bottom of the screen on whole width // Compute area between bottom of last div and bottom of the screen on whole width
const area2 = game.offsetWidth const area2 = game.offsetWidth * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
if (area1 < 0 && area2 < 0) { if (area1 < 0 && area2 < 0) {
// If screen is full, let's not attempt something foolish and simply center character in the middle. // If screen is full, let's not attempt something foolish and simply center character in the middle.
@ -38,28 +37,30 @@ function findBiggestAvailableArea(): Box {
xStart: 0, xStart: 0,
yStart: 0, yStart: 0,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
if (area1 <= area2) { if (area1 <= area2) {
return { return {
xStart: 0, xStart: 0,
yStart: lastDiv.offsetTop + lastDiv.offsetHeight, yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} else { } else {
return { return {
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
yStart: lastDiv.offsetTop, yStart: lastDiv.offsetTop,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
} else { } else {
// Possible destinations: at the center bottom or at the right bottom. // Possible destinations: at the center bottom or at the right bottom.
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values()); const mainSectionChildren = Array.from(
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values()); document.querySelectorAll<HTMLDivElement>("div.main-section > div").values()
);
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>("aside.sidebar > div").values());
// No presentation? Let's center on the screen // No presentation? Let's center on the screen
if (mainSectionChildren.length === 0) { if (mainSectionChildren.length === 0) {
@ -67,60 +68,58 @@ function findBiggestAvailableArea(): Box {
xStart: 0, xStart: 0,
yStart: 0, yStart: 0,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
// At this point, we know we have at least one element in the main section. // At this point, we know we have at least one element in the main section.
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length - 1];
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) const presentationArea =
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) *
(lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
let leftSideBar: number; let leftSideBar: number;
let bottomSideBar: number; let bottomSideBar: number;
if (sidebarChildren.length === 0) { if (sidebarChildren.length === 0) {
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft; leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("sidebar").offsetLeft;
bottomSideBar = 0; bottomSideBar = 0;
} else { } else {
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
leftSideBar = lastSideBarChildren.offsetLeft; leftSideBar = lastSideBarChildren.offsetLeft;
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
} }
const sideBarArea = (game.offsetWidth - leftSideBar) const sideBarArea = (game.offsetWidth - leftSideBar) * (game.offsetHeight - bottomSideBar);
* (game.offsetHeight - bottomSideBar);
if (presentationArea <= sideBarArea) { if (presentationArea <= sideBarArea) {
return { return {
xStart: leftSideBar, xStart: leftSideBar,
yStart: bottomSideBar, yStart: bottomSideBar,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} else { } else {
return { return {
xStart: 0, xStart: 0,
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth, // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
} }
} }
/** /**
* A store that contains the list of (video) peers we are connected to. * A store that contains the list of (video) peers we are connected to.
*/ */
function createBiggestAvailableAreaStore() { function createBiggestAvailableAreaStore() {
const { subscribe, set } = writable<Box>({ xStart: 0, yStart: 0, xEnd: 1, yEnd: 1 });
const { subscribe, set } = writable<Box>({xStart:0, yStart: 0, xEnd: 1, yEnd: 1});
return { return {
subscribe, subscribe,
recompute: () => { recompute: () => {
set(findBiggestAvailableArea()); set(findBiggestAvailableArea());
} },
}; };
} }

View File

@ -1,4 +1,4 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
/** /**
* A store that contains whether the game overlay is shown or not. * A store that contains whether the game overlay is shown or not.

View File

@ -1,14 +1,14 @@
import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
import {localUserStore} from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import {userMovingStore} from "./GameStore"; import { userMovingStore } from "./GameStore";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import {BrowserTooOldError} from "./Errors/BrowserTooOldError"; import { BrowserTooOldError } from "./Errors/BrowserTooOldError";
import {errorStore} from "./ErrorStore"; import { errorStore } from "./ErrorStore";
import {isIOS} from "../WebRtc/DeviceUtils"; import { isIOS } from "../WebRtc/DeviceUtils";
import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS"; import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import {peerStore} from "./PeerStore"; import { peerStore } from "./PeerStore";
import {privacyShutdownStore} from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -57,7 +57,7 @@ export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilit
* A store containing whether the webcam was enabled in the last 10 seconds * A store containing whether the webcam was enabled in the last 10 seconds
*/ */
const enabledWebCam10secondsAgoStore = readable(false, function start(set) { const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null; let timeout: NodeJS.Timeout | null = null;
const unsubscribe = requestedCameraState.subscribe((enabled) => { const unsubscribe = requestedCameraState.subscribe((enabled) => {
if (enabled === true) { if (enabled === true) {
@ -71,7 +71,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
} else { } else {
set(false); set(false);
} }
}) });
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
@ -82,7 +82,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
* A store containing whether the webcam was enabled in the last 5 seconds * A store containing whether the webcam was enabled in the last 5 seconds
*/ */
const userMoved5SecondsAgoStore = readable(false, function start(set) { const userMoved5SecondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null; let timeout: NodeJS.Timeout | null = null;
const unsubscribe = userMovingStore.subscribe((moving) => { const unsubscribe = userMovingStore.subscribe((moving) => {
if (moving === true) { if (moving === true) {
@ -94,45 +94,51 @@ const userMoved5SecondsAgoStore = readable(false, function start(set) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
set(false); set(false);
}, 5000); }, 5000);
} }
}) });
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
}; };
}); });
/** /**
* A store containing whether the mouse is getting close the bottom right corner. * A store containing whether the mouse is getting close the bottom right corner.
*/ */
const mouseInBottomRight = readable(false, function start(set) { const mouseInBottomRight = readable(false, function start(set) {
let lastInBottomRight = false; let lastInBottomRight = false;
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game'); const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game");
const detectInBottomRight = (event: MouseEvent) => { const detectInBottomRight = (event: MouseEvent) => {
const rect = gameDiv.getBoundingClientRect(); const rect = gameDiv.getBoundingClientRect();
const inBottomRight = event.x - rect.left > rect.width * 3 / 4 && event.y - rect.top > rect.height * 3 / 4; const inBottomRight = event.x - rect.left > (rect.width * 3) / 4 && event.y - rect.top > (rect.height * 3) / 4;
if (inBottomRight !== lastInBottomRight) { if (inBottomRight !== lastInBottomRight) {
lastInBottomRight = inBottomRight; lastInBottomRight = inBottomRight;
set(inBottomRight); set(inBottomRight);
} }
}; };
document.addEventListener('mousemove', detectInBottomRight); document.addEventListener("mousemove", detectInBottomRight);
return function stop() { return function stop() {
document.removeEventListener('mousemove', detectInBottomRight); document.removeEventListener("mousemove", detectInBottomRight);
} };
}); });
/** /**
* A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation. * A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation.
*/ */
export const cameraEnergySavingStore = derived([userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight], ([$userMoved5SecondsAgoStore,$peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => { export const cameraEnergySavingStore = derived(
return !$mouseInBottomRight && !$userMoved5SecondsAgoStore && $peerStore.size === 0 && !$enabledWebCam10secondsAgoStore; [userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight],
}); ([$userMoved5SecondsAgoStore, $peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
return (
!$mouseInBottomRight &&
!$userMoved5SecondsAgoStore &&
$peerStore.size === 0 &&
!$enabledWebCam10secondsAgoStore
);
}
);
/** /**
* A store that contains video constraints. * A store that contains video constraints.
@ -143,28 +149,30 @@ function createVideoConstraintStore() {
height: { min: 400, ideal: 720 }, height: { min: 400, ideal: 720 },
frameRate: { ideal: localUserStore.getVideoQualityValue() }, frameRate: { ideal: localUserStore.getVideoQualityValue() },
facingMode: "user", facingMode: "user",
resizeMode: 'crop-and-scale', resizeMode: "crop-and-scale",
aspectRatio: 1.777777778 aspectRatio: 1.777777778,
} as MediaTrackConstraints); } as MediaTrackConstraints);
return { return {
subscribe, subscribe,
setDeviceId: (deviceId: string|undefined) => update((constraints) => { setDeviceId: (deviceId: string | undefined) =>
if (deviceId !== undefined) { update((constraints) => {
constraints.deviceId = { if (deviceId !== undefined) {
exact: deviceId constraints.deviceId = {
}; exact: deviceId,
} else { };
delete constraints.deviceId; } else {
} delete constraints.deviceId;
}
return constraints; return constraints;
}), }),
setFrameRate: (frameRate: number) => update((constraints) => { setFrameRate: (frameRate: number) =>
constraints.frameRate = { ideal: frameRate }; update((constraints) => {
constraints.frameRate = { ideal: frameRate };
return constraints; return constraints;
}) }),
}; };
} }
@ -178,39 +186,39 @@ function createAudioConstraintStore() {
//TODO: make these values configurable in the game settings menu and store them in localstorage //TODO: make these values configurable in the game settings menu and store them in localstorage
autoGainControl: false, autoGainControl: false,
echoCancellation: true, echoCancellation: true,
noiseSuppression: true noiseSuppression: true,
} as boolean|MediaTrackConstraints); } as boolean | MediaTrackConstraints);
let selectedDeviceId = null; let selectedDeviceId = null;
return { return {
subscribe, subscribe,
setDeviceId: (deviceId: string|undefined) => update((constraints) => { setDeviceId: (deviceId: string | undefined) =>
selectedDeviceId = deviceId; update((constraints) => {
selectedDeviceId = deviceId;
if (typeof(constraints) === 'boolean') { if (typeof constraints === "boolean") {
constraints = {} constraints = {};
} }
if (deviceId !== undefined) { if (deviceId !== undefined) {
constraints.deviceId = { constraints.deviceId = {
exact: selectedDeviceId exact: selectedDeviceId,
}; };
} else { } else {
delete constraints.deviceId; delete constraints.deviceId;
} }
return constraints; return constraints;
}) }),
}; };
} }
export const audioConstraintStore = createAudioConstraintStore(); export const audioConstraintStore = createAudioConstraintStore();
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false; let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false; let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
/** /**
* A store containing the media constraints we want to apply. * A store containing the media constraints we want to apply.
@ -225,7 +233,8 @@ export const mediaStreamConstraintsStore = derived(
audioConstraintStore, audioConstraintStore,
privacyShutdownStore, privacyShutdownStore,
cameraEnergySavingStore, cameraEnergySavingStore,
], ( ],
(
[ [
$requestedCameraState, $requestedCameraState,
$requestedMicrophoneState, $requestedMicrophoneState,
@ -235,92 +244,97 @@ export const mediaStreamConstraintsStore = derived(
$audioConstraintStore, $audioConstraintStore,
$privacyShutdownStore, $privacyShutdownStore,
$cameraEnergySavingStore, $cameraEnergySavingStore,
], set ],
set
) => { ) => {
let currentVideoConstraint: boolean | MediaTrackConstraints = $videoConstraintStore;
let currentAudioConstraint: boolean | MediaTrackConstraints = $audioConstraintStore;
let currentVideoConstraint: boolean|MediaTrackConstraints = $videoConstraintStore; if ($enableCameraSceneVisibilityStore) {
let currentAudioConstraint: boolean|MediaTrackConstraints = $audioConstraintStore;
if ($enableCameraSceneVisibilityStore) {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
return;
}
// Disable webcam if the user requested so
if ($requestedCameraState === false) {
currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== 'boolean') {
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
}
if (typeof previousComputedAudioConstraint !== 'boolean') {
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
set({ set({
video: currentVideoConstraint, video: currentVideoConstraint,
audio: currentAudioConstraint, audio: currentAudioConstraint,
}); });
}, 100); return;
} }
}, {
video: false, // Disable webcam if the user requested so
audio: false if ($requestedCameraState === false) {
} as MediaStreamConstraints); currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (
previousComputedVideoConstraint != currentVideoConstraint ||
previousComputedAudioConstraint != currentAudioConstraint
) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== "boolean") {
previousComputedVideoConstraint = { ...previousComputedVideoConstraint };
}
if (typeof previousComputedAudioConstraint !== "boolean") {
previousComputedAudioConstraint = { ...previousComputedAudioConstraint };
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
}, 100);
}
},
{
video: false,
audio: false,
} as MediaStreamConstraints
);
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue; export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
interface StreamSuccessValue { interface StreamSuccessValue {
type: "success", type: "success";
stream: MediaStream|null, stream: MediaStream | null;
// The constraints that we got (and not the one that have been requested) // The constraints that we got (and not the one that have been requested)
constraints: MediaStreamConstraints constraints: MediaStreamConstraints;
} }
interface StreamErrorValue { interface StreamErrorValue {
type: "error", type: "error";
error: Error, error: Error;
constraints: MediaStreamConstraints constraints: MediaStreamConstraints;
} }
let currentStream : MediaStream|null = null; let currentStream: MediaStream | null = null;
/** /**
* Stops the camera from filming * Stops the camera from filming
@ -347,84 +361,94 @@ function stopMicrophone(): void {
/** /**
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred) * A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
*/ */
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(mediaStreamConstraintsStore, ($mediaStreamConstraintsStore, set) => { export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
const constraints = { ...$mediaStreamConstraintsStore }; mediaStreamConstraintsStore,
($mediaStreamConstraintsStore, set) => {
const constraints = { ...$mediaStreamConstraintsStore };
if (navigator.mediaDevices === undefined) { if (navigator.mediaDevices === undefined) {
if (window.location.protocol === 'http:') { if (window.location.protocol === "http:") {
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'); //throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
set({
type: "error",
error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."),
constraints,
});
return;
} else if (isIOS()) {
set({
type: "error",
error: new WebviewOnOldIOS(),
constraints,
});
return;
} else {
set({
type: "error",
error: new BrowserTooOldError(),
constraints,
});
return;
}
}
if (constraints.audio === false) {
stopMicrophone();
}
if (constraints.video === false) {
stopCamera();
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({ set({
type: 'error', type: "success",
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'), stream: null,
constraints constraints,
});
return;
} else if (isIOS()) {
set({
type: 'error',
error: new WebviewOnOldIOS(),
constraints
});
return;
} else {
set({
type: 'error',
error: new BrowserTooOldError(),
constraints
}); });
return; return;
} }
}
if (constraints.audio === false) { (async () => {
stopMicrophone(); try {
} stopMicrophone();
if (constraints.video === false) { stopCamera();
stopCamera(); currentStream = await navigator.mediaDevices.getUserMedia(constraints);
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({
type: 'success',
stream: null,
constraints
});
return;
}
(async () => {
try {
stopMicrophone();
stopCamera();
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'success',
stream: currentStream,
constraints
});
return;
} catch (e) {
if (constraints.video !== false) {
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
// TODO: does it make sense to pop this error when retrying?
set({ set({
type: 'error', type: "success",
error: e, stream: currentStream,
constraints constraints,
}); });
// Let's try without video constraints return;
requestedCameraState.disableWebcam(); } catch (e) {
} else { if (constraints.video !== false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e); console.info(
set({ "Error. Unable to get microphone and/or camera access. Trying audio only.",
type: 'error', $mediaStreamConstraintsStore,
error: e, e
constraints );
}); // TODO: does it make sense to pop this error when retrying?
} set({
type: "error",
error: e,
constraints,
});
// Let's try without video constraints
requestedCameraState.disableWebcam();
} else {
console.info(
"Error. Unable to get microphone and/or camera access.",
$mediaStreamConstraintsStore,
e
);
set({
type: "error",
error: e,
constraints,
});
}
/*constraints.video = false; /*constraints.video = false;
if (constraints.audio === false) { if (constraints.audio === false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e); console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({ set({
@ -453,9 +477,10 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
}); });
} }
}*/ }*/
} }
})(); })();
}); }
);
/** /**
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system) * A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
@ -472,12 +497,15 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
const queryDeviceList = () => { const queryDeviceList = () => {
// Note: so far, we are ignoring any failures. // Note: so far, we are ignoring any failures.
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => { navigator.mediaDevices
set(mediaDeviceInfos); .enumerateDevices()
}).catch((e) => { .then((mediaDeviceInfos) => {
console.error(e); set(mediaDeviceInfos);
throw e; })
}); .catch((e) => {
console.error(e);
throw e;
});
}; };
const unsubscribe = localStreamStore.subscribe((streamResult) => { const unsubscribe = localStreamStore.subscribe((streamResult) => {
@ -490,23 +518,23 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
}); });
if (navigator.mediaDevices) { if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList); navigator.mediaDevices.addEventListener("devicechange", queryDeviceList);
} }
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
if (navigator.mediaDevices) { if (navigator.mediaDevices) {
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList); navigator.mediaDevices.removeEventListener("devicechange", queryDeviceList);
} }
}; };
}); });
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => { export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'videoinput'); return $deviceListStore.filter((device) => device.kind === "videoinput");
}); });
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => { export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'audioinput'); return $deviceListStore.filter((device) => device.kind === "audioinput");
}); });
// TODO: detect the new webcam and automatically switch on it. // TODO: detect the new webcam and automatically switch on it.
@ -519,7 +547,7 @@ cameraListStore.subscribe((devices) => {
// If we cannot find the device ID, let's remove it. // If we cannot find the device ID, let's remove it.
// @ts-ignore // @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) { if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
videoConstraintStore.setDeviceId(undefined); videoConstraintStore.setDeviceId(undefined);
} }
}); });
@ -527,7 +555,7 @@ cameraListStore.subscribe((devices) => {
microphoneListStore.subscribe((devices) => { microphoneListStore.subscribe((devices) => {
// If the selected camera is unplugged, let's remove the constraint on deviceId // If the selected camera is unplugged, let's remove the constraint on deviceId
const constraints = get(audioConstraintStore); const constraints = get(audioConstraintStore);
if (typeof constraints === 'boolean') { if (typeof constraints === "boolean") {
return; return;
} }
if (!constraints.deviceId) { if (!constraints.deviceId) {
@ -536,13 +564,13 @@ microphoneListStore.subscribe((devices) => {
// If we cannot find the device ID, let's remove it. // If we cannot find the device ID, let's remove it.
// @ts-ignore // @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) { if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
audioConstraintStore.setDeviceId(undefined); audioConstraintStore.setDeviceId(undefined);
} }
}); });
localStreamStore.subscribe(streamResult => { localStreamStore.subscribe((streamResult) => {
if (streamResult.type === 'error') { if (streamResult.type === "error") {
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) { if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
errorStore.addErrorMessage(streamResult.error); errorStore.addErrorMessage(streamResult.error);
} }

View File

@ -1,7 +1,7 @@
import {readable, writable} from "svelte/store"; import { readable, writable } from "svelte/store";
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import {VideoPeer} from "../WebRtc/VideoPeer"; import { VideoPeer } from "../WebRtc/VideoPeer";
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
/** /**
* A store that contains the list of (video) peers we are connected to. * A store that contains the list of (video) peers we are connected to.
@ -19,20 +19,20 @@ function createPeerStore() {
simplePeer.registerPeerConnectionListener({ simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) { onConnect(peer: RemotePeer) {
if (peer instanceof VideoPeer) { if (peer instanceof VideoPeer) {
update(users => { update((users) => {
users.set(peer.userId, peer); users.set(peer.userId, peer);
return users; return users;
}); });
} }
}, },
onDisconnect(userId: number) { onDisconnect(userId: number) {
update(users => { update((users) => {
users.delete(userId); users.delete(userId);
return users; return users;
}); });
} },
}) });
} },
}; };
} }
@ -52,20 +52,20 @@ function createScreenSharingPeerStore() {
simplePeer.registerPeerConnectionListener({ simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) { onConnect(peer: RemotePeer) {
if (peer instanceof ScreenSharingPeer) { if (peer instanceof ScreenSharingPeer) {
update(users => { update((users) => {
users.set(peer.userId, peer); users.set(peer.userId, peer);
return users; return users;
}); });
} }
}, },
onDisconnect(userId: number) { onDisconnect(userId: number) {
update(users => { update((users) => {
users.delete(userId); users.delete(userId);
return users; return users;
}); });
} },
}) });
} },
}; };
} }
@ -79,8 +79,7 @@ function createScreenSharingStreamStore() {
let peers = new Map<number, ScreenSharingPeer>(); let peers = new Map<number, ScreenSharingPeer>();
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) { return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
let unsubscribes: (() => void)[] = [];
let unsubscribes: (()=>void)[] = [];
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => { const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
for (const unsubscribe of unsubscribes) { for (const unsubscribe of unsubscribes) {
@ -91,24 +90,23 @@ function createScreenSharingStreamStore() {
peers = new Map<number, ScreenSharingPeer>(); peers = new Map<number, ScreenSharingPeer>();
screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => { screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => {
if (screenSharingPeer.isReceivingScreenSharingStream()) { if (screenSharingPeer.isReceivingScreenSharingStream()) {
peers.set(key, screenSharingPeer); peers.set(key, screenSharingPeer);
} }
unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => { unsubscribes.push(
if (stream) { screenSharingPeer.streamStore.subscribe((stream) => {
peers.set(key, screenSharingPeer); if (stream) {
} else { peers.set(key, screenSharingPeer);
peers.delete(key); } else {
} peers.delete(key);
set(peers); }
})); set(peers);
})
);
}); });
set(peers); set(peers);
}); });
return function stop() { return function stop() {
@ -117,9 +115,7 @@ function createScreenSharingStreamStore() {
unsubscribe(); unsubscribe();
} }
}; };
}) });
} }
export const screenSharingStreamStore = createScreenSharingStreamStore(); export const screenSharingStreamStore = createScreenSharingStreamStore();

View File

@ -1,6 +1,6 @@
import {get, writable} from "svelte/store"; import { get, writable } from "svelte/store";
import {peerStore} from "./PeerStore"; import { peerStore } from "./PeerStore";
import {visibilityStore} from "./VisibilityStore"; import { visibilityStore } from "./VisibilityStore";
/** /**
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion. * A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
@ -28,7 +28,6 @@ function createPrivacyShutdownStore() {
} }
}); });
return { return {
subscribe, subscribe,
}; };

View File

@ -1,12 +1,10 @@
import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
import {peerStore} from "./PeerStore"; import { peerStore } from "./PeerStore";
import type { import type { LocalStreamStoreValue } from "./MediaStore";
LocalStreamStoreValue, import { DivImportance } from "../WebRtc/LayoutManager";
} from "./MediaStore"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import {DivImportance} from "../WebRtc/LayoutManager";
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -23,7 +21,7 @@ function createRequestedScreenSharingState() {
export const requestedScreenSharingState = createRequestedScreenSharingState(); export const requestedScreenSharingState = createRequestedScreenSharingState();
let currentStream : MediaStream|null = null; let currentStream: MediaStream | null = null;
/** /**
* Stops the camera from filming * Stops the camera from filming
@ -37,27 +35,17 @@ function stopScreenSharing(): void {
currentStream = null; currentStream = null;
} }
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false; let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false; let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
/** /**
* A store containing the media constraints we want to apply. * A store containing the media constraints we want to apply.
*/ */
export const screenSharingConstraintsStore = derived( export const screenSharingConstraintsStore = derived(
[ [requestedScreenSharingState, gameOverlayVisibilityStore, peerStore],
requestedScreenSharingState, ([$requestedScreenSharingState, $gameOverlayVisibilityStore, $peerStore], set) => {
gameOverlayVisibilityStore, let currentVideoConstraint: boolean | MediaTrackConstraints = true;
peerStore, let currentAudioConstraint: boolean | MediaTrackConstraints = false;
], (
[
$requestedScreenSharingState,
$gameOverlayVisibilityStore,
$peerStore,
], set
) => {
let currentVideoConstraint: boolean|MediaTrackConstraints = true;
let currentAudioConstraint: boolean|MediaTrackConstraints = false;
// Disable screen sharing if the user requested so // Disable screen sharing if the user requested so
if (!$requestedScreenSharingState) { if (!$requestedScreenSharingState) {
@ -78,7 +66,10 @@ export const screenSharingConstraintsStore = derived(
} }
// Let's make the changes only if the new value is different from the old one. // Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) { if (
previousComputedVideoConstraint != currentVideoConstraint ||
previousComputedAudioConstraint != currentAudioConstraint
) {
previousComputedVideoConstraint = currentVideoConstraint; previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint; previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects. // Let's copy the objects.
@ -94,85 +85,89 @@ export const screenSharingConstraintsStore = derived(
audio: currentAudioConstraint, audio: currentAudioConstraint,
}); });
} }
}, { },
{
video: false, video: false,
audio: false audio: false,
} as MediaStreamConstraints); } as MediaStreamConstraints
);
/** /**
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred) * A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
*/ */
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(screenSharingConstraintsStore, ($screenSharingConstraintsStore, set) => { export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
const constraints = $screenSharingConstraintsStore; screenSharingConstraintsStore,
($screenSharingConstraintsStore, set) => {
const constraints = $screenSharingConstraintsStore;
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) { if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
set({
type: 'success',
stream: null,
constraints
});
return;
}
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({constraints});
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
} else {
stopScreenSharing();
set({
type: 'error',
error: new Error('Your browser does not support sharing screen'),
constraints
});
return;
}
(async () => {
try {
stopScreenSharing(); stopScreenSharing();
currentStream = await currentStreamPromise; requestedScreenSharingState.disableScreenSharing();
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: 'success',
stream: null,
constraints: {
video: false,
audio: false
}
});
};
}
set({ set({
type: 'success', type: "success",
stream: currentStream, stream: null,
constraints constraints,
}); });
return; return;
} catch (e) {
currentStream = null;
requestedScreenSharingState.disableScreenSharing();
console.info("Error. Unable to share screen.", e);
set({
type: 'error',
error: e,
constraints
});
} }
})();
}); let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({ constraints });
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints });
} else {
stopScreenSharing();
set({
type: "error",
error: new Error("Your browser does not support sharing screen"),
constraints,
});
return;
}
(async () => {
try {
stopScreenSharing();
currentStream = await currentStreamPromise;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: "success",
stream: null,
constraints: {
video: false,
audio: false,
},
});
};
}
set({
type: "success",
stream: currentStream,
constraints,
});
return;
} catch (e) {
currentStream = null;
requestedScreenSharingState.disableScreenSharing();
console.info("Error. Unable to share screen.", e);
set({
type: "error",
error: e,
constraints,
});
}
})();
}
);
/** /**
* A store containing whether the screen sharing button should be displayed or hidden. * A store containing whether the screen sharing button should be displayed or hidden.
@ -188,19 +183,18 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
export interface ScreenSharingLocalMedia { export interface ScreenSharingLocalMedia {
uniqueId: string; uniqueId: string;
stream: MediaStream|null; stream: MediaStream | null;
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; //subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
} }
/** /**
* The representation of the screen sharing stream. * The representation of the screen sharing stream.
*/ */
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(null, function start(set) { export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia | null>(null, function start(set) {
const localMedia: ScreenSharingLocalMedia = { const localMedia: ScreenSharingLocalMedia = {
uniqueId: "localScreenSharingStream", uniqueId: "localScreenSharingStream",
stream: null stream: null,
} };
const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => { const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => {
if (screenSharingLocalStream.type === "success") { if (screenSharingLocalStream.type === "success") {
@ -214,4 +208,4 @@ export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(nu
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
}; };
}) });

View File

@ -1,3 +1,3 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null); export const showReportScreenStore = writable<{ userId: number; userName: string } | null>(null);

View File

@ -1,8 +1,8 @@
import {derived, get, Readable, writable} from "svelte/store"; import { derived, get, Readable, writable } from "svelte/store";
import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore"; import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore";
import { peerStore, screenSharingStreamStore} from "./PeerStore"; import { peerStore, screenSharingStreamStore } from "./PeerStore";
import type {RemotePeer} from "../WebRtc/SimplePeer"; import type { RemotePeer } from "../WebRtc/SimplePeer";
import {LayoutMode} from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
export type Streamable = RemotePeer | ScreenSharingLocalMedia; export type Streamable = RemotePeer | ScreenSharingLocalMedia;
@ -12,32 +12,25 @@ export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream) * A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
*/ */
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> { function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
return derived(
[screenSharingStreamStore, peerStore, screenSharingLocalMedia],
([$screenSharingStreamStore, $peerStore, $screenSharingLocalMedia], set) => {
const peers = new Map<string, Streamable>();
return derived([ const addPeer = (peer: Streamable) => {
screenSharingStreamStore, peers.set(peer.uniqueId, peer);
peerStore, };
screenSharingLocalMedia,
], ([
$screenSharingStreamStore,
$peerStore,
$screenSharingLocalMedia,
], set) => {
const peers = new Map<string, Streamable>(); $screenSharingStreamStore.forEach(addPeer);
$peerStore.forEach(addPeer);
const addPeer = (peer: Streamable) => { if ($screenSharingLocalMedia?.stream) {
peers.set(peer.uniqueId, peer); addPeer($screenSharingLocalMedia);
}; }
$screenSharingStreamStore.forEach(addPeer); set(peers);
$peerStore.forEach(addPeer);
if ($screenSharingLocalMedia?.stream) {
addPeer($screenSharingLocalMedia);
} }
);
set(peers);
});
} }
export const streamableCollectionStore = createStreamableCollectionStore(); export const streamableCollectionStore = createStreamableCollectionStore();

View File

@ -1,8 +1,8 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import {VideoPeer} from "../WebRtc/VideoPeer"; import { VideoPeer } from "../WebRtc/VideoPeer";
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import type {Streamable} from "./StreamableCollectionStore"; import type { Streamable } from "./StreamableCollectionStore";
/** /**
* A store that contains the peer / media that has currently the "importance" focus. * A store that contains the peer / media that has currently the "importance" focus.
@ -32,15 +32,17 @@ function createVideoFocusStore() {
}, },
connectToSimplePeer: (simplePeer: SimplePeer) => { connectToSimplePeer: (simplePeer: SimplePeer) => {
simplePeer.registerPeerConnectionListener({ simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) { onConnect(peer: RemotePeer) {},
},
onDisconnect(userId: number) { onDisconnect(userId: number) {
if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) { if (
(focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) &&
focusedMedia.userId === userId
) {
set(null); set(null);
} }
} },
}) });
} },
}; };
} }

View File

@ -1,16 +1,16 @@
import {readable} from "svelte/store"; import { readable } from "svelte/store";
/** /**
* A store containing whether the current page is visible or not. * A store containing whether the current page is visible or not.
*/ */
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) { export const visibilityStore = readable(document.visibilityState === "visible", function start(set) {
const onVisibilityChange = () => { const onVisibilityChange = () => {
set(document.visibilityState === 'visible'); set(document.visibilityState === "visible");
}; };
document.addEventListener('visibilitychange', onVisibilityChange); document.addEventListener("visibilitychange", onVisibilityChange);
return function stop() { return function stop() {
document.removeEventListener('visibilitychange', onVisibilityChange); document.removeEventListener("visibilitychange", onVisibilityChange);
}; };
}); });

View File

@ -1,11 +1,11 @@
import {HtmlUtils} from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager"; import { connectionManager } from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager"; import { GameConnexionTypes } from "../Url/UrlManager";
import {iframeListener} from "../Api/IframeListener"; import { iframeListener } from "../Api/IframeListener";
import {showReportScreenStore} from "../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../Stores/ShowReportScreenStore";
export type SendMessageCallback = (message:string) => void; export type SendMessageCallback = (message: string) => void;
export class DiscussionManager { export class DiscussionManager {
private mainContainer: HTMLDivElement; private mainContainer: HTMLDivElement;
@ -15,80 +15,81 @@ export class DiscussionManager {
private nbpParticipants?: HTMLParagraphElement; private nbpParticipants?: HTMLParagraphElement;
private divMessages?: HTMLParagraphElement; private divMessages?: HTMLParagraphElement;
private participants: Map<number|string, HTMLDivElement> = new Map<number|string, HTMLDivElement>(); private participants: Map<number | string, HTMLDivElement> = new Map<number | string, HTMLDivElement>();
private activeDiscussion: boolean = false; private activeDiscussion: boolean = false;
private sendMessageCallBack : Map<number|string, SendMessageCallback> = new Map<number|string, SendMessageCallback>(); private sendMessageCallBack: Map<number | string, SendMessageCallback> = new Map<
number | string,
SendMessageCallback
>();
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
constructor() { constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
this.createDiscussPart(''); //todo: why do we always use empty string? 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); this.addMessage(chatEvent.author, chatEvent.message, false);
this.showDiscussion(); this.showDiscussion();
}); });
this.onSendMessageCallback('iframe_listener', (message) => { this.onSendMessageCallback("iframe_listener", (message) => {
iframeListener.sendUserInputChat(message); iframeListener.sendUserInputChat(message);
}) });
} }
private createDiscussPart(name: string) { private createDiscussPart(name: string) {
this.divDiscuss = document.createElement('div'); this.divDiscuss = document.createElement("div");
this.divDiscuss.classList.add('discussion'); this.divDiscuss.classList.add("discussion");
const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button'); const buttonCloseDiscussion: HTMLButtonElement = document.createElement("button");
buttonCloseDiscussion.classList.add('close-btn'); buttonCloseDiscussion.classList.add("close-btn");
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`; buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
buttonCloseDiscussion.addEventListener('click', () => { buttonCloseDiscussion.addEventListener("click", () => {
this.hideDiscussion(); this.hideDiscussion();
}); });
this.divDiscuss.appendChild(buttonCloseDiscussion); this.divDiscuss.appendChild(buttonCloseDiscussion);
const myName: HTMLParagraphElement = document.createElement('p'); const myName: HTMLParagraphElement = document.createElement("p");
myName.innerText = name.toUpperCase(); myName.innerText = name.toUpperCase();
this.nbpParticipants = document.createElement('p'); this.nbpParticipants = document.createElement("p");
this.nbpParticipants.innerText = 'PARTICIPANTS (1)'; this.nbpParticipants.innerText = "PARTICIPANTS (1)";
this.divParticipants = document.createElement('div'); this.divParticipants = document.createElement("div");
this.divParticipants.classList.add('participants'); this.divParticipants.classList.add("participants");
this.divMessages = document.createElement('div'); this.divMessages = document.createElement("div");
this.divMessages.classList.add('messages'); this.divMessages.classList.add("messages");
this.divMessages.innerHTML = "<h2>Local messages</h2>" this.divMessages.innerHTML = "<h2>Local messages</h2>";
this.divDiscuss.appendChild(myName); this.divDiscuss.appendChild(myName);
this.divDiscuss.appendChild(this.nbpParticipants); this.divDiscuss.appendChild(this.nbpParticipants);
this.divDiscuss.appendChild(this.divParticipants); this.divDiscuss.appendChild(this.divParticipants);
this.divDiscuss.appendChild(this.divMessages); this.divDiscuss.appendChild(this.divMessages);
const sendDivMessage: HTMLDivElement = document.createElement('div'); const sendDivMessage: HTMLDivElement = document.createElement("div");
sendDivMessage.classList.add('send-message'); sendDivMessage.classList.add("send-message");
const inputMessage: HTMLInputElement = document.createElement('input'); const inputMessage: HTMLInputElement = document.createElement("input");
inputMessage.onfocus = () => { inputMessage.onfocus = () => {
if(this.userInputManager) { if (this.userInputManager) {
this.userInputManager.disableControls(); this.userInputManager.disableControls();
} }
} };
inputMessage.onblur = () => { inputMessage.onblur = () => {
if(this.userInputManager) { if (this.userInputManager) {
this.userInputManager.restoreControls(); this.userInputManager.restoreControls();
} }
} };
inputMessage.type = "text"; inputMessage.type = "text";
inputMessage.addEventListener('keyup', (event: KeyboardEvent) => { inputMessage.addEventListener("keyup", (event: KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
if(inputMessage.value === null if (inputMessage.value === null || inputMessage.value === "" || inputMessage.value === undefined) {
|| inputMessage.value === ''
|| inputMessage.value === undefined) {
return; return;
} }
this.addMessage(name, inputMessage.value, true); this.addMessage(name, inputMessage.value, true);
for(const callback of this.sendMessageCallBack.values()) { for (const callback of this.sendMessageCallBack.values()) {
callback(inputMessage.value); callback(inputMessage.value);
} }
inputMessage.value = ""; inputMessage.value = "";
@ -100,44 +101,44 @@ export class DiscussionManager {
//append in main container //append in main container
this.mainContainer.appendChild(this.divDiscuss); this.mainContainer.appendChild(this.divDiscuss);
this.addParticipant('me', 'Moi', undefined, true); this.addParticipant("me", "Moi", undefined, true);
} }
public addParticipant( public addParticipant(
userId: number|'me', userId: number | "me",
name: string|undefined, name: string | undefined,
img?: string|undefined, img?: string | undefined,
isMe: boolean = false, isMe: boolean = false
) { ) {
const divParticipant: HTMLDivElement = document.createElement('div'); const divParticipant: HTMLDivElement = document.createElement("div");
divParticipant.classList.add('participant'); divParticipant.classList.add("participant");
divParticipant.id = `participant-${userId}`; divParticipant.id = `participant-${userId}`;
const divImgParticipant: HTMLImageElement = document.createElement('img'); const divImgParticipant: HTMLImageElement = document.createElement("img");
divImgParticipant.src = 'resources/logos/boy.svg'; divImgParticipant.src = "resources/logos/boy.svg";
if (img !== undefined) { if (img !== undefined) {
divImgParticipant.src = img; divImgParticipant.src = img;
} }
const divPParticipant: HTMLParagraphElement = document.createElement('p'); const divPParticipant: HTMLParagraphElement = document.createElement("p");
if(!name){ if (!name) {
name = 'Anonymous'; name = "Anonymous";
} }
divPParticipant.innerText = name; divPParticipant.innerText = name;
divParticipant.appendChild(divImgParticipant); divParticipant.appendChild(divImgParticipant);
divParticipant.appendChild(divPParticipant); divParticipant.appendChild(divPParticipant);
if( if (
!isMe !isMe &&
&& connectionManager.getConnexionType connectionManager.getConnexionType &&
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous connectionManager.getConnexionType !== GameConnexionTypes.anonymous &&
&& userId !== 'me' userId !== "me"
) { ) {
const reportBanUserAction: HTMLButtonElement = document.createElement('button'); const reportBanUserAction: HTMLButtonElement = document.createElement("button");
reportBanUserAction.classList.add('report-btn') reportBanUserAction.classList.add("report-btn");
reportBanUserAction.innerText = 'Report'; reportBanUserAction.innerText = "Report";
reportBanUserAction.addEventListener('click', () => { reportBanUserAction.addEventListener("click", () => {
showReportScreenStore.set({ userId: userId, userName: name ? name : ''}); showReportScreenStore.set({ userId: userId, userName: name ? name : "" });
}); });
divParticipant.appendChild(reportBanUserAction); divParticipant.appendChild(reportBanUserAction);
} }
@ -157,16 +158,16 @@ export class DiscussionManager {
} }
public addMessage(name: string, message: string, isMe: boolean = false) { public addMessage(name: string, message: string, isMe: boolean = false) {
const divMessage: HTMLDivElement = document.createElement('div'); const divMessage: HTMLDivElement = document.createElement("div");
divMessage.classList.add('message'); divMessage.classList.add("message");
if(isMe){ if (isMe) {
divMessage.classList.add('me'); divMessage.classList.add("me");
} }
const pMessage: HTMLParagraphElement = document.createElement('p'); const pMessage: HTMLParagraphElement = document.createElement("p");
const date = new Date(); const date = new Date();
if(isMe){ if (isMe) {
name = 'Me'; name = "Me";
} else { } else {
name = HtmlUtils.escapeHtml(name); name = HtmlUtils.escapeHtml(name);
} }
@ -176,15 +177,15 @@ export class DiscussionManager {
</span>`; </span>`;
divMessage.appendChild(pMessage); divMessage.appendChild(pMessage);
const userMessage: HTMLParagraphElement = document.createElement('p'); const userMessage: HTMLParagraphElement = document.createElement("p");
userMessage.innerHTML = HtmlUtils.urlify(message); userMessage.innerHTML = HtmlUtils.urlify(message);
userMessage.classList.add('body');
userMessage.classList.add('nes-balloon'); userMessage.classList.add('nes-balloon');
if (isMe) { if (isMe) {
userMessage.classList.add('from-left'); userMessage.classList.add('from-left');
} else { } else {
userMessage.classList.add('from-right'); userMessage.classList.add('from-right');
} }
userMessage.classList.add("body");
divMessage.appendChild(userMessage); divMessage.appendChild(userMessage);
this.divMessages?.appendChild(divMessage); this.divMessages?.appendChild(divMessage);
@ -192,14 +193,14 @@ export class DiscussionManager {
setTimeout(() => { setTimeout(() => {
this.divMessages?.scroll({ this.divMessages?.scroll({
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y, top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
behavior: 'smooth' behavior: "smooth",
}); });
}, 200); }, 200);
} }
public removeParticipant(userId: number|string){ public removeParticipant(userId: number | string) {
const element = this.participants.get(userId); const element = this.participants.get(userId);
if(element){ if (element) {
element.remove(); element.remove();
this.participants.delete(userId); this.participants.delete(userId);
} }
@ -208,29 +209,29 @@ export class DiscussionManager {
this.sendMessageCallBack.delete(userId); this.sendMessageCallBack.delete(userId);
} }
public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void { public onSendMessageCallback(userId: string | number, callback: SendMessageCallback): void {
this.sendMessageCallBack.set(userId, callback); this.sendMessageCallBack.set(userId, callback);
} }
get activatedDiscussion(){ get activatedDiscussion() {
return this.activeDiscussion; return this.activeDiscussion;
} }
private showDiscussion(){ private showDiscussion() {
this.activeDiscussion = true; this.activeDiscussion = true;
this.divDiscuss?.classList.add('active'); this.divDiscuss?.classList.add("active");
} }
private hideDiscussion(){ private hideDiscussion() {
this.activeDiscussion = false; this.activeDiscussion = false;
this.divDiscuss?.classList.remove('active'); this.divDiscuss?.classList.remove("active");
} }
public setUserInputManager(userInputManager : UserInputManager){ public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager; this.userInputManager = userInputManager;
} }
public showDiscussionPart(){ public showDiscussionPart() {
this.showDiscussion(); this.showDiscussion();
} }
} }

View File

@ -1,5 +1,5 @@
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {HtmlUtils} from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
const sanitizeHtml = require('sanitize-html'); const sanitizeHtml = require('sanitize-html');
export enum LayoutMode { export enum LayoutMode {
@ -16,24 +16,24 @@ export enum DivImportance {
Normal = "Normal", Normal = "Normal",
} }
export const ON_ACTION_TRIGGER_BUTTON = 'onaction'; export const ON_ACTION_TRIGGER_BUTTON = "onaction";
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger'; export const TRIGGER_WEBSITE_PROPERTIES = "openWebsiteTrigger";
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger'; export const TRIGGER_JITSI_PROPERTIES = "jitsiTrigger";
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage'; export const WEBSITE_MESSAGE_PROPERTIES = "openWebsiteTriggerMessage";
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage'; export const JITSI_MESSAGE_PROPERTIES = "jitsiTriggerMessage";
export const AUDIO_VOLUME_PROPERTY = 'audioVolume'; export const AUDIO_VOLUME_PROPERTY = "audioVolume";
export const AUDIO_LOOP_PROPERTY = 'audioLoop'; export const AUDIO_LOOP_PROPERTY = "audioLoop";
export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number}; export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
class LayoutManager { class LayoutManager {
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>(); private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>(); private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){ public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
//delete previous element //delete previous element
this.removeActionButton(id, userInputManager); this.removeActionButton(id, userInputManager);
@ -44,14 +44,14 @@ class LayoutManager {
p.classList.add('is-dark'); p.classList.add('is-dark');
p.innerHTML = sanitizeHtml(text); p.innerHTML = sanitizeHtml(text);
const div = document.createElement('div'); const div = document.createElement("div");
div.classList.add('action'); div.classList.add("action");
div.id = id; div.id = id;
div.appendChild(p); div.appendChild(p);
this.actionButtonInformation.set(id, div); this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div); mainContainer.appendChild(div);
//add trigger action //add trigger action
@ -60,42 +60,42 @@ class LayoutManager {
userInputManager.addSpaceEventListner(callBack); userInputManager.addSpaceEventListner(callBack);
} }
public removeActionButton(id: string, userInputManager?: UserInputManager){ public removeActionButton(id: string, userInputManager?: UserInputManager) {
//delete previous element //delete previous element
const previousDiv = this.actionButtonInformation.get(id); const previousDiv = this.actionButtonInformation.get(id);
if(previousDiv){ if (previousDiv) {
previousDiv.remove(); previousDiv.remove();
this.actionButtonInformation.delete(id); this.actionButtonInformation.delete(id);
} }
const previousEventCallback = this.actionButtonTrigger.get(id); const previousEventCallback = this.actionButtonTrigger.get(id);
if(previousEventCallback && userInputManager){ if (previousEventCallback && userInputManager) {
userInputManager.removeSpaceEventListner(previousEventCallback); userInputManager.removeSpaceEventListner(previousEventCallback);
} }
} }
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager){ public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
//delete previous element //delete previous element
for ( const [key, value] of this.actionButtonInformation ) { for (const [key, value] of this.actionButtonInformation) {
this.removeActionButton(key, userInputManager); this.removeActionButton(key, userInputManager);
} }
//create div and text html component //create div and text html component
const p = document.createElement('p'); const p = document.createElement("p");
p.classList.add('action-body'); p.classList.add("action-body");
p.innerText = text; p.innerText = text;
const div = document.createElement('div'); const div = document.createElement("div");
div.classList.add('action'); div.classList.add("action");
div.classList.add(id); div.classList.add(id);
div.id = id; div.id = id;
div.appendChild(p); div.appendChild(p);
this.actionButtonInformation.set(id, div); this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div); mainContainer.appendChild(div);
//add trigger action //add trigger action
if(callBack){ if (callBack) {
div.onpointerdown = () => { div.onpointerdown = () => {
callBack(); callBack();
this.removeActionButton(id, userInputManager); this.removeActionButton(id, userInputManager);
@ -105,7 +105,7 @@ class LayoutManager {
//remove it after 10 sec //remove it after 10 sec
setTimeout(() => { setTimeout(() => {
this.removeActionButton(id, userInputManager); this.removeActionButton(id, userInputManager);
}, 10000) }, 10000);
} }
} }

View File

@ -6,26 +6,20 @@ import { localUserStore } from "../Connexion/LocalUserStore";
import type { UserSimplePeerInterface } from "./SimplePeer"; import type { UserSimplePeerInterface } from "./SimplePeer";
import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import { import { localStreamStore } from "../Stores/MediaStore";
localStreamStore, import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
} from "../Stores/MediaStore"; import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import {
screenSharingLocalStreamStore
} from "../Stores/ScreenSharingStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void; export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void; export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void;
import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
export class MediaManager { export class MediaManager {
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private focused: boolean = true; private focused: boolean = true;
@ -34,40 +28,49 @@ export class MediaManager {
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
constructor() { constructor() {
//Check of ask notification navigator permission //Check of ask notification navigator permission
this.getNotification(); this.getNotification();
localStreamStore.subscribe((result) => { localStreamStore.subscribe((result) => {
if (result.type === 'error') { if (result.type === "error") {
console.error(result.error); console.error(result.error);
layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => { layoutManager.addInformation(
helpCameraSettingsVisibleStore.set(true); "warning",
}, this.userInputManager); "Camera access denied. Click here and check your browser permissions.",
() => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
return; return;
} }
}); });
screenSharingLocalStreamStore.subscribe((result) => { screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === 'error') { if (result.type === "error") {
console.error(result.error); console.error(result.error);
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => { layoutManager.addInformation(
helpCameraSettingsVisibleStore.set(true); "warning",
}, this.userInputManager); "Screen sharing denied. Click here and check your browser permissions.",
() => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
return; return;
} }
}); });
} }
public showGameOverlay(): void { public showGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.add('active'); gameOverlay.classList.add("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => { const functionTrigger = () => {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} };
buttonCloseFrame.removeEventListener('click', () => { buttonCloseFrame.removeEventListener("click", () => {
buttonCloseFrame.blur(); buttonCloseFrame.blur();
functionTrigger(); functionTrigger();
}); });
@ -76,14 +79,14 @@ export class MediaManager {
} }
public hideGameOverlay(): void { public hideGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.remove('active'); gameOverlay.classList.remove("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => { const functionTrigger = () => {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} };
buttonCloseFrame.addEventListener('click', () => { buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur(); buttonCloseFrame.blur();
functionTrigger(); functionTrigger();
}); });
@ -100,7 +103,7 @@ export class MediaManager {
if (!element) { if (!element) {
return; return;
} }
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'? element.classList.add("active"); //todo: why does a method 'disable' add a class 'active'?
} }
enabledMicrophoneByUserId(userId: number) { enabledMicrophoneByUserId(userId: number) {
@ -108,7 +111,7 @@ export class MediaManager {
if (!element) { if (!element) {
return; return;
} }
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'? element.classList.remove("active"); //todo: why does a method 'enable' remove a class 'active'?
} }
disabledVideoByUserId(userId: number) { disabledVideoByUserId(userId: number) {
@ -134,8 +137,8 @@ export class MediaManager {
} }
toggleBlockLogo(userId: number, show: boolean): void { toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId); const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>("blocking-" + userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); show ? blockLogoElement.classList.add("active") : blockLogoElement.classList.remove("active");
} }
isError(userId: string): void { isError(userId: string): void {
@ -144,27 +147,28 @@ export class MediaManager {
if (!element) { if (!element) {
return; return;
} }
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement | null; const errorDiv = element.getElementsByClassName("rtc-error").item(0) as HTMLDivElement | null;
if (errorDiv === null) { if (errorDiv === null) {
return; return;
} }
errorDiv.style.display = 'block'; errorDiv.style.display = "block";
} }
isErrorScreenSharing(userId: string): void { isErrorScreenSharing(userId: string): void {
this.isError(this.getScreenSharingId(userId)); this.isError(this.getScreenSharingId(userId));
} }
private getSpinner(userId: string): HTMLDivElement | null { private getSpinner(userId: string): HTMLDivElement | null {
const element = document.getElementById(`div-${userId}`); const element = document.getElementById(`div-${userId}`);
if (!element) { if (!element) {
return null; return null;
} }
const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; const connectingSpinnerDiv = element
.getElementsByClassName("connecting-spinner")
.item(0) as HTMLDivElement | null;
return connectingSpinnerDiv; return connectingSpinnerDiv;
} }
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
this.triggerCloseJistiFrame.set(id, Function); this.triggerCloseJistiFrame.set(id, Function);
} }
@ -191,12 +195,12 @@ export class MediaManager {
discussionManager.onSendMessageCallback(userId, callback); discussionManager.onSendMessageCallback(userId, callback);
} }
public setUserInputManager(userInputManager : UserInputManager){ public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager; this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager); discussionManager.setUserInputManager(userInputManager);
} }
public getNotification(){ public getNotification() {
//Get notification //Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") { if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
if (this.checkNotificationPromise()) { if (this.checkNotificationPromise()) {
@ -218,24 +222,24 @@ export class MediaManager {
private checkNotificationPromise(): boolean { private checkNotificationPromise(): boolean {
try { try {
Notification.requestPermission().then(); Notification.requestPermission().then();
} catch(e) { } catch (e) {
return false; return false;
} }
return true; return true;
} }
public createNotification(userName: string){ public createNotification(userName: string) {
if(this.focused){ if (this.focused) {
return; return;
} }
if (window.Notification && Notification.permission === "granted") { if (window.Notification && Notification.permission === "granted") {
const title = 'WorkAdventure'; const title = "WorkAdventure";
const options = { const options = {
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`, 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!`); //new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);

View File

@ -1,13 +1,13 @@
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 { 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, writable, Writable } from "svelte/store";
import {videoFocusStore} from "../Stores/VideoFocusStore"; import { videoFocusStore } from "../Stores/VideoFocusStore";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
/** /**
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
@ -16,7 +16,7 @@ export class ScreenSharingPeer extends Peer {
/** /**
* Whether this connection is currently receiving a video stream from a remote user. * Whether this connection is currently receiving a video stream from a remote user.
*/ */
private isReceivingStream:boolean = false; private isReceivingStream: boolean = false;
public toClose: boolean = false; public toClose: boolean = false;
public _connected: boolean = false; public _connected: boolean = false;
public readonly userId: number; public readonly userId: number;
@ -24,29 +24,37 @@ export class ScreenSharingPeer extends Peer {
public readonly streamStore: Readable<MediaStream | null>; public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>; public readonly statusStore: Readable<PeerStatus>;
constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) { constructor(
user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
stream: MediaStream | null
) {
super({ super({
initiator: initiator ? initiator : false, initiator: initiator ? initiator : false,
//reconnectTimer: 10000, //reconnectTimer: 10000,
config: { config: {
iceServers: [ iceServers: [
{ {
urls: STUN_SERVER.split(',') urls: STUN_SERVER.split(","),
}, },
TURN_SERVER !== '' ? { TURN_SERVER !== ""
urls: TURN_SERVER.split(','), ? {
username: user.webRtcUser || TURN_USER, urls: TURN_SERVER.split(","),
credential: user.webRtcPassword || TURN_PASSWORD username: user.webRtcUser || TURN_USER,
} : undefined, credential: user.webRtcPassword || TURN_PASSWORD,
].filter((value) => value !== undefined) }
} : undefined,
].filter((value) => value !== undefined),
},
}); });
this.userId = user.userId; this.userId = user.userId;
this.uniqueId = 'screensharing_'+this.userId; this.uniqueId = "screensharing_" + this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => { this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream|null) => { const onStream = (stream: MediaStream | null) => {
videoFocusStore.focus(this); videoFocusStore.focus(this);
set(stream); set(stream);
}; };
@ -54,71 +62,71 @@ export class ScreenSharingPeer extends Peer {
// We unfortunately need to rely on an event to let the other party know a stream has stopped. // We unfortunately need to rely on an event to let the other party know a stream has stopped.
// It seems there is no native way to detect that. // It seems there is no native way to detect that.
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if (message.streamEnded !== true) { if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection'); console.error("Unexpected message on screen sharing peer connection");
return; return;
} }
set(null); set(null);
} };
this.on('stream', onStream); this.on("stream", onStream);
this.on('data', onData); this.on("data", onData);
return () => { return () => {
this.off('stream', onStream); this.off("stream", onStream);
this.off('data', onData); this.off("data", onData);
}; };
}); });
this.statusStore = readable<PeerStatus>("connecting", (set) => { this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => { const onConnect = () => {
set('connected'); set("connected");
}; };
const onError = () => { const onError = () => {
set('error'); set("error");
}; };
const onClose = () => { const onClose = () => {
set('closed'); set("closed");
}; };
this.on('connect', onConnect); this.on("connect", onConnect);
this.on('error', onError); this.on("error", onError);
this.on('close', onClose); this.on("close", onClose);
return () => { return () => {
this.off('connect', onConnect); this.off("connect", onConnect);
this.off('error', onError); this.off("error", onError);
this.off('close', onClose); this.off("close", onClose);
}; };
}); });
//start listen signal for the peer connection //start listen signal for the peer connection
this.on('signal', (data: unknown) => { this.on("signal", (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data); this.sendWebrtcScreenSharingSignal(data);
}); });
this.on('stream', (stream: MediaStream) => { this.on("stream", (stream: MediaStream) => {
this.stream(stream); this.stream(stream);
}); });
this.on('close', () => { this.on("close", () => {
this._connected = false; this._connected = false;
this.toClose = true; this.toClose = true;
this.destroy(); this.destroy();
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => { this.on("error", (err: any) => {
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err); console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
}); });
this.on('connect', () => { this.on("connect", () => {
this._connected = true; this._connected = true;
console.info(`connect => ${this.userId}`); console.info(`connect => ${this.userId}`);
}); });
this.once('finish', () => { this.once("finish", () => {
this._onFinish(); this._onFinish();
}); });
@ -130,7 +138,7 @@ export class ScreenSharingPeer extends Peer {
private sendWebrtcScreenSharingSignal(data: unknown) { private sendWebrtcScreenSharingSignal(data: unknown) {
try { try {
this.connection.sendWebrtcScreenSharingSignal(data, this.userId); this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
}catch (e) { } catch (e) {
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e); console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
} }
} }
@ -139,7 +147,7 @@ export class ScreenSharingPeer extends Peer {
* Sends received stream to screen. * Sends received stream to screen.
*/ */
private stream(stream?: MediaStream) { private stream(stream?: MediaStream) {
if(!stream){ if (!stream) {
this.isReceivingStream = false; this.isReceivingStream = false;
} else { } else {
this.isReceivingStream = true; this.isReceivingStream = true;
@ -152,8 +160,8 @@ export class ScreenSharingPeer extends Peer {
public destroy(error?: Error): void { public destroy(error?: Error): void {
try { try {
this._connected = false this._connected = false;
if(!this.toClose){ if (!this.toClose) {
return; return;
} }
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
@ -162,24 +170,24 @@ export class ScreenSharingPeer extends Peer {
super.destroy(error); super.destroy(error);
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) { } catch (err) {
console.error("ScreenSharingPeer::destroy", err) console.error("ScreenSharingPeer::destroy", err);
} }
} }
_onFinish () { _onFinish() {
if (this.destroyed) return if (this.destroyed) return;
const destroySoon = () => { const destroySoon = () => {
this.destroy(); this.destroy();
} };
if (this._connected) { if (this._connected) {
destroySoon(); destroySoon();
} else { } else {
this.once('connect', destroySoon); this.once("connect", destroySoon);
} }
} }
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) { public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream); this.removeStream(stream);
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true}))); this.write(new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true })));
} }
} }

View File

@ -2,26 +2,22 @@ import type {
WebRtcDisconnectMessageInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "../Connexion/ConnexionModels"; } from "../Connexion/ConnexionModels";
import { import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
mediaManager, import { ScreenSharingPeer } from "./ScreenSharingPeer";
StartScreenSharingCallback, import { MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer } from "./VideoPeer";
StopScreenSharingCallback, import type { RoomConnection } from "../Connexion/RoomConnection";
} from "./MediaManager"; import { blackListManager } from "./BlackListManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer"; import { get } from "svelte/store";
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import type {RoomConnection} from "../Connexion/RoomConnection"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import {blackListManager} from "./BlackListManager"; import { discussionManager } from "./DiscussionManager";
import {get} from "svelte/store";
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
import {discussionManager} from "./DiscussionManager";
export interface UserSimplePeerInterface{ export interface UserSimplePeerInterface {
userId: number; userId: number;
name?: string; name?: string;
initiator?: boolean; initiator?: boolean;
webRtcUser?: string|undefined; webRtcUser?: string | undefined;
webRtcPassword?: string|undefined; webRtcPassword?: string | undefined;
} }
export type RemotePeer = VideoPeer | ScreenSharingPeer; export type RemotePeer = VideoPeer | ScreenSharingPeer;
@ -45,36 +41,40 @@ export class SimplePeer {
private readonly unsubscribers: (() => void)[] = []; private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>(); private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number; private readonly userId: number;
private lastWebrtcUserName: string|undefined; private lastWebrtcUserName: string | undefined;
private lastWebrtcPassword: string|undefined; private lastWebrtcPassword: string | undefined;
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) { constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this); this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
this.unsubscribers.push(localStreamStore.subscribe((streamResult) => { this.unsubscribers.push(
this.sendLocalVideoStream(streamResult); localStreamStore.subscribe((streamResult) => {
})); this.sendLocalVideoStream(streamResult);
})
);
let localScreenCapture: MediaStream|null = null; let localScreenCapture: MediaStream | null = null;
this.unsubscribers.push(screenSharingLocalStreamStore.subscribe((streamResult) => { this.unsubscribers.push(
if (streamResult.type === 'error') { screenSharingLocalStreamStore.subscribe((streamResult) => {
// Let's ignore screen sharing errors, we will deal with those in a different way. if (streamResult.type === "error") {
return; // Let's ignore screen sharing errors, we will deal with those in a different way.
} return;
if (streamResult.stream !== null) {
localScreenCapture = streamResult.stream;
this.sendLocalScreenSharingStream(localScreenCapture);
} else {
if (localScreenCapture) {
this.stopLocalScreenSharingStream(localScreenCapture);
localScreenCapture = null;
} }
}
})); if (streamResult.stream !== null) {
localScreenCapture = streamResult.stream;
this.sendLocalScreenSharingStream(localScreenCapture);
} else {
if (localScreenCapture) {
this.stopLocalScreenSharingStream(localScreenCapture);
localScreenCapture = null;
}
}
})
);
this.userId = Connection.getUserId(); this.userId = Connection.getUserId();
this.initialise(); this.initialise();
@ -92,7 +92,6 @@ export class SimplePeer {
* permit to listen when user could start visio * permit to listen when user could start visio
*/ */
private initialise() { private initialise() {
//receive signal by gemer //receive signal by gemer
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => { this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcSignal(message); this.receiveWebrtcSignal(message);
@ -122,12 +121,12 @@ export class SimplePeer {
// This would be symmetrical to the way we handle disconnection. // This would be symmetrical to the way we handle disconnection.
//start connection //start connection
if(!user.initiator){ if (!user.initiator) {
return; return;
} }
const streamResult = get(localStreamStore); const streamResult = get(localStreamStore);
let stream : MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream) { if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream; stream = streamResult.stream;
} }
@ -137,15 +136,15 @@ export class SimplePeer {
/** /**
* create peer connection to bind users * create peer connection to bind users
*/ */
private createPeerConnection(user : UserSimplePeerInterface, localStream: MediaStream | null) : VideoPeer | null { private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId) const peerConnection = this.PeerConnectionArray.get(user.userId);
if (peerConnection) { if (peerConnection) {
if (peerConnection.destroyed) { if (peerConnection.destroyed) {
peerConnection.toClose = true; peerConnection.toClose = true;
peerConnection.destroy(); peerConnection.destroy();
const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId); const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId);
if (!peerConnexionDeleted) { if (!peerConnexionDeleted) {
throw 'Error to delete peer connection'; throw "Error to delete peer connection";
} }
//return this.createPeerConnection(user, localStream); //return this.createPeerConnection(user, localStream);
} else { } else {
@ -167,23 +166,32 @@ export class SimplePeer {
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 //permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => { 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.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_MESSAGE,
name: this.myName.toUpperCase(),
userId: this.userId,
message: message,
})
)
);
}); });
peer.toClose = false; peer.toClose = false;
// When a connection is established to a video stream, and if a screen sharing is taking place, // When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user! // the user sharing screen should also initiate a connection to the remote user!
peer.on('connect', () => { peer.on("connect", () => {
const streamResult = get(screenSharingLocalStreamStore); const streamResult = get(screenSharingLocalStreamStore);
if (streamResult.type === 'success' && streamResult.stream !== null) { if (streamResult.type === "success" && streamResult.stream !== null) {
this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream); this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
} }
}); });
//Create a notification for first user in circle discussion //Create a notification for first user in circle discussion
if(this.PeerConnectionArray.size === 0){ if (this.PeerConnectionArray.size === 0) {
mediaManager.createNotification(user.name??''); mediaManager.createNotification(user.name ?? "");
} }
this.PeerConnectionArray.set(user.userId, peer); this.PeerConnectionArray.set(user.userId, peer);
@ -196,27 +204,30 @@ export class SimplePeer {
private getName(userId: number): string { private getName(userId: number): string {
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
if (userSearch) { if (userSearch) {
return userSearch.name || ''; return userSearch.name || "";
} else { } else {
return ''; return "";
} }
} }
/** /**
* create peer connection to bind users * create peer connection to bind users
*/ */
private createPeerScreenSharingConnection(user : UserSimplePeerInterface, stream: MediaStream | null) : ScreenSharingPeer | null{ private createPeerScreenSharingConnection(
user: UserSimplePeerInterface,
stream: MediaStream | null
): ScreenSharingPeer | null {
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId); const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
if(peerConnection){ if (peerConnection) {
if(peerConnection.destroyed){ if (peerConnection.destroyed) {
peerConnection.toClose = true; peerConnection.toClose = true;
peerConnection.destroy(); peerConnection.destroy();
const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId); const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId);
if(!peerConnexionDeleted){ if (!peerConnexionDeleted) {
throw 'Error to delete peer connection'; throw "Error to delete peer connection";
} }
this.createPeerConnection(user, stream); this.createPeerConnection(user, stream);
}else { } else {
peerConnection.toClose = false; peerConnection.toClose = false;
} }
return null; return null;
@ -230,7 +241,13 @@ export class SimplePeer {
const name = this.getName(user.userId); const name = this.getName(user.userId);
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream); const peer = new ScreenSharingPeer(
user,
user.initiator ? user.initiator : false,
name,
this.Connection,
stream
);
this.PeerScreenSharingConnectionArray.set(user.userId, peer); this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { for (const peerConnectionListener of this.peerConnectionListeners) {
@ -242,11 +259,13 @@ export class SimplePeer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*/ */
private closeConnection(userId : number) { private closeConnection(userId: number) {
try { try {
const peer = this.PeerConnectionArray.get(userId); const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
console.warn("closeConnection => Tried to close connection for user "+userId+" but could not find user"); console.warn(
"closeConnection => Tried to close connection for user " + userId + " but could not find user"
);
return; return;
} }
//create temp perr to close //create temp perr to close
@ -257,18 +276,18 @@ export class SimplePeer {
this.closeScreenSharingConnection(userId); this.closeScreenSharingConnection(userId);
const userIndex = this.Users.findIndex(user => user.userId === userId); const userIndex = this.Users.findIndex((user) => user.userId === userId);
if(userIndex < 0){ if (userIndex < 0) {
throw 'Couldn\'t delete user'; throw "Couldn't delete user";
} else { } else {
this.Users.splice(userIndex, 1); this.Users.splice(userIndex, 1);
} }
} catch (err) { } catch (err) {
console.error("closeConnection", err) console.error("closeConnection", err);
} }
//if user left discussion, clear array peer connection of sharing //if user left discussion, clear array peer connection of sharing
if(this.Users.length === 0) { if (this.Users.length === 0) {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) { for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId); this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId); this.PeerScreenSharingConnectionArray.delete(userId);
@ -283,12 +302,16 @@ export class SimplePeer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*/ */
private closeScreenSharingConnection(userId : number) { private closeScreenSharingConnection(userId: number) {
try { try {
//mediaManager.removeActiveScreenSharingVideo("" + userId); //mediaManager.removeActiveScreenSharingVideo("" + userId);
const peer = this.PeerScreenSharingConnectionArray.get(userId); const peer = this.PeerScreenSharingConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user") console.warn(
"closeScreenSharingConnection => Tried to close connection for user " +
userId +
" but could not find user"
);
return; return;
} }
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
@ -301,7 +324,7 @@ export class SimplePeer {
}*/ }*/
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) { } catch (err) {
console.error("closeConnection", err) console.error("closeConnection", err);
} }
} }
@ -328,10 +351,10 @@ export class SimplePeer {
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) { private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
try { try {
//if offer type, create peer connection //if offer type, create peer connection
if(data.signal.type === "offer"){ if (data.signal.type === "offer") {
const streamResult = get(localStreamStore); const streamResult = get(localStreamStore);
let stream : MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream) { if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream; stream = streamResult.stream;
} }
@ -341,7 +364,7 @@ export class SimplePeer {
if (peer !== undefined) { if (peer !== undefined) {
peer.signal(data.signal); peer.signal(data.signal);
} else { } else {
console.error('Could not find peer whose ID is "'+data.userId+'" in PeerConnectionArray'); console.error('Could not find peer whose ID is "' + data.userId + '" in PeerConnectionArray');
} }
} catch (e) { } catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e); console.error(`receiveWebrtcSignal => ${data.userId}`, e);
@ -352,22 +375,24 @@ export class SimplePeer {
if (blackListManager.isBlackListed(data.userId)) return; if (blackListManager.isBlackListed(data.userId)) return;
console.log("receiveWebrtcScreenSharingSignal", data); console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore); const streamResult = get(screenSharingLocalStreamStore);
let stream : MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream !== null) { if (streamResult.type === "success" && streamResult.stream !== null) {
stream = streamResult.stream; stream = streamResult.stream;
} }
try { try {
//if offer type, create peer connection //if offer type, create peer connection
if(data.signal.type === "offer"){ if (data.signal.type === "offer") {
this.createPeerScreenSharingConnection(data, stream); this.createPeerScreenSharingConnection(data, stream);
} }
const peer = this.PeerScreenSharingConnectionArray.get(data.userId); const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
if (peer !== undefined) { if (peer !== undefined) {
peer.signal(data.signal); peer.signal(data.signal);
} else { } else {
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal'); console.error(
console.info('Attempt to create new peer connexion'); 'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
);
console.info("Attempt to create new peer connexion");
if (stream) { if (stream) {
this.sendLocalScreenSharingStreamToUser(data.userId, stream); this.sendLocalScreenSharingStreamToUser(data.userId, stream);
} }
@ -384,17 +409,19 @@ export class SimplePeer {
try { try {
const PeerConnection = this.PeerConnectionArray.get(userId); const PeerConnection = this.PeerConnectionArray.get(userId);
if (!PeerConnection) { if (!PeerConnection) {
throw new Error('While adding media, cannot find user with ID ' + userId); throw new Error("While adding media, cannot find user with ID " + userId);
} }
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints}))); PeerConnection.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints }))
);
if (streamResult.type === 'error') { if (streamResult.type === "error") {
return; return;
} }
const localStream: MediaStream | null = streamResult.stream; const localStream: MediaStream | null = streamResult.stream;
if(!localStream){ if (!localStream) {
return; return;
} }
@ -404,7 +431,7 @@ export class SimplePeer {
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any (track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
PeerConnection.addTrack(track, localStream); PeerConnection.addTrack(track, localStream);
} }
}catch (e) { } catch (e) {
console.error(`pushVideoToRemoteUser => ${userId}`, e); console.error(`pushVideoToRemoteUser => ${userId}`, e);
} }
} }
@ -412,7 +439,7 @@ export class SimplePeer {
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) { private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId); const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) { if (!PeerConnection) {
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId); throw new Error("While pushing screen sharing, cannot find user with ID " + userId);
} }
for (const track of localScreenCapture.getTracks()) { for (const track of localScreenCapture.getTracks()) {
@ -421,7 +448,7 @@ export class SimplePeer {
return; return;
} }
public sendLocalVideoStream(streamResult: LocalStreamStoreValue){ public sendLocalVideoStream(streamResult: LocalStreamStoreValue) {
for (const user of this.Users) { for (const user of this.Users) {
this.pushVideoToRemoteUser(user.userId, streamResult); this.pushVideoToRemoteUser(user.userId, streamResult);
} }
@ -455,9 +482,12 @@ export class SimplePeer {
const screenSharingUser: UserSimplePeerInterface = { const screenSharingUser: UserSimplePeerInterface = {
userId, userId,
initiator: true initiator: true,
}; };
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser, localScreenCapture); const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(
screenSharingUser,
localScreenCapture
);
if (!PeerConnectionScreenSharing) { if (!PeerConnectionScreenSharing) {
return; return;
} }
@ -466,7 +496,7 @@ export class SimplePeer {
private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void { private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void {
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId); const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnectionScreenSharing) { if (!PeerConnectionScreenSharing) {
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found') throw new Error("Weird, screen sharing connection to user " + userId + "not found");
} }
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing); console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);

View File

@ -1,22 +1,22 @@
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 { 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 } from "svelte/store";
import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import {discussionManager} from "./DiscussionManager"; import { discussionManager } from "./DiscussionManager";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
export type PeerStatus = "connecting" | "connected" | "error" | "closed"; export type PeerStatus = "connecting" | "connected" | "error" | "closed";
export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; export const MESSAGE_TYPE_CONSTRAINT = "constraint";
export const MESSAGE_TYPE_MESSAGE = 'message'; export const MESSAGE_TYPE_MESSAGE = "message";
export const MESSAGE_TYPE_BLOCKED = 'blocked'; export const MESSAGE_TYPE_BLOCKED = "blocked";
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked'; export const MESSAGE_TYPE_UNBLOCKED = "unblocked";
/** /**
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
*/ */
@ -31,116 +31,124 @@ export class VideoPeer extends Peer {
private onUnBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>; public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>; public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints|null>; public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) { constructor(
public user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
localStream: MediaStream | null
) {
super({ super({
initiator: initiator ? initiator : false, initiator: initiator ? initiator : false,
//reconnectTimer: 10000, //reconnectTimer: 10000,
config: { config: {
iceServers: [ iceServers: [
{ {
urls: STUN_SERVER.split(',') urls: STUN_SERVER.split(","),
}, },
TURN_SERVER !== '' ? { TURN_SERVER !== ""
urls: TURN_SERVER.split(','), ? {
username: user.webRtcUser || TURN_USER, urls: TURN_SERVER.split(","),
credential: user.webRtcPassword || TURN_PASSWORD username: user.webRtcUser || TURN_USER,
} : undefined, credential: user.webRtcPassword || TURN_PASSWORD,
].filter((value) => value !== undefined) }
} : undefined,
].filter((value) => value !== undefined),
},
}); });
this.userId = user.userId; this.userId = user.userId;
this.uniqueId = 'video_'+this.userId; this.uniqueId = "video_" + this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => { this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream|null) => { const onStream = (stream: MediaStream | null) => {
set(stream); set(stream);
}; };
const onData = (chunk: Buffer) => { const onData = (chunk: Buffer) => {
this.on('data', (chunk: Buffer) => { this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (!message.video) { if (!message.video) {
set(null); set(null);
} }
} }
}); });
} };
this.on('stream', onStream); this.on("stream", onStream);
this.on('data', onData); this.on("data", onData);
return () => { return () => {
this.off('stream', onStream); this.off("stream", onStream);
this.off('data', onData); this.off("data", onData);
}; };
}); });
this.constraintsStore = readable<MediaStreamConstraints|null>(null, (set) => { this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => {
const onData = (chunk: Buffer) => { const onData = (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if(message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
set(message); set(message);
} }
} };
this.on('data', onData); this.on("data", onData);
return () => { return () => {
this.off('data', onData); this.off("data", onData);
}; };
}); });
this.statusStore = readable<PeerStatus>("connecting", (set) => { this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => { const onConnect = () => {
set('connected'); set("connected");
}; };
const onError = () => { const onError = () => {
set('error'); set("error");
}; };
const onClose = () => { const onClose = () => {
set('closed'); set("closed");
}; };
this.on('connect', onConnect); this.on("connect", onConnect);
this.on('error', onError); this.on("error", onError);
this.on('close', onClose); this.on("close", onClose);
return () => { return () => {
this.off('connect', onConnect); this.off("connect", onConnect);
this.off('error', onError); this.off("error", onError);
this.off('close', onClose); this.off("close", onClose);
}; };
}); });
//start listen signal for the peer connection //start listen signal for the peer connection
this.on('signal', (data: unknown) => { this.on("signal", (data: unknown) => {
this.sendWebrtcSignal(data); this.sendWebrtcSignal(data);
}); });
this.on('stream', (stream: MediaStream) => this.stream(stream)); this.on("stream", (stream: MediaStream) => this.stream(stream));
this.on('close', () => { this.on("close", () => {
this._connected = false; this._connected = false;
this.toClose = true; this.toClose = true;
this.destroy(); this.destroy();
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => { this.on("error", (err: any) => {
console.error(`error => ${this.userId} => ${err.code}`, err); console.error(`error => ${this.userId} => ${err.code}`, err);
mediaManager.isError("" + this.userId); mediaManager.isError("" + this.userId);
}); });
this.on('connect', () => { this.on("connect", () => {
this._connected = true; this._connected = true;
}); });
this.on('data', (chunk: Buffer) => { this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if(message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (message.audio) { if (message.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId); mediaManager.enabledMicrophoneByUserId(this.userId);
} else { } else {
@ -152,23 +160,23 @@ export class VideoPeer extends Peer {
} else { } else {
mediaManager.disabledVideoByUserId(this.userId); mediaManager.disabledVideoByUserId(this.userId);
} }
} else if(message.type === MESSAGE_TYPE_MESSAGE) { } else if (message.type === MESSAGE_TYPE_MESSAGE) {
if (!blackListManager.isBlackListed(message.userId)) { if (!blackListManager.isBlackListed(message.userId)) {
mediaManager.addNewMessage(message.name, message.message); mediaManager.addNewMessage(message.name, message.message);
} }
} else if(message.type === MESSAGE_TYPE_BLOCKED) { } else if (message.type === MESSAGE_TYPE_BLOCKED) {
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream. //FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
// Find a way to block A's output stream in A's js client // Find a way to block A's output stream in A's js client
//However, the output stream stream B is correctly blocked in A client //However, the output stream stream B is correctly blocked in A client
this.blocked = true; this.blocked = true;
this.toggleRemoteStream(false); this.toggleRemoteStream(false);
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) { } else if (message.type === MESSAGE_TYPE_UNBLOCKED) {
this.blocked = false; this.blocked = false;
this.toggleRemoteStream(true); this.toggleRemoteStream(true);
} }
}); });
this.once('finish', () => { this.once("finish", () => {
this._onFinish(); this._onFinish();
}); });
@ -187,23 +195,32 @@ export class VideoPeer extends Peer {
}); });
if (blackListManager.isBlackListed(this.userId)) { if (blackListManager.isBlackListed(this.userId)) {
this.sendBlockMessage(true) this.sendBlockMessage(true);
} }
} }
private sendBlockMessage(blocking: boolean) { private sendBlockMessage(blocking: boolean) {
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''}))); this.write(
new Buffer(
JSON.stringify({
type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED,
name: this.userName.toUpperCase(),
userId: this.userId,
message: "",
})
)
);
} }
private toggleRemoteStream(enable: boolean) { private toggleRemoteStream(enable: boolean) {
this.remoteStream.getTracks().forEach(track => track.enabled = enable); this.remoteStream.getTracks().forEach((track) => (track.enabled = enable));
mediaManager.toggleBlockLogo(this.userId, !enable); mediaManager.toggleBlockLogo(this.userId, !enable);
} }
private sendWebrtcSignal(data: unknown) { private sendWebrtcSignal(data: unknown) {
try { try {
this.connection.sendWebrtcSignal(data, this.userId); this.connection.sendWebrtcSignal(data, this.userId);
}catch (e) { } catch (e) {
console.error(`sendWebrtcSignal => ${this.userId}`, e); console.error(`sendWebrtcSignal => ${this.userId}`, e);
} }
} }
@ -217,7 +234,7 @@ export class VideoPeer extends Peer {
if (blackListManager.isBlackListed(this.userId) || this.blocked) { if (blackListManager.isBlackListed(this.userId) || this.blocked) {
this.toggleRemoteStream(false); this.toggleRemoteStream(false);
} }
}catch (err){ } catch (err) {
console.error(err); console.error(err);
} }
} }
@ -227,8 +244,8 @@ export class VideoPeer extends Peer {
*/ */
public destroy(error?: Error): void { public destroy(error?: Error): void {
try { try {
this._connected = false this._connected = false;
if(!this.toClose){ if (!this.toClose) {
return; return;
} }
this.onBlockSubscribe.unsubscribe(); this.onBlockSubscribe.unsubscribe();
@ -238,34 +255,36 @@ export class VideoPeer extends Peer {
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
super.destroy(error); super.destroy(error);
} catch (err) { } catch (err) {
console.error("VideoPeer::destroy", err) console.error("VideoPeer::destroy", err);
} }
} }
_onFinish () { _onFinish() {
if (this.destroyed) return if (this.destroyed) return;
const destroySoon = () => { const destroySoon = () => {
this.destroy(); this.destroy();
} };
if (this._connected) { if (this._connected) {
destroySoon(); destroySoon();
} else { } else {
this.once('connect', destroySoon); this.once("connect", destroySoon);
} }
} }
private pushVideoToRemoteUser(localStream: MediaStream | null) { private pushVideoToRemoteUser(localStream: MediaStream | null) {
try { try {
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)}))); this.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore) }))
);
if(!localStream){ if (!localStream) {
return; return;
} }
for (const track of localStream.getTracks()) { for (const track of localStream.getTracks()) {
this.addTrack(track, localStream); this.addTrack(track, localStream);
} }
}catch (e) { } catch (e) {
console.error(`pushVideoToRemoteUser => ${this.userId}`, e); console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
} }
} }

View File

@ -1,12 +1,12 @@
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
import { import {
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap, isIframeAnswerEvent, isIframeErrorAnswerEvent,
isIframeResponseEventWrapper, isIframeResponseEventWrapper,
TypedMessageEvent TypedMessageEvent,
} from "./Api/Events/IframeEvent"; } from "./Api/Events/IframeEvent";
import chat from "./Api/iframe/chat"; import chat from "./Api/iframe/chat";
import type { IframeCallback } from './Api/iframe/IframeApiContribution'; import type { IframeCallback } from "./Api/iframe/IframeApiContribution";
import nav from "./Api/iframe/nav"; import nav from "./Api/iframe/nav";
import controls from "./Api/iframe/controls"; import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui"; import ui from "./Api/iframe/ui";
@ -16,7 +16,7 @@ import player from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound"; import type { Sound } from "./Api/iframe/Sound/Sound";
import {sendToWorkadventure} from "./Api/iframe/IframeApiContribution"; import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
const wa = { const wa = {
ui, ui,
@ -34,7 +34,7 @@ const wa = {
* @deprecated Use WA.chat.sendChatMessage instead * @deprecated Use WA.chat.sendChatMessage instead
*/ */
sendChatMessage(message: string, author: string): void { sendChatMessage(message: string, author: string): void {
console.warn('Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead'); console.warn("Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead");
chat.sendChatMessage(message, author); chat.sendChatMessage(message, author);
}, },
@ -42,7 +42,9 @@ const wa = {
* @deprecated Use WA.chat.disablePlayerControls instead * @deprecated Use WA.chat.disablePlayerControls instead
*/ */
disablePlayerControls(): void { disablePlayerControls(): void {
console.warn('Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead'); console.warn(
"Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead"
);
controls.disablePlayerControls(); controls.disablePlayerControls();
}, },
@ -50,7 +52,9 @@ const wa = {
* @deprecated Use WA.controls.restorePlayerControls instead * @deprecated Use WA.controls.restorePlayerControls instead
*/ */
restorePlayerControls(): void { restorePlayerControls(): void {
console.warn('Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead'); console.warn(
"Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead"
);
controls.restorePlayerControls(); controls.restorePlayerControls();
}, },
@ -58,7 +62,7 @@ const wa = {
* @deprecated Use WA.ui.displayBubble instead * @deprecated Use WA.ui.displayBubble instead
*/ */
displayBubble(): void { displayBubble(): void {
console.warn('Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead'); console.warn("Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead");
ui.displayBubble(); ui.displayBubble();
}, },
@ -66,7 +70,7 @@ const wa = {
* @deprecated Use WA.ui.removeBubble instead * @deprecated Use WA.ui.removeBubble instead
*/ */
removeBubble(): void { removeBubble(): void {
console.warn('Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead'); console.warn("Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead");
ui.removeBubble(); ui.removeBubble();
}, },
@ -74,7 +78,7 @@ const wa = {
* @deprecated Use WA.nav.openTab instead * @deprecated Use WA.nav.openTab instead
*/ */
openTab(url: string): void { openTab(url: string): void {
console.warn('Method WA.openTab is deprecated. Please use WA.nav.openTab instead'); console.warn("Method WA.openTab is deprecated. Please use WA.nav.openTab instead");
nav.openTab(url); nav.openTab(url);
}, },
@ -82,7 +86,7 @@ const wa = {
* @deprecated Use WA.sound.loadSound instead * @deprecated Use WA.sound.loadSound instead
*/ */
loadSound(url: string): Sound { loadSound(url: string): Sound {
console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead'); console.warn("Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead");
return sound.loadSound(url); return sound.loadSound(url);
}, },
@ -90,7 +94,7 @@ const wa = {
* @deprecated Use WA.nav.goToPage instead * @deprecated Use WA.nav.goToPage instead
*/ */
goToPage(url: string): void { goToPage(url: string): void {
console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead'); console.warn("Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead");
nav.goToPage(url); nav.goToPage(url);
}, },
@ -98,7 +102,7 @@ const wa = {
* @deprecated Use WA.nav.goToRoom instead * @deprecated Use WA.nav.goToRoom instead
*/ */
goToRoom(url: string): void { goToRoom(url: string): void {
console.warn('Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead'); console.warn("Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead");
nav.goToRoom(url); nav.goToRoom(url);
}, },
@ -106,7 +110,7 @@ const wa = {
* @deprecated Use WA.nav.openCoWebSite instead * @deprecated Use WA.nav.openCoWebSite instead
*/ */
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void { openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead");
nav.openCoWebSite(url, allowApi, allowPolicy); nav.openCoWebSite(url, allowApi, allowPolicy);
}, },
@ -114,7 +118,7 @@ const wa = {
* @deprecated Use WA.nav.closeCoWebSite instead * @deprecated Use WA.nav.closeCoWebSite instead
*/ */
closeCoWebSite(): void { closeCoWebSite(): void {
console.warn('Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead'); console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead");
nav.closeCoWebSite(); nav.closeCoWebSite();
}, },
@ -129,21 +133,21 @@ const wa = {
* @deprecated Use WA.chat.onChatMessage instead * @deprecated Use WA.chat.onChatMessage instead
*/ */
onChatMessage(callback: (message: string) => void): void { onChatMessage(callback: (message: string) => void): void {
console.warn('Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead'); console.warn("Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead");
chat.onChatMessage(callback); chat.onChatMessage(callback);
}, },
/** /**
* @deprecated Use WA.room.onEnterZone instead * @deprecated Use WA.room.onEnterZone instead
*/ */
onEnterZone(name: string, callback: () => void): void { onEnterZone(name: string, callback: () => void): void {
console.warn('Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead'); console.warn("Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead");
room.onEnterZone(name, callback); room.onEnterZone(name, callback);
}, },
/** /**
* @deprecated Use WA.room.onLeaveZone instead * @deprecated Use WA.room.onLeaveZone instead
*/ */
onLeaveZone(name: string, callback: () => void): void { onLeaveZone(name: string, callback: () => void): void {
console.warn('Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead'); console.warn("Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead");
room.onLeaveZone(name, callback); room.onLeaveZone(name, callback);
}, },
}; };
@ -151,30 +155,54 @@ const wa = {
export type WorkAdventureApi = typeof wa; export type WorkAdventureApi = typeof wa;
declare global { declare global {
interface Window { interface Window {
WA: WorkAdventureApi WA: WorkAdventureApi;
} }
let WA: WorkAdventureApi let WA: WorkAdventureApi;
} }
window.WA = wa; window.WA = wa;
window.addEventListener('message', <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => { window.addEventListener(
"message", <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
if (message.source !== window.parent) { if (message.source !== window.parent) {
return; // Skip message in this event listener return; // Skip message in this event listener
} }
const payload = message.data; const payload = message.data;
console.debug(payload); console.debug(payload);
if (isIframeResponseEventWrapper(payload)) { if (isIframeAnswerEvent(payload)) {
const queryId = payload.id;
const payloadData = payload.data; const payloadData = payload.data;
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined const resolver = answerPromises.get(queryId);
if (callback?.typeChecker(payloadData)) { if (resolver === undefined) {
callback?.callback(payloadData) throw new Error('In Iframe API, got an answer for a question that we have no track of.');
} }
} resolver.resolve(payloadData);
// ... answerPromises.delete(queryId);
}); } else if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error('In Iframe API, got an error answer for a question that we have no track of.');
}
resolver.reject(payloadError);
answerPromises.delete(queryId);
} else if (isIframeResponseEventWrapper(payload)) {
const payloadData = payload.data;
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined;
if (callback?.typeChecker(payloadData)) {
callback?.callback(payloadData);
}
}
// ...
}
);

View File

@ -1,57 +1,57 @@
import "jasmine"; import "jasmine";
import {Room} from "../../../src/Connexion/Room"; import { Room } from "../../../src/Connexion/Room";
describe("Room getIdFromIdentifier()", () => { describe("Room getIdFromIdentifier()", () => {
it("should work with an absolute room id and no hash as parameter", () => { it("should work with an absolute room id and no hash as parameter", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', ''); const { roomId, hash } = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with an absolute room id and a hash as parameters", () => { 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', '', ''); const { roomId, hash } = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start"); expect(hash).toEqual("start");
}); });
it("should work with an absolute room id, regardless of baseUrl or instance", () => { 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'); 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(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link and no hash as parameters", () => { 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'); const { roomId, hash } = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link with no dot", () => { 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'); const { roomId, hash } = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link two levels deep", () => { 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'); 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(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link that rewrite the map domain", () => { 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'); 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(roomId).toEqual('_/global/maps.workadventure.localhost/Floor1/floor1.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link that rewrite the map instance", () => { 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'); 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(roomId).toEqual('_/notglobal/maps.workadventu.re/Floor1/floor1.json');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link that change the map type", () => { 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'); const { roomId, hash } = Room.getIdFromIdentifier('../../../../@/tcm/is/great', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('@/tcm/is/great'); expect(roomId).toEqual('@/tcm/is/great');
expect(hash).toEqual(''); expect(hash).toEqual(null);
}); });
it("should work with a relative file link and a hash as parameters", () => { 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'); 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(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start"); expect(hash).toEqual("start");
}); });

View File

@ -1,150 +1,155 @@
import "jasmine"; import "jasmine";
import {Room} from "../../../src/Connexion/Room"; import { Room } from "../../../src/Connexion/Room";
import {flattenGroupLayersMap} from "../../../src/Phaser/Map/LayersFlattener"; import { flattenGroupLayersMap } from "../../../src/Phaser/Map/LayersFlattener";
import type {ITiledMapLayer} from "../../../src/Phaser/Map/ITiledMap"; import type { ITiledMapLayer } from "../../../src/Phaser/Map/ITiledMap";
describe("Layers flattener", () => { describe("Layers flattener", () => {
it("should iterate maps with no group", () => { it("should iterate maps with no group", () => {
let flatLayers:ITiledMapLayer[] = []; let flatLayers: ITiledMapLayer[] = [];
flatLayers = flattenGroupLayersMap({ flatLayers = flattenGroupLayersMap({
"compressionlevel": -1, compressionlevel: -1,
"height": 2, height: 2,
"infinite": false, infinite: false,
"layers": [ layers: [
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 1, id: 1,
"name": "Tile Layer 1", name: "Tile Layer 1",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}, },
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 1, id: 1,
"name": "Tile Layer 2", name: "Tile Layer 2",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}], },
"nextlayerid": 2, ],
"nextobjectid": 1, nextlayerid: 2,
"orientation": "orthogonal", nextobjectid: 1,
"renderorder": "right-down", orientation: "orthogonal",
"tiledversion": "2021.03.23", renderorder: "right-down",
"tileheight": 32, tiledversion: "2021.03.23",
"tilesets": [], tileheight: 32,
"tilewidth": 32, tilesets: [],
"type": "map", tilewidth: 32,
"version": 1.5, type: "map",
"width": 2 version: 1.5,
}) width: 2,
});
const layers = []; const layers = [];
for (const layer of flatLayers) { for (const layer of flatLayers) {
layers.push(layer.name); layers.push(layer.name);
} }
expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']); expect(layers).toEqual(["Tile Layer 1", "Tile Layer 2"]);
}); });
it("should iterate maps with recursive groups", () => { it("should iterate maps with recursive groups", () => {
let flatLayers:ITiledMapLayer[] = []; let flatLayers: ITiledMapLayer[] = [];
flatLayers = flattenGroupLayersMap({ flatLayers = flattenGroupLayersMap({
"compressionlevel": -1, compressionlevel: -1,
"height": 2, height: 2,
"infinite": false, infinite: false,
"layers": [ layers: [
{ {
"id": 6, id: 6,
"layers": [ layers: [
{ {
"id": 5, id: 5,
"layers": [ layers: [
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 10, id: 10,
"name": "Tile3", name: "Tile3",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}, },
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 9, id: 9,
"name": "Tile2", name: "Tile2",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}], },
"name": "Group 3", ],
"opacity": 1, name: "Group 3",
"type": "group", opacity: 1,
"visible": true, type: "group",
"x": 0, visible: true,
"y": 0 x: 0,
y: 0,
}, },
{ {
"id": 7, id: 7,
"layers": [ layers: [
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 8, id: 8,
"name": "Tile1", name: "Tile1",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}], },
"name": "Group 2", ],
"opacity": 1, name: "Group 2",
"type": "group", opacity: 1,
"visible": true, type: "group",
"x": 0, visible: true,
"y": 0 x: 0,
}], y: 0,
"name": "Group 1", },
"opacity": 1, ],
"type": "group", name: "Group 1",
"visible": true, opacity: 1,
"x": 0, type: "group",
"y": 0 visible: true,
}], x: 0,
"nextlayerid": 11, y: 0,
"nextobjectid": 1, },
"orientation": "orthogonal", ],
"renderorder": "right-down", nextlayerid: 11,
"tiledversion": "2021.03.23", nextobjectid: 1,
"tileheight": 32, orientation: "orthogonal",
"tilesets": [], renderorder: "right-down",
"tilewidth": 32, tiledversion: "2021.03.23",
"type": "map", tileheight: 32,
"version": 1.5, tilesets: [],
"width": 2 tilewidth: 32,
}) type: "map",
version: 1.5,
width: 2,
});
const layers = []; const layers = [];
for (const layer of flatLayers) { for (const layer of flatLayers) {
layers.push(layer.name); layers.push(layer.name);
} }
expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']); expect(layers).toEqual(["Group 1/Group 3/Tile3", "Group 1/Group 3/Tile2", "Group 1/Group 2/Tile1"]);
}); });
}); });

View File

@ -286,11 +286,6 @@
"name":"jitsiTrigger", "name":"jitsiTrigger",
"type":"string", "type":"string",
"value":"onaction" "value":"onaction"
},
{
"name":"jitsiUrl",
"type":"string",
"value":"meet.jit.si"
}] }]
}, },
{ {

View File

@ -0,0 +1,33 @@
{ "columns":2,
"image":"function_tiles.png",
"imageheight":64,
"imagewidth":64,
"margin":0,
"name":"function_tiles",
"spacing":0,
"tilecount":4,
"tiledversion":"1.6.0",
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"start",
"type":"string",
"value":"S1"
}]
},
{
"id":1,
"properties":[
{
"name":"start",
"type":"string",
"value":"S2"
}]
}],
"tilewidth":32,
"type":"tileset",
"version":"1.6"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -170,6 +170,22 @@
<a href="#" class="testLink" data-testmap="animated_tiles.json" target="_blank">Test animated tiles</a> <a href="#" class="testLink" data-testmap="animated_tiles.json" target="_blank">Test animated tiles</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-start-tile-S1"> Success <input type="radio" name="test-start-tile-S1"> Failure <input type="radio" name="test-start-tile-S1" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="start-tile.json#S1" target="_blank">Test start tile (S1)</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-start-tile-S2"> Success <input type="radio" name="test-start-tile-S2"> Failure <input type="radio" name="test-start-tile-S2" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="start-tile.json#S2" target="_blank">Test start tile (S2)</a>
</td>
</tr>
<tr> <tr>
<td> <td>
<input type="radio" name="test-cowebsite-allowAPI"> Success <input type="radio" name="test-cowebsite-allowAPI"> Failure <input type="radio" name="test-cowebsite-allowAPI" checked> Pending <input type="radio" name="test-cowebsite-allowAPI"> Success <input type="radio" name="test-cowebsite-allowAPI"> Failure <input type="radio" name="test-cowebsite-allowAPI" checked> Pending

101
maps/tests/start-tile.json Normal file
View File

@ -0,0 +1,101 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
"height":5,
"id":4,
"name":"background",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 2, 3, 3, 0, 2, 2],
"height":5,
"id":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":66.6666666666667,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":11,
"text":"If URL contains hash #S1, player starts on S1.\nIf URL contains hash #S2, player starts on S2.",
"wrap":true
},
"type":"",
"visible":true,
"width":155.104166666667,
"x":3.28125,
"y":2.5
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":2,
"firstgid":1,
"image":"function_tiles.png",
"imageheight":64,
"imagewidth":64,
"margin":0,
"name":"function_tiles",
"spacing":0,
"tilecount":4,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"start",
"type":"string",
"value":"S1"
}]
},
{
"id":1,
"properties":[
{
"name":"start",
"type":"string",
"value":"S2"
}]
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":5
}