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

This commit is contained in:
Lurkars 2021-08-08 19:33:58 +02:00
commit 06290cdd78
184 changed files with 7255 additions and 12294 deletions

View file

@ -26,7 +26,6 @@
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error",
// TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off",

View file

@ -34,6 +34,7 @@
<title>WorkAdventure</title>
</head>
<body id="body" style="margin: 0; background-color: #000">
<div class="main-container" id="main-container">
<!-- Create the editor container -->
<div id="game" class="game">

View file

@ -60,6 +60,10 @@
<section>
<button id="enableNotification">Enable notifications</button>
</section>
<!-- TODO activate authentication -->
<section hidden>
<button id="oidcLogin">Oauth Login</button>
</section>
<section>
<button id="sparkButton">Create map</button>
</section>

View file

@ -1,18 +0,0 @@
<style>
#warningMain {
border-radius: 5px;
height: 100px;
width: 300px;
background-color: red;
text-align: center;
}
#warningMain h2 {
padding: 5px;
}
</style>
<main id="warningMain">
<h2>Warning!</h2>
<p>This world is close to its limit!</p>
</main>

View file

@ -8,7 +8,6 @@ self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
@ -48,6 +47,14 @@ self.addEventListener('fetch', function(event) {
);
});
self.addEventListener('activate', function(event) {
//TODO activate service worker
self.addEventListener('wait', function(event) {
//TODO wait
});
self.addEventListener('update', function(event) {
//TODO update
});
self.addEventListener('beforeinstallprompt', (e) => {
//TODO change prompt
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

View file

@ -48,41 +48,10 @@
"type": "image\/png"
},
{
"src": "/static/images/favicons/android-icon-36x36.png",
"sizes": "36x36",
"src": "/static/images/favicons/apple-icon.png",
"sizes": "192x192",
"type": "image\/png",
"density": "0.75"
},
{
"src": "/static/images/favicons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "/static/images/favicons/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-16x16.png",
"sizes": "16x16",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/favicon-32x32.png",
"sizes": "32x32",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
"purpose": "any"
},
{
@ -122,6 +91,25 @@
"density": "4.0",
"purpose": "any maskable"
},
{
"src": "/static/images/favicons/favicon-16x16.png",
"sizes": "16x16",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/favicon-32x32.png",
"sizes": "32x32",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "/static/images/favicons/icon-512x512.png",
"sizes": "512x512",
@ -132,6 +120,7 @@
"background_color": "#000000",
"display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone",
"orientation": "portrait-primary",
"scope": "/",
"lang": "en",
"theme_color": "#000000",

8393
front/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.3.0",
"@types/quill": "^1.3.7",
"@types/uuidv4": "^5.0.0",
"@types/webpack-dev-server": "^3.11.4",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
@ -50,10 +51,12 @@
"phaser3-rex-plugins": "^1.1.42",
"queue-typescript": "^1.0.1",
"quill": "1.3.6",
"quill-delta-to-html": "^0.12.0",
"rxjs": "^6.6.3",
"simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4"
"standardized-audio-context": "^25.2.4",
"uuidv4": "^6.2.10"
},
"scripts": {
"start": "run-p templater serve svelte-check-watch",

View file

@ -1,92 +0,0 @@
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import {soundManager} from "../Phaser/Game/SoundManager";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export class GlobalMessageManager {
constructor(private Connection: RoomConnection) {
this.initialise();
}
initialise(){
//receive signal to show message
this.Connection.receivePlayGlobalMessage((message: PlayGlobalMessageInterface) => {
this.playMessage(message);
});
//receive signal to close message
this.Connection.receiveStopGlobalMessage((messageId: string) => {
this.stopMessage(messageId);
});
//receive signal to close message
this.Connection.receiveTeleportMessage((map: string) => {
console.log('map to teleport user', map);
//TODO teleport user on map
});
}
private playMessage(message : PlayGlobalMessageInterface){
const previousMessage = document.getElementById(this.getHtmlMessageId(message.id));
if(previousMessage){
previousMessage.remove();
}
if(AdminMessageEventTypes.audio === message.type){
this.playAudioMessage(message.id, message.message);
}
if(AdminMessageEventTypes.admin === message.type){
this.playTextMessage(message.id, message.message);
}
}
private playAudioMessage(messageId : string, urlMessage: string) {
soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
}
private playTextMessage(messageId : string, htmlMessage: string){
//add button to clear message
const buttonText = document.createElement('p');
buttonText.id = 'button-clear-message';
buttonText.innerText = 'Clear';
const buttonMainConsole = document.createElement('div');
buttonMainConsole.classList.add('clear');
buttonMainConsole.appendChild(buttonText);
buttonMainConsole.addEventListener('click', () => {
messageContainer.style.top = '-80%';
setTimeout(() => {
messageContainer.remove();
buttonMainConsole.remove();
});
});
//create content message
const messageCotent = document.createElement('div');
messageCotent.innerHTML = htmlMessage;
messageCotent.className = "content-message";
//add message container
const messageContainer = document.createElement('div');
messageContainer.id = this.getHtmlMessageId(messageId);
messageContainer.className = "message-container";
messageContainer.appendChild(messageCotent);
messageContainer.appendChild(buttonMainConsole);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(messageContainer);
}
private stopMessage(messageId: string){
HtmlUtils.removeElementByIdOrFail<HTMLDivElement>(this.getHtmlMessageId(messageId));
}
private getHtmlMessageId(messageId: string) : string{
return `message-${messageId}`;
}
}

View file

@ -1,95 +0,0 @@
import type {TypeMessageInterface} from "./UserMessageManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
let modalTimeOut : NodeJS.Timeout;
export class TypeMessageExt implements TypeMessageInterface{
private nbSecond = 0;
private maxNbSecond = 10;
private titleMessage = 'IMPORTANT !';
showMessage(message: string, canDeleteMessage: boolean = true): void {
//delete previous modal
try{
if(modalTimeOut){
clearTimeout(modalTimeOut);
}
const modal = HtmlUtils.getElementByIdOrFail('report-message-user');
modal.remove();
}catch (err){
console.error(err);
}
//create new modal
const div : HTMLDivElement = document.createElement('div');
div.classList.add('modal-report-user');
div.id = 'report-message-user';
div.style.backgroundColor = '#000000e0';
const img : HTMLImageElement = document.createElement('img');
img.src = 'resources/logos/report.svg';
div.appendChild(img);
const title : HTMLParagraphElement = document.createElement('p');
title.id = 'title-report-user';
title.innerText = `${this.titleMessage} (${this.maxNbSecond})`;
div.appendChild(title);
const p : HTMLParagraphElement = document.createElement('p');
p.id = 'body-report-user'
p.innerText = message;
div.appendChild(p);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(div);
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
// FIXME: this will fail on iOS
// We should move the sound playing into the GameScene and listen to the event of a report using a store
try {
reportMessageAudio.play();
} catch (e) {
console.error(e);
}
this.nbSecond = this.maxNbSecond;
setTimeout((c) => {
this.forMessage(title, canDeleteMessage);
}, 1000);
}
forMessage(title: HTMLParagraphElement, canDeleteMessage: boolean = true){
this.nbSecond -= 1;
title.innerText = `${this.titleMessage} (${this.nbSecond})`;
if(this.nbSecond > 0){
modalTimeOut = setTimeout(() => {
this.forMessage(title, canDeleteMessage);
}, 1000);
}else {
title.innerText = this.titleMessage;
if (!canDeleteMessage) {
return;
}
const imgCancel: HTMLImageElement = document.createElement('img');
imgCancel.id = 'cancel-report-user';
imgCancel.src = 'resources/logos/close.svg';
const div = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('report-message-user');
div.appendChild(imgCancel);
imgCancel.addEventListener('click', () => {
div.remove();
});
}
}
}
export class Message extends TypeMessageExt {}
export class Ban extends TypeMessageExt {}
export class Banned extends TypeMessageExt {
showMessage(message: string){
super.showMessage(message, false);
}
}

View file

@ -1,43 +1,34 @@
import * as TypeMessages from "./TypeMessage";
import {Banned} from "./TypeMessage";
import {adminMessagesService} from "../Connexion/AdminMessagesService";
export interface TypeMessageInterface {
showMessage(message: string): void;
}
import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService";
import { textMessageContentStore, textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import { UPLOADER_URL } from "../Enum/EnvironmentVariable";
import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
class UserMessageManager {
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
receiveBannedMessageListener!: Function;
constructor() {
const valueTypeMessageTab = Object.values(TypeMessages);
Object.keys(TypeMessages).forEach((value: string, index: number) => {
const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface);
this.typeMessages.set(value.toLowerCase(), typeMessageInstance);
});
adminMessagesService.messageStream.subscribe((event) => {
const typeMessage = this.showMessage(event.type, event.text);
if(typeMessage instanceof Banned) {
textMessageVisibleStore.set(false);
banMessageVisibleStore.set(false);
if (event.type === AdminMessageEventTypes.admin) {
textMessageContentStore.set(event.text);
textMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.audio) {
soundPlayingStore.playSound(UPLOADER_URL + event.text);
} else if (event.type === AdminMessageEventTypes.ban) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.banned) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
this.receiveBannedMessageListener();
}
})
});
}
showMessage(type: string, message: string) {
const classTypeMessage = this.typeMessages.get(type.toLowerCase());
if (!classTypeMessage) {
console.error('Message unknown');
return;
}
classTypeMessage.showMessage(message);
return classTypeMessage;
}
setReceiveBanListener(callback: Function){
setReceiveBanListener(callback: Function) {
this.receiveBannedMessageListener = callback;
}
}
export const userMessageManager = new UserMessageManager()
export const userMessageManager = new UserMessageManager();

View file

@ -0,0 +1,48 @@
import * as tg from "generic-type-guard";
export const isRectangle = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
})
.get();
export const isEmbeddedWebsiteEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.withOptionalProperties({
url: tg.isString,
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
})
.get();
export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
url: tg.isString,
position: isRectangle,
})
.withOptionalProperties({
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to modify an embedded website
*/
export type ModifyEmbeddedWebsiteEvent = tg.GuardedType<typeof isEmbeddedWebsiteEvent>;
export type CreateEmbeddedWebsiteEvent = tg.GuardedType<typeof isCreateEmbeddedWebsiteEvent>;
// TODO: make a variation that is all optional (except for the name)
export type Rectangle = tg.GuardedType<typeof isRectangle>;

View file

@ -4,10 +4,11 @@ export const isGameStateEvent = new tg.IsInterface()
.withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull),
nickname: tg.isString,
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),
variables: tg.isObject,
})
.get();
/**

View file

@ -1,4 +1,4 @@
import type { GameStateEvent } from "./GameStateEvent";
import * as tg from "generic-type-guard";
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent";
@ -9,7 +9,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { DataLayerEvent } from "./DataLayerEvent";
import type { MapDataEvent } from "./MapDataEvent";
import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
@ -18,6 +18,21 @@ import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite";
import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
import type {
MessageReferenceEvent,
removeActionMessage,
triggerActionMessage,
TriggerActionMessageEvent,
} from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -43,13 +58,14 @@ export type IframeEventMap = {
showLayer: LayerEvent;
hideLayer: LayerEvent;
setProperty: SetPropertyEvent;
getDataLayer: undefined;
loadSound: LoadSoundEvent;
playSound: PlaySoundEvent;
stopSound: null;
getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent;
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
};
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
@ -66,8 +82,9 @@ export interface IframeResponseEventMap {
leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent;
messageTriggered: MessageReferenceEvent;
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T;
@ -79,20 +96,63 @@ 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
* 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.
* Types are defined using Type guards that will actually bused to enforce and check types.
*/
export type IframeQueryMap = {
export const iframeQueryMapTypeGuards = {
getState: {
query: undefined,
answer: GameStateEvent
query: tg.isUndefined,
answer: isGameStateEvent,
},
}
getMapData: {
query: tg.isUndefined,
answer: isMapDataEvent,
},
setVariable: {
query: isSetVariableEvent,
answer: tg.isUndefined,
},
loadTileset: {
query: isLoadTilesetEvent,
answer: tg.isNumber,
},
triggerActionMessage: {
query: isTriggerActionMessageEvent,
answer: tg.isUndefined,
},
removeActionMessage: {
query: isMessageReferenceEvent,
answer: tg.isUndefined,
},
getEmbeddedWebsite: {
query: tg.isString,
answer: isCreateEmbeddedWebsiteEvent,
},
deleteEmbeddedWebsite: {
query: tg.isString,
answer: tg.isUndefined,
},
createEmbeddedWebsite: {
query: isCreateEmbeddedWebsiteEvent,
answer: tg.isUndefined,
},
};
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
type UnknownToVoid<T> = undefined extends T ? void : T;
export type IframeQueryMap = {
[key in keyof IframeQueryMapTypeGuardsType]: {
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
};
};
export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T;
data: IframeQueryMap[T]['query'];
data: IframeQueryMap[T]["query"];
}
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
@ -100,19 +160,41 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
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';
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
return type in iframeQueryMapTypeGuards;
};
// 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 const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => {
const type = event.type;
if (typeof type !== "string") {
return false;
}
if (!isIframeQueryKey(type)) {
return false;
}
const result = iframeQueryMapTypeGuards[type].query(event.data);
if (!result) {
console.warn('Received a query with type "' + type + '" but the payload is invalid.');
}
return result;
};
// 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'];
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 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;
@ -120,4 +202,9 @@ export interface IframeErrorAnswerEvent {
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';
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

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

View file

@ -1,6 +1,6 @@
import * as tg from "generic-type-guard";
export const isDataLayerEvent = new tg.IsInterface()
export const isMapDataEvent = new tg.IsInterface()
.withProperties({
data: tg.isObject,
})
@ -9,4 +9,4 @@ export const isDataLayerEvent = new tg.IsInterface()
/**
* 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
*/
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;
export type MapDataEvent = tg.GuardedType<typeof isMapDataEvent>;

View file

@ -0,0 +1,20 @@
import * as tg from "generic-type-guard";
import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
export const isSetVariableEvent = new tg.IsInterface()
.withProperties({
key: tg.isString,
value: tg.isUnknown,
})
.get();
/**
* A message sent from the iFrame to the game to change the value of the property of the layer
*/
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>;
export const isSetVariableIframeEvent = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString("setVariable"),
data: isSetVariableEvent,
})
.get();

View file

@ -0,0 +1,26 @@
import * as tg from "generic-type-guard";
export const triggerActionMessage = "triggerActionMessage";
export const removeActionMessage = "removeActionMessage";
export const isActionMessageType = tg.isSingletonStringUnion("message", "warning");
export type ActionMessageType = tg.GuardedType<typeof isActionMessageType>;
export const isTriggerActionMessageEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
uuid: tg.isString,
type: isActionMessageType,
})
.get();
export type TriggerActionMessageEvent = tg.GuardedType<typeof isTriggerActionMessageEvent>;
export const isMessageReferenceEvent = new tg.IsInterface()
.withProperties({
uuid: tg.isString,
})
.get();
export type MessageReferenceEvent = tg.GuardedType<typeof isMessageReferenceEvent>;

View file

@ -0,0 +1,24 @@
import {
isMessageReferenceEvent,
isTriggerActionMessageEvent,
removeActionMessage,
triggerActionMessage,
} from "./TriggerActionMessageEvent";
import * as tg from "generic-type-guard";
const isTriggerMessageEventObject = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString(triggerActionMessage),
data: isTriggerActionMessageEvent,
})
.get();
const isTriggerMessageRemoveEventObject = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString(removeActionMessage),
data: isMessageReferenceEvent,
})
.get();
export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject);

View file

@ -1,4 +1,5 @@
import { Subject } from "rxjs";
import type * as tg from "generic-type-guard";
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
@ -12,7 +13,8 @@ import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent
import {
IframeErrorAnswerEvent,
IframeEvent,
IframeEventMap, IframeQueryMap,
IframeEventMap,
IframeQueryMap,
IframeResponseEvent,
IframeResponseEventMap,
isIframeEventWrapper,
@ -25,21 +27,27 @@ import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type { DataLayerEvent } from "./Events/DataLayerEvent";
import type { GameStateEvent } from "./Events/GameStateEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"],
source: MessageEventSource | null
) => IframeQueryMap[T]["answer"] | PromiseLike<IframeQueryMap[T]["answer"]>;
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _readyStream: Subject<HTMLIFrameElement> = new Subject();
public readonly readyStream = this._readyStream.asObservable();
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
@ -85,9 +93,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
@ -106,19 +111,23 @@ class IframeListener {
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable();
private readonly _modifyEmbeddedWebsiteStream: Subject<ModifyEmbeddedWebsiteEvent> = new Subject();
public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
// Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>
[str in keyof IframeQueryMap]?: unknown;
} = {};
init() {
window.addEventListener(
"message",
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
(message: MessageEvent<unknown>) => {
// Do we trust the sender of this message?
// 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).
@ -152,109 +161,123 @@ class IframeListener {
const queryId = payload.id;
const query = payload.query;
const answerer = this.answerers[query.type];
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined;
if (answerer === undefined) {
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
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, '*');
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;
const errorHandler = (reason: unknown) => {
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();
} else if (typeof reason === "object") {
reasonMsg = reason ? reason.toString() : "";
} else if (typeof reason === "string") {
reasonMsg = reason;
}
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: reasonMsg
} as IframeErrorAnswerEvent, '*');
});
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
error: reasonMsg,
} as IframeErrorAnswerEvent,
"*"
);
};
try {
Promise.resolve(answerer(query.data, message.source))
.then((value) => {
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
data: value,
},
"*"
);
})
.catch(errorHandler);
} catch (reason) {
errorHandler(reason);
}
} 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._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);
}
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} 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);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
this._modifyEmbeddedWebsiteStream.next(payload.data);
}
}
},
false
);
}
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
type: "dataLayer",
data: dataLayerEvent,
});
}
/**
* Allows the passed iFrame to send/receive messages via the API.
*/
@ -394,6 +417,22 @@ class IframeListener {
});
}
setVariable(setVariableEvent: SetVariableEvent) {
this.postMessage({
type: "setVariable",
data: setVariableEvent,
});
}
sendActionMessageTriggered(uuid: string): void {
this.postMessage({
type: "messageTriggered",
data: {
uuid,
},
});
}
/**
* Sends the message... to all allowed iframes.
*/
@ -411,13 +450,31 @@ class IframeListener {
* @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 {
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T>): void {
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
dispatchVariableToOtherIframes(key: string, value: unknown, source: MessageEventSource | null) {
// Let's dispatch the message to the other iframes
for (const iframe of this.iframes) {
if (iframe.contentWindow !== source) {
iframe.contentWindow?.postMessage(
{
type: "setVariable",
data: {
key,
value,
},
},
"*"
);
}
}
}
}
export const iframeListener = new IframeListener();

View file

@ -1,51 +1,66 @@
import type * as tg from "generic-type-guard";
import type {
IframeEvent,
IframeEventMap, IframeQuery,
IframeEventMap,
IframeQuery,
IframeQueryMap,
IframeResponseEventMap
} from '../Events/IframeEvent';
import type {IframeQueryWrapper} from "../Events/IframeEvent";
IframeResponseEventMap,
} from "../Events/IframeEvent";
import type { IframeQueryWrapper } from "../Events/IframeEvent";
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 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>, "*");
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
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>> {
typeChecker: Guard,
callback: (payloadData: T) => void
export interface IframeCallback<
Key extends keyof IframeResponseEventMap,
T = IframeResponseEventMap[Key],
Guard = tg.TypeGuard<T>
> {
typeChecker: Guard;
callback: (payloadData: T) => void;
}
export interface IframeCallbackContribution<Key extends keyof IframeResponseEventMap> extends IframeCallback<Key> {
type: Key
type: Key;
}
/**
@ -54,9 +69,10 @@ export interface IframeCallbackContribution<Key extends keyof IframeResponseEven
*
*/
export abstract class IframeApiContribution<T extends {
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>,
}> {
abstract callbacks: T["callbacks"]
export abstract class IframeApiContribution<
T extends {
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>;
}
> {
abstract callbacks: T["callbacks"];
}

View file

@ -0,0 +1,90 @@
import { sendToWorkadventure } from "../IframeApiContribution";
import type {
CreateEmbeddedWebsiteEvent,
ModifyEmbeddedWebsiteEvent,
Rectangle,
} from "../../Events/EmbeddedWebsiteEvent";
export class EmbeddedWebsite {
public readonly name: string;
private _url: string;
private _visible: boolean;
private _allow: string;
private _allowApi: boolean;
private _position: Rectangle;
constructor(private config: CreateEmbeddedWebsiteEvent) {
this.name = config.name;
this._url = config.url;
this._visible = config.visible ?? true;
this._allow = config.allow ?? "";
this._allowApi = config.allowApi ?? false;
this._position = config.position;
}
public set url(url: string) {
this._url = url;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
url: this._url,
},
});
}
public set visible(visible: boolean) {
this._visible = visible;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
visible: this._visible,
},
});
}
public set x(x: number) {
this._position.x = x;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
x: this._position.x,
},
});
}
public set y(y: number) {
this._position.y = y;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
y: this._position.y,
},
});
}
public set width(width: number) {
this._position.width = width;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
width: this._position.width,
},
});
}
public set height(height: number) {
this._position.height = height;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
height: this._position.height,
},
});
}
}

View file

@ -0,0 +1,56 @@
import {
ActionMessageType,
MessageReferenceEvent,
removeActionMessage,
triggerActionMessage,
TriggerActionMessageEvent,
} from "../../Events/ui/TriggerActionMessageEvent";
import { queryWorkadventure } from "../IframeApiContribution";
import type { ActionMessageOptions } from "../ui";
function uuidv4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0,
v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export class ActionMessage {
public readonly uuid: string;
private readonly type: ActionMessageType;
private readonly message: string;
private readonly callback: () => void;
constructor(actionMessageOptions: ActionMessageOptions, private onRemove: () => void) {
this.uuid = uuidv4();
this.message = actionMessageOptions.message;
this.type = actionMessageOptions.type ?? "message";
this.callback = actionMessageOptions.callback;
this.create();
}
private async create() {
await queryWorkadventure({
type: triggerActionMessage,
data: {
message: this.message,
type: this.type,
uuid: this.uuid,
} as TriggerActionMessageEvent,
});
}
async remove() {
await queryWorkadventure({
type: removeActionMessage,
data: {
uuid: this.uuid,
} as MessageReferenceEvent,
});
this.onRemove();
}
triggerCallback() {
this.callback();
}
}

View file

@ -2,17 +2,28 @@ import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribut
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks";
import { getGameState } from "./room";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
interface User {
id: string | undefined;
nickName: string | null;
tags: string[];
}
const moveStream = new Subject<HasPlayerMovedEvent>();
let playerName: string | undefined;
export const setPlayerName = (name: string) => {
playerName = name;
};
let tags: string[] | undefined;
export const setTags = (_tags: string[]) => {
tags = _tags;
};
let uuid: string | undefined;
export const setUuid = (_uuid: string | undefined) => {
uuid = _uuid;
};
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [
apiCallback({
@ -31,10 +42,30 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
data: null,
});
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
get name(): string {
if (playerName === undefined) {
throw new Error(
"Player name not initialized yet. You should call WA.player.name within a WA.onInit callback."
);
}
return playerName;
}
get tags(): string[] {
if (tags === undefined) {
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
}
return tags;
}
get id(): string | undefined {
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
// while uuid could.
if (playerName === undefined) {
throw new Error("Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.");
}
return uuid;
}
}

View file

@ -1,28 +1,16 @@
import { Subject } from "rxjs";
import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
import type { GameStateEvent } from "../Events/GameStateEvent";
import type { WorkadventureRoomWebsiteCommands } from "./website";
import website from "./website";
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>();
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
interface Room {
id: string;
mapUrl: string;
map: ITiledMap;
startLayer: string | null;
}
interface TileDescriptor {
x: number;
@ -31,19 +19,17 @@ interface TileDescriptor {
layer: string;
}
export function getGameState(): Promise<GameStateEvent> {
if (immutableDataPromise === undefined) {
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
}
return immutableDataPromise;
}
let roomId: string | undefined;
function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver);
sendToWorkadventure({ type: "getDataLayer", data: null });
});
}
export const setRoomId = (id: string) => {
roomId = id;
};
let mapURL: string | undefined;
export const setMapURL = (url: string) => {
mapURL = url;
};
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [
@ -61,13 +47,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
leaveStreams.get(payloadData.name)?.next();
},
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
},
}),
];
onEnterZone(name: string, callback: () => void): void {
@ -102,17 +81,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
},
});
}
getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => {
return {
id: gameState.roomId,
map: mapJson.data as ITiledMap,
mapUrl: gameState.mapUrl,
startLayer: gameState.startLayerName,
};
});
});
async getTiledMap(): Promise<ITiledMap> {
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
return event.data as ITiledMap;
}
setTiles(tiles: TileDescriptor[]) {
sendToWorkadventure({
@ -120,6 +91,35 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
data: tiles,
});
}
get id(): string {
if (roomId === undefined) {
throw new Error("Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.");
}
return roomId;
}
get mapURL(): string {
if (mapURL === undefined) {
throw new Error(
"mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback."
);
}
return mapURL;
}
async loadTileset(url: string): Promise<number> {
return await queryWorkadventure({
type: "loadTileset",
data: {
url: url,
},
});
}
get website(): WorkadventureRoomWebsiteCommands {
return website;
}
}
export default new WorkadventureRoomCommands();

View file

@ -0,0 +1,90 @@
import { Observable, Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
const setVariableResolvers = new Subject<SetVariableEvent>();
const variables = new Map<string, unknown>();
const variableSubscribers = new Map<string, Subject<unknown>>();
export const initVariables = (_variables: Map<string, unknown>): void => {
for (const [name, value] of _variables.entries()) {
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
if (!variables.has(name)) {
variables.set(name, value);
}
}
};
setVariableResolvers.subscribe((event) => {
const oldValue = variables.get(event.key);
// If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return;
}*/
variables.set(event.key, event.value);
const subject = variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
callbacks = [
apiCallback({
type: "setVariable",
typeChecker: isSetVariableEvent,
callback: (payloadData) => {
setVariableResolvers.next(payloadData);
},
}),
];
saveVariable(key: string, value: unknown): Promise<void> {
variables.set(key, value);
return queryWorkadventure({
type: "setVariable",
data: {
key,
value,
},
});
}
loadVariable(key: string): unknown {
return variables.get(key);
}
onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key);
if (subject === undefined) {
subject = new Subject<unknown>();
variableSubscribers.set(key, subject);
}
return subject.asObservable();
}
}
const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
if (p in target) {
return Reflect.get(target, p, receiver);
}
return target.loadVariable(p.toString());
},
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
// Note: when using "set", there is no way to wait, so we ignore the return of the promise.
// User must use WA.state.saveVariable to have error message.
target.saveVariable(p.toString(), value);
return true;
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
export default proxyCommand;

View file

@ -1,10 +1,11 @@
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup";
import { ActionMessage } from "./Ui/ActionMessage";
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
@ -14,6 +15,7 @@ const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
>();
const menuCallbacks: Map<string, (command: string) => void> = new Map();
const actionMessages = new Map<string, ActionMessage>();
interface ZonedPopupOptions {
zone: string;
@ -23,6 +25,12 @@ interface ZonedPopupOptions {
popupOptions: Array<ButtonDescriptor>;
}
export interface ActionMessageOptions {
message: string;
type?: "message" | "warning";
callback: () => void;
}
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
callbacks = [
apiCallback({
@ -49,6 +57,16 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
}
},
}),
apiCallback({
type: "messageTriggered",
typeChecker: isMessageReferenceEvent,
callback: (event) => {
const actionMessage = actionMessages.get(event.uuid);
if (actionMessage) {
actionMessage.triggerCallback();
}
},
}),
];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
@ -103,6 +121,14 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
removeBubble(): void {
sendToWorkadventure({ type: "removeBubble", data: null });
}
displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {
const actionMessage = new ActionMessage(actionMessageOptions, () => {
actionMessages.delete(actionMessage.uuid);
});
actionMessages.set(actionMessage.uuid, actionMessage);
return actionMessage;
}
}
export default new WorkAdventureUiCommands();

View file

@ -0,0 +1,38 @@
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { Sound } from "./Sound/Sound";
import { EmbeddedWebsite } from "./Room/EmbeddedWebsite";
import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent";
export class WorkadventureRoomWebsiteCommands extends IframeApiContribution<WorkadventureRoomWebsiteCommands> {
callbacks = [];
async get(objectName: string): Promise<EmbeddedWebsite> {
const websiteEvent = await queryWorkadventure({
type: "getEmbeddedWebsite",
data: objectName,
});
return new EmbeddedWebsite(websiteEvent);
}
create(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent): EmbeddedWebsite {
queryWorkadventure({
type: "createEmbeddedWebsite",
data: createEmbeddedWebsiteEvent,
}).catch((e) => {
console.error(e);
});
return new EmbeddedWebsite(createEmbeddedWebsiteEvent);
}
async delete(objectName: string): Promise<void> {
return await queryWorkadventure({
type: "deleteEmbeddedWebsite",
data: objectName,
});
}
}
export default new WorkadventureRoomWebsiteCommands();

View file

@ -27,6 +27,16 @@
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
import {warningContainerStore} from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore";
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte"
export let game: Game;
@ -58,11 +68,31 @@
<EnableCameraScene game={game}></EnableCameraScene>
</div>
{/if}
{#if $banMessageVisibleStore}
<div>
<AdminMessage></AdminMessage>
</div>
{/if}
{#if $textMessageVisibleStore}
<div>
<TextMessage></TextMessage>
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
{#if $audioManagerVisibilityStore}
<div>
<AudioManager></AudioManager>
</div>
{/if}
{#if $layoutManagerVisibilityStore}
<div>
<LayoutManager></LayoutManager>
</div>
{/if}
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay></VideoOverlay>
@ -91,4 +121,7 @@
{#if $chatVisibilityStore}
<Chat></Chat>
{/if}
{#if $warningContainerStore}
<WarningContainer></WarningContainer>
{/if}
</div>

View file

@ -0,0 +1,119 @@
<script lang="ts">
import audioImg from "../images/audio.svg";
import audioMuteImg from "../images/audio-mute.svg";
import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import {
audioManagerFileStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import {get} from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import {onDestroy, onMount} from "svelte";
let HTMLAudioPlayer: HTMLAudioElement;
let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null;
let volume: number = 1;
let decreaseWhileTalking: boolean = true;
onMount(() => {
unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{
HTMLAudioPlayer.pause();
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted;
HTMLAudioPlayer.play();
});
unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => {
const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking;
if (reduceVolume && !audioManager.volumeReduced) {
audioManager.volume *= 0.5;
} else if (!reduceVolume && audioManager.volumeReduced) {
audioManager.volume *= 2.0;
}
audioManager.volumeReduced = reduceVolume;
HTMLAudioPlayer.volume = audioManager.volume;
HTMLAudioPlayer.muted = audioManager.muted;
HTMLAudioPlayer.loop = audioManager.loop;
})
})
onDestroy(() => {
if (unsubscriberFileStore) {
unsubscriberFileStore();
}
if (unsubscriberVolumeStore) {
unsubscriberVolumeStore();
}
})
function onMute() {
audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted);
localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted);
}
function setVolume() {
audioManagerVolumeStore.setVolume(volume)
localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume);
}
function setDecrease() {
audioManagerVolumeStore.setDecreaseWhileTalking(decreaseWhileTalking);
}
</script>
<div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume">
<img src={$audioManagerVolumeStore.muted ? audioMuteImg : audioImg} alt="player volume" on:click={onMute}>
<input type="range" min="0" max="1" step="0.025" bind:value={volume} on:change={setVolume}>
</div>
<div class="audio-manager-reduce-conversation">
<label>
reduce in conversations
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease}>
</label>
<section class="audio-manager-file">
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
<source src={$audioManagerFileStore}>
</audio>
</section>
</div>
</div>
<style lang="scss">
div.main-audio-manager.nes-container.is-rounded {
position: relative;
top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw);
padding: 3px 3px;
margin-left: auto;
margin-right: auto;
background-color: rgb(0,0,0,0.5);
display: grid;
grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
div.audio-manager-player-volume {
display: grid;
grid-template-columns: 50px 1fr;
img {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
}
}
section.audio-manager-file {
display: none;
}
}
</style>

View file

@ -3,9 +3,12 @@
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte';
import { afterUpdate, beforeUpdate } from "svelte";
import {afterUpdate, beforeUpdate} from "svelte";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
let listDom: HTMLElement;
let chatWindowElement: HTMLElement;
let handleFormBlur: { blur():void };
let autoscroll: boolean;
beforeUpdate(() => {
@ -16,6 +19,12 @@
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
});
function onClick(event: MouseEvent) {
if (HtmlUtils.isClickedOutside(event, chatWindowElement)) {
handleFormBlur.blur();
}
}
function closeChat() {
chatVisibilityStore.set(false);
}
@ -26,10 +35,10 @@
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<svelte:window on:keydown={onKeyDown} on:click={onClick}/>
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}" bind:this={chatWindowElement}>
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
@ -40,7 +49,7 @@
</ul>
</section>
<section class="messageForm">
<ChatMessageForm></ChatMessageForm>
<ChatMessageForm bind:handleForm={handleFormBlur}></ChatMessageForm>
</section>
</aside>

View file

@ -7,13 +7,14 @@
export let message: ChatMessage;
export let line: number;
const chatStyleLink = "color: white; text-decoration: underline;";
$: author = message.author as PlayerInterface;
$: targets = message.targets || [];
$: texts = message.text || [];
function urlifyText(text: string): string {
return HtmlUtils.urlify(text)
return HtmlUtils.urlify(text, chatStyleLink);
}
function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, {

View file

@ -1,6 +1,12 @@
<script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
export const handleForm = {
blur() {
inputElement.blur();
}
}
let inputElement: HTMLElement;
let newMessageText = '';
function onFocus() {
@ -18,7 +24,7 @@
</script>
<form on:submit|preventDefault={saveMessage}>
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} bind:this={inputElement}>
<button type="submit">
<img src="/static/images/send.png" alt="Send" width="20">
</button>

View file

@ -1,12 +1,27 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte";
import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte";
import {gameManager} from "../../Phaser/Game/GameManager";
import type {Game} from "../../Phaser/Game/Game";
import { gameManager } from "../../Phaser/Game/GameManager";
import type { Game } from "../../Phaser/Game/Game";
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
export let game: Game;
let inputSendTextActive = true;
let uploadMusicActive = false;
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
let broadcastToWorld = false;
function closeConsoleGlobalMessage() {
consoleGlobalMessageManagerVisibleStore.set(false)
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeConsoleGlobalMessage();
}
}
function inputSendTextActivate() {
inputSendTextActive = true;
@ -17,28 +32,121 @@
uploadMusicActive = true;
inputSendTextActive = false;
}
function send() {
if (inputSendTextActive) {
handleSendText.sendTextMessage(broadcastToWorld);
}
if (uploadMusicActive) {
handleSendAudio.sendAudioMessage(broadcastToWorld);
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="main-console nes-container is-rounded">
<!-- <div class="console nes-container is-rounded">
<img class="btn-close" src="resources/logos/send-yellow.svg" alt="Close">
</div>-->
<div class="main-global-message">
<h2> Global Message </h2>
<div class="global-message">
<div class="menu">
<button class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
<button class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
</div>
<div class="main-input">
{#if inputSendTextActive}
<InputTextGlobalMessage game={game} gameManager={gameManager}></InputTextGlobalMessage>
{/if}
{#if uploadMusicActive}
<UploadAudioGlobalMessage game={game} gameManager={gameManager}></UploadAudioGlobalMessage>
{/if}
</div>
<div class="console-global-message">
<div class="menu-console-global-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
<button type="button" class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
</div>
<div class="main-console-global-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<div class="title-console-global-message">
<h2>Global Message</h2>
<button type="button" class="nes-btn is-error" on:click|preventDefault={closeConsoleGlobalMessage}><i class="nes-icon close is-small"></i></button>
</div>
<div class="content-console-global-message">
{#if inputSendTextActive}
<InputTextGlobalMessage game={game} gameManager={gameManager} bind:handleSending={handleSendText}/>
{/if}
{#if uploadMusicActive}
<UploadAudioGlobalMessage game={game} gameManager={gameManager} bind:handleSending={handleSendAudio}/>
{/if}
</div>
<div class="footer-console-global-message">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
<span>Broadcast to all rooms of the world</span>
</label>
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button>
</div>
</div>
</div>
</div>
<style lang="scss">
.nes-container {
padding: 0 5px;
}
div.console-global-message {
top: 20vh;
width: 50vw;
height: 50vh;
position: relative;
display: flex;
flex-direction: row;
margin-left: auto;
margin-right: auto;
padding: 0;
pointer-events: auto;
div.menu-console-global-message {
flex: 1 1 auto;
max-width: 180px;
text-align: center;
background-color: #333333;
button {
width: 136px;
margin-bottom: 10px;
}
}
div.main-console-global-message {
flex: 1 1 auto;
display: flex;
flex-direction: column;
background-color: #333333;
div.title-console-global-message {
flex: 0 0 auto;
height: 50px;
margin-bottom: 10px;
text-align: center;
color: whitesmoke;
.nes-btn {
position: absolute;
top: 0;
right: 0;
}
}
div.content-console-global-message {
flex: 1 1 auto;
max-height: calc(100% - 120px);
}
div.footer-console-global-message {
height: 50px;
margin-top: 10px;
text-align: center;
label {
margin: 0;
position: absolute;
left: 0;
max-width: 30%;
}
}
}
}
</style>

View file

@ -1,15 +1,14 @@
<script lang="ts">
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {onMount} from "svelte";
import type {Game} from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
import type {Quill} from "quill";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {onDestroy, onMount} from "svelte";
import type { Game } from "../../Phaser/Game/Game";
import type { GameManager } from "../../Phaser/Game/GameManager";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { Quill } from "quill";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
//toolbar
export const toolbarOptions = [
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
@ -35,12 +34,31 @@
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName));
const gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let quill: Quill;
let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
const MESSAGE_TYPE = AdminMessageEventTypes.admin;
export const handleSending = {
sendTextMessage(broadcastToWorld: boolean) {
if (gameScene == undefined) {
return;
}
const text = JSON.stringify(quill.getContents(0, quill.getLength()));
const textGlobalMessage: PlayGlobalMessageInterface = {
type: MESSAGE_TYPE,
content: text,
broadcastToWorld: broadcastToWorld
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(textGlobalMessage);
disableConsole();
}
}
//Quill
onMount(async () => {
@ -48,49 +66,28 @@
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(INPUT_CONSOLE_MESSAGE, {
placeholder: 'Enter your message here...',
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
});
quill.on('selection-change', function (range, oldRange) {
if (range === null && oldRange !== null) {
consoleGlobalMessageManagerFocusStore.set(false);
} else if (range !== null && oldRange === null)
consoleGlobalMessageManagerFocusStore.set(true);
});
consoleGlobalMessageManagerFocusStore.set(true);
});
onDestroy(() => {
consoleGlobalMessageManagerFocusStore.set(false);
})
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
function SendTextMessage() {
if (gameScene == undefined) {
return;
}
const text = quill.getText(0, quill.getLength());
const GlobalMessage: PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID?
message: text,
type: MESSAGE_TYPE
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
</script>
<section class="section-input-send-text">
<div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendTextMessage}>Send</button>
</div>
</section>

View file

@ -1,12 +1,11 @@
<script lang="ts">
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import type {Game} from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager";
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type { Game } from "../../Phaser/Game/Game";
import type { GameManager } from "../../Phaser/Game/GameManager";
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import uploadFile from "../images/music-file.svg";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
interface EventTargetFiles extends EventTarget {
files: Array<File>;
@ -15,38 +14,39 @@
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName));
let fileinput: HTMLInputElement;
let filename: string;
let filesize: string;
let errorfile: boolean;
let gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let fileInput: HTMLInputElement;
let fileName: string;
let fileSize: string;
let errorFile: boolean;
const AUDIO_TYPE = AdminMessageEventTypes.audio;
export const handleSending = {
async sendAudioMessage(broadcast: boolean) {
if (gameScene == undefined) {
return;
}
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorFile = true;
throw 'no file selected';
}
async function SendAudioMessage() {
if (gameScene == undefined) {
return;
}
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorfile = true;
throw 'no file selected';
}
const fd = new FormData();
fd.append('file', selectedFile);
const res = await gameScene.connection?.uploadAudio(fd);
const fd = new FormData();
fd.append('file', selectedFile);
const res = await gameScene.connection?.uploadAudio(fd);
const GlobalMessage: PlayGlobalMessageInterface = {
id: (res as { id: string }).id,
message: (res as { path: string }).path,
type: AUDIO_TYPE
const audioGlobalMessage: PlayGlobalMessageInterface = {
content: (res as { path: string }).path,
type: AUDIO_TYPE,
broadcastToWorld: broadcast
}
inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
disableConsole();
}
inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
function inputAudioFile(event: Event) {
@ -60,9 +60,9 @@
return;
}
filename = file.name;
filesize = getFileSize(file.size);
errorfile = false;
fileName = file.name;
fileSize = getFileSize(file.size);
errorFile = false;
}
function getFileSize(number: number) {
@ -85,46 +85,46 @@
<section class="section-input-send-audio">
<div class="input-send-audio">
<img src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileinput.click();}}>
{#if filename != undefined}
<label for="input-send-audio">{filename} : {filesize}</label>
{/if}
{#if errorfile}
<p class="err">No file selected. You need to upload a file before sending it.</p>
{/if}
<input type="file" id="input-send-audio" bind:this={fileinput} on:change={(e) => {inputAudioFile(e)}}>
</div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendAudioMessage}>Send</button>
</div>
<img class="nes-pointer" src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileInput.click();}}>
{#if fileName !== undefined}
<p>{fileName} : {fileSize}</p>
{/if}
{#if errorFile}
<p class="err">No file selected. You need to upload a file before sending it.</p>
{/if}
<input type="file" id="input-send-audio" bind:this={fileInput} on:change={(e) => {inputAudioFile(e)}}>
</section>
<style lang="scss">
//UploadAudioGlobalMessage
.section-input-send-audio {
margin: 10px;
}
section.section-input-send-audio {
display: flex;
flex-direction: column;
.section-input-send-audio .input-send-audio {
height: 100%;
text-align: center;
}
.section-input-send-audio #input-send-audio{
display: none;
}
img {
flex: 1 1 auto;
.section-input-send-audio div.input-send-audio label{
color: white;
}
max-height: 80%;
margin-bottom: 20px;
}
.section-input-send-audio div.input-send-audio p.err {
color: #ce372b;
text-align: center;
}
p {
flex: 1 1 auto;
.section-input-send-audio div.input-send-audio img{
height: 150px;
cursor: url('../../../style/images/cursor_pointer.png'), pointer;
margin-bottom: 5px;
color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}
}
</style>

View file

@ -0,0 +1,57 @@
<script lang="ts">
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
function onClick(callback: () => void) {
callback();
}
</script>
<div class="layout-manager-list">
{#each $layoutManagerActionStore as action}
<div class="nes-container is-rounded {action.type}" on:click={() => onClick(action.callback)}>
<p>{action.message}</p>
</div>
{/each}
</div>
<style lang="scss">
div.layout-manager-list {
pointer-events: auto;
position: absolute;
left: 0;
right: 0;
bottom: 40px;
margin: 0 auto;
padding: 0;
width: clamp(200px, 20vw, 20vw);
display: flex;
flex-direction: column;
animation: moveMessage .5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
div.nes-container.is-rounded {
padding: 8px 4px;
text-align: center;
font-family: Lato;
color: whitesmoke;
background-color: rgb(0,0,0,0.5);
&.warning {
background-color: #ff9800eb;
color: #000;
}
}
@keyframes moveMessage {
0% {bottom: 40px;}
50% {bottom: 30px;}
100% {bottom: 40px;}
}
</style>

View file

@ -0,0 +1,96 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore";
import {onMount} from "svelte";
const text = $banMessageContentStore;
const NAME_BUTTON = 'Ok';
let nbSeconds = 10;
let nameButton = '';
onMount(() => {
timeToRead()
})
function timeToRead() {
nbSeconds -= 1;
nameButton = nbSeconds.toString();
if ( nbSeconds > 0 ) {
setTimeout( () => {
timeToRead();
}, 1000);
} else {
nameButton = NAME_BUTTON;
}
}
function closeBanMessage() {
banMessageVisibleStore.set(false);
}
</script>
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2>
<div class="content-ban-message">
<p>{text}</p>
</div>
<div class="footer-ban-message">
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button>
</div>
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>
</div>
<style lang="scss">
div.main-ban-message {
display: flex;
flex-direction: column;
position: relative;
top: 15vh;
height: 70vh;
width: 60vw;
margin-left: auto;
margin-right: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
h2.title-ban-message {
flex: 1 1 auto;
max-height: 50px;
margin-bottom: 20px;
text-align: center;
img {
height: 50px;
}
}
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
}
</style>

View file

@ -0,0 +1,59 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
const content = JSON.parse($textMessageContentStore);
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true});
const NAME_BUTTON = 'Ok';
function closeTextMessage() {
textMessageVisibleStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeTextMessage();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<div class="content-text-message">
{@html converter.convert()}
</div>
<div class="footer-text-message">
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button>
</div>
</div>
<style lang="scss">
div.main-text-message {
display: flex;
flex-direction: column;
max-height: 25vh;
width: 80vw;
margin-right: auto;
margin-left: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
div.content-text-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
color: whitesmoke;
overflow: auto;
}
div.footer-text-message {
height: 50px;
text-align: center;
}
}
</style>

View file

@ -0,0 +1,37 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {userIsAdminStore} from "../../Stores/GameStore";
import {ADMIN_URL} from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL+'/pricing';
</script>
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}">
<h2>Warning!</h2>
{#if $userIsAdminStore}
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p>
{:else}
<p>This world is close to its limit!</p>
{/if}
</main>
<style lang="scss">
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
font-family: Lato;
min-width: 300px;
opacity: 0.9;
z-index: 2;
h2 {
padding: 5px;
}
}
</style>

View file

@ -0,0 +1,3 @@
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
</svg>

After

Width:  |  Height:  |  Size: 376 B

View file

@ -0,0 +1,8 @@
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<g>
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 885 B

View file

@ -1,146 +1,227 @@
import Axios from "axios";
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "./RoomConnection";
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
import {localUserStore} from "./LocalUserStore";
import {CharacterTexture, LocalUser} from "./LocalUser";
import {Room} from "./Room";
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
import { RoomConnection } from "./RoomConnection";
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker";
class ConnectionManager {
private localUser!:LocalUser;
private localUser!: LocalUser;
private connexionType?: GameConnexionTypes
private reconnectingTimeout: NodeJS.Timeout|null = null;
private _unloading:boolean = false;
private connexionType?: GameConnexionTypes;
private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false;
private authToken: string | null = null;
get unloading () {
private serviceWorker?: _ServiceWorker;
get unloading() {
return this._unloading;
}
constructor() {
window.addEventListener('beforeunload', () => {
window.addEventListener("beforeunload", () => {
this._unloading = true;
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
})
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
});
}
public loadOpenIDScreen() {
localUserStore.setAuthToken(null);
const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce();
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
}
public logout() {
localUserStore.setAuthToken(null);
window.location.reload();
}
/**
* Tries to login to the node server and return the starting map url to be loaded
*/
public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
if(connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
let room: Room | null = null;
if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(room);
} else if (connexionType === GameConnexionTypes.register) {
//@deprecated
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
(res) => res.data
);
this.localUser = new LocalUser(data.userUuid, data.textures);
this.authToken = data.authToken;
localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
const roomUrl = data.roomUrl;
const room = await Room.createRoom(new URL(window.location.protocol + '//' + window.location.host + roomUrl + window.location.search + window.location.hash));
room = await Room.createRoom(
new URL(
window.location.protocol +
"//" +
window.location.host +
roomUrl +
window.location.search +
window.location.hash
)
);
urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
let localUser = localUserStore.getLocalUser();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
this.localUser = localUser;
try {
await this.verifyToken(localUser.jwtToken);
} catch(e) {
// If the token is invalid, let's generate an anonymous one.
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
await this.anonymousLogin();
}
}else{
} else if (
connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous ||
connexionType === GameConnexionTypes.empty
) {
this.authToken = localUserStore.getAuthToken();
//todo: add here some kind of warning if authToken has expired.
if (!this.authToken) {
await this.anonymousLogin();
}
localUser = localUserStore.getLocalUser();
if(!localUser){
throw "Error to store local user data";
}
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
let roomPath: string;
if (connexionType === GameConnexionTypes.empty) {
roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
//get last room path from cache api
try {
const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi();
if (lastRoomUrl != undefined) {
roomPath = lastRoomUrl;
}
} catch (err) {
console.error(err);
}
} else {
roomPath = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + window.location.hash;
roomPath =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
window.location.search +
window.location.hash;
}
//get detail map for anonymous login and set texture in local storage
const room = await Room.createRoom(new URL(roomPath));
if(room.textures != undefined && room.textures.length > 0) {
room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) {
//check if texture was changed
if(localUser.textures.length === 0){
localUser.textures = room.textures;
}else{
if (this.localUser.textures.length === 0) {
this.localUser.textures = room.textures;
} else {
room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return;
}
localUser?.textures.push(newTexture)
this.localUser.textures.push(newTexture);
});
}
this.localUser = localUser;
localUserStore.saveUser(localUser);
localUserStore.saveUser(this.localUser);
}
return Promise.resolve(room);
}
if (room == undefined) {
return Promise.reject(new Error("Invalid URL"));
}
return Promise.reject(new Error('Invalid URL'));
}
private async verifyToken(token: string): Promise<void> {
await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
this.serviceWorker = new _ServiceWorker();
return Promise.resolve(room);
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
if (!isBenchmark) { // In benchmark, we don't have a local storage.
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, []);
this.authToken = data.authToken;
if (!isBenchmark) {
// In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
}
}
public initBenchmark(): void {
this.localUser = new LocalUser('', 'test', []);
this.localUser = new LocalUser("", []);
}
public connectToRoomSocket(roomUrl: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
public connectToRoomSocket(
roomUrl: string,
name: string,
characterLayers: string[],
position: PositionInterface,
viewport: ViewportInterface,
companion: string | null
): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(this.localUser.jwtToken, roomUrl, name, characterLayers, position, viewport, companion);
const connection = new RoomConnection(
this.authToken,
roomUrl,
name,
characterLayers,
position,
viewport,
companion
);
connection.onConnectError((error: object) => {
console.log('An error occurred while connecting to socket server. Retrying');
console.log("An error occurred while connecting to socket server. Retrying");
reject(error);
});
connection.onConnectingError((event: CloseEvent) => {
console.log('An error occurred while connecting to socket server. Retrying');
reject(new Error('An error occurred while connecting to socket server. Retrying. Code: '+event.code+', Reason: '+event.reason));
console.log("An error occurred while connecting to socket server. Retrying");
reject(
new Error(
"An error occurred while connecting to socket server. Retrying. Code: " +
event.code +
", Reason: " +
event.reason
)
);
});
connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect);
});
}).catch((err) => {
// Let's retry in 4-6 seconds
return new Promise<OnConnectInterface>((resolve, reject) => {
this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
}, 4000 + Math.floor(Math.random() * 2000) );
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
(connection) => resolve(connection)
);
}, 4000 + Math.floor(Math.random() * 2000));
});
});
}
get getConnexionType(){
get getConnexionType() {
return this.connexionType;
}
}

View file

@ -31,6 +31,7 @@ export enum EventMessage {
TELEPORT = "teleport",
USER_MESSAGE = "user-message",
START_JITSI_ROOM = "start-jitsi-room",
SET_VARIABLE = "set-variable",
}
export interface PointInterface {
@ -105,12 +106,13 @@ export interface RoomJoinedMessageInterface {
//users: MessageUserPositionInterface[],
//groups: GroupCreatedUpdatedMessageInterface[],
items: { [itemId: number]: unknown };
variables: Map<string, unknown>;
}
export interface PlayGlobalMessageInterface {
id: string;
type: string;
message: string;
content: string;
broadcastToWorld: boolean;
}
export interface OnConnectInterface {

View file

@ -1,10 +1,10 @@
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
export interface CharacterTexture {
id: number,
level: number,
url: string,
rights: string
id: number;
level: number;
url: string;
rights: string;
}
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
}
export class LocalUser {
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
}
constructor(public readonly uuid: string, public textures: CharacterTexture[]) {}
}

View file

@ -1,60 +1,67 @@
import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser";
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid";
const playerNameKey = 'playerName';
const selectedPlayerKey = 'selectedPlayer';
const customCursorPositionKey = 'customCursorPosition';
const characterLayersKey = 'characterLayers';
const companionKey = 'companion';
const gameQualityKey = 'gameQuality';
const videoQualityKey = 'videoQuality';
const audioPlayerVolumeKey = 'audioVolume';
const audioPlayerMuteKey = 'audioMute';
const helpCameraSettingsShown = 'helpCameraSettingsShown';
const fullscreenKey = 'fullscreen';
const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer";
const customCursorPositionKey = "customCursorPosition";
const characterLayersKey = "characterLayers";
const companionKey = "companion";
const gameQualityKey = "gameQuality";
const videoQualityKey = "videoQuality";
const audioPlayerVolumeKey = "audioVolume";
const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen";
const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
const nonce = "nonce";
const cacheAPIIndex = "workavdenture-cache-v1";
class LocalUserStore {
saveUser(localUser: LocalUser) {
localStorage.setItem('localUser', JSON.stringify(localUser));
localStorage.setItem("localUser", JSON.stringify(localUser));
}
getLocalUser(): LocalUser|null {
const data = localStorage.getItem('localUser');
getLocalUser(): LocalUser | null {
const data = localStorage.getItem("localUser");
return data ? JSON.parse(data) : null;
}
setName(name:string): void {
setName(name: string): void {
localStorage.setItem(playerNameKey, name);
}
getName(): string|null {
const value = localStorage.getItem(playerNameKey) || '';
getName(): string | null {
const value = localStorage.getItem(playerNameKey) || "";
return isUserNameValid(value) ? value : null;
}
setPlayerCharacterIndex(playerCharacterIndex: number): void {
localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex);
localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex);
}
getPlayerCharacterIndex(): number {
return parseInt(localStorage.getItem(selectedPlayerKey) || '');
return parseInt(localStorage.getItem(selectedPlayerKey) || "");
}
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void {
localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers}));
setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void {
localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers }));
}
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null {
getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null {
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
}
setCharacterLayers(layers: string[]): void {
localStorage.setItem(characterLayersKey, JSON.stringify(layers));
}
getCharacterLayers(): string[]|null {
getCharacterLayers(): string[] | null {
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
return areCharacterLayersValid(value) ? value : null;
}
setCompanion(companion: string|null): void {
setCompanion(companion: string | null): void {
return localStorage.setItem(companionKey, JSON.stringify(companion));
}
getCompanion(): string|null {
getCompanion(): string | null {
const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
if (typeof companion !== "string" || companion === "") {
@ -68,45 +75,95 @@ class LocalUserStore {
}
setGameQualityValue(value: number): void {
localStorage.setItem(gameQualityKey, '' + value);
localStorage.setItem(gameQualityKey, "" + value);
}
getGameQualityValue(): number {
return parseInt(localStorage.getItem(gameQualityKey) || '60');
return parseInt(localStorage.getItem(gameQualityKey) || "60");
}
setVideoQualityValue(value: number): void {
localStorage.setItem(videoQualityKey, '' + value);
localStorage.setItem(videoQualityKey, "" + value);
}
getVideoQualityValue(): number {
return parseInt(localStorage.getItem(videoQualityKey) || '20');
return parseInt(localStorage.getItem(videoQualityKey) || "20");
}
setAudioPlayerVolume(value: number): void {
localStorage.setItem(audioPlayerVolumeKey, '' + value);
localStorage.setItem(audioPlayerVolumeKey, "" + value);
}
getAudioPlayerVolume(): number {
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1');
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1");
}
setAudioPlayerMuted(value: boolean): void {
localStorage.setItem(audioPlayerMuteKey, value.toString());
}
getAudioPlayerMuted(): boolean {
return localStorage.getItem(audioPlayerMuteKey) === 'true';
return localStorage.getItem(audioPlayerMuteKey) === "true";
}
setHelpCameraSettingsShown(): void {
localStorage.setItem(helpCameraSettingsShown, '1');
localStorage.setItem(helpCameraSettingsShown, "1");
}
getHelpCameraSettingsShown(): boolean {
return localStorage.getItem(helpCameraSettingsShown) === '1';
return localStorage.getItem(helpCameraSettingsShown) === "1";
}
setFullscreen(value: boolean): void {
localStorage.setItem(fullscreenKey, value.toString());
}
getFullscreen(): boolean {
return localStorage.getItem(fullscreenKey) === 'true';
return localStorage.getItem(fullscreenKey) === "true";
}
setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
caches.open(cacheAPIIndex).then((cache) => {
const stringResponse = new Response(JSON.stringify({ roomUrl }));
cache.put(`/${lastRoomUrl}`, stringResponse);
});
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
}
getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => {
return cache.match(`/${lastRoomUrl}`).then((res) => {
return res?.json().then((data) => {
return data.roomUrl;
});
});
});
}
setAuthToken(value: string | null) {
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
}
getAuthToken(): string | null {
return localStorage.getItem(authToken);
}
generateState(): string {
const newState = uuidv4();
localStorage.setItem(state, newState);
return newState;
}
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value;
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
const oldValue = localStorage.getItem(nonce);
localStorage.removeItem(nonce);
return oldValue;
}
}

View file

@ -32,6 +32,8 @@ import {
EmotePromptMessage,
SendUserMessage,
BanUserMessage,
VariableMessage,
ErrorMessage,
} from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
@ -53,9 +55,9 @@ import {
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream";
import { worldFullWarningStream } from "./WorldFullWarningStream";
import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream";
import { warningContainerStore } from "../Stores/MenuStore";
const manualPingDelay = 20000;
@ -74,7 +76,7 @@ export class RoomConnection implements RoomConnection {
/**
*
* @param token A JWT token containing the UUID of the user
* @param token A JWT token containing the email of the user
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
*/
public constructor(
@ -164,6 +166,12 @@ export class RoomConnection implements RoomConnection {
} else if (subMessage.hasEmoteeventmessage()) {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else if (subMessage.hasErrormessage()) {
const errorMessage = subMessage.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
} else if (subMessage.hasVariablemessage()) {
event = EventMessage.SET_VARIABLE;
payload = subMessage.getVariablemessage();
} else {
throw new Error("Unexpected batch message type");
}
@ -180,6 +188,22 @@ export class RoomConnection implements RoomConnection {
items[item.getItemid()] = JSON.parse(item.getStatejson());
}
const variables = new Map<string, unknown>();
for (const variable of roomJoinedMessage.getVariableList()) {
try {
variables.set(variable.getName(), JSON.parse(variable.getValue()));
} catch (e) {
console.error(
'Unable to unserialize value received from server for variable "' +
variable.getName() +
'". Value received: "' +
variable.getValue() +
'". Error: ',
e
);
}
}
this.userId = roomJoinedMessage.getCurrentuserid();
this.tags = roomJoinedMessage.getTagList();
@ -187,11 +211,15 @@ export class RoomConnection implements RoomConnection {
connection: this,
room: {
items,
variables,
} as RoomJoinedMessageInterface,
});
} else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage();
this.closed = true;
} else if (message.hasTokenexpiredmessage()) {
connectionManager.loadOpenIDScreen();
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
} else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true;
@ -219,7 +247,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasBanusermessage()) {
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage();
warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else {
@ -536,6 +564,17 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
emitSetVariableEvent(name: string, value: unknown): void {
const variableMessage = new VariableMessage();
variableMessage.setName(name);
variableMessage.setValue(JSON.stringify(value));
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setVariablemessage(variableMessage);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void {
this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => {
callback({
@ -558,7 +597,7 @@ export class RoomConnection implements RoomConnection {
});
}
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
/* public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({
id: message.getId(),
@ -566,7 +605,7 @@ export class RoomConnection implements RoomConnection {
message: message.getMessage(),
});
});
}
}*/
public receiveStopGlobalMessage(callback: (messageId: string) => void) {
return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => {
@ -580,11 +619,11 @@ export class RoomConnection implements RoomConnection {
});
}
public emitGlobalMessage(message: PlayGlobalMessageInterface) {
public emitGlobalMessage(message: PlayGlobalMessageInterface): void {
const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id);
playGlobalMessage.setType(message.type);
playGlobalMessage.setMessage(message.message);
playGlobalMessage.setContent(message.content);
playGlobalMessage.setBroadcasttoworld(message.broadcastToWorld);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setPlayglobalmessage(playGlobalMessage);
@ -622,6 +661,29 @@ export class RoomConnection implements RoomConnection {
});
}
public onSetVariable(callback: (name: string, value: unknown) => void): void {
this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => {
const name = message.getName();
const serializedValue = message.getValue();
let value: unknown = undefined;
if (serializedValue) {
try {
value = JSON.parse(serializedValue);
} catch (e) {
console.error(
'Unable to unserialize value received from server for variable "' +
name +
'". Value received: "' +
serializedValue +
'". Error: ',
e
);
}
}
callback(name, value);
});
}
public hasTag(tag: string): boolean {
return this.tags.includes(tag);
}

View file

@ -1,14 +0,0 @@
import {Subject} from "rxjs";
class WorldFullWarningStream {
private _stream:Subject<void> = new Subject();
public stream = this._stream.asObservable();
onMessage() {
this._stream.next();
}
}
export const worldFullWarningStream = new WorldFullWarningStream();

View file

@ -1,22 +1,24 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json';
const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost';
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
const START_ROOM_URL: string =
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
const TURN_USER: string = process.env.TURN_USER || '';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
const TURN_USER: string = process.env.TURN_USER || "";
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || "";
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true";
const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true';
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) );
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export {
DEBUG_MODE,
@ -32,5 +34,5 @@ export {
TURN_USER,
TURN_PASSWORD,
JITSI_URL,
JITSI_PRIVATE_MODE
}
JITSI_PRIVATE_MODE,
};

View file

@ -0,0 +1,18 @@
export class _ServiceWorker {
constructor() {
if ("serviceWorker" in navigator) {
this.init();
}
}
init() {
navigator.serviceWorker
.register("/service-worker.js")
.then((serviceWorker) => {
console.info("Service Worker registered: ", serviceWorker);
})
.catch((error) => {
console.error("Error registering the Service Worker: ", error);
});
}
}

View file

@ -1,12 +1,12 @@
import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js';
import {waScaleManager} from "../Services/WaScaleManager";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
import VirtualJoystick from "phaser3-rex-plugins/plugins/virtualjoystick.js";
import { waScaleManager } from "../Services/WaScaleManager";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free
export const joystickBaseKey = 'joystickBase';
export const joystickBaseImg = 'resources/objects/joystickSplitted.png';
export const joystickThumbKey = 'joystickThumb';
export const joystickThumbImg = 'resources/objects/smallHandleFilledGrey.png';
export const joystickBaseKey = "joystickBase";
export const joystickBaseImg = "resources/objects/joystickSplitted.png";
export const joystickThumbKey = "joystickThumb";
export const joystickThumbImg = "resources/objects/smallHandleFilledGrey.png";
const baseSize = 50;
const thumbSize = 25;
@ -20,15 +20,27 @@ export class MobileJoystick extends VirtualJoystick {
x: -1000,
y: -1000,
radius: radius * window.devicePixelRatio,
base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
base: scene.add
.image(0, 0, joystickBaseKey)
.setDisplaySize(
(baseSize / waScaleManager.zoomModifier) * window.devicePixelRatio,
(baseSize / waScaleManager.zoomModifier) * window.devicePixelRatio
)
.setDepth(DEPTH_INGAME_TEXT_INDEX),
thumb: scene.add
.image(0, 0, joystickThumbKey)
.setDisplaySize(
(thumbSize / waScaleManager.zoomModifier) * window.devicePixelRatio,
(thumbSize / waScaleManager.zoomModifier) * window.devicePixelRatio
)
.setDepth(DEPTH_INGAME_TEXT_INDEX),
enable: true,
dir: "8dir",
});
this.visible = false;
this.enable = false;
this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
this.scene.input.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
if (!pointer.wasTouch) {
return;
}
@ -44,7 +56,7 @@ export class MobileJoystick extends VirtualJoystick {
this.enable = false;
}
});
this.scene.input.on('pointerup', () => {
this.scene.input.on("pointerup", () => {
this.visible = false;
this.enable = false;
});
@ -52,10 +64,16 @@ export class MobileJoystick extends VirtualJoystick {
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
}
private resize() {
this.base.setDisplaySize(baseSize / waScaleManager.zoomModifier * window.devicePixelRatio, baseSize / waScaleManager.zoomModifier * window.devicePixelRatio);
this.thumb.setDisplaySize(thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio, thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio);
this.setRadius(radius / waScaleManager.zoomModifier * window.devicePixelRatio);
public resize() {
this.base.setDisplaySize(this.getDisplaySizeByElement(baseSize), this.getDisplaySizeByElement(baseSize));
this.thumb.setDisplaySize(this.getDisplaySizeByElement(thumbSize), this.getDisplaySizeByElement(thumbSize));
this.setRadius(
(radius / (waScaleManager.zoomModifier * waScaleManager.uiScalingFactor)) * window.devicePixelRatio
);
}
private getDisplaySizeByElement(element: integer): integer {
return (element / (waScaleManager.zoomModifier * waScaleManager.uiScalingFactor)) * window.devicePixelRatio;
}
public destroy() {

View file

@ -1,90 +0,0 @@
const IGNORED_KEYS = new Set([
'Esc',
'Escape',
'Alt',
'Meta',
'Control',
'Ctrl',
'Space',
'Backspace'
])
export class TextInput extends Phaser.GameObjects.BitmapText {
private minUnderLineLength = 4;
private underLine: Phaser.GameObjects.Text;
private domInput = document.createElement('input');
constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string,
onChange: (text: string) => void) {
super(scene, x, y, 'main_font', text, 32);
this.setOrigin(0.5).setCenterAlign();
this.scene.add.existing(this);
const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'};
this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style);
this.underLine.setOrigin(0.5);
this.domInput.maxLength = maxLength;
this.domInput.style.opacity = "0";
if (text) {
this.domInput.value = text;
}
this.domInput.addEventListener('keydown', event => {
if (IGNORED_KEYS.has(event.key)) {
return;
}
if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) {
event.preventDefault();
}
});
this.domInput.addEventListener('input', (event) => {
if (event.defaultPrevented) {
return;
}
this.text = this.domInput.value;
this.underLine.text = this.getUnderLineBody(this.text.length);
onChange(this.text);
});
document.body.append(this.domInput);
this.focus();
}
private getUnderLineBody(textLength:number): string {
if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength;
let text = '_______';
for (let i = this.minUnderLineLength; i < textLength; i++) {
text += '__';
}
return text;
}
getText(): string {
return this.text;
}
setX(x: number): this {
super.setX(x);
this.underLine.x = x;
return this;
}
setY(y: number): this {
super.setY(y);
this.underLine.y = y+1;
return this;
}
focus() {
this.domInput.focus();
}
destroy(): void {
super.destroy();
this.domInput.remove();
}
}

View file

@ -1,35 +1,43 @@
import type {ITiledMapObject} from "../Map/ITiledMap";
import type {GameScene} from "../Game/GameScene";
import type { ITiledMapObject } from "../Map/ITiledMap";
import type { GameScene } from "../Game/GameScene";
import { type } from "os";
export class TextUtils {
public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void {
if (object.text === undefined) {
throw new Error('This object has not textual representation.');
throw new Error("This object has not textual representation.");
}
const options: {
fontStyle?: string,
fontSize?: string,
fontFamily?: string,
color?: string,
align?: string,
fontStyle?: string;
fontSize?: string;
fontFamily?: string;
color?: string;
align?: string;
wordWrap?: {
width: number,
useAdvancedWrap?: boolean
}
width: number;
useAdvancedWrap?: boolean;
};
} = {};
if (object.text.italic) {
options.fontStyle = 'italic';
options.fontStyle = "italic";
}
// Note: there is no support for "strikeout" and "underline"
let fontSize: number = 16;
if (object.text.pixelsize) {
fontSize = object.text.pixelsize;
}
options.fontSize = fontSize + 'px';
options.fontSize = fontSize + "px";
if (object.text.fontfamily) {
options.fontFamily = '"'+object.text.fontfamily+'"';
options.fontFamily = '"' + object.text.fontfamily + '"';
}
let color = '#000000';
if (object.properties !== undefined) {
for (const property of object.properties) {
if (property.name === "font-family" && typeof property.value === "string") {
options.fontFamily = property.value;
}
}
}
let color = "#000000";
if (object.text.color !== undefined) {
color = object.text.color;
}
@ -38,7 +46,7 @@ export class TextUtils {
options.wordWrap = {
width: object.width,
//useAdvancedWrap: true
}
};
}
if (object.text.halign !== undefined) {
options.align = object.text.halign;

View file

@ -1,14 +0,0 @@
export const warningContainerKey = 'warningContainer';
export const warningContainerHtml = 'resources/html/warningContainer.html';
export class WarningContainer extends Phaser.GameObjects.DOMElement {
constructor(scene: Phaser.Scene) {
super(scene, 100, 0);
this.setOrigin(0, 0);
this.createFromCache(warningContainerKey);
this.scene.add.existing(this);
}
}

View file

@ -107,7 +107,7 @@ export const createLoadingPromise = (
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
const errorCallback = (file: { src: string }) => {
if (file.src !== playerResourceDescriptor.img) return;
console.error("failed loading player ressource: ", playerResourceDescriptor);
console.error("failed loading player resource: ", playerResourceDescriptor);
rej(playerResourceDescriptor);
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
loadPlugin.off("loaderror", errorCallback);

View file

@ -1,5 +1,5 @@
import type {PointInterface} from "../../Connexion/ConnexionModels";
import type {PlayerInterface} from "./PlayerInterface";
import type { PointInterface } from "../../Connexion/ConnexionModels";
import type { PlayerInterface } from "./PlayerInterface";
export interface AddPlayerInterface extends PlayerInterface {
position: PointInterface;

View file

@ -0,0 +1,198 @@
import type { GameScene } from "./GameScene";
import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { CreateEmbeddedWebsiteEvent, ModifyEmbeddedWebsiteEvent } from "../../Api/Events/EmbeddedWebsiteEvent";
import DOMElement = Phaser.GameObjects.DOMElement;
type EmbeddedWebsite = CreateEmbeddedWebsiteEvent & { iframe: HTMLIFrameElement; phaserObject: DOMElement };
export class EmbeddedWebsiteManager {
private readonly embeddedWebsites = new Map<string, EmbeddedWebsite>();
private readonly subscription: Subscription;
constructor(private gameScene: GameScene) {
iframeListener.registerAnswerer("getEmbeddedWebsite", (name: string) => {
const website = this.embeddedWebsites.get(name);
if (website === undefined) {
throw new Error('Cannot find embedded website with name "' + name + '"');
}
const rect = website.iframe.getBoundingClientRect();
return {
url: website.url,
name: website.name,
visible: website.visible,
allowApi: website.allowApi,
allow: website.allow,
position: {
x: website.phaserObject.x,
y: website.phaserObject.y,
width: rect["width"],
height: rect["height"],
},
};
});
iframeListener.registerAnswerer("deleteEmbeddedWebsite", (name: string) => {
const website = this.embeddedWebsites.get(name);
if (!website) {
throw new Error('Could not find website to delete with the name "' + name + '" in your map');
}
website.iframe.remove();
website.phaserObject.destroy();
this.embeddedWebsites.delete(name);
});
iframeListener.registerAnswerer(
"createEmbeddedWebsite",
(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent) => {
if (this.embeddedWebsites.has(createEmbeddedWebsiteEvent.name)) {
throw new Error('An embedded website with the name "' + name + '" already exists in your map');
}
this.createEmbeddedWebsite(
createEmbeddedWebsiteEvent.name,
createEmbeddedWebsiteEvent.url,
createEmbeddedWebsiteEvent.position.x,
createEmbeddedWebsiteEvent.position.y,
createEmbeddedWebsiteEvent.position.width,
createEmbeddedWebsiteEvent.position.height,
createEmbeddedWebsiteEvent.visible ?? true,
createEmbeddedWebsiteEvent.allowApi ?? false,
createEmbeddedWebsiteEvent.allow ?? ""
);
}
);
this.subscription = iframeListener.modifyEmbeddedWebsiteStream.subscribe(
(embeddedWebsiteEvent: ModifyEmbeddedWebsiteEvent) => {
const website = this.embeddedWebsites.get(embeddedWebsiteEvent.name);
if (!website) {
throw new Error(
'Could not find website with the name "' + embeddedWebsiteEvent.name + '" in your map'
);
}
gameScene.markDirty();
if (embeddedWebsiteEvent.url !== undefined) {
website.url = embeddedWebsiteEvent.url;
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
website.iframe.src = absoluteUrl;
}
if (embeddedWebsiteEvent.visible !== undefined) {
website.visible = embeddedWebsiteEvent.visible;
website.phaserObject.visible = embeddedWebsiteEvent.visible;
}
if (embeddedWebsiteEvent.allowApi !== undefined) {
website.allowApi = embeddedWebsiteEvent.allowApi;
if (embeddedWebsiteEvent.allowApi) {
iframeListener.registerIframe(website.iframe);
} else {
iframeListener.unregisterIframe(website.iframe);
}
}
if (embeddedWebsiteEvent.allow !== undefined) {
website.allow = embeddedWebsiteEvent.allow;
website.iframe.allow = embeddedWebsiteEvent.allow;
}
if (embeddedWebsiteEvent?.x !== undefined) {
website.phaserObject.x = embeddedWebsiteEvent.x;
}
if (embeddedWebsiteEvent?.y !== undefined) {
website.phaserObject.y = embeddedWebsiteEvent.y;
}
if (embeddedWebsiteEvent?.width !== undefined) {
website.iframe.style.width = embeddedWebsiteEvent.width + "px";
}
if (embeddedWebsiteEvent?.height !== undefined) {
website.iframe.style.height = embeddedWebsiteEvent.height + "px";
}
}
);
}
public createEmbeddedWebsite(
name: string,
url: string,
x: number,
y: number,
width: number,
height: number,
visible: boolean,
allowApi: boolean,
allow: string
): void {
if (this.embeddedWebsites.has(name)) {
throw new Error('An embedded website with the name "' + name + '" already exists in your map');
}
const embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent = {
name,
url,
/*x,
y,
width,
height,*/
allow,
allowApi,
visible,
position: {
x,
y,
width,
height,
},
};
const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible);
this.embeddedWebsites.set(name, embeddedWebsite);
}
private doCreateEmbeddedWebsite(
embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent,
visible: boolean
): EmbeddedWebsite {
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
const iframe = document.createElement("iframe");
iframe.src = absoluteUrl;
iframe.style.width = embeddedWebsiteEvent.position.width + "px";
iframe.style.height = embeddedWebsiteEvent.position.height + "px";
iframe.style.margin = "0";
iframe.style.padding = "0";
iframe.style.border = "none";
const embeddedWebsite = {
...embeddedWebsiteEvent,
phaserObject: this.gameScene.add
.dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe)
.setVisible(visible)
.setOrigin(0, 0),
iframe: iframe,
};
if (embeddedWebsiteEvent.allowApi) {
iframeListener.registerIframe(iframe);
}
return embeddedWebsite;
}
close(): void {
for (const [key, website] of this.embeddedWebsites) {
if (website.allowApi) {
iframeListener.unregisterIframe(website.iframe);
}
}
this.subscription.unsubscribe();
iframeListener.unregisterAnswerer("getEmbeddedWebsite");
iframeListener.unregisterAnswerer("deleteEmbeddedWebsite");
iframeListener.unregisterAnswerer("createEmbeddedWebsite");
}
}

View file

@ -1,7 +1,7 @@
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import {waScaleManager} from "../Services/WaScaleManager";
import {ResizableScene} from "../Login/ResizableScene";
import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { waScaleManager } from "../Services/WaScaleManager";
import { ResizableScene } from "../Login/ResizableScene";
const Events = Phaser.Core.Events;
@ -14,10 +14,8 @@ const Events = Phaser.Core.Events;
* It also automatically calls "onResize" on any scenes extending ResizableScene.
*/
export class Game extends Phaser.Game {
private _isDirty = false;
constructor(GameConfig: Phaser.Types.Core.GameConfig) {
super(GameConfig);
@ -27,7 +25,7 @@ export class Game extends Phaser.Game {
scene.onResize();
}
}
})
});
/*window.addEventListener('resize', (event) => {
// Let's trigger the onResize method of any active scene that is a ResizableScene
@ -39,11 +37,9 @@ export class Game extends Phaser.Game {
});*/
}
public step(time: number, delta: number)
{
public step(time: number, delta: number) {
// @ts-ignore
if (this.pendingDestroy)
{
if (this.pendingDestroy) {
// @ts-ignore
return this.runDestroy();
}
@ -100,15 +96,17 @@ export class Game extends Phaser.Game {
}
// Loop through the scenes in forward order
for (let i = 0; i < this.scene.scenes.length; i++)
{
for (let i = 0; i < this.scene.scenes.length; i++) {
const scene = this.scene.scenes[i];
const sys = scene.sys;
if (sys.settings.visible && sys.settings.status >= Phaser.Scenes.LOADING && sys.settings.status < Phaser.Scenes.SLEEPING)
{
if (
sys.settings.visible &&
sys.settings.status >= Phaser.Scenes.LOADING &&
sys.settings.status < Phaser.Scenes.SLEEPING
) {
// @ts-ignore
if(typeof scene.isDirty === 'function') {
if (typeof scene.isDirty === "function") {
// @ts-ignore
const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0;
if (isDirty) {
@ -129,4 +127,11 @@ export class Game extends Phaser.Game {
public markDirty(): void {
this._isDirty = true;
}
/**
* Return the first scene found in the game
*/
public findAnyScene(): Phaser.Scene {
return this.scene.getScenes()[0];
}
}

View file

@ -1,4 +1,4 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
@ -19,7 +19,7 @@ export class GameMap {
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = [];
@ -61,7 +61,7 @@ export class GameMap {
}
}
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
public getPropertiesForIndex(index: number): Array<ITiledMapProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
@ -151,7 +151,7 @@ export class GameMap {
return this.map;
}
private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
private getTileProperty(index: number): Array<ITiledMapProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}

View file

@ -1,5 +1,4 @@
import type { Subscription } from "rxjs";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager";
import { iframeListener } from "../../Api/IframeListener";
import { connectionManager } from "../../Connexion/ConnectionManager";
@ -21,7 +20,6 @@ import {
AUDIO_VOLUME_PROPERTY,
Box,
JITSI_MESSAGE_PROPERTIES,
layoutManager,
ON_ACTION_TRIGGER_BUTTON,
TRIGGER_JITSI_PROPERTIES,
TRIGGER_WEBSITE_PROPERTIES,
@ -34,7 +32,6 @@ 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";
@ -47,13 +44,7 @@ import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledMapObject,
ITiledTileSet,
} from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
@ -81,8 +72,6 @@ import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey }
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events;
import RenderTexture = Phaser.GameObjects.RenderTexture;
import Tilemap = Phaser.Tilemaps.Tilemap;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles";
@ -91,8 +80,20 @@ import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import {
audioManagerFileStore,
audioManagerVisibilityStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import { PropertyUtils } from "../Map/PropertyUtils";
import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { get } from "svelte/store";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -160,7 +161,6 @@ export class GameScene extends DirtyScene {
private playersPositionInterpolator = new PlayersPositionInterpolator();
public connection: RoomConnection | undefined;
private simplePeer!: SimplePeer;
private GlobalMessageManager!: GlobalMessageManager;
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
private connectionAnswerPromiseResolve!: (
value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>
@ -199,10 +199,13 @@ export class GameScene extends DirtyScene {
private popUpElements: Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
private originalMapUrl: string | undefined;
private pinchManager: PinchManager | undefined;
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time.
private emoteManager!: EmoteManager;
private preloading: boolean = true;
startPositionCalculator!: StartPositionCalculator;
private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager;
private objectsByType = new Map<string, ITiledMapObject[]>();
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({
@ -225,6 +228,9 @@ export class GameScene extends DirtyScene {
//hook preload scene
preload(): void {
//initialize frame event of scripting API
this.listenToIframeEvents();
const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures;
if (textures) {
@ -339,27 +345,27 @@ export class GameScene extends DirtyScene {
});
// Scan the object layers for objects to load and load them.
const objects = new Map<string, ITiledMapObject[]>();
this.objectsByType = new Map<string, ITiledMapObject[]>();
for (const layer of this.mapFile.layers) {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
let objectsOfType: ITiledMapObject[] | undefined;
if (!objects.has(object.type)) {
if (!this.objectsByType.has(object.type)) {
objectsOfType = new Array<ITiledMapObject>();
} else {
objectsOfType = objects.get(object.type);
objectsOfType = this.objectsByType.get(object.type);
if (objectsOfType === undefined) {
throw new Error("Unexpected object type not found");
}
}
objectsOfType.push(object);
objects.set(object.type, objectsOfType);
this.objectsByType.set(object.type, objectsOfType);
}
}
}
for (const [itemType, objectsOfType] of objects) {
for (const [itemType, objectsOfType] of this.objectsByType) {
// FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin.
let itemFactory: ItemFactoryInterface;
@ -440,7 +446,7 @@ export class GameScene extends DirtyScene {
this.characterLayers = gameManager.getCharacterLayers();
this.companion = gameManager.getCompanion();
//initalise map
//initialise map
this.Map = this.add.tilemap(this.MapUrlFile);
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
@ -459,6 +465,8 @@ export class GameScene extends DirtyScene {
//permit to set bound collision
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this);
//add layer on map
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
for (const layer of this.gameMap.flatLayers) {
@ -479,6 +487,28 @@ export class GameScene extends DirtyScene {
if (object.text) {
TextUtils.createTextFromITiledMapObject(this, object);
}
if (object.type === "website") {
// Let's load iframes in the map
const url = PropertyUtils.mustFindStringProperty(
"url",
object.properties,
'in the "' + object.name + '" object of type "website"'
);
const allowApi = PropertyUtils.findBooleanProperty("allowApi", object.properties);
// TODO: add a "allow" property to iframe
this.embeddedWebsiteManager.createEmbeddedWebsite(
object.name,
url,
object.x,
object.y,
object.width,
object.height,
object.visible,
allowApi ?? false,
""
);
}
}
}
}
@ -553,7 +583,6 @@ export class GameScene extends DirtyScene {
);
this.triggerOnMapLayerPropertyChange();
this.listenToIframeEvents();
if (!this.room.isDisconnected()) {
this.connect();
@ -608,6 +637,8 @@ export class GameScene extends DirtyScene {
playersStore.connectToRoomConnection(this.connection);
userIsAdminStore.set(this.connection.hasTag("admin"));
this.connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = {
userId: message.userId,
@ -694,19 +725,18 @@ export class GameScene extends DirtyScene {
peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(peer) {
//self.openChatIcon.setVisible(true);
audioManager.decreaseVolume();
audioManagerVolumeStore.setTalking(true);
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
//self.openChatIcon.setVisible(false);
audioManager.restoreVolume();
audioManagerVolumeStore.setTalking(false);
}
},
});
@ -718,6 +748,13 @@ export class GameScene extends DirtyScene {
this.gameMap.setPosition(event.x, event.y);
});
// Set up variables manager
this.sharedVariablesManager = new SharedVariablesManager(
this.connection,
this.gameMap,
onConnect.room.variables
);
//this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console.
@ -787,7 +824,7 @@ export class GameScene extends DirtyScene {
});
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManager.removeActionButton("openWebsite", this.userInputManager);
layoutManagerActionStore.removeAction("openWebsite");
coWebsiteManager.closeCoWebsite();
} else {
const openWebsiteFunction = () => {
@ -797,7 +834,7 @@ export class GameScene extends DirtyScene {
allProps.get("openWebsiteAllowApi") as boolean | undefined,
allProps.get("openWebsitePolicy") as string | undefined
);
layoutManager.removeActionButton("openWebsite", this.userInputManager);
layoutManagerActionStore.removeAction("openWebsite");
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
@ -806,14 +843,13 @@ export class GameScene extends DirtyScene {
if (message === undefined) {
message = "Press SPACE or touch here to open web site";
}
layoutManager.addActionButton(
"openWebsite",
message.toString(),
() => {
openWebsiteFunction();
},
this.userInputManager
);
layoutManagerActionStore.addAction({
uuid: "openWebsite",
type: "message",
message: message,
callback: () => openWebsiteFunction(),
userInputManager: this.userInputManager,
});
} else {
openWebsiteFunction();
}
@ -821,7 +857,7 @@ export class GameScene extends DirtyScene {
});
this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManager.removeActionButton("jitsiRoom", this.userInputManager);
layoutManagerActionStore.removeAction("jitsi");
this.stopJitsi();
} else {
const openJitsiRoomFunction = () => {
@ -834,7 +870,7 @@ export class GameScene extends DirtyScene {
} else {
this.startJitsi(roomName, undefined);
}
layoutManager.removeActionButton("jitsiRoom", this.userInputManager);
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
@ -843,14 +879,13 @@ export class GameScene extends DirtyScene {
if (message === undefined) {
message = "Press SPACE or touch here to enter Jitsi Meet room";
}
layoutManager.addActionButton(
"jitsiRoom",
message.toString(),
() => {
openJitsiRoomFunction();
},
this.userInputManager
);
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.userInputManager,
});
} else {
openJitsiRoomFunction();
}
@ -867,14 +902,16 @@ export class GameScene extends DirtyScene {
const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined;
const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined;
newValue === undefined
? audioManager.unloadAudio()
: audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop);
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange("playAudioLoop", (newValue, oldValue) => {
newValue === undefined
? audioManager.unloadAudio()
: audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
this.gameMap.onPropertyChange("zone", (newValue, oldValue) => {
@ -906,7 +943,7 @@ export class GameScene extends DirtyScene {
let html = `<div id="container" hidden><div class="nes-container with-title is-centered">
${escapedMessage}
</div> `;
const buttonContainer = `<div class="buttonContainer"</div>`;
const buttonContainer = '<div class="buttonContainer"</div>';
html += buttonContainer;
let id = 0;
for (const button of openPopupEvent.buttons) {
@ -1053,20 +1090,24 @@ ${escapedMessage}
})
);
this.iframeSubscriptionList.push(
iframeListener.dataLayerChangeStream.subscribe(() => {
iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() });
})
);
iframeListener.registerAnswerer("getMapData", () => {
return {
data: this.gameMap.getMap(),
};
});
iframeListener.registerAnswerer("getState", () => {
iframeListener.registerAnswerer("getState", async () => {
// The sharedVariablesManager is not instantiated before the connection is established. So we need to wait
// for the connection to send back the answer.
await this.connectionAnswerPromise;
return {
mapUrl: this.MapUrlFile,
startLayerName: this.startPositionCalculator.startLayerName,
uuid: localUserStore.getLocalUser()?.uuid,
nickname: localUserStore.getName(),
nickname: this.playerName,
roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables,
};
});
this.iframeSubscriptionList.push(
@ -1074,6 +1115,110 @@ ${escapedMessage}
for (const eventTile of eventTiles) {
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
}
this.markDirty();
})
);
iframeListener.registerAnswerer("loadTileset", (eventTileset) => {
return this.connectionAnswerPromise.then(() => {
const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/"));
//Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1
let newFirstgid = 1;
const lastTileset = this.mapFile.tilesets[this.mapFile.tilesets.length - 1];
if (lastTileset) {
//If there is at least one tileset in the tilemap then calculate the firstgid of the new tileset
newFirstgid = lastTileset.firstgid + lastTileset.tilecount;
}
return new Promise((resolve, reject) => {
this.load.on("filecomplete-json-" + eventTileset.url, () => {
let jsonTileset = this.cache.json.get(eventTileset.url);
const imageUrl = jsonTilesetDir + "/" + jsonTileset.image;
this.load.image(imageUrl, imageUrl);
this.load.on("filecomplete-image-" + imageUrl, () => {
//Add the firstgid of the tileset to the json file
jsonTileset = { ...jsonTileset, firstgid: newFirstgid };
this.mapFile.tilesets.push(jsonTileset);
this.Map.tilesets.push(
new Tileset(
jsonTileset.name,
jsonTileset.firstgid,
jsonTileset.tileWidth,
jsonTileset.tileHeight,
jsonTileset.margin,
jsonTileset.spacing,
jsonTileset.tiles
)
);
this.Terrains.push(
this.Map.addTilesetImage(
jsonTileset.name,
imageUrl,
jsonTileset.tilewidth,
jsonTileset.tileheight,
jsonTileset.margin,
jsonTileset.spacing
)
);
//destroy the tilemapayer because they are unique and we need to reuse their key and layerdData
for (const layer of this.Map.layers) {
layer.tilemapLayer.destroy(false);
}
//Create a new GameMap with the changed file
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
//Destroy the colliders of the old tilemapLayer
this.physics.add.world.colliders.destroy();
//Create new colliders with the new GameMap
this.createCollisionWithPlayer();
//Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange();
resolve(newFirstgid);
});
});
this.load.on("loaderror", () => {
console.error("Error while loading " + eventTileset.url + ".");
reject(-1);
});
this.load.json(eventTileset.url, eventTileset.url);
this.load.start();
});
});
});
iframeListener.registerAnswerer("triggerActionMessage", (message) =>
layoutManagerActionStore.addAction({
uuid: message.uuid,
type: "message",
message: message.message,
callback: () => {
layoutManagerActionStore.removeAction(message.uuid);
iframeListener.sendActionMessageTriggered(message.uuid);
},
userInputManager: this.userInputManager,
})
);
iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid);
});
this.iframeSubscriptionList.push(
iframeListener.modifyEmbeddedWebsiteStream.subscribe((embeddedWebsite) => {
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
})
);
}
@ -1143,7 +1288,7 @@ ${escapedMessage}
let targetRoom: Room;
try {
targetRoom = await Room.createRoom(roomUrl);
} catch (e: unknown) {
} catch (e /*: unknown*/) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false;
return;
@ -1184,7 +1329,7 @@ ${escapedMessage}
}
this.stopJitsi();
audioManager.unloadAudio();
audioManagerFileStore.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection?.closeConnection();
this.simplePeer?.closeAllConnections();
@ -1197,6 +1342,13 @@ ${escapedMessage}
this.chatVisibilityUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset");
iframeListener.unregisterAnswerer("getMapData");
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("triggerActionMessage");
iframeListener.unregisterAnswerer("removeActionMessage");
this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close();
mediaManager.hideGameOverlay();
@ -1236,12 +1388,12 @@ ${escapedMessage}
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
const properties: ITiledMapProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;
@ -1250,12 +1402,12 @@ ${escapedMessage}
}
private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
const properties: ITiledMapProperty[] | undefined = layer.properties;
if (!properties) {
return [];
}
return properties
.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase())
.filter((property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase())
.map((property) => property.value);
}
@ -1268,7 +1420,7 @@ ${escapedMessage}
try {
const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene);
} catch (e: unknown) {
} catch (e /*: unknown*/) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
}
}

View file

@ -0,0 +1,167 @@
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { GameMap } from "./GameMap";
import type { ITile, ITiledMapObject } from "../Map/ITiledMap";
import type { Var } from "svelte/types/compiler/interfaces";
import { init } from "svelte/internal";
interface Variable {
defaultValue: unknown;
readableBy?: string;
writableBy?: string;
}
/**
* Stores variables and provides a bridge between scripts and the pusher server.
*/
export class SharedVariablesManager {
private _variables = new Map<string, unknown>();
private variableObjects: Map<string, Variable>;
constructor(
private roomConnection: RoomConnection,
private gameMap: GameMap,
serverVariables: Map<string, unknown>
) {
// We initialize the list of variable object at room start. The objects cannot be edited later
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap);
// Let's initialize default values
for (const [name, variableObject] of this.variableObjects.entries()) {
if (variableObject.readableBy && !this.roomConnection.hasTag(variableObject.readableBy)) {
// Do not initialize default value for variables that are not readable
continue;
}
this._variables.set(name, variableObject.defaultValue);
}
// Override default values with the variables from the server:
for (const [name, value] of serverVariables) {
this._variables.set(name, value);
}
roomConnection.onSetVariable((name, value) => {
this._variables.set(name, value);
// On server change, let's notify the iframes
iframeListener.setVariable({
key: name,
value: value,
});
});
// When a variable is modified from an iFrame
iframeListener.registerAnswerer("setVariable", (event, source) => {
const key = event.key;
const object = this.variableObjects.get(key);
if (object === undefined) {
const errMsg =
'A script is trying to modify variable "' +
key +
'" but this variable is not defined in the map.' +
'There should be an object in the map whose name is "' +
key +
'" and whose type is "variable"';
console.error(errMsg);
throw new Error(errMsg);
}
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
const errMsg =
'A script is trying to modify variable "' +
key +
'" but this variable is only writable for users with tag "' +
object.writableBy +
'".';
console.error(errMsg);
throw new Error(errMsg);
}
// Let's stop any propagation of the value we set is the same as the existing value.
if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
return;
}
this._variables.set(key, event.value);
// Dispatch to the room connection.
this.roomConnection.emitSetVariableEvent(key, event.value);
// Dispatch to other iframes
iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
});
}
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
const objects = new Map<string, Variable>();
for (const layer of gameMap.getMap().layers) {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
}
return objects;
}
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = {
defaultValue: undefined,
};
if (object.properties) {
for (const property of object.properties) {
const value = property.value;
switch (property.name) {
case "default":
variable.defaultValue = value;
break;
case "writableBy":
if (typeof value !== "string") {
throw new Error(
'The writableBy property of variable "' + object.name + '" must be a string'
);
}
if (value) {
variable.writableBy = value;
}
break;
case "readableBy":
if (typeof value !== "string") {
throw new Error(
'The readableBy property of variable "' + object.name + '" must be a string'
);
}
if (value) {
variable.readableBy = value;
}
break;
}
}
}
return variable;
}
public close(): void {
iframeListener.unregisterAnswerer("setVariable");
}
get variables(): Map<string, unknown> {
return this._variables;
}
}

View file

@ -1,5 +1,5 @@
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { GameMap } from "./GameMap";
const defaultStartLayerName = "start";
@ -45,7 +45,7 @@ export class StartPositionCalculator {
/**
*
* @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
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} did not yield any start points
*/
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
if (!selectedOrDefaultLayer) {
@ -73,7 +73,7 @@ export class StartPositionCalculator {
/**
*
* @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
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} did not yield any start points
*/
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
const tiles = selectedOrDefaultLayer.data;
@ -112,12 +112,12 @@ export class StartPositionCalculator {
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
const properties: ITiledMapProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;

View file

@ -244,6 +244,7 @@ export class CustomizeScene extends AbstractCharacterScene {
update(time: number, delta: number): void {
if (this.lazyloadingAttempt) {
this.moveLayers();
this.doMoveCursorHorizontally(this.moveHorizontally);
this.lazyloadingAttempt = false;
}

View file

@ -16,7 +16,7 @@ export interface ITiledMap {
* Map orientation (orthogonal)
*/
orientation: string;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
/**
* Render order (right-down)
@ -33,7 +33,7 @@ export interface ITiledMap {
type?: string;
}
export interface ITiledMapLayerProperty {
export interface ITiledMapProperty {
name: string;
type: string;
value: string | boolean | number | undefined;
@ -51,7 +51,7 @@ export interface ITiledMapGroupLayer {
id?: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
type: "group";
visible: boolean;
@ -69,7 +69,7 @@ export interface ITiledMapTileLayer {
height: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
encoding?: string;
compression?: string;
@ -91,7 +91,7 @@ export interface ITiledMapObjectLayer {
height: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
encoding?: string;
compression?: string;
@ -117,7 +117,7 @@ export interface ITiledMapObject {
gid: number;
height: number;
name: string;
properties: { [key: string]: string };
properties?: ITiledMapProperty[];
rotation: number;
type: string;
visible: boolean;
@ -141,6 +141,7 @@ export interface ITiledMapObject {
polyline: { x: number; y: number }[];
text?: ITiledText;
template?: string;
}
export interface ITiledText {
@ -163,7 +164,7 @@ export interface ITiledTileSet {
imagewidth: number;
margin: number;
name: string;
properties: { [key: string]: string };
properties?: ITiledMapProperty[];
spacing: number;
tilecount: number;
tileheight: number;
@ -182,7 +183,7 @@ export interface ITile {
id: number;
type?: string;
properties?: Array<ITiledMapLayerProperty>;
properties?: ITiledMapProperty[];
}
export interface ITiledMapTerrain {

View file

@ -0,0 +1,53 @@
import type { ITiledMapProperty } from "./ITiledMap";
export class PropertyUtils {
public static findProperty(
name: string,
properties: ITiledMapProperty[] | undefined
): string | boolean | number | undefined {
return properties?.find((property) => property.name === name)?.value;
}
public static findBooleanProperty(
name: string,
properties: ITiledMapProperty[] | undefined,
context?: string
): boolean | undefined {
const property = PropertyUtils.findProperty(name, properties);
if (property === undefined) {
return undefined;
}
if (typeof property !== "boolean") {
throw new Error(
'Expected property "' + name + '" to be a boolean. ' + (context ? " (" + context + ")" : "")
);
}
return property;
}
public static mustFindProperty(
name: string,
properties: ITiledMapProperty[] | undefined,
context?: string
): string | boolean | number {
const property = PropertyUtils.findProperty(name, properties);
if (property === undefined) {
throw new Error('Could not find property "' + name + '"' + (context ? " (" + context + ")" : ""));
}
return property;
}
public static mustFindStringProperty(
name: string,
properties: ITiledMapProperty[] | undefined,
context?: string
): string {
const property = PropertyUtils.mustFindProperty(name, properties, context);
if (typeof property !== "string") {
throw new Error(
'Expected property "' + name + '" to be a string. ' + (context ? " (" + context + ")" : "")
);
}
return property;
}
}

View file

@ -6,8 +6,6 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager";
import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
import { menuIconVisible } from "../../Stores/MenuStore";
import { videoConstraintStore } from "../../Stores/MediaStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
@ -21,6 +19,7 @@ import { get } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore";
import { mediaManager } from "../../WebRtc/MediaManager";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
export const MenuSceneName = "MenuScene";
const gameMenuKey = "gameMenu";
@ -45,8 +44,6 @@ export class MenuScene extends Phaser.Scene {
private gameQualityValue: number;
private videoQualityValue: number;
private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription();
constructor() {
super({ key: MenuSceneName });
@ -91,7 +88,6 @@ export class MenuScene extends Phaser.Scene {
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource);
this.load.html(warningContainerKey, warningContainerHtml);
}
create() {
@ -147,7 +143,6 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.addListener("click");
this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
chatVisibilityStore.subscribe((v) => {
this.menuButton.setVisible(!v);
});
@ -194,20 +189,6 @@ export class MenuScene extends Phaser.Scene {
});
}
private showWorldCapacityWarning() {
if (!this.warningContainer) {
this.warningContainer = new WarningContainer(this);
}
if (this.warningContainerTimeout) {
clearTimeout(this.warningContainerTimeout);
}
this.warningContainerTimeout = setTimeout(() => {
this.warningContainer?.destroy();
this.warningContainer = null;
this.warningContainerTimeout = null;
}, 120000);
}
private closeSideMenu(): void {
if (!this.sideMenuOpened) return;
this.sideMenuOpened = false;
@ -363,6 +344,9 @@ export class MenuScene extends Phaser.Scene {
case "editGameSettingsButton":
this.openGameSettingsMenu();
break;
case "oidcLogin":
connectionManager.loadOpenIDScreen();
break;
case "toggleFullscreen":
this.toggleFullscreen();
break;
@ -403,6 +387,10 @@ export class MenuScene extends Phaser.Scene {
private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO.
//const sparkHost = ADMIN_URL + "/getting-started";
//The redirection must be only on workadventu.re domain
//To day the domain staging cannot be use by customer
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
}

View file

@ -1,14 +1,14 @@
import {TextField} from "../Components/TextField";
import { TextField } from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text;
import ScenePlugin = Phaser.Scenes.ScenePlugin;
import {WAError} from "./WAError";
import { WAError } from "./WAError";
export const ErrorSceneName = "ErrorScene";
enum Textures {
icon = "icon",
mainFont = "main_font"
mainFont = "main_font",
}
export class ErrorScene extends Phaser.Scene {
@ -23,25 +23,21 @@ export class ErrorScene extends Phaser.Scene {
constructor() {
super({
key: ErrorSceneName
key: ErrorSceneName,
});
}
init({title, subTitle, message}: { title?: string, subTitle?: string, message?: string }) {
this.title = title ? title : '';
this.subTitle = subTitle ? subTitle : '';
this.message = message ? message : '';
init({ title, subTitle, message }: { title?: string; subTitle?: string; message?: string }) {
this.title = title ? title : "";
this.subTitle = subTitle ? subTitle : "";
this.message = message ? message : "";
}
preload() {
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.load.spritesheet(
'cat',
'resources/characters/pipoya/Cat 01-1.png',
{frameWidth: 32, frameHeight: 32}
);
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
@ -50,15 +46,25 @@ export class ErrorScene extends Phaser.Scene {
this.titleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, this.title);
this.subTitleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, this.subTitle);
this.subTitleField = new TextField(
this,
this.game.renderer.width / 2,
this.game.renderer.height / 2 + 24,
this.subTitle
);
this.messageField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 48, this.message, {
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
fontSize: '10px'
});
this.messageField = this.add.text(
this.game.renderer.width / 2,
this.game.renderer.height / 2 + 48,
this.message,
{
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
fontSize: "10px",
}
);
this.messageField.setOrigin(0.5, 0.5);
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6);
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat", 6);
this.cat.flipY = true;
}
@ -69,38 +75,38 @@ export class ErrorScene extends Phaser.Scene {
public static showError(error: any, scene: ScenePlugin): void {
console.error(error);
if (typeof error === 'string' || error instanceof String) {
if (typeof error === "string" || error instanceof String) {
scene.start(ErrorSceneName, {
title: 'An error occurred',
subTitle: error
title: "An error occurred",
subTitle: error,
});
} else if (error instanceof WAError) {
scene.start(ErrorSceneName, {
title: error.title,
subTitle: error.subTitle,
message: error.details
message: error.details,
});
} else if (error.response) {
// Axios HTTP error
// client received an error response (5xx, 4xx)
scene.start(ErrorSceneName, {
title: 'HTTP ' + error.response.status + ' - ' + error.response.statusText,
subTitle: 'An error occurred while accessing URL:',
message: error.response.config.url
title: "HTTP " + error.response.status + " - " + error.response.statusText,
subTitle: "An error occurred while accessing URL:",
message: error.response.config.url,
});
} else if (error.request) {
// Axios HTTP error
// client never received a response, or request never left
scene.start(ErrorSceneName, {
title: 'Network error',
subTitle: error.message
title: "Network error",
subTitle: error.message,
});
} else if (error instanceof Error) {
// Error
scene.start(ErrorSceneName, {
title: 'An error occurred',
title: "An error occurred",
subTitle: error.name,
message: error.message
message: error.message,
});
} else {
throw error;
@ -114,7 +120,7 @@ export class ErrorScene extends Phaser.Scene {
scene.start(ErrorSceneName, {
title,
subTitle,
message
message,
});
}
}

View file

@ -1,11 +1,11 @@
import {TextField} from "../Components/TextField";
import { TextField } from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
export const ReconnectingSceneName = "ReconnectingScene";
enum ReconnectingTextures {
icon = "icon",
mainFont = "main_font"
mainFont = "main_font",
}
export class ReconnectingScene extends Phaser.Scene {
@ -14,35 +14,40 @@ export class ReconnectingScene extends Phaser.Scene {
constructor() {
super({
key: ReconnectingSceneName
key: ReconnectingSceneName,
});
}
preload() {
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.load.spritesheet(
'cat',
'resources/characters/pipoya/Cat 01-1.png',
{frameWidth: 32, frameHeight: 32}
);
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, ReconnectingTextures.icon);
this.logo = new Image(
this,
this.game.renderer.width - 30,
this.game.renderer.height - 20,
ReconnectingTextures.icon
);
this.add.existing(this.logo);
this.reconnectingField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "Connection lost. Reconnecting...");
this.reconnectingField = new TextField(
this,
this.game.renderer.width / 2,
this.game.renderer.height / 2,
"Connection lost. Reconnecting..."
);
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat');
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('cat', { start: 6, end: 8 }),
key: "right",
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),
frameRate: 10,
repeat: -1
repeat: -1,
});
cat.play('right');
cat.play("right");
}
}

View file

@ -1,12 +1,12 @@
import type { Direction } from "../../types";
import type {GameScene} from "../Game/GameScene";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {MobileJoystick} from "../Components/MobileJoystick";
import {enableUserInputsStore} from "../../Stores/UserInputStore";
import type { GameScene } from "../Game/GameScene";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { MobileJoystick } from "../Components/MobileJoystick";
import { enableUserInputsStore } from "../../Stores/UserInputStore";
interface UserInputManagerDatum {
keyInstance: Phaser.Input.Keyboard.Key;
event: UserInputEvent
event: UserInputEvent;
}
export enum UserInputEvent {
@ -20,10 +20,9 @@ export enum UserInputEvent {
JoystickMove,
}
//we cannot use a map structure so we have to create a replacment
//we cannot use a map structure so we have to create a replacement
export class ActiveEventList {
private eventMap : Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>();
private eventMap: Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>();
get(event: UserInputEvent): boolean {
return this.eventMap.get(event) || false;
@ -43,7 +42,7 @@ export class ActiveEventList {
export class UserInputManager {
private KeysCode!: UserInputManagerDatum[];
private Scene: GameScene;
private isInputDisabled : boolean;
private isInputDisabled: boolean;
private joystick!: MobileJoystick;
private joystickEvents = new ActiveEventList();
@ -61,8 +60,8 @@ export class UserInputManager {
}
enableUserInputsStore.subscribe((enable) => {
enable ? this.restoreControls() : this.disableControls()
})
enable ? this.restoreControls() : this.disableControls();
});
}
initVirtualJoystick() {
@ -91,39 +90,81 @@ export class UserInputManager {
});
}
initKeyBoardEvent(){
initKeyBoardEvent() {
this.KeysCode = [
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) },
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) },
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) },
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) },
{
event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false),
},
{
event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false),
},
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false) },
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false) },
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false) },
{
event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false),
},
{event: UserInputEvent.SpeedUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false) },
{
event: UserInputEvent.SpeedUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false),
},
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false) },
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false) },
{event: UserInputEvent.Shout, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false) },
{
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false),
},
{
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
},
{
event: UserInputEvent.Shout,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
];
}
clearAllListeners(){
clearAllListeners() {
this.Scene.input.keyboard.removeAllListeners();
}
//todo: should we also disable the joystick?
disableControls(){
disableControls() {
this.Scene.input.keyboard.removeAllKeys();
this.isInputDisabled = true;
}
restoreControls(){
restoreControls() {
this.initKeyBoardEvent();
this.isInputDisabled = false;
}
@ -135,27 +176,27 @@ export class UserInputManager {
this.joystickEvents.forEach((value, key) => {
if (value) {
switch (key) {
case UserInputEvent.MoveUp:
case UserInputEvent.MoveDown:
this.joystickForceAccuY += this.joystick.forceY;
if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) {
eventsMap.set(key, value);
this.joystickForceAccuY = 0;
}
break;
case UserInputEvent.MoveLeft:
case UserInputEvent.MoveRight:
this.joystickForceAccuX += this.joystick.forceX;
if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) {
eventsMap.set(key, value);
this.joystickForceAccuX = 0;
}
break;
case UserInputEvent.MoveUp:
case UserInputEvent.MoveDown:
this.joystickForceAccuY += this.joystick.forceY;
if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) {
eventsMap.set(key, value);
this.joystickForceAccuY = 0;
}
break;
case UserInputEvent.MoveLeft:
case UserInputEvent.MoveRight:
this.joystickForceAccuX += this.joystick.forceX;
if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) {
eventsMap.set(key, value);
this.joystickForceAccuX = 0;
}
break;
}
}
});
eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any());
this.KeysCode.forEach(d => {
this.KeysCode.forEach((d) => {
if (d.keyInstance.isDown) {
eventsMap.set(d.event, true);
}
@ -163,18 +204,18 @@ export class UserInputManager {
return eventsMap;
}
spaceEvent(callback : Function){
this.Scene.input.keyboard.on('keyup-SPACE', (event: Event) => {
spaceEvent(callback: Function) {
this.Scene.input.keyboard.on("keyup-SPACE", (event: Event) => {
callback();
return event;
});
}
addSpaceEventListner(callback : Function){
this.Scene.input.keyboard.addListener('keyup-SPACE', callback);
addSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.addListener("keyup-SPACE", callback);
}
removeSpaceEventListner(callback : Function){
this.Scene.input.keyboard.removeListener('keyup-SPACE', callback);
removeSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.removeListener("keyup-SPACE", callback);
}
destroy(): void {
@ -182,8 +223,11 @@ export class UserInputManager {
}
private initMouseWheel() {
this.Scene.input.on('wheel', (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
this.Scene.zoomByFactor(1 - deltaY / 53 * 0.1);
});
this.Scene.input.on(
"wheel",
(pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
this.Scene.zoomByFactor(1 - (deltaY / 53) * 0.1);
}
);
}
}

View file

@ -0,0 +1,105 @@
import { get, writable } from "svelte/store";
export interface audioManagerVolume {
muted: boolean;
volume: number;
decreaseWhileTalking: boolean;
volumeReduced: boolean;
loop: boolean;
talking: boolean;
}
function createAudioManagerVolumeStore() {
const { subscribe, update } = writable<audioManagerVolume>({
muted: false,
volume: 1,
decreaseWhileTalking: true,
volumeReduced: false,
loop: false,
talking: false,
});
return {
subscribe,
setMuted: (newMute: boolean): void => {
update((audioPlayerVolume: audioManagerVolume) => {
audioPlayerVolume.muted = newMute;
return audioPlayerVolume;
});
},
setVolume: (newVolume: number): void => {
update((audioPlayerVolume: audioManagerVolume) => {
audioPlayerVolume.volume = newVolume;
return audioPlayerVolume;
});
},
setDecreaseWhileTalking: (newDecrease: boolean): void => {
update((audioManagerVolume: audioManagerVolume) => {
audioManagerVolume.decreaseWhileTalking = newDecrease;
return audioManagerVolume;
});
},
setVolumeReduced: (newVolumeReduced: boolean): void => {
update((audioManagerVolume: audioManagerVolume) => {
audioManagerVolume.volumeReduced = newVolumeReduced;
return audioManagerVolume;
});
},
setLoop: (newLoop: boolean): void => {
update((audioManagerVolume: audioManagerVolume) => {
audioManagerVolume.loop = newLoop;
return audioManagerVolume;
});
},
setTalking: (newTalk: boolean): void => {
update((audioManagerVolume: audioManagerVolume) => {
audioManagerVolume.talking = newTalk;
return audioManagerVolume;
});
},
};
}
function createAudioManagerFileStore() {
const { subscribe, update } = writable<string>("");
return {
subscribe,
playAudio: (
url: string | number | boolean,
mapDirUrl: string,
volume: number | undefined,
loop = false
): void => {
update((file: string) => {
const audioPath = url as string;
if (audioPath.indexOf("://") > 0) {
// remote file or stream
file = audioPath;
} else {
// local file, include it relative to map directory
file = mapDirUrl + "/" + url;
}
audioManagerVolumeStore.setVolume(
volume ? Math.min(volume, get(audioManagerVolumeStore).volume) : get(audioManagerVolumeStore).volume
);
audioManagerVolumeStore.setLoop(loop);
return file;
});
},
unloadAudio: () => {
update((file: string) => {
audioManagerVolumeStore.setLoop(false);
return "";
});
},
};
}
export const audioManagerVisibilityStore = writable(false);
export const audioManagerVolumeStore = createAudioManagerVolumeStore();
export const audioManagerFileStore = createAudioManagerFileStore();

View file

@ -2,4 +2,6 @@ import { writable } from "svelte/store";
export const userMovingStore = writable(false);
export const requestVisitCardsStore = writable<string|null>(null);
export const requestVisitCardsStore = writable<string | null>(null);
export const userIsAdminStore = writable(false);

View file

@ -0,0 +1,56 @@
import { derived, writable } from "svelte/store";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
export interface LayoutManagerAction {
uuid: string;
type: "warning" | "message";
message: string | number | boolean | undefined;
callback: () => void;
userInputManager: UserInputManager | undefined;
}
function createLayoutManagerAction() {
const { subscribe, set, update } = writable<LayoutManagerAction[]>([]);
return {
subscribe,
addAction: (newAction: LayoutManagerAction): void => {
update((list: LayoutManagerAction[]) => {
let found = false;
for (const action of list) {
if (action.uuid === newAction.uuid) {
found = true;
}
}
if (!found) {
list.push(newAction);
newAction.userInputManager?.addSpaceEventListner(newAction.callback);
}
return list;
});
},
removeAction: (uuid: string): void => {
update((list: LayoutManagerAction[]) => {
const index = list.findIndex((action) => action.uuid === uuid);
if (index !== -1) {
list[index].userInputManager?.removeSpaceEventListner(list[index].callback);
list.splice(index, 1);
}
return list;
});
},
clearActions: (): void => {
set([]);
},
};
}
export const layoutManagerActionStore = createLayoutManagerAction();
export const layoutManagerVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => {
return !!$layoutManagerActionStore.length;
});

View file

@ -274,12 +274,12 @@ export const mediaStreamConstraintsStore = derived(
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
// Disable webcam for privacy reasons (the game is not visible and we were talking to no one)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
// Disable webcam for energy reasons (the user is not moving and we are talking to no one)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;

View file

@ -1,3 +1,23 @@
import { derived, writable, Writable } from "svelte/store";
import { writable } from "svelte/store";
import Timeout = NodeJS.Timeout;
export const menuIconVisible = writable(false);
let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() {
const { subscribe, set } = writable<boolean>(false);
return {
subscribe,
activateWarningContainer() {
set(true);
if (warningContainerTimeout) clearTimeout(warningContainerTimeout);
warningContainerTimeout = setTimeout(() => {
set(false);
warningContainerTimeout = null;
}, 120000);
},
};
}
export const warningContainerStore = createWarningContainerStore();

View file

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
export const banMessageVisibleStore = writable(false);
export const banMessageContentStore = writable("");

View file

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
export const textMessageVisibleStore = writable(false);
export const textMessageContentStore = writable("");

View file

@ -1,45 +1,46 @@
import type {Room} from "../Connexion/Room";
import type { Room } from "../Connexion/Room";
export enum GameConnexionTypes {
anonymous=1,
anonymous = 1,
organization,
register,
empty,
unknown,
jwt,
}
//this class is responsible with analysing and editing the game's url
class UrlManager {
//todo: use that to detect if we can find a token in localstorage
public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString();
if (url.includes('_/')) {
if (url === "/jwt") {
return GameConnexionTypes.jwt;
} else if (url.includes("_/")) {
return GameConnexionTypes.anonymous;
} else if (url.includes('@/')) {
} else if (url.includes("@/")) {
return GameConnexionTypes.organization;
} else if(url.includes('register/')) {
} else if (url.includes("register/")) {
return GameConnexionTypes.register;
} else if(url === '/') {
} else if (url === "/") {
return GameConnexionTypes.empty;
} else {
return GameConnexionTypes.unknown;
}
}
public getOrganizationToken(): string|null {
public getOrganizationToken(): string | null {
const match = /\/register\/(.+)/.exec(window.location.pathname.toString());
return match ? match [1] : null;
return match ? match[1] : null;
}
public pushRoomIdToUrl(room:Room): void {
public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id) return;
const hash = window.location.hash;
const search = room.search.toString();
history.pushState({}, 'WorkAdventure', room.id+(search?'?'+search:'')+hash);
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);
}
public getStartLayerNameFromUrl(): string|null {
public getStartLayerNameFromUrl(): string | null {
const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null;
}

View file

@ -1,188 +0,0 @@
import {HtmlUtils} from "./HtmlUtils";
import {isUndefined} from "generic-type-guard";
import {localUserStore} from "../Connexion/LocalUserStore";
enum audioStates {
closed = 0,
loading = 1,
playing = 2
}
const audioPlayerDivId = "audioplayer";
const audioPlayerCtrlId = "audioplayerctrl";
const audioPlayerVolId = "audioplayer_volume";
const audioPlayerMuteId = "audioplayer_volume_icon_playing";
const animationTime = 500;
class AudioManager {
private opened = audioStates.closed;
private audioPlayerDiv: HTMLDivElement;
private audioPlayerCtrl: HTMLDivElement;
private audioPlayerElem: HTMLAudioElement | undefined;
private audioPlayerVol: HTMLInputElement;
private audioPlayerMute: HTMLInputElement;
private volume = 1;
private muted = false;
private decreaseWhileTalking = true;
private volumeReduced = false;
constructor() {
this.audioPlayerDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(audioPlayerDivId);
this.audioPlayerCtrl = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(audioPlayerCtrlId);
this.audioPlayerVol = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(audioPlayerVolId);
this.audioPlayerMute = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(audioPlayerMuteId);
this.volume = localUserStore.getAudioPlayerVolume();
this.audioPlayerVol.value = '' + this.volume;
this.muted = localUserStore.getAudioPlayerMuted();
if (this.muted) {
this.audioPlayerMute.classList.add('muted');
}
}
public playAudio(url: string|number|boolean, mapDirUrl: string, volume: number|undefined, loop=false): void {
const audioPath = url as string;
let realAudioPath = '';
if (audioPath.indexOf('://') > 0) {
// remote file or stream
realAudioPath = audioPath;
} else {
// local file, include it relative to map directory
realAudioPath = mapDirUrl + '/' + url;
}
this.loadAudio(realAudioPath, volume);
if (loop) {
this.loop();
}
}
private close(): void {
this.audioPlayerCtrl.classList.remove('loading');
this.audioPlayerCtrl.classList.add('hidden');
this.opened = audioStates.closed;
}
private load(): void {
this.audioPlayerCtrl.classList.remove('hidden');
this.audioPlayerCtrl.classList.add('loading');
this.opened = audioStates.loading;
}
private open(): void {
this.audioPlayerCtrl.classList.remove('hidden', 'loading');
this.opened = audioStates.playing;
}
private changeVolume(talking = false): void {
if (isUndefined(this.audioPlayerElem)) {
return;
}
const reduceVolume = talking && this.decreaseWhileTalking;
if (reduceVolume && !this.volumeReduced) {
this.volume *= 0.5;
} else if (!reduceVolume && this.volumeReduced) {
this.volume *= 2.0;
}
this.volumeReduced = reduceVolume;
this.audioPlayerElem.volume = this.volume;
this.audioPlayerVol.value = '' + this.volume;
this.audioPlayerElem.muted = this.muted;
}
private setVolume(volume: number): void {
this.volume = volume;
localUserStore.setAudioPlayerVolume(volume);
}
private loadAudio(url: string, volume: number|undefined): void {
this.load();
/* Solution 1, remove whole audio player */
this.audioPlayerDiv.innerHTML = ''; // necessary, if switching from one audio context to another! (else both streams would play simultaneously)
this.audioPlayerElem = document.createElement('audio');
this.audioPlayerElem.id = 'audioplayerelem';
this.audioPlayerElem.controls = false;
this.audioPlayerElem.preload = 'none';
const srcElem = document.createElement('source');
srcElem.type = "audio/mp3";
srcElem.src = url;
this.audioPlayerElem.append(srcElem);
this.audioPlayerDiv.append(this.audioPlayerElem);
this.volume = volume ? Math.min(volume, this.volume) : this.volume;
this.changeVolume();
this.audioPlayerElem.play();
const muteElem = HtmlUtils.getElementByIdOrFail<HTMLInputElement>('audioplayer_mute');
muteElem.onclick = (ev: Event) => {
this.muted = !this.muted;
this.changeVolume();
localUserStore.setAudioPlayerMuted(this.muted);
if (this.muted) {
this.audioPlayerMute.classList.add('muted');
} else {
this.audioPlayerMute.classList.remove('muted');
}
}
this.audioPlayerVol.oninput = (ev: Event)=> {
this.setVolume(parseFloat((<HTMLInputElement>ev.currentTarget).value));
this.changeVolume();
(<HTMLInputElement>ev.currentTarget).blur();
}
const decreaseElem = HtmlUtils.getElementByIdOrFail<HTMLInputElement>('audioplayer_decrease_while_talking');
decreaseElem.oninput = (ev: Event)=> {
this.decreaseWhileTalking = (<HTMLInputElement>ev.currentTarget).checked;
this.changeVolume();
}
this.open();
}
private loop(): void {
if (this.audioPlayerElem !== undefined) {
this.audioPlayerElem.loop = true;
}
}
public unloadAudio(): void {
try {
const audioElem = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('audioplayerelem');
this.volume = audioElem.volume;
this.muted = audioElem.muted;
audioElem.pause();
audioElem.loop = false;
audioElem.src = "";
audioElem.innerHTML = "";
audioElem.load();
} catch (e) {
console.log('No audio element loaded to unload');
}
this.close();
}
public decreaseVolume(): void {
this.changeVolume(true);
}
public restoreVolume(): void {
this.changeVolume(false);
}
}
export const audioManager = new AudioManager();

View file

@ -1,7 +1,8 @@
import {HtmlUtils} from "./HtmlUtils";
import {Subject} from "rxjs";
import {iframeListener} from "../Api/IframeListener";
import {touchScreenManager} from "../Touch/TouchScreenManager";
import { HtmlUtils } from "./HtmlUtils";
import { Subject } from "rxjs";
import { iframeListener } from "../Api/IframeListener";
import { touchScreenManager } from "../Touch/TouchScreenManager";
import { waScaleManager } from "../Phaser/Services/WaScaleManager";
enum iframeStates {
closed = 1,
@ -9,13 +10,13 @@ enum iframeStates {
opened,
}
const cowebsiteDivId = 'cowebsite'; // the id of the whole container.
const cowebsiteMainDomId = 'cowebsite-main'; // the id of the parent div of the iframe.
const cowebsiteAsideDomId = 'cowebsite-aside'; // the id of the parent div of the iframe.
export const cowebsiteCloseButtonId = 'cowebsite-close';
const cowebsiteFullScreenButtonId = 'cowebsite-fullscreen';
const cowebsiteOpenFullScreenImageId = 'cowebsite-fullscreen-open';
const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close';
const cowebsiteDivId = "cowebsite"; // the id of the whole container.
const cowebsiteMainDomId = "cowebsite-main"; // the id of the parent div of the iframe.
const cowebsiteAsideDomId = "cowebsite-aside"; // the id of the parent div of the iframe.
export const cowebsiteCloseButtonId = "cowebsite-close";
const cowebsiteFullScreenButtonId = "cowebsite-fullscreen";
const cowebsiteOpenFullScreenImageId = "cowebsite-fullscreen-open";
const cowebsiteCloseFullScreenImageId = "cowebsite-fullscreen-close";
const animationTime = 500; //time used by the css transitions, in ms.
interface TouchMoveCoordinates {
@ -24,7 +25,6 @@ interface TouchMoveCoordinates {
}
class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed;
private _onResize: Subject<void> = new Subject();
@ -38,14 +38,14 @@ class CoWebsiteManager {
private resizing: boolean = false;
private cowebsiteMainDom: HTMLDivElement;
private cowebsiteAsideDom: HTMLDivElement;
private previousTouchMoveCoordinates: TouchMoveCoordinates|null = null; //only use on touchscreens to track touch movement
private previousTouchMoveCoordinates: TouchMoveCoordinates | null = null; //only use on touchscreens to track touch movement
get width(): number {
return this.cowebsiteDiv.clientWidth;
}
set width(width: number) {
this.cowebsiteDiv.style.width = width+'px';
this.cowebsiteDiv.style.width = width + "px";
}
get height(): number {
@ -53,7 +53,7 @@ class CoWebsiteManager {
}
set height(height: number) {
this.cowebsiteDiv.style.height = height+'px';
this.cowebsiteDiv.style.height = height + "px";
}
get verticalMode(): boolean {
@ -75,56 +75,55 @@ class CoWebsiteManager {
this.initResizeListeners(false);
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
buttonCloseFrame.addEventListener('click', () => {
buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur();
this.closeCoWebsite();
});
const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
buttonFullScreenFrame.addEventListener('click', () => {
buttonFullScreenFrame.addEventListener("click", () => {
buttonFullScreenFrame.blur();
this.fullscreen();
});
}
private initResizeListeners(touchMode:boolean) {
const movecallback = (event:MouseEvent|TouchEvent) => {
private initResizeListeners(touchMode: boolean) {
const movecallback = (event: MouseEvent | TouchEvent) => {
let x, y;
if (event.type === 'mousemove') {
if (event.type === "mousemove") {
x = (event as MouseEvent).movementX / this.getDevicePixelRatio();
y = (event as MouseEvent).movementY / this.getDevicePixelRatio();
} else {
const touchEvent = (event as TouchEvent).touches[0];
const last = {x: touchEvent.pageX, y: touchEvent.pageY};
const last = { x: touchEvent.pageX, y: touchEvent.pageY };
const previous = this.previousTouchMoveCoordinates as TouchMoveCoordinates;
this.previousTouchMoveCoordinates = last;
x = last.x - previous.x;
y = last.y - previous.y;
}
this.verticalMode ? this.height += y : this.width -= x;
this.fire();
}
this.cowebsiteAsideDom.addEventListener( touchMode ? 'touchstart' : 'mousedown', (event) => {
this.verticalMode ? (this.height += y) : (this.width -= x);
this.fire();
};
this.cowebsiteAsideDom.addEventListener(touchMode ? "touchstart" : "mousedown", (event) => {
this.resizing = true;
this.getIframeDom().style.display = 'none';
this.getIframeDom().style.display = "none";
if (touchMode) {
const touchEvent = (event as TouchEvent).touches[0];
this.previousTouchMoveCoordinates = {x: touchEvent.pageX, y: touchEvent.pageY};
this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY };
}
document.addEventListener(touchMode ? 'touchmove' : 'mousemove', movecallback);
document.addEventListener(touchMode ? "touchmove" : "mousemove", movecallback);
});
document.addEventListener(touchMode ? 'touchend' : 'mouseup', (event) => {
document.addEventListener(touchMode ? "touchend" : "mouseup", (event) => {
if (!this.resizing) return;
if (touchMode) {
this.previousTouchMoveCoordinates = null;
}
document.removeEventListener(touchMode ? 'touchmove' : 'mousemove', movecallback);
this.getIframeDom().style.display = 'block';
document.removeEventListener(touchMode ? "touchmove" : "mousemove", movecallback);
this.getIframeDom().style.display = "block";
this.resizing = false;
});
}
@ -132,34 +131,34 @@ class CoWebsiteManager {
private getDevicePixelRatio(): number {
//on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels
//so on chrome-based browser we need to adjust using 'devicePixelRatio'
return window.navigator.userAgent.includes('Firefox') ? 1 : window.devicePixelRatio;
return window.navigator.userAgent.includes("Firefox") ? 1 : window.devicePixelRatio;
}
private close(): void {
this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('hidden');
this.cowebsiteDiv.classList.remove("loaded"); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add("hidden");
this.opened = iframeStates.closed;
this.resetStyle();
}
private load(): void {
this.cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('loading');
this.cowebsiteDiv.classList.remove("hidden"); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add("loading");
this.opened = iframeStates.loading;
}
private open(): void {
this.cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.remove("loading", "hidden"); //edit the css class to trigger the transition
this.opened = iframeStates.opened;
this.resetStyle();
}
public resetStyle() {
this.cowebsiteDiv.style.width = '';
this.cowebsiteDiv.style.height = '';
this.cowebsiteDiv.style.width = "";
this.cowebsiteDiv.style.height = "";
}
private getIframeDom(): HTMLIFrameElement {
const iframe = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId).querySelector('iframe');
if (!iframe) throw new Error('Could not find iframe!');
const iframe = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId).querySelector("iframe");
if (!iframe) throw new Error("Could not find iframe!");
return iframe;
}
@ -167,9 +166,9 @@ class CoWebsiteManager {
this.load();
this.cowebsiteMainDom.innerHTML = ``;
const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe';
iframe.src = (new URL(url, base)).toString();
const iframe = document.createElement("iframe");
iframe.id = "cowebsite-iframe";
iframe.src = new URL(url, base).toString();
if (allowPolicy) {
iframe.allow = allowPolicy;
}
@ -183,15 +182,18 @@ class CoWebsiteManager {
const onTimeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 2000);
});
this.currentOperationPromise = this.currentOperationPromise.then(() =>Promise.race([onloadPromise, onTimeoutPromise])).then(() => {
this.open();
setTimeout(() => {
this.fire();
}, animationTime)
}).catch((err) => {
console.error('Error loadCoWebsite => ', err);
this.closeCoWebsite()
});
this.currentOperationPromise = this.currentOperationPromise
.then(() => Promise.race([onloadPromise, onTimeoutPromise]))
.then(() => {
this.open();
setTimeout(() => {
this.fire();
}, animationTime);
})
.catch((err) => {
console.error("Error loadCoWebsite => ", err);
this.closeCoWebsite();
});
}
/**
@ -200,56 +202,63 @@ class CoWebsiteManager {
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
this.load();
this.cowebsiteMainDom.innerHTML = ``;
this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteMainDom)).then(() => {
this.open();
setTimeout(() => {
this.fire();
}, animationTime);
}).catch((err) => {
console.error('Error insertCoWebsite => ', err);
this.closeCoWebsite();
});
this.currentOperationPromise = this.currentOperationPromise
.then(() => callback(this.cowebsiteMainDom))
.then(() => {
this.open();
setTimeout(() => {
this.fire();
}, animationTime);
})
.catch((err) => {
console.error("Error insertCoWebsite => ", err);
this.closeCoWebsite();
});
}
public closeCoWebsite(): Promise<void> {
this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => {
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close();
this.fire();
const iframe = this.cowebsiteDiv.querySelector('iframe');
if (iframe) {
iframeListener.unregisterIframe(iframe);
}
setTimeout(() => {
this.cowebsiteMainDom.innerHTML = ``;
resolve();
}, animationTime)
}));
this.currentOperationPromise = this.currentOperationPromise.then(
() =>
new Promise((resolve, reject) => {
if (this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close();
this.fire();
const iframe = this.cowebsiteDiv.querySelector("iframe");
if (iframe) {
iframeListener.unregisterIframe(iframe);
}
setTimeout(() => {
this.cowebsiteMainDom.innerHTML = ``;
resolve();
}, animationTime);
})
);
return this.currentOperationPromise;
}
public getGameSize(): {width: number, height: number} {
public getGameSize(): { width: number; height: number } {
if (this.opened !== iframeStates.opened) {
return {
width: window.innerWidth,
height: window.innerHeight
}
height: window.innerHeight,
};
}
if (!this.verticalMode) {
return {
width: window.innerWidth - this.width,
height: window.innerHeight
}
height: window.innerHeight,
};
} else {
return {
width: window.innerWidth,
height: window.innerHeight - this.height,
}
};
}
}
private fire(): void {
this._onResize.next();
waScaleManager.applyNewSize();
}
private fullscreen(): void {
@ -257,13 +266,13 @@ class CoWebsiteManager {
this.resetStyle();
this.fire();
//we don't trigger a resize of the phaser game since it won't be visible anyway.
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = 'inline';
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = 'none';
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = "inline";
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = "none";
} else {
this.verticalMode ? this.height = window.innerHeight : this.width = window.innerWidth;
this.verticalMode ? (this.height = window.innerHeight) : (this.width = window.innerWidth);
//we don't trigger a resize of the phaser game since it won't be visible anyway.
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = 'none';
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = 'inline';
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = "none";
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = "inline";
}
}
}

View file

@ -2,9 +2,9 @@ export class HtmlUtils {
public static getElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id);
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
return elem;
}
throw new Error("Cannot find HTML element with id '"+id+"'");
throw new Error("Cannot find HTML element with id '" + id + "'");
}
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
@ -12,7 +12,7 @@ export class HtmlUtils {
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
}
throw new Error("Cannot find HTML element with selector '"+selector+"'");
throw new Error("Cannot find HTML element with selector '" + selector + "'");
}
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
@ -21,29 +21,38 @@ export class HtmlUtils {
elem.remove();
return elem;
}
throw new Error("Cannot find HTML element with id '"+id+"'");
throw new Error("Cannot find HTML element with id '" + id + "'");
}
public static escapeHtml(html: string): string {
const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g,'<br/>'));
const p = document.createElement('p');
const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g, "<br/>"));
const p = document.createElement("p");
p.appendChild(text);
return p.innerHTML;
}
public static urlify(text: string): string {
public static urlify(text: string, style: string = ""): string {
const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => {
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
const text = document.createTextNode(url);
link.appendChild(text);
link.setAttribute("style", style);
return link.outerHTML;
});
}
public static isClickedInside(event: MouseEvent, target: HTMLElement): boolean {
return !!event.composedPath().find((et) => et === target);
}
public static isClickedOutside(event: MouseEvent, target: HTMLElement): boolean {
return !this.isClickedInside(event, target);
}
private static isHtmlElement<T extends HTMLElement>(elem: HTMLElement | null): elem is T {
return elem !== null;
}

View file

@ -1,6 +1,3 @@
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import { HtmlUtils } from "./HtmlUtils";
export enum LayoutMode {
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
Presentation = "Presentation",
@ -27,85 +24,3 @@ export const AUDIO_VOLUME_PROPERTY = "audioVolume";
export const AUDIO_LOOP_PROPERTY = "audioLoop";
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
class LayoutManager {
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
//delete previous element
this.removeActionButton(id, userInputManager);
//create div and text html component
const p = document.createElement("p");
p.classList.add("action-body");
p.innerText = text;
const div = document.createElement("div");
div.classList.add("action");
div.id = id;
div.appendChild(p);
this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div);
//add trigger action
div.onpointerdown = () => callBack();
this.actionButtonTrigger.set(id, callBack);
userInputManager.addSpaceEventListner(callBack);
}
public removeActionButton(id: string, userInputManager?: UserInputManager) {
//delete previous element
const previousDiv = this.actionButtonInformation.get(id);
if (previousDiv) {
previousDiv.remove();
this.actionButtonInformation.delete(id);
}
const previousEventCallback = this.actionButtonTrigger.get(id);
if (previousEventCallback && userInputManager) {
userInputManager.removeSpaceEventListner(previousEventCallback);
}
}
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
//delete previous element
for (const [key, value] of this.actionButtonInformation) {
this.removeActionButton(key, userInputManager);
}
//create div and text html component
const p = document.createElement("p");
p.classList.add("action-body");
p.innerText = text;
const div = document.createElement("div");
div.classList.add("action");
div.classList.add(id);
div.id = id;
div.appendChild(p);
this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div);
//add trigger action
if (callBack) {
div.onpointerdown = () => {
callBack();
this.removeActionButton(id, userInputManager);
};
}
//remove it after 10 sec
setTimeout(() => {
this.removeActionButton(id, userInputManager);
}, 10000);
}
}
const layoutManager = new LayoutManager();
export { layoutManager };

View file

@ -1,4 +1,3 @@
import { layoutManager } from "./LayoutManager";
import { HtmlUtils } from "./HtmlUtils";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import { localStreamStore } from "../Stores/MediaStore";
@ -10,6 +9,8 @@ export type StopScreenSharingCallback = (media: MediaStream) => void;
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import { get } from "svelte/store";
export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
@ -23,14 +24,19 @@ export class MediaManager {
localStreamStore.subscribe((result) => {
if (result.type === "error") {
console.error(result.error);
layoutManager.addInformation(
"warning",
"Camera access denied. Click here and check your browser permissions.",
() => {
layoutManagerActionStore.addAction({
uuid: "cameraAccessDenied",
type: "warning",
message: "Camera access denied. Click here and check your browser permissions.",
callback: () => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
userInputManager: this.userInputManager,
});
//remove it after 10 sec
setTimeout(() => {
layoutManagerActionStore.removeAction("cameraAccessDenied");
}, 10000);
return;
}
});
@ -38,14 +44,19 @@ export class MediaManager {
screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === "error") {
console.error(result.error);
layoutManager.addInformation(
"warning",
"Screen sharing denied. Click here and check your browser permissions.",
() => {
layoutManagerActionStore.addAction({
uuid: "screenSharingAccessDenied",
type: "warning",
message: "Screen sharing denied. Click here and check your browser permissions.",
callback: () => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
userInputManager: this.userInputManager,
});
//remove it after 10 sec
setTimeout(() => {
layoutManagerActionStore.removeAction("screenSharingAccessDenied");
}, 10000);
return;
}
});

View file

@ -295,7 +295,7 @@ export class SimplePeer {
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
peer.destroy();
//Comment this peer connexion because if we delete and try to reshare screen, the RTCPeerConnection send renegociate event. This array will be remove when user left circle discussion
//Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
/*if(!this.PeerScreenSharingConnectionArray.delete(userId)){
throw 'Couln\'t delete peer screen sharing connexion';
}*/
@ -370,14 +370,14 @@ export class SimplePeer {
console.error(
'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
);
console.info("Attempt to create new peer connexion");
console.info("Attempt to create new peer connection");
if (stream) {
this.sendLocalScreenSharingStreamToUser(data.userId, stream);
}
}
} catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
//Comment this peer connexion because if we delete and try to reshare screen, the RTCPeerConnection send renegociate event. This array will be remove when user left circle discussion
//Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
//this.PeerScreenSharingConnectionArray.delete(data.userId);
this.receiveWebrtcScreenSharingSignal(data);
}
@ -485,7 +485,7 @@ export class SimplePeer {
if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) {
PeerConnectionScreenSharing.destroy();
//Comment this peer connexion because if we delete and try to reshare screen, the RTCPeerConnection send renegociate event. This array will be remove when user left circle discussion
//Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
//this.PeerScreenSharingConnectionArray.delete(userId);
}
}

View file

@ -1,7 +1,9 @@
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
import {
IframeResponseEvent,
IframeResponseEventMap, isIframeAnswerEvent, isIframeErrorAnswerEvent,
IframeResponseEventMap,
isIframeAnswerEvent,
isIframeErrorAnswerEvent,
isIframeResponseEventWrapper,
TypedMessageEvent,
} from "./Api/Events/IframeEvent";
@ -11,12 +13,26 @@ import nav from "./Api/iframe/nav";
import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui";
import sound from "./Api/iframe/sound";
import room from "./Api/iframe/room";
import player from "./Api/iframe/player";
import room, { setMapURL, setRoomId } from "./Api/iframe/room";
import state, { initVariables } from "./Api/iframe/state";
import player, { setPlayerName, setTags, setUuid } from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
import { answerPromises, queryWorkadventure, sendToWorkadventure } from "./Api/iframe/IframeApiContribution";
// Notify WorkAdventure that we are ready to receive data
const initPromise = queryWorkadventure({
type: "getState",
data: undefined,
}).then((state) => {
setPlayerName(state.nickname);
setRoomId(state.roomId);
setMapURL(state.mapUrl);
setTags(state.tags);
setUuid(state.uuid);
initVariables(state.variables as Map<string, unknown>);
});
const wa = {
ui,
@ -26,6 +42,11 @@ const wa = {
sound,
room,
player,
state,
onInit(): Promise<void> {
return initPromise;
},
// All methods below are deprecated and should not be used anymore.
// They are kept here for backward compatibility.
@ -164,38 +185,39 @@ declare global {
window.WA = wa;
window.addEventListener(
"message", <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
if (message.source !== window.parent) {
return; // Skip message in this event listener
}
const payload = message.data;
console.debug(payload);
if (isIframeAnswerEvent(payload)) {
const queryId = payload.id;
const payloadData = payload.data;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error('In Iframe API, got an answer for a question that we have no track of.');
"message",
<T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
if (message.source !== window.parent) {
return; // Skip message in this event listener
}
resolver.resolve(payloadData);
const payload = message.data;
answerPromises.delete(queryId);
} else if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
//console.debug(payload);
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);
if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
answerPromises.delete(queryId);
} else if (isIframeResponseEventWrapper(payload)) {
const payloadData = payload.data;
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(new Error(payloadError));
answerPromises.delete(queryId);
} else if (isIframeAnswerEvent(payload)) {
const queryId = payload.id;
const payloadData = payload.data;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
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 (isIframeResponseEventWrapper(payload)) {
const payloadData = payload.data;
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined;
if (callback?.typeChecker(payloadData)) {

View file

@ -1,35 +1,34 @@
import 'phaser';
import "phaser";
import GameConfig = Phaser.Types.Core.GameConfig;
import "../style/index.scss";
import {DEBUG_MODE, isMobile} from "./Enum/EnvironmentVariable";
import {LoginScene} from "./Phaser/Login/LoginScene";
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene";
import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene";
import {CustomizeScene} from "./Phaser/Login/CustomizeScene";
import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugin.js';
import OutlinePipelinePlugin from 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js';
import {EntryScene} from "./Phaser/Login/EntryScene";
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
import {MenuScene} from "./Phaser/Menu/MenuScene";
import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener";
import { SelectCharacterMobileScene } from './Phaser/Login/SelectCharacterMobileScene';
import {HdpiManager} from "./Phaser/Services/HdpiManager";
import {waScaleManager} from "./Phaser/Services/WaScaleManager";
import {Game} from "./Phaser/Game/Game";
import App from './Components/App.svelte';
import {HtmlUtils} from "./WebRtc/HtmlUtils";
import { DEBUG_MODE, isMobile } from "./Enum/EnvironmentVariable";
import { LoginScene } from "./Phaser/Login/LoginScene";
import { ReconnectingScene } from "./Phaser/Reconnecting/ReconnectingScene";
import { SelectCharacterScene } from "./Phaser/Login/SelectCharacterScene";
import { SelectCompanionScene } from "./Phaser/Login/SelectCompanionScene";
import { EnableCameraScene } from "./Phaser/Login/EnableCameraScene";
import { CustomizeScene } from "./Phaser/Login/CustomizeScene";
import WebFontLoaderPlugin from "phaser3-rex-plugins/plugins/webfontloader-plugin.js";
import OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { EntryScene } from "./Phaser/Login/EntryScene";
import { coWebsiteManager } from "./WebRtc/CoWebsiteManager";
import { MenuScene } from "./Phaser/Menu/MenuScene";
import { localUserStore } from "./Connexion/LocalUserStore";
import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene";
import { iframeListener } from "./Api/IframeListener";
import { SelectCharacterMobileScene } from "./Phaser/Login/SelectCharacterMobileScene";
import { HdpiManager } from "./Phaser/Services/HdpiManager";
import { waScaleManager } from "./Phaser/Services/WaScaleManager";
import { Game } from "./Phaser/Game/Game";
import App from "./Components/App.svelte";
import { HtmlUtils } from "./WebRtc/HtmlUtils";
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
const {width, height} = coWebsiteManager.getGameSize();
const { width, height } = coWebsiteManager.getGameSize();
const valueGameQuality = localUserStore.getGameQualityValue();
const fps : Phaser.Types.Core.FPSConfig = {
const fps: Phaser.Types.Core.FPSConfig = {
/**
* The minimum acceptable rendering rate, in frames per second.
*/
@ -53,30 +52,30 @@ const fps : Phaser.Types.Core.FPSConfig = {
/**
* Apply delta smoothing during the game update to help avoid spikes?
*/
smoothStep: false
}
smoothStep: false,
};
// the ?phaserMode=canvas parameter can be used to force Canvas usage
const params = new URLSearchParams(document.location.search.substring(1));
const phaserMode = params.get("phaserMode");
let mode: number;
switch (phaserMode) {
case 'auto':
case "auto":
case null:
mode = Phaser.AUTO;
break;
case 'canvas':
case "canvas":
mode = Phaser.CANVAS;
break;
case 'webgl':
case "webgl":
mode = Phaser.WEBGL;
break;
default:
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
}
const hdpiManager = new HdpiManager(640*480, 196*196);
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({width, height});
const hdpiManager = new HdpiManager(640 * 480, 196 * 196);
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({ width, height });
const config: GameConfig = {
type: mode,
@ -87,9 +86,10 @@ const config: GameConfig = {
height: gameSize.height,
zoom: realSize.width / gameSize.width,
autoRound: true,
resizeInterval: 999999999999
resizeInterval: 999999999999,
},
scene: [EntryScene,
scene: [
EntryScene,
LoginScene,
isMobile() ? SelectCharacterMobileScene : SelectCharacterScene,
SelectCompanionScene,
@ -102,37 +102,39 @@ const config: GameConfig = {
//resolution: window.devicePixelRatio / 2,
fps: fps,
dom: {
createContainer: true
createContainer: true,
},
render: {
pixelArt: true,
roundPixels: true,
antialias: false
antialias: false,
},
plugins: {
global: [{
key: 'rexWebFontLoader',
plugin: WebFontLoaderPlugin,
start: true
}]
global: [
{
key: "rexWebFontLoader",
plugin: WebFontLoaderPlugin,
start: true,
},
],
},
physics: {
default: "arcade",
arcade: {
debug: DEBUG_MODE,
}
},
},
// Instruct systems with 2 GPU to choose the low power one. We don't need that extra power and we want to save battery
powerPreference: "low-power",
callbacks: {
postBoot: game => {
postBoot: (game) => {
// Install rexOutlinePipeline only if the renderer is WebGL.
const renderer = game.renderer;
if (renderer instanceof WebGLRenderer) {
game.plugins.install('rexOutlinePipeline', OutlinePipelinePlugin, true);
game.plugins.install("rexOutlinePipeline", OutlinePipelinePlugin, true);
}
}
}
},
},
};
//const game = new Phaser.Game(config);
@ -140,7 +142,7 @@ const game = new Game(config);
waScaleManager.setGame(game);
window.addEventListener('resize', function (event) {
window.addEventListener("resize", function (event) {
coWebsiteManager.resetStyle();
waScaleManager.applyNewSize();
@ -153,22 +155,10 @@ coWebsiteManager.onResize.subscribe(() => {
iframeListener.init();
const app = new App({
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
target: HtmlUtils.getElementByIdOrFail("svelte-overlay"),
props: {
game: game
game: game,
},
})
});
export default app
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/resources/service-worker.js')
.then(serviceWorker => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch(error => {
console.error("Error registering the Service Worker: ", error);
});
});
}
export default app;

View file

@ -3,4 +3,4 @@
@import "style";
@import "mobile-style.scss";
@import "fonts.scss";
@import "svelte-style.scss";
@import "inputTextGlobalMessageSvelte-Style.scss";

View file

@ -0,0 +1,31 @@
//InputTextGlobalMessage
section.section-input-send-text {
height: 100%;
.ql-toolbar{
max-height: 100px;
background: whitesmoke;
}
div.input-send-text{
height: calc(100% - 100px);
overflow: auto;
color: whitesmoke;
font-size: 1rem;
.ql-editor.ql-blank::before {
color: whitesmoke;
font-size: 1rem;
}
.ql-tooltip {
top: 40% !important;
left: 20% !important;
color: whitesmoke;
background-color: #333333;
}
}
}

View file

@ -385,6 +385,10 @@ body {
#game {
position: relative; /* Position relative is needed for the game-overlay. */
iframe {
pointer-events: all;
}
}
.audioplayer:first-child {

View file

@ -1,60 +0,0 @@
//Contains all styles not unique to a svelte component.
//ConsoleGlobalMessage
div.main-console.nes-container {
pointer-events: auto;
margin-left: auto;
margin-right: auto;
top: 20vh;
width: 50vw;
height: 50vh;
padding: 0;
background-color: #333333;
.btn-action{
margin: 10px;
text-align: center;
}
.main-global-message {
width: 100%;
max-height: 100%;
}
.main-global-message h2 {
text-align: center;
color: white;
}
div.global-message {
display: flex;
max-height: 100%;
width: 100%;
}
div.menu {
flex: auto;
}
div.menu button {
margin: 7px;
}
.main-input {
width: 95%;
}
//InputTextGlobalMessage
.section-input-send-text {
margin: 10px;
}
.section-input-send-text .input-send-text .ql-editor{
color: white;
min-height: 200px;
}
.section-input-send-text .ql-toolbar{
background: white;
}
}

Some files were not shown because too many files have changed in this diff Show more