Merge branch 'develop' of github.com:thecodingmachine/workadventure into trigger-message-refv3

This commit is contained in:
David Négrier 2021-08-03 19:01:16 +02:00
commit 4713010929
90 changed files with 2336 additions and 10674 deletions

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

@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- TRACK CODE -->
<!-- END TRACK CODE -->
<link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicons/favicon-16x16.png">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="/static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<title>WorkAdventure PWA</title>
<style>
body{
font-family: Whitney, Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
body img{
position: absolute;
top: calc( 50% - 25px);
height: 59px;
width: 307px;
left: calc( 50% - 150px);
}
body p{
position: absolute;
text-align: center;
top: calc( 50% + 50px);
left: calc( 50% - 150px);
height: 59px;
width: 307px;
font-size: 20px;
}
</style>
</head>
<body>
<img src="/static/images/logo.png" alt="WorkAdventure logo"/>
<p>Charging your workspace ...</p>
<script>
setTimeout(() => {
window.location = localStorage.getItem('lastRoomUrl');
}, 4000);
</script>
</body>
</html>

View file

@ -48,6 +48,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
});

View file

@ -128,11 +128,12 @@
"type": "image\/png"
}
],
"start_url": "/",
"start_url": "/resources/service-worker.html",
"background_color": "#000000",
"display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone",
"scope": "/",
"orientation": "portrait-primary",
"scope": "/resources/",
"lang": "en",
"theme_color": "#000000",
"shortcuts": [

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

@ -1,5 +1,4 @@
import * as tg from "generic-type-guard";
import type { GameStateEvent } from "./GameStateEvent";
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent";
@ -19,16 +18,18 @@ 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 { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
import type {
MessageReferenceEvent,
removeTriggerMessage,
triggerMessage,
TriggerMessageEvent,
} from "./ui/TriggerMessageEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "./ui/TriggerMessageEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
@ -59,6 +60,7 @@ export type IframeEventMap = {
playSound: PlaySoundEvent;
stopSound: null;
getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent;
@ -111,6 +113,10 @@ export const iframeQueryMapTypeGuards = {
query: isSetVariableEvent,
answer: tg.isUndefined,
},
loadTileset: {
query: isLoadTilesetEvent,
answer: tg.isNumber,
},
triggerMessage: {
query: isTriggerMessageEvent,
answer: tg.isUndefined,

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

@ -27,9 +27,6 @@ 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 { MapDataEvent } from "./Events/MapDataEvent";
import type { GameStateEvent } from "./Events/GameStateEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";

View file

@ -105,6 +105,14 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
}
return mapURL;
}
async loadTileset(url: string): Promise<number> {
return await queryWorkadventure({
type: "loadTileset",
data: {
url: url,
},
});
}
}
export default new WorkadventureRoomCommands();

View file

@ -27,6 +27,12 @@
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";
export let game: Game;
@ -58,6 +64,16 @@
<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} />
@ -91,4 +107,7 @@
{#if $chatVisibilityStore}
<Chat></Chat>
{/if}
{#if $warningContainerStore}
<WarningContainer></WarningContainer>
{/if}
</div>

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,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

@ -6,6 +6,7 @@ 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;
@ -13,6 +14,9 @@ class ConnectionManager {
private connexionType?: GameConnexionTypes;
private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false;
private authToken: string | null = null;
private serviceWorker?: _ServiceWorker;
get unloading() {
return this._unloading;
@ -24,23 +28,58 @@ class ConnectionManager {
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) {
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.authToken, data.textures);
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(
room = await Room.createRoom(
new URL(
window.location.protocol +
"//" +
@ -51,30 +90,17 @@ class ConnectionManager {
)
);
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 {
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) {
@ -90,44 +116,44 @@ class ConnectionManager {
}
//get detail map for anonymous login and set texture in local storage
const room = await Room.createRoom(new URL(roomPath));
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;
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, []);
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(
@ -140,7 +166,7 @@ class ConnectionManager {
): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(
this.localUser.jwtToken,
this.authToken,
roomUrl,
name,
characterLayers,
@ -148,6 +174,7 @@ class ConnectionManager {
viewport,
companion
);
connection.onConnectError((error: object) => {
console.log("An error occurred while connecting to socket server. Retrying");
reject(error);
@ -166,6 +193,9 @@ class ConnectionManager {
});
connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect);
});
}).catch((err) => {

View file

@ -110,9 +110,9 @@ export interface RoomJoinedMessageInterface {
}
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,65 @@
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";
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 +73,82 @@ 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());
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
}
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

@ -55,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;
@ -76,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(
@ -217,6 +217,9 @@ export class RoomConnection implements RoomConnection {
} 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;
@ -244,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 {
@ -594,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(),
@ -602,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) => {
@ -616,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);

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,20 @@
export class _ServiceWorker {
constructor() {
if ("serviceWorker" in navigator) {
this.init();
}
}
init() {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/resources/service-worker.js")
.then((serviceWorker) => {
console.info("Service Worker registered: ", serviceWorker);
})
.catch((error) => {
console.error("Error registering the Service Worker: ", error);
});
});
}
}

View file

@ -1,10 +1,11 @@
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;
@ -18,18 +19,25 @@ export class TextUtils {
};
} = {};
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 + '"';
}
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;
}

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,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,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";
@ -75,8 +74,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";
@ -85,10 +82,12 @@ import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "../../Api/Events/ui/TriggerMessageEvent";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "../../Api/Events/ui/TriggerMessageEvent";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -156,7 +155,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>
@ -195,7 +193,7 @@ 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;
private startPositionCalculator!: StartPositionCalculator;
@ -222,6 +220,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) {
@ -437,7 +438,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) => {
@ -550,7 +551,6 @@ export class GameScene extends DirtyScene {
);
this.triggerOnMapLayerPropertyChange();
this.listenToIframeEvents();
if (!this.room.isDisconnected()) {
this.connect();
@ -605,6 +605,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,
@ -691,7 +693,6 @@ 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;
@ -1082,8 +1083,74 @@ ${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(
"triggerMessage",
@ -1171,7 +1238,7 @@ ${escapedMessage}
let targetRoom: Room;
try {
targetRoom = await Room.createRoom(roomUrl);
} catch (e: unknown) {
} catch (e) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false;
return;
@ -1224,6 +1291,8 @@ ${escapedMessage}
this.peerStoreUnsubscribe();
this.chatVisibilityUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset");
iframeListener.unregisterAnswerer("getMapData");
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("triggerMessage");
@ -1300,7 +1369,7 @@ ${escapedMessage}
try {
const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene);
} catch (e: unknown) {
} catch (e) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
}
}

View file

@ -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;

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

@ -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,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

@ -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

@ -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

@ -31,7 +31,7 @@ export class HtmlUtils {
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) => {
@ -40,10 +40,19 @@ export class HtmlUtils {
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

@ -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

@ -162,16 +162,3 @@ const app = new App({
});
export default app;
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("/resources/service-worker.js")
.then((serviceWorker) => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch((error) => {
console.error("Error registering the Service Worker: ", error);
});
});
}

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

@ -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;
}
}

View file

@ -1,28 +1,28 @@
import "jasmine";
import {getRessourceDescriptor} from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager";
import { getRessourceDescriptor } from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager";
describe("getRessourceDescriptor()", () => {
it(", if given a valid descriptor as parameter, should return it", () => {
const desc = getRessourceDescriptor({name: 'name', img: 'url'});
expect(desc.name).toEqual('name');
expect(desc.img).toEqual('url');
const desc = getRessourceDescriptor({ name: "name", img: "url" });
expect(desc.name).toEqual("name");
expect(desc.img).toEqual("url");
});
it(", if given a string as parameter, should search trough hardcoded values", () => {
const desc = getRessourceDescriptor('male1');
expect(desc.name).toEqual('male1');
it(", if given a string as parameter, should search through hardcoded values", () => {
const desc = getRessourceDescriptor("male1");
expect(desc.name).toEqual("male1");
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
});
it(", if given a string as parameter, should search trough hardcoded values (bis)", () => {
const desc = getRessourceDescriptor('color_2');
expect(desc.name).toEqual('color_2');
it(", if given a string as parameter, should search through hardcoded values (bis)", () => {
const desc = getRessourceDescriptor("color_2");
expect(desc.name).toEqual("color_2");
expect(desc.img).toEqual("resources/customisation/character_color/character_color1.png");
});
it(", if given a descriptor without url as parameter, should search trough hardcoded values", () => {
const desc = getRessourceDescriptor({name: 'male1', img: ''});
expect(desc.name).toEqual('male1');
it(", if given a descriptor without url as parameter, should search through hardcoded values", () => {
const desc = getRessourceDescriptor({ name: "male1", img: "" });
expect(desc.name).toEqual("male1");
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
});
});
});

View file

@ -189,7 +189,7 @@ module.exports = {
DISABLE_NOTIFICATIONS: false,
PUSHER_URL: undefined,
UPLOADER_URL: null,
ADMIN_URL: null,
ADMIN_URL: undefined,
DEBUG_MODE: null,
STUN_SERVER: null,
TURN_SERVER: null,

View file

@ -291,6 +291,18 @@
dependencies:
source-map "^0.6.1"
"@types/uuid@8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
"@types/uuidv4@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/uuidv4/-/uuidv4-5.0.0.tgz#2c94e67b0c06d5adb28fb7ced1a1b5f0866ecd50"
integrity sha512-xUrhYSJnkTq9CP79cU3svoKTLPCIbMMnu9Twf/tMpHATYSHCAAeDNeb2a/29YORhk5p4atHhCTMsIBU/tvdh6A==
dependencies:
uuidv4 "*"
"@types/webpack-dev-server@^3.11.4":
version "3.11.4"
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07"
@ -3463,6 +3475,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@ -4485,6 +4502,13 @@ queue-typescript@^1.0.1:
dependencies:
linked-list-typescript "^1.0.11"
quill-delta-to-html@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/quill-delta-to-html/-/quill-delta-to-html-0.12.0.tgz#ee572cfa208390d99d7646795358ddb81eaea17c"
integrity sha512-Yy6U2e7ov+ZlrFbj5/GbqOBCRjyNu+vuphy0Pk+7668zIMTVHJglZY2JNa++1/zkSiqptPBmP/CpsDzC4Wznsw==
dependencies:
lodash.isequal "^4.5.0"
quill-delta@^3.6.2:
version "3.6.3"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
@ -5775,11 +5799,24 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuidv4@*, uuidv4@^6.2.10:
version "6.2.10"
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.10.tgz#42fc1c12b6f85ad536c2c5c1e836079d1e15003c"
integrity sha512-FMo1exd9l5UvoUPHRR6NrtJ/OJRePh0ca7IhPwBuMNuYRqjtuh8lE3WDxAUvZ4Yss5FbCOsPFjyWJf9lVTEmnw==
dependencies:
"@types/uuid" "8.3.0"
uuid "8.3.2"
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"