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

This commit is contained in:
Lurkars 2021-09-06 18:23:40 +02:00
commit b29f649822
104 changed files with 1907 additions and 1304 deletions

View file

@ -61,6 +61,10 @@ jobs:
run: yarn run lint run: yarn run lint
working-directory: "front" working-directory: "front"
- name: "Pretty"
run: yarn run pretty
working-directory: "front"
- name: "Jasmine" - name: "Jasmine"
run: yarn test run: yarn test
working-directory: "front" working-directory: "front"

View file

@ -40,7 +40,7 @@
}, },
"homepage": "https://github.com/thecodingmachine/workadventure#readme", "homepage": "https://github.com/thecodingmachine/workadventure#readme",
"dependencies": { "dependencies": {
"@workadventure/tiled-map-type-guard": "^1.0.0", "@workadventure/tiled-map-type-guard": "^1.0.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"busboy": "^0.3.1", "busboy": "^0.3.1",
"circular-json": "^0.5.9", "circular-json": "^0.5.9",

View file

@ -1,7 +1,12 @@
/** /**
* Handles variables shared between the scripting API and the server. * Handles variables shared between the scripting API and the server.
*/ */
import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist"; import {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapObjectLayer,
} from "@workadventure/tiled-map-type-guard/dist";
import { User } from "_Model/User"; import { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository"; import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient"; import { redisClient } from "./RedisClient";
@ -83,25 +88,33 @@ export class VariablesManager {
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> { private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
const objects = new Map<string, Variable>(); const objects = new Map<string, Variable>();
for (const layer of map.layers) { for (const layer of map.layers) {
if (layer.type === "objectgroup") { this.recursiveFindVariablesInLayer(layer, objects);
for (const object of (layer as ITiledMapObjectLayer).objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
continue;
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
} }
return objects; return objects;
} }
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
continue;
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
} else if (layer.type === "group") {
for (const innerLayer of layer.layers) {
this.recursiveFindVariablesInLayer(innerLayer, objects);
}
}
}
private static iTiledObjectToVariable(object: ITiledMapObject): Variable { private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = {}; const variable: Variable = {};

View file

@ -194,10 +194,10 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@workadventure/tiled-map-type-guard@^1.0.0": "@workadventure/tiled-map-type-guard@^1.0.2":
version "1.0.0" version "1.0.2"
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba" resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.2.tgz#4171550f6cd71be19791faef48360d65d698bcb0"
integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A== integrity sha512-RCtygGV5y9cb7QoyGMINBE9arM5pyXjkxvXgA5uXEv4GDbXKorhFim/rHgwbVR+eFnVF3rDgWbRnk3DIaHt+lQ==
dependencies: dependencies:
generic-type-guard "^3.4.1" generic-type-guard "^3.4.1"

View file

@ -14,7 +14,7 @@ WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subsc
WA.state.[any property]: unknown WA.state.[any property]: unknown
``` ```
These methods and properties can be used to save, load and track changes in variables related to the current room. These methods and properties can be used to save, load and track changes in [variables related to the current room](variables.md).
Variables stored in `WA.state` can be any value that is serializable in JSON. Variables stored in `WA.state` can be any value that is serializable in JSON.
@ -63,44 +63,11 @@ that you get the expected type).
For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data). For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data).
Variables storage is subject to an authorization process. Read below to learn more. Variables storage is subject to an authorization process. Read below to learn more.
### Declaring allowed keys ## Defining a variable
In order to declare allowed keys related to a room, you need to add **objects** in an "object layer" of the map. Out of the box, you cannot edit *any* variable. Variables MUST be declared in the map.
Each object will represent a variable.
<div class="row">
<div class="col">
<img src="images/object_variable.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
The name of the variable is the name of the object.
The object **type** MUST be **variable**.
You can set a default value for the object in the `default` property.
### Persisting variables state
Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
server restarts).
{.alert.alert-info}
Do not use `persist` for highly dynamic values that have a short life spawn.
### Managing access rights to variables
With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
representing a "tag". Anyone having this "tag" can read/write in the variable.
{.alert.alert-warning}
`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags
is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
Trying to set a variable to a value that is not compatible with the schema will fail.
Check the [dedicated variables page](variables.md) to learn how to declare a variable in a map.
## Tracking variables changes ## Tracking variables changes

59
docs/maps/variables.md Normal file
View file

@ -0,0 +1,59 @@
{.section-title.accent.text-primary}
# Variables
Maps can contain **variables**. Variables are piece of information that store some data. In computer science, we like
to say variables are storing the "state" of the room.
- Variables are shared amongst all players in a given room. When the value of a variable changes for one player, it changes
for everyone.
- Variables are **invisible**. There are plenty of ways they can act on the room, but by default, you don't see them.
## Declaring a variable
In order to declare allowed variables in a room, you need to add **objects** in an "object layer" of the map.
Each object will represent a variable.
<div class="row">
<div class="col">
<img src="images/object_variable.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
The name of the variable is the name of the object.
The object **type** MUST be **variable**.
You can set a default value for the object in the `default` property.
## Persisting variables state
Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
server restarts).
{.alert.alert-info}
Do not use `persist` for highly dynamic values that have a short life spawn.
## Managing access rights to variables
With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
representing a "tag". Anyone having this "tag" can read/write in the variable.
{.alert.alert-warning}
`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags
is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
In a future release, the `jsonSchema` property will contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
Trying to set a variable to a value that is not compatible with the schema will fail.
## Using variables
There are plenty of ways to use variables in WorkAdventure:
- Using the [scripting API](api-state.md), you can read, edit or track the content of variables.
- Using the [Action zones](https://workadventu.re/map-building-extra/generic-action-zones.md), you can set the value of a variable when someone is entering or leaving a zone
- By [binding variable values to properties in the map](https://workadventu.re/map-building-extra/variable-to-property-binding.md)
- By [using automatically generated configuration screens](https://workadventu.re/map-building-extra/automatic-configuration.md) to create forms to edit the value of variables
In general, variables can be used by third party libraries that you can embed in your map to add extra features.
A good example of such a library is the ["Scripting API Extra" library](https://workadventu.re/map-building-extra/about.md)

View file

View file

@ -1,10 +1,11 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isButtonClickedEvent = export const isButtonClickedEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
popupId: tg.isNumber, popupId: tg.isNumber,
buttonId: tg.isNumber, buttonId: tg.isNumber,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/ */

View file

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

View file

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

View file

@ -1,9 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isEnterLeaveEvent = export const isEnterLeaveEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
name: tg.isString, name: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/ */

View file

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

View file

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

View file

@ -1,18 +1,20 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
const isButtonDescriptor = const isButtonDescriptor = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
label: tg.isString, label: tg.isString,
className: tg.isOptional(tg.isString) className: tg.isOptional(tg.isString),
}).get(); })
.get();
export const isOpenPopupEvent = export const isOpenPopupEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
popupId: tg.isNumber, popupId: tg.isNumber,
targetObject: tg.isString, targetObject: tg.isString,
message: tg.isString, message: tg.isString,
buttons: tg.isArray(isButtonDescriptor) buttons: tg.isArray(isButtonDescriptor),
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View file

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

View file

@ -1,22 +1,23 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
const isSoundConfig = new tg.IsInterface()
const isSoundConfig = .withProperties({
new tg.IsInterface().withProperties({
volume: tg.isOptional(tg.isNumber), volume: tg.isOptional(tg.isNumber),
loop: tg.isOptional(tg.isBoolean), loop: tg.isOptional(tg.isBoolean),
mute: tg.isOptional(tg.isBoolean), mute: tg.isOptional(tg.isBoolean),
rate: tg.isOptional(tg.isNumber), rate: tg.isOptional(tg.isNumber),
detune: tg.isOptional(tg.isNumber), detune: tg.isOptional(tg.isNumber),
seek: tg.isOptional(tg.isNumber), seek: tg.isOptional(tg.isNumber),
delay: tg.isOptional(tg.isNumber) delay: tg.isOptional(tg.isNumber),
}).get(); })
.get();
export const isPlaySoundEvent = export const isPlaySoundEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
url: tg.isString, url: tg.isString,
config : tg.isOptional(isSoundConfig), config: tg.isOptional(isSoundConfig),
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import type {Popup} from "./Popup"; import type { Popup } from "./Popup";
export type ButtonClickedCallback = (popup: Popup) => void; export type ButtonClickedCallback = (popup: Popup) => void;
@ -6,13 +6,13 @@ export interface ButtonDescriptor {
/** /**
* The label of the button * The label of the button
*/ */
label: string, label: string;
/** /**
* The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled" * The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled"
*/ */
className?: "normal" | "primary" | "success" | "warning" | "error" | "disabled", className?: "normal" | "primary" | "success" | "warning" | "error" | "disabled";
/** /**
* Callback called if the button is pressed * Callback called if the button is pressed
*/ */
callback: ButtonClickedCallback, callback: ButtonClickedCallback;
} }

View file

@ -1,19 +1,18 @@
import {sendToWorkadventure} from "../IframeApiContribution"; import { sendToWorkadventure } from "../IframeApiContribution";
import type {ClosePopupEvent} from "../../Events/ClosePopupEvent"; import type { ClosePopupEvent } from "../../Events/ClosePopupEvent";
export class Popup { export class Popup {
constructor(private id: number) { constructor(private id: number) {}
}
/** /**
* Closes the popup * Closes the popup
*/ */
public close(): void { public close(): void {
sendToWorkadventure({ sendToWorkadventure({
'type': 'closePopup', type: "closePopup",
'data': { data: {
'popupId': this.id, popupId: this.id,
} as ClosePopupEvent } as ClosePopupEvent,
}); });
} }
} }

View file

@ -1,16 +1,18 @@
import type {IframeResponseEventMap} from "../../Api/Events/IframeEvent"; import type { IframeResponseEventMap } from "../../Api/Events/IframeEvent";
import type {IframeCallback} from "../../Api/iframe/IframeApiContribution"; import type { IframeCallback } from "../../Api/iframe/IframeApiContribution";
import type {IframeCallbackContribution} from "../../Api/iframe/IframeApiContribution"; import type { IframeCallbackContribution } from "../../Api/iframe/IframeApiContribution";
export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback<K> } = {} export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback<K> } = {};
export function apiCallback<T extends keyof IframeResponseEventMap>(callbackData: IframeCallbackContribution<T>): IframeCallbackContribution<keyof IframeResponseEventMap> { export function apiCallback<T extends keyof IframeResponseEventMap>(
callbackData: IframeCallbackContribution<T>
): IframeCallbackContribution<keyof IframeResponseEventMap> {
const iframeCallback = { const iframeCallback = {
typeChecker: callbackData.typeChecker, typeChecker: callbackData.typeChecker,
callback: callbackData.callback callback: callbackData.callback,
} as IframeCallback<T>; } as IframeCallback<T>;
const newCallback = { [callbackData.type]: iframeCallback }; const newCallback = { [callbackData.type]: iframeCallback };
Object.assign(registeredCallbacks, newCallback) Object.assign(registeredCallbacks, newCallback);
return callbackData as unknown as IframeCallbackContribution<keyof IframeResponseEventMap>; return callbackData as unknown as IframeCallbackContribution<keyof IframeResponseEventMap>;
} }

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import audioImg from "../images/audio.svg";
import audioMuteImg from "../images/audio-mute.svg";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore"; import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import { import {
@ -12,6 +10,8 @@
import {onDestroy, onMount} from "svelte"; import {onDestroy, onMount} from "svelte";
let HTMLAudioPlayer: HTMLAudioElement; let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement;
let audioPlayerVol: HTMLInputElement;
let unsubscriberFileStore: Unsubscriber | null = null; let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null; let unsubscriberVolumeStore: Unsubscriber | null = null;
@ -19,6 +19,10 @@
let decreaseWhileTalking: boolean = true; let decreaseWhileTalking: boolean = true;
onMount(() => { onMount(() => {
volume = localUserStore.getAudioPlayerVolume();
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
setVolume();
unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{ unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{
HTMLAudioPlayer.pause(); HTMLAudioPlayer.pause();
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop; HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
@ -52,9 +56,26 @@
function onMute() { function onMute() {
audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted); audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted);
localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted); localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted);
setVolume();
} }
function setVolume() { function setVolume() {
if (get(audioManagerVolumeStore).muted) {
audioPlayerVolumeIcon.classList.add('muted');
audioPlayerVol.value = "0";
} else {
audioPlayerVol.value = "" + volume;
audioPlayerVolumeIcon.classList.remove('muted');
if (volume < 0.3) {
audioPlayerVolumeIcon.classList.add('low');
} else if (volume < 0.7) {
audioPlayerVolumeIcon.classList.remove('low');
audioPlayerVolumeIcon.classList.add('mid');
} else {
audioPlayerVolumeIcon.classList.remove('low');
audioPlayerVolumeIcon.classList.remove('mid');
}
}
audioManagerVolumeStore.setVolume(volume) audioManagerVolumeStore.setVolume(volume)
localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume); localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume);
} }
@ -67,8 +88,28 @@
<div class="main-audio-manager nes-container is-rounded"> <div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume"> <div class="audio-manager-player-volume">
<img src={$audioManagerVolumeStore.muted ? audioMuteImg : audioImg} alt="player volume" on:click={onMute}> <span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon}
<input type="range" min="0" max="1" step="0.025" bind:value={volume} on:change={setVolume}> on:click={onMute}>
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<g id="audioplayer_volume_icon_playing_high">
<path
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
</g>
<g id="audioplayer_volume_icon_playing_mid">
<path
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
</g>
<g id="audioplayer_volume_icon_playing_low">
<path
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
</g>
</svg>
</span>
<input type="range" min="0" max="1" step="0.025" bind:value={volume} bind:this={audioPlayerVol}
on:change={setVolume}>
</div> </div>
<div class="audio-manager-reduce-conversation"> <div class="audio-manager-reduce-conversation">
<label> <label>
@ -86,34 +127,66 @@
<style lang="scss"> <style lang="scss">
div.main-audio-manager.nes-container.is-rounded { div.main-audio-manager.nes-container.is-rounded {
position: relative; position: relative;
top: 0.5rem; top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw); width: clamp(200px, 15vw, 15vw);
padding: 3px 3px; padding: 3px 3px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
background-color: rgb(0,0,0,0.5); background-color: rgb(0,0,0,0.5);
display: grid;
grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
div.audio-manager-player-volume {
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
img { div.audio-manager-player-volume {
height: 100%; display: grid;
width: calc(100% - 10px); grid-template-columns: 50px 1fr;
margin-right: 10px;
span svg {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
}
} }
}
section.audio-manager-file { section.audio-manager-file {
display: none; display: none;
} }
#audioplayer_volume_icon_playing.muted {
#audioplayer_volume_icon_playing_low {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing_mid {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing_high {
visibility: hidden;
display: none;
}
}
#audioplayer_volume_icon_playing.low #audioplayer_volume_icon_playing_high {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing.low #audioplayer_volume_icon_playing_mid {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing.mid #audioplayer_volume_icon_playing_high {
visibility: hidden;
display: none;
}
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore"; import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore"; import {isSilentStore, requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import monitorImg from "./images/monitor.svg"; import monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg"; import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg"; import cinemaImg from "./images/cinema.svg";
@ -12,6 +12,7 @@
import {layoutModeStore} from "../Stores/StreamableCollectionStore"; import {layoutModeStore} from "../Stores/StreamableCollectionStore";
import {LayoutMode} from "../WebRtc/LayoutManager"; import {LayoutMode} from "../WebRtc/LayoutManager";
import {peerStore} from "../Stores/PeerStore"; import {peerStore} from "../Stores/PeerStore";
import {onDestroy} from "svelte";
function screenSharingClick(): void { function screenSharingClick(): void {
if ($requestedScreenSharingState === true) { if ($requestedScreenSharingState === true) {
@ -44,6 +45,12 @@
$layoutModeStore = LayoutMode.Presentation; $layoutModeStore = LayoutMode.Presentation;
} }
} }
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div>
@ -55,22 +62,22 @@
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode"> <img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
{/if} {/if}
</div> </div>
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}> <div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore || isSilent} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState} {#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing"> <img src={monitorImg} alt="Start screen sharing">
{:else} {:else}
<img src={monitorCloseImg} alt="Stop screen sharing"> <img src={monitorCloseImg} alt="Stop screen sharing">
{/if} {/if}
</div> </div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}> <div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{#if $requestedCameraState} {#if $requestedCameraState && !isSilent}
<img src={cinemaImg} alt="Turn on webcam"> <img src={cinemaImg} alt="Turn on webcam">
{:else} {:else}
<img src={cinemaCloseImg} alt="Turn off webcam"> <img src={cinemaCloseImg} alt="Turn off webcam">
{/if} {/if}
</div> </div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}> <div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState} {#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone"> <img src={microphoneImg} alt="Turn on microphone">
{:else} {:else}
<img src={microphoneCloseImg} alt="Turn off microphone"> <img src={microphoneCloseImg} alt="Turn off microphone">

View file

@ -1,13 +1,17 @@
<script lang="typescript"> <script lang="typescript">
import {gameManager} from "../../Phaser/Game/GameManager"; import {gameManager} from "../../Phaser/Game/GameManager";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene"; import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../../Stores/MenuStore"; import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore"; import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene"; import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore"; import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore"; import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene"; import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
//import {connectionManager} from "../../Connexion/ConnectionManager"; import {connectionManager} from "../../Connexion/ConnectionManager";
import {PROFILE_URL} from "../../Enum/EnvironmentVariable";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {enableCameraSceneVisibilityStore} from "../../Stores/MediaStore";
function disableMenuStores(){ function disableMenuStores(){
@ -33,22 +37,50 @@
gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene());
} }
function logOut(){
disableMenuStores();
loginSceneVisibleStore.set(true);
connectionManager.logout();
}
function getProfileUrl(){
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
}
function openEnableCameraScene(){
disableMenuStores();
enableCameraSceneVisibilityStore.showEnableCameraScene();
gameManager.leaveGame(EnableCameraSceneName,new EnableCameraScene());
}
//TODO: Uncomment when login will be completely developed //TODO: Uncomment when login will be completely developed
/*function clickLogin() { /*function clickLogin() {
connectionManager.loadOpenIDScreen(); connectionManager.loadOpenIDScreen();
}*/ }*/
</script> </script>
<div class="customize-main"> <div class="customize-main">
{#if $userIsConnected}
<section>
{#if PROFILE_URL != undefined}
<iframe title="profile" src="{getProfileUrl()}"></iframe>
{/if}
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}>Log out</button>
</section>
{:else}
<section>
<a type="button" class="nes-btn" href="/login">Sing in</a>
</section>
{/if}
<section> <section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>Edit Name</button> <button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>Edit Name</button>
</section>
<section>
<button type="button" class="nes-btn is-rounded" on:click|preventDefault={openEditSkinScene}>Edit Skin</button> <button type="button" class="nes-btn is-rounded" on:click|preventDefault={openEditSkinScene}>Edit Skin</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
</section> </section>
<section> <section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button> <button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>Setup camera</button>
</section> </section>
<!-- <section> <!-- <section>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={clickLogin}>Login</button> <button type="button" class="nes-btn is-primary" on:click|preventDefault={clickLogin}>Login</button>
@ -63,6 +95,12 @@
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
iframe{
width: 100%;
height: 50vh;
border: none;
}
button { button {
height: 50px; height: 50px;
width: 250px; width: 250px;

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {localStreamStore} from "../Stores/MediaStore"; import {localStreamStore, isSilentStore} from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte"; import SoundMeterWidget from "./SoundMeterWidget.svelte";
import {onDestroy} from "svelte"; import {onDestroy} from "svelte";
import {srcObject} from "./Video/utils"; import {srcObject} from "./Video/utils";
@ -17,14 +17,25 @@
onDestroy(unsubscribe); onDestroy(unsubscribe);
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div>
<div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video}> <div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream} {#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline></video> <video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline></video>
<SoundMeterWidget stream={stream}></SoundMeterWidget> <SoundMeterWidget stream={stream}></SoundMeterWidget>
{/if} {/if}
</div> </div>
<div class="is-silent" class:hide={isSilent}>
Silent zone
</div>
</div> </div>

View file

@ -12,7 +12,9 @@
<div class="main-section"> <div class="main-section">
{#if $videoFocusStore } {#if $videoFocusStore }
<MediaBox streamable={$videoFocusStore}></MediaBox> {#key $videoFocusStore.uniqueId}
<MediaBox streamable={$videoFocusStore}></MediaBox>
{/key}
{/if} {/if}
</div> </div>
<aside class="sidebar"> <aside class="sidebar">

View file

@ -1,15 +1,15 @@
import {Subject} from "rxjs"; import { Subject } from "rxjs";
import type {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb"; import type { BanUserMessage, SendUserMessage } from "../Messages/generated/messages_pb";
export enum AdminMessageEventTypes { export enum AdminMessageEventTypes {
admin = 'message', admin = "message",
audio = 'audio', audio = "audio",
ban = 'ban', ban = "ban",
banned = 'banned', banned = "banned",
} }
interface AdminMessageEvent { interface AdminMessageEvent {
type: AdminMessageEventTypes, type: AdminMessageEventTypes;
text: string; text: string;
//todo add optional properties for other event types //todo add optional properties for other event types
} }
@ -21,14 +21,14 @@ class AdminMessagesService {
public messageStream = this._messageStream.asObservable(); public messageStream = this._messageStream.asObservable();
constructor() { constructor() {
this.messageStream.subscribe((event) => console.log('message', event)) this.messageStream.subscribe((event) => console.log("message", event));
} }
onSendusermessage(message: SendUserMessage|BanUserMessage) { onSendusermessage(message: SendUserMessage | BanUserMessage) {
this._messageStream.next({ this._messageStream.next({
type: message.getType() as unknown as AdminMessageEventTypes, type: message.getType() as unknown as AdminMessageEventTypes,
text: message.getMessage(), text: message.getMessage(),
}) });
} }
} }

View file

@ -7,6 +7,8 @@ import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser"; import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room"; import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker"; import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@ -15,6 +17,7 @@ class ConnectionManager {
private reconnectingTimeout: NodeJS.Timeout | null = null; private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false; private _unloading: boolean = false;
private authToken: string | null = null; private authToken: string | null = null;
private _currentRoom: Room | null = null;
private serviceWorker?: _ServiceWorker; private serviceWorker?: _ServiceWorker;
@ -30,28 +33,39 @@ class ConnectionManager {
} }
/** /**
* @return Promise<void> * TODO fix me to be move in game manager
*/ */
public loadOpenIDScreen(): Promise<void> { public loadOpenIDScreen() {
const state = localUserStore.generateState(); const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce(); const nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null); localUserStore.setAuthToken(null);
//TODO refactor this and don't realise previous call //TODO fix me to redirect this URL by pusher
return Axios.get(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`) if (!this._currentRoom || !this._currentRoom.iframeAuthentication) {
.then(() => { loginSceneVisibleIframeStore.set(false);
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`); return null;
}) }
.catch((err) => { const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}`;
console.error(err, "We don't have URL to regenerate authentication user"); window.location.assign(redirectUrl);
//TODO show modal login return redirectUrl;
window.location.reload();
});
} }
public logout() { /**
* Logout
*/
public async logout() {
//user logout, set connected store for menu at false
userIsConnected.set(false);
//Logout user in pusher and hydra
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/logout-callback`, { params: { token } }).then(
(res) => res.data
);
localUserStore.setAuthToken(null); localUserStore.setAuthToken(null);
window.location.reload();
//Go on login page can permit to clear token and start authentication process
window.location.assign("/login");
} }
/** /**
@ -60,8 +74,13 @@ class ConnectionManager {
public async initGameConnexion(): Promise<Room> { public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType(); const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType; this.connexionType = connexionType;
let room: Room | null = null; this._currentRoom = null;
if (connexionType === GameConnexionTypes.jwt) { if (connexionType === GameConnexionTypes.login) {
//TODO clear all cash and redirect on login scene (iframe)
localUserStore.setAuthToken(null);
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code"); const code = urlParams.get("code");
const state = urlParams.get("state"); const state = urlParams.get("state");
@ -71,14 +90,15 @@ class ConnectionManager {
if (!code) { if (!code) {
throw "No Auth code provided"; throw "No Auth code provided";
} }
const nonce = localUserStore.getNonce(); localUserStore.setCode(code);
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then( try {
(res) => res.data await this.checkAuthUserConnexion();
); } catch (err) {
localUserStore.setAuthToken(authToken); console.error(err);
this.authToken = authToken; this.loadOpenIDScreen();
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl())); }
urlManager.pushRoomIdToUrl(room); this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.register) { } else if (connexionType === GameConnexionTypes.register) {
//@deprecated //@deprecated
const organizationMemberToken = urlManager.getOrganizationToken(); const organizationMemberToken = urlManager.getOrganizationToken();
@ -92,7 +112,7 @@ class ConnectionManager {
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
room = await Room.createRoom( this._currentRoom = await Room.createRoom(
new URL( new URL(
window.location.protocol + window.location.protocol +
"//" + "//" +
@ -102,7 +122,7 @@ class ConnectionManager {
window.location.hash window.location.hash
) )
); );
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(this._currentRoom);
} else if ( } else if (
connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.anonymous ||
@ -112,12 +132,18 @@ class ConnectionManager {
//todo: add here some kind of warning if authToken has expired. //todo: add here some kind of warning if authToken has expired.
if (!this.authToken) { if (!this.authToken) {
await this.anonymousLogin(); await this.anonymousLogin();
} else {
try {
await this.checkAuthUserConnexion();
} catch (err) {
console.error(err);
}
} }
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
let roomPath: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL; roomPath = localUserStore.getLastRoomUrl();
//get last room path from cache api //get last room path from cache api
try { try {
const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi(); const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi();
@ -138,13 +164,13 @@ class ConnectionManager {
} }
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
room = await Room.createRoom(new URL(roomPath)); this._currentRoom = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) { if (this._currentRoom.textures != undefined && this._currentRoom.textures.length > 0) {
//check if texture was changed //check if texture was changed
if (this.localUser.textures.length === 0) { if (this.localUser.textures.length === 0) {
this.localUser.textures = room.textures; this.localUser.textures = this._currentRoom.textures;
} else { } else {
room.textures.forEach((newTexture) => { this._currentRoom.textures.forEach((newTexture) => {
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id); const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) { if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return; return;
@ -155,12 +181,12 @@ class ConnectionManager {
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
} }
} }
if (room == undefined) { if (this._currentRoom == undefined) {
return Promise.reject(new Error("Invalid URL")); return Promise.reject(new Error("Invalid URL"));
} }
this.serviceWorker = new _ServiceWorker(); this.serviceWorker = new _ServiceWorker();
return Promise.resolve(room); return Promise.resolve(this._currentRoom);
} }
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
@ -215,9 +241,6 @@ class ConnectionManager {
}); });
connection.onConnect((connect: OnConnectInterface) => { connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect); resolve(connect);
}); });
}).catch((err) => { }).catch((err) => {
@ -237,6 +260,34 @@ class ConnectionManager {
get getConnexionType() { get getConnexionType() {
return this.connexionType; return this.connexionType;
} }
async checkAuthUserConnexion() {
//set connected store for menu at false
userIsConnected.set(false);
const state = localUserStore.getState();
const code = localUserStore.getCode();
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
//user connected, set connected store for menu at true
userIsConnected.set(true);
}
get currentRoom() {
return this._currentRoom;
}
} }
export const connectionManager = new ConnectionManager(); export const connectionManager = new ConnectionManager();

View file

@ -1,19 +1,17 @@
import {Subject} from "rxjs"; import { Subject } from "rxjs";
interface EmoteEvent { interface EmoteEvent {
userId: number, userId: number;
emoteName: string, emoteName: string;
} }
class EmoteEventStream { class EmoteEventStream {
private _stream: Subject<EmoteEvent> = new Subject();
private _stream:Subject<EmoteEvent> = new Subject();
public stream = this._stream.asObservable(); public stream = this._stream.asObservable();
fire(userId: number, emoteName: string) {
fire(userId: number, emoteName:string) { this._stream.next({ userId, emoteName });
this._stream.next({userId, emoteName});
} }
} }
export const emoteEventStream = new EmoteEventStream(); export const emoteEventStream = new EmoteEventStream();

View file

@ -1,5 +1,6 @@
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser"; import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { START_ROOM_URL } from "../Enum/EnvironmentVariable";
const playerNameKey = "playerName"; const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer"; const selectedPlayerKey = "selectedPlayer";
@ -17,6 +18,8 @@ const authToken = "authToken";
const state = "state"; const state = "state";
const nonce = "nonce"; const nonce = "nonce";
const notification = "notificationPermission"; const notification = "notificationPermission";
const code = "code";
const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache"; const cacheAPIIndex = "workavdenture-cache";
@ -125,7 +128,9 @@ class LocalUserStore {
}); });
} }
getLastRoomUrl(): string { getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? ""; return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
} }
getLastRoomUrlCacheApi(): Promise<string | undefined> { getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => { return caches.open(cacheAPIIndex).then((cache) => {
@ -160,19 +165,32 @@ class LocalUserStore {
verifyState(value: string): boolean { verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state); const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value; return oldValue === value;
} }
getState(): string | null {
return localStorage.getItem(state);
}
generateNonce(): string { generateNonce(): string {
const newNonce = uuidv4(); const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce); localStorage.setItem(nonce, newNonce);
return newNonce; return newNonce;
} }
getNonce(): string | null { getNonce(): string | null {
const oldValue = localStorage.getItem(nonce); return localStorage.getItem(nonce);
localStorage.removeItem(nonce); }
return oldValue; setCode(value: string): void {
localStorage.setItem(code, value);
}
getCode(): string | null {
return localStorage.getItem(code);
}
setCameraSetup(cameraId: string) {
localStorage.setItem(cameraSetup, cameraId);
}
getCameraSetup(): { video: unknown; audio: unknown } | undefined {
const cameraSetupValues = localStorage.getItem(cameraSetup);
return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined;
} }
} }

View file

@ -14,6 +14,8 @@ export interface RoomRedirect {
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; public readonly isPublic: boolean;
private _authenticationMandatory: boolean = false;
private _iframeAuthentication?: string;
private _mapUrl: string | undefined; private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined; private _textures: CharacterTexture[] | undefined;
private instance: string | undefined; private instance: string | undefined;
@ -101,6 +103,8 @@ export class Room {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl); console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl; this._mapUrl = data.mapUrl;
this._textures = data.textures; this._textures = data.textures;
this._authenticationMandatory = data.authenticationMandatory || false;
this._iframeAuthentication = data.iframeAuthentication;
return new MapDetail(data.mapUrl, data.textures); return new MapDetail(data.mapUrl, data.textures);
} }
@ -186,4 +190,12 @@ export class Room {
} }
return this._mapUrl; return this._mapUrl;
} }
get authenticationMandatory(): boolean {
return this._authenticationMandatory;
}
get iframeAuthentication(): string | undefined {
return this._iframeAuthentication;
}
} }

View file

@ -78,6 +78,11 @@ export class RoomConnection implements RoomConnection {
* *
* @param token A JWT token containing the email 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]" * @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
* @param name
* @param characterLayers
* @param position
* @param viewport
* @param companion
*/ */
public constructor( public constructor(
token: string | null, token: string | null,
@ -218,7 +223,7 @@ export class RoomConnection implements RoomConnection {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
this.closed = true; this.closed = true;
} else if (message.hasTokenexpiredmessage()) { } else if (message.hasTokenexpiredmessage()) {
connectionManager.loadOpenIDScreen(); connectionManager.logout();
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency 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()) { } else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());

View file

@ -1,14 +1,12 @@
import {Subject} from "rxjs"; import { Subject } from "rxjs";
class WorldFullMessageStream { class WorldFullMessageStream {
private _stream: Subject<string | null> = new Subject<string | null>();
private _stream:Subject<string|null> = new Subject<string|null>();
public stream = this._stream.asObservable(); public stream = this._stream.asObservable();
onMessage(message?: string) {
onMessage(message? :string) {
this._stream.next(message); this._stream.next(message);
} }
} }
export const worldFullMessageStream = new WorldFullMessageStream(); export const worldFullMessageStream = new WorldFullMessageStream();

View file

@ -19,6 +19,7 @@ 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 DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const NODE_ENV = process.env.NODE_ENV || "development"; export const NODE_ENV = process.env.NODE_ENV || "development";
export const CONTACT_URL = process.env.CONTACT_URL || undefined; export const CONTACT_URL = process.env.CONTACT_URL || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;

View file

@ -1 +1 @@
export class TextureError extends Error{} export class TextureError extends Error {}

View file

@ -1,19 +1,21 @@
export class MessageUI { export class MessageUI {
static warningMessage(text: string) {
static warningMessage(text: string){
this.removeMessage(); this.removeMessage();
const body = document.getElementById("body"); const body = document.getElementById("body");
body?.insertAdjacentHTML('afterbegin', ` body?.insertAdjacentHTML(
"afterbegin",
`
<div id="message-reconnect" class="message-info warning"> <div id="message-reconnect" class="message-info warning">
${text} ${text}
</div> </div>
`); `
);
} }
static removeMessage(id : string|null = null) { static removeMessage(id: string | null = null) {
if(!id){ if (!id) {
const messages = document.getElementsByClassName("message-info"); const messages = document.getElementsByClassName("message-info");
for (let i = 0; i < messages.length; i++){ for (let i = 0; i < messages.length; i++) {
messages.item(i)?.remove(); messages.item(i)?.remove();
} }
return; return;

View file

@ -1,23 +1,22 @@
import {PositionMessage} from "../Messages/generated/messages_pb"; import { PositionMessage } from "../Messages/generated/messages_pb";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import type {PointInterface} from "../Connexion/ConnexionModels"; import type { PointInterface } from "../Connexion/ConnexionModels";
export class ProtobufClientUtils { export class ProtobufClientUtils {
public static toPointInterface(position: PositionMessage): PointInterface { public static toPointInterface(position: PositionMessage): PointInterface {
let direction: string; let direction: string;
switch (position.getDirection()) { switch (position.getDirection()) {
case Direction.UP: case Direction.UP:
direction = 'up'; direction = "up";
break; break;
case Direction.DOWN: case Direction.DOWN:
direction = 'down'; direction = "down";
break; break;
case Direction.LEFT: case Direction.LEFT:
direction = 'left'; direction = "left";
break; break;
case Direction.RIGHT: case Direction.RIGHT:
direction = 'right'; direction = "right";
break; break;
default: default:
throw new Error("Unexpected direction"); throw new Error("Unexpected direction");

View file

@ -16,7 +16,7 @@ export class Companion extends Container {
private delta: number; private delta: number;
private invisible: boolean; private invisible: boolean;
private updateListener: Function; private updateListener: Function;
private target: { x: number, y: number, direction: PlayerAnimationDirections }; private target: { x: number; y: number; direction: PlayerAnimationDirections };
private companionName: string; private companionName: string;
private direction: PlayerAnimationDirections; private direction: PlayerAnimationDirections;
@ -36,10 +36,10 @@ export class Companion extends Container {
this.companionName = name; this.companionName = name;
texturePromise.then(resource => { texturePromise.then((resource) => {
this.addResource(resource); this.addResource(resource);
this.invisible = false; this.invisible = false;
}) });
this.scene.physics.world.enableBody(this); this.scene.physics.world.enableBody(this);
@ -52,7 +52,7 @@ export class Companion extends Container {
this.setDepth(-1); this.setDepth(-1);
this.updateListener = this.step.bind(this); this.updateListener = this.step.bind(this);
this.scene.events.addListener('update', this.updateListener); this.scene.events.addListener("update", this.updateListener);
this.scene.add.existing(this); this.scene.add.existing(this);
} }
@ -62,7 +62,7 @@ export class Companion extends Container {
} }
public step(time: number, delta: number) { public step(time: number, delta: number) {
if (typeof this.target === 'undefined') return; if (typeof this.target === "undefined") return;
this.delta += delta; this.delta += delta;
if (this.delta < 128) { if (this.delta < 128) {
@ -87,7 +87,10 @@ export class Companion extends Container {
const yDir = yDist / Math.max(Math.abs(yDist), 1); const yDir = yDist / Math.max(Math.abs(yDist), 1);
const speed = 256; const speed = 256;
this.getBody().setVelocity(Math.min(Math.abs(xDist * 2.5), speed) * xDir, Math.min(Math.abs(yDist * 2.5), speed) * yDir); this.getBody().setVelocity(
Math.min(Math.abs(xDist * 2.5), speed) * xDir,
Math.min(Math.abs(yDist * 2.5), speed) * yDir
);
if (Math.abs(xDist) > Math.abs(yDist)) { if (Math.abs(xDist) > Math.abs(yDist)) {
if (xDist < 0) { if (xDist < 0) {
@ -116,8 +119,8 @@ export class Companion extends Container {
y, y,
direction, direction,
moving: animationType === PlayerAnimationTypes.Walk, moving: animationType === PlayerAnimationTypes.Walk,
name: companionName name: companionName,
} };
} }
private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void { private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void {
@ -133,7 +136,7 @@ export class Companion extends Container {
this.add(sprite); this.add(sprite);
this.getAnimations(resource).forEach(animation => { this.getAnimations(resource).forEach((animation) => {
this.scene.anims.create(animation); this.scene.anims.create(animation);
}); });
@ -145,60 +148,60 @@ export class Companion extends Container {
return [ return [
{ {
key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`, key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [1]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [1] }),
frameRate: 10, frameRate: 10,
repeat: 1 repeat: 1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`, key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [4]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [4] }),
frameRate: 10, frameRate: 10,
repeat: 1 repeat: 1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`, key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [7]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [7] }),
frameRate: 10, frameRate: 10,
repeat: 1 repeat: 1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`, key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [10]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [10] }),
frameRate: 10, frameRate: 10,
repeat: 1 repeat: 1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`, key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [0, 1, 2]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [0, 1, 2] }),
frameRate: 15, frameRate: 15,
repeat: -1 repeat: -1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`, key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [3, 4, 5]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [3, 4, 5] }),
frameRate: 15, frameRate: 15,
repeat: -1 repeat: -1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`, key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [6, 7, 8]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [6, 7, 8] }),
frameRate: 15, frameRate: 15,
repeat: -1 repeat: -1,
}, },
{ {
key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`, key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [9, 10, 11]}), frames: this.scene.anims.generateFrameNumbers(resource, { frames: [9, 10, 11] }),
frameRate: 15, frameRate: 15,
repeat: -1 repeat: -1,
} },
] ];
} }
private getBody(): Phaser.Physics.Arcade.Body { private getBody(): Phaser.Physics.Arcade.Body {
const body = this.body; const body = this.body;
if (!(body instanceof Phaser.Physics.Arcade.Body)) { if (!(body instanceof Phaser.Physics.Arcade.Body)) {
throw new Error('Container does not have arcade body'); throw new Error("Container does not have arcade body");
} }
return body; return body;
@ -212,7 +215,7 @@ export class Companion extends Container {
} }
if (this.scene) { if (this.scene) {
this.scene.events.removeListener('update', this.updateListener); this.scene.events.removeListener("update", this.updateListener);
} }
super.destroy(); super.destroy();

View file

@ -1,7 +1,7 @@
export interface CompanionResourceDescriptionInterface { export interface CompanionResourceDescriptionInterface {
name: string, name: string;
img: string, img: string;
behaviour: "dog" | "cat" behaviour: "dog" | "cat";
} }
export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [ export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [
@ -11,4 +11,4 @@ export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [
{ name: "cat1", img: "resources/characters/pipoya/Cat 01-1.png", behaviour: "cat" }, { name: "cat1", img: "resources/characters/pipoya/Cat 01-1.png", behaviour: "cat" },
{ name: "cat2", img: "resources/characters/pipoya/Cat 01-2.png", behaviour: "cat" }, { name: "cat2", img: "resources/characters/pipoya/Cat 01-2.png", behaviour: "cat" },
{ name: "cat3", img: "resources/characters/pipoya/Cat 01-3.png", behaviour: "cat" }, { name: "cat3", img: "resources/characters/pipoya/Cat 01-3.png", behaviour: "cat" },
] ];

View file

@ -7,23 +7,23 @@ export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourc
}); });
return COMPANION_RESOURCES; return COMPANION_RESOURCES;
} };
export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise<string> => { export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const resource = COMPANION_RESOURCES.find(item => item.name === name); const resource = COMPANION_RESOURCES.find((item) => item.name === name);
if (typeof resource === 'undefined') { if (typeof resource === "undefined") {
return reject(`Texture '${name}' not found!`); return reject(`Texture '${name}' not found!`);
} }
if (loader.textureManager.exists(resource.name)) { if (loader.textureManager.exists(resource.name)) {
return resolve(resource.name); return resolve(resource.name);
} }
loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 }); loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 });
loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name)); loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name));
loader.start(); // It's only automatically started during the Scene preload. loader.start(); // It's only automatically started during the Scene preload.
}); });
} };

View file

@ -2,7 +2,7 @@ import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
export class ChatModeIcon extends Phaser.GameObjects.Sprite { export class ChatModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 3); super(scene, x, y, "layout_modes", 3);
scene.add.existing(this); scene.add.existing(this);
this.setScrollFactor(0, 0); this.setScrollFactor(0, 0);
this.setOrigin(0, 1); this.setOrigin(0, 1);
@ -10,4 +10,4 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite {
this.setVisible(false); this.setVisible(false);
this.setDepth(DEPTH_INGAME_TEXT_INDEX); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
} }
} }

View file

@ -1,11 +1,8 @@
export class ClickButton extends Phaser.GameObjects.Image {
export class ClickButton extends Phaser.GameObjects.Image{
constructor(scene: Phaser.Scene, x: number, y: number, textureName: string, callback: Function) { constructor(scene: Phaser.Scene, x: number, y: number, textureName: string, callback: Function) {
super(scene, x, y, textureName); super(scene, x, y, textureName);
this.scene.add.existing(this); this.scene.add.existing(this);
this.setInteractive(); this.setInteractive();
this.on("pointerup", callback); this.on("pointerup", callback);
} }
}
}

View file

@ -70,3 +70,9 @@ export const addLoader = (scene: Phaser.Scene): void => {
} }
}); });
}; };
export const removeLoader = (scene: Phaser.Scene): void => {
if (scene.load.textureManager.exists(LogoNameIndex)) {
scene.load.textureManager.remove(LogoNameIndex);
}
};

View file

@ -1,8 +1,8 @@
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
export class PresentationModeIcon extends Phaser.GameObjects.Sprite { export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 0); super(scene, x, y, "layout_modes", 0);
scene.add.existing(this); scene.add.existing(this);
this.setScrollFactor(0, 0); this.setScrollFactor(0, 0);
this.setOrigin(0, 1); this.setOrigin(0, 1);
@ -10,4 +10,4 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
this.setVisible(false); this.setVisible(false);
this.setDepth(DEPTH_INGAME_TEXT_INDEX); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
} }
} }

View file

@ -1,20 +1,20 @@
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes"; import { DEPTH_UI_INDEX } from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
export interface RadialMenuItem { export interface RadialMenuItem {
image: string, image: string;
name: string, name: string;
} }
export const RadialMenuClickEvent = 'radialClick'; export const RadialMenuClickEvent = "radialClick";
export class RadialMenu extends Phaser.GameObjects.Container { export class RadialMenu extends Phaser.GameObjects.Container {
private resizeCallback: OmitThisParameter<() => void>; private resizeCallback: OmitThisParameter<() => void>;
constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) { constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) {
super(scene, x, y); super(scene, x, y);
this.setDepth(DEPTH_UI_INDEX) this.setDepth(DEPTH_UI_INDEX);
this.scene.add.existing(this); this.scene.add.existing(this);
this.initItems(); this.initItems();
@ -22,45 +22,45 @@ export class RadialMenu extends Phaser.GameObjects.Container {
this.resizeCallback = this.resize.bind(this); this.resizeCallback = this.resize.bind(this);
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback); this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
} }
private initItems() { private initItems() {
const itemsNumber = this.items.length; const itemsNumber = this.items.length;
const menuRadius = 70 + (waScaleManager.uiScalingFactor - 1) * 20; const menuRadius = 70 + (waScaleManager.uiScalingFactor - 1) * 20;
this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber, menuRadius)) this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber, menuRadius));
} }
private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number, menuRadius: number) { private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number, menuRadius: number) {
const image = new Sprite(this.scene, 0, menuRadius, item.image); const image = new Sprite(this.scene, 0, menuRadius, item.image);
this.add(image); this.add(image);
this.scene.sys.updateList.add(image); this.scene.sys.updateList.add(image);
const scalingFactor = waScaleManager.uiScalingFactor * 0.075; const scalingFactor = waScaleManager.uiScalingFactor * 0.075;
image.setScale(scalingFactor) image.setScale(scalingFactor);
image.setInteractive({ image.setInteractive({
useHandCursor: true, useHandCursor: true,
}); });
image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item)); image.on("pointerdown", () => this.emit(RadialMenuClickEvent, item));
image.on('pointerover', () => { image.on("pointerover", () => {
this.scene.tweens.add({ this.scene.tweens.add({
targets: image, targets: image,
props: { props: {
scale: 2 * scalingFactor, scale: 2 * scalingFactor,
}, },
duration: 500, duration: 500,
ease: 'Power3', ease: "Power3",
}) });
}); });
image.on('pointerout', () => { image.on("pointerout", () => {
this.scene.tweens.add({ this.scene.tweens.add({
targets: image, targets: image,
props: { props: {
scale: scalingFactor, scale: scalingFactor,
}, },
duration: 500, duration: 500,
ease: 'Power3', ease: "Power3",
}) });
}); });
const angle = 2 * Math.PI * index / itemsNumber; const angle = (2 * Math.PI * index) / itemsNumber;
Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius); Phaser.Actions.RotateAroundDistance([image], { x: 0, y: 0 }, angle, menuRadius);
} }
private resize() { private resize() {
@ -71,4 +71,4 @@ export class RadialMenu extends Phaser.GameObjects.Container {
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback); this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
super.destroy(); super.destroy();
} }
} }

View file

@ -1,4 +1,4 @@
import type {IAnalyserNode, IAudioContext, IMediaStreamAudioSourceNode} from 'standardized-audio-context'; import type { IAnalyserNode, IAudioContext, IMediaStreamAudioSourceNode } from "standardized-audio-context";
/** /**
* Class to measure the sound volume of a media stream * Class to measure the sound volume of a media stream
@ -7,10 +7,10 @@ export class SoundMeter {
private instant: number; private instant: number;
private clip: number; private clip: number;
//private script: ScriptProcessorNode; //private script: ScriptProcessorNode;
private analyser: IAnalyserNode<IAudioContext>|undefined; private analyser: IAnalyserNode<IAudioContext> | undefined;
private dataArray: Uint8Array|undefined; private dataArray: Uint8Array | undefined;
private context: IAudioContext|undefined; private context: IAudioContext | undefined;
private source: IMediaStreamAudioSourceNode<IAudioContext>|undefined; private source: IMediaStreamAudioSourceNode<IAudioContext> | undefined;
constructor() { constructor() {
this.instant = 0.0; this.instant = 0.0;
@ -27,8 +27,7 @@ export class SoundMeter {
this.dataArray = new Uint8Array(bufferLength); this.dataArray = new Uint8Array(bufferLength);
} }
public connectToSource(stream: MediaStream, context: IAudioContext): void public connectToSource(stream: MediaStream, context: IAudioContext): void {
{
if (this.source !== undefined) { if (this.source !== undefined) {
this.stop(); this.stop();
} }
@ -42,8 +41,6 @@ export class SoundMeter {
//analyser.connect(distortion); //analyser.connect(distortion);
//distortion.connect(this.context.destination); //distortion.connect(this.context.destination);
//this.analyser.connect(this.context.destination); //this.analyser.connect(this.context.destination);
} }
public getVolume(): number { public getVolume(): number {
@ -52,16 +49,15 @@ export class SoundMeter {
} }
this.analyser.getByteFrequencyData(this.dataArray); this.analyser.getByteFrequencyData(this.dataArray);
const input = this.dataArray; const input = this.dataArray;
let i; let i;
let sum = 0.0; let sum = 0.0;
//let clipcount = 0; //let clipcount = 0;
for (i = 0; i < input.length; ++i) { for (i = 0; i < input.length; ++i) {
sum += input[i] * input[i]; sum += input[i] * input[i];
// if (Math.abs(input[i]) > 0.99) { // if (Math.abs(input[i]) > 0.99) {
// clipcount += 1; // clipcount += 1;
// } // }
} }
this.instant = Math.sqrt(sum / input.length); this.instant = Math.sqrt(sum / input.length);
//this.slow = 0.95 * that.slow + 0.05 * that.instant; //this.slow = 0.95 * that.slow + 0.05 * that.instant;
@ -84,6 +80,4 @@ export class SoundMeter {
this.dataArray = undefined; this.dataArray = undefined;
this.source = undefined; this.source = undefined;
} }
} }

View file

@ -1,10 +1,9 @@
export class TextField extends Phaser.GameObjects.BitmapText { export class TextField extends Phaser.GameObjects.BitmapText {
constructor(scene: Phaser.Scene, x: number, y: number, text: string | string[], center: boolean = true) { constructor(scene: Phaser.Scene, x: number, y: number, text: string | string[], center: boolean = true) {
super(scene, x, y, 'main_font', text, 8); super(scene, x, y, "main_font", text, 8);
this.scene.add.existing(this); this.scene.add.existing(this);
if (center) { if (center) {
this.setOrigin(0.5).setCenterAlign() this.setOrigin(0.5).setCenterAlign();
} }
} }
} }

View file

@ -1,29 +1,30 @@
import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation"; import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
import {SpeechBubble} from "./SpeechBubble"; import { SpeechBubble } from "./SpeechBubble";
import Text = Phaser.GameObjects.Text; import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container; import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import {TextureError} from "../../Exception/TextureError"; import { TextureError } from "../../Exception/TextureError";
import {Companion} from "../Companion/Companion"; import { Companion } from "../Companion/Companion";
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { isSilentStore } from "../../Stores/MediaStore";
const playerNameY = - 25; const playerNameY = -25;
interface AnimationData { interface AnimationData {
key: string; key: string;
frameRate: number; frameRate: number;
repeat: number; repeat: number;
frameModel: string; //todo use an enum frameModel: string; //todo use an enum
frames : number[] frames: number[];
} }
const interactiveRadius = 35; const interactiveRadius = 35;
export abstract class Character extends Container { export abstract class Character extends Container {
private bubble: SpeechBubble|null = null; private bubble: SpeechBubble | null = null;
private readonly playerName: Text; private readonly playerName: Text;
public PlayerValue: string; public PlayerValue: string;
public sprites: Map<string, Sprite>; public sprites: Map<string, Sprite>;
@ -32,35 +33,41 @@ export abstract class Character extends Container {
private invisible: boolean; private invisible: boolean;
public companion?: Companion; public companion?: Companion;
private emote: Phaser.GameObjects.Sprite | null = null; private emote: Phaser.GameObjects.Sprite | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null; private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene; scene: GameScene;
constructor(scene: GameScene, constructor(
x: number, scene: GameScene,
y: number, x: number,
texturesPromise: Promise<string[]>, y: number,
name: string, texturesPromise: Promise<string[]>,
direction: PlayerAnimationDirections, name: string,
moving: boolean, direction: PlayerAnimationDirections,
frame: string | number, moving: boolean,
isClickable: boolean, frame: string | number,
companion: string|null, isClickable: boolean,
companionTexturePromise?: Promise<string> companion: string | null,
companionTexturePromise?: Promise<string>
) { ) {
super(scene, x, y/*, texture, frame*/); super(scene, x, y /*, texture, frame*/);
this.scene = scene; this.scene = scene;
this.PlayerValue = name; this.PlayerValue = name;
this.invisible = true this.invisible = true;
this.sprites = new Map<string, Sprite>(); this.sprites = new Map<string, Sprite>();
//textures are inside a Promise in case they need to be lazyloaded before use. //textures are inside a Promise in case they need to be lazyloaded before use.
texturesPromise.then((textures) => { texturesPromise.then((textures) => {
this.addTextures(textures, frame); this.addTextures(textures, frame);
this.invisible = false this.invisible = false;
}) });
this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"}); this.playerName = new Text(scene, 0, playerNameY, name, {
fontFamily: '"Press Start 2P"',
fontSize: "8px",
strokeThickness: 2,
stroke: "gray",
});
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX); this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName); this.add(this.playerName);
@ -71,18 +78,17 @@ export abstract class Character extends Container {
useHandCursor: true, useHandCursor: true,
}); });
this.on('pointerover',() => { this.on("pointerover", () => {
this.getOutlinePlugin()?.add(this.playerName, { this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2, thickness: 2,
outlineColor: 0xffff00 outlineColor: 0xffff00,
}); });
this.scene.markDirty(); this.scene.markDirty();
}); });
this.on('pointerout',() => { this.on("pointerout", () => {
this.getOutlinePlugin()?.remove(this.playerName); this.getOutlinePlugin()?.remove(this.playerName);
this.scene.markDirty(); this.scene.markDirty();
}) });
} }
scene.add.existing(this); scene.add.existing(this);
@ -97,38 +103,38 @@ export abstract class Character extends Container {
this.playAnimation(direction, moving); this.playAnimation(direction, moving);
if (typeof companion === 'string') { if (typeof companion === "string") {
this.addCompanion(companion, companionTexturePromise); this.addCompanion(companion, companionTexturePromise);
} }
} }
private getOutlinePlugin(): OutlinePipelinePlugin|undefined { private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined; return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
} }
public addCompanion(name: string, texturePromise?: Promise<string>): void { public addCompanion(name: string, texturePromise?: Promise<string>): void {
if (typeof texturePromise !== 'undefined') { if (typeof texturePromise !== "undefined") {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
} }
} }
public addTextures(textures: string[], frame?: string | number): void { public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) { for (const texture of textures) {
if(this.scene && !this.scene.textures.exists(texture)){ if (this.scene && !this.scene.textures.exists(texture)) {
throw new TextureError('texture not found'); throw new TextureError("texture not found");
} }
const sprite = new Sprite(this.scene, 0, 0, texture, frame); const sprite = new Sprite(this.scene, 0, 0, texture, frame);
this.add(sprite); this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => { this.getPlayerAnimations(texture).forEach((d) => {
this.scene.anims.create({ this.scene.anims.create({
key: d.key, key: d.key,
frames: this.scene.anims.generateFrameNumbers(d.frameModel, {frames: d.frames}), frames: this.scene.anims.generateFrameNumbers(d.frameModel, { frames: d.frames }),
frameRate: d.frameRate, frameRate: d.frameRate,
repeat: d.repeat repeat: d.repeat,
}); });
}) });
// Needed, otherwise, animations are not handled correctly. // Needed, otherwise, animations are not handled correctly.
if(this.scene) { if (this.scene) {
this.scene.sys.updateList.add(sprite); this.scene.sys.updateList.add(sprite);
} }
this.sprites.set(texture, sprite); this.sprites.set(texture, sprite);
@ -136,68 +142,77 @@ export abstract class Character extends Container {
} }
private getPlayerAnimations(name: string): AnimationData[] { private getPlayerAnimations(name: string): AnimationData[] {
return [{ return [
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`, {
frameModel: name, key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frames: [0, 1, 2, 1], frameModel: name,
frameRate: 10, frames: [0, 1, 2, 1],
repeat: -1 frameRate: 10,
}, { repeat: -1,
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`, },
frameModel: name, {
frames: [3, 4, 5, 4], key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frameRate: 10, frameModel: name,
repeat: -1 frames: [3, 4, 5, 4],
}, { frameRate: 10,
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`, repeat: -1,
frameModel: name, },
frames: [6, 7, 8, 7], {
frameRate: 10, key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
repeat: -1 frameModel: name,
}, { frames: [6, 7, 8, 7],
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`, frameRate: 10,
frameModel: name, repeat: -1,
frames: [9, 10, 11, 10], },
frameRate: 10, {
repeat: -1 key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
},{ frameModel: name,
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`, frames: [9, 10, 11, 10],
frameModel: name, frameRate: 10,
frames: [1], repeat: -1,
frameRate: 10, },
repeat: 1 {
}, { key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`, frameModel: name,
frameModel: name, frames: [1],
frames: [4], frameRate: 10,
frameRate: 10, repeat: 1,
repeat: 1 },
}, { {
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`, key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frameModel: name, frameModel: name,
frames: [7], frames: [4],
frameRate: 10, frameRate: 10,
repeat: 1 repeat: 1,
}, { },
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`, {
frameModel: name, key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frames: [10], frameModel: name,
frameRate: 10, frames: [7],
repeat: 1 frameRate: 10,
}]; repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [10],
frameRate: 10,
repeat: 1,
},
];
} }
protected playAnimation(direction : PlayerAnimationDirections, moving: boolean): void { protected playAnimation(direction: PlayerAnimationDirections, moving: boolean): void {
if (this.invisible) return; if (this.invisible) return;
for (const [texture, sprite] of this.sprites.entries()) { for (const [texture, sprite] of this.sprites.entries()) {
if (!sprite.anims) { if (!sprite.anims) {
console.error('ANIMS IS NOT DEFINED!!!'); console.error("ANIMS IS NOT DEFINED!!!");
return; return;
} }
if (moving && (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== direction)) { if (moving && (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== direction)) {
sprite.play(texture+'-'+direction+'-'+PlayerAnimationTypes.Walk, true); sprite.play(texture + "-" + direction + "-" + PlayerAnimationTypes.Walk, true);
} else if (!moving) { } else if (!moving) {
sprite.anims.play(texture + '-' + direction + '-'+PlayerAnimationTypes.Idle, true); sprite.anims.play(texture + "-" + direction + "-" + PlayerAnimationTypes.Idle, true);
} }
} }
} }
@ -205,7 +220,7 @@ export abstract class Character extends Container {
protected getBody(): Phaser.Physics.Arcade.Body { protected getBody(): Phaser.Physics.Arcade.Body {
const body = this.body; const body = this.body;
if (!(body instanceof Phaser.Physics.Arcade.Body)) { if (!(body instanceof Phaser.Physics.Arcade.Body)) {
throw new Error('Container does not have arcade body'); throw new Error("Container does not have arcade body");
} }
return body; return body;
} }
@ -216,16 +231,20 @@ export abstract class Character extends Container {
body.setVelocity(x, y); body.setVelocity(x, y);
// up or down animations are prioritized over left and right // up or down animations are prioritized over left and right
if (body.velocity.y < 0) { //moving up if (body.velocity.y < 0) {
//moving up
this.lastDirection = PlayerAnimationDirections.Up; this.lastDirection = PlayerAnimationDirections.Up;
this.playAnimation(PlayerAnimationDirections.Up, true); this.playAnimation(PlayerAnimationDirections.Up, true);
} else if (body.velocity.y > 0) { //moving down } else if (body.velocity.y > 0) {
//moving down
this.lastDirection = PlayerAnimationDirections.Down; this.lastDirection = PlayerAnimationDirections.Down;
this.playAnimation(PlayerAnimationDirections.Down, true); this.playAnimation(PlayerAnimationDirections.Down, true);
} else if (body.velocity.x > 0) { //moving right } else if (body.velocity.x > 0) {
//moving right
this.lastDirection = PlayerAnimationDirections.Right; this.lastDirection = PlayerAnimationDirections.Right;
this.playAnimation(PlayerAnimationDirections.Right, true); this.playAnimation(PlayerAnimationDirections.Right, true);
} else if (body.velocity.x < 0) { //moving left } else if (body.velocity.x < 0) {
//moving left
this.lastDirection = PlayerAnimationDirections.Left; this.lastDirection = PlayerAnimationDirections.Left;
this.playAnimation(PlayerAnimationDirections.Left, true); this.playAnimation(PlayerAnimationDirections.Left, true);
} }
@ -237,32 +256,39 @@ export abstract class Character extends Container {
} }
} }
stop(){ stop() {
this.getBody().setVelocity(0, 0); this.getBody().setVelocity(0, 0);
this.playAnimation(this.lastDirection, false); this.playAnimation(this.lastDirection, false);
} }
say(text: string) { say(text: string) {
if (this.bubble) return; if (this.bubble) return;
this.bubble = new SpeechBubble(this.scene, this, text) this.bubble = new SpeechBubble(this.scene, this, text);
setTimeout(() => { setTimeout(() => {
if (this.bubble !== null) { if (this.bubble !== null) {
this.bubble.destroy(); this.bubble.destroy();
this.bubble = null; this.bubble = null;
} }
}, 3000) }, 3000);
} }
destroy(): void { destroy(): void {
for (const sprite of this.sprites.values()) { for (const sprite of this.sprites.values()) {
if(this.scene) { if (this.scene) {
this.scene.sys.updateList.remove(sprite); this.scene.sys.updateList.remove(sprite);
} }
} }
this.list.forEach(objectContaining => objectContaining.destroy()) this.list.forEach((objectContaining) => objectContaining.destroy());
super.destroy(); super.destroy();
} }
isSilent() {
isSilentStore.set(true);
}
noSilent() {
isSilentStore.set(false);
}
playEmote(emoteKey: string) { playEmote(emoteKey: string) {
this.cancelPreviousEmote(); this.cancelPreviousEmote();
@ -270,7 +296,7 @@ export abstract class Character extends Container {
const emoteY = -30 - scalingFactor * 10; const emoteY = -30 - scalingFactor * 10;
this.playerName.setVisible(false); this.playerName.setVisible(false);
this.emote = new Sprite(this.scene, 0, 0, emoteKey); this.emote = new Sprite(this.scene, 0, 0, emoteKey);
this.emote.setAlpha(0); this.emote.setAlpha(0);
this.emote.setScale(0.1 * scalingFactor); this.emote.setScale(0.1 * scalingFactor);
this.add(this.emote); this.add(this.emote);
@ -287,11 +313,11 @@ export abstract class Character extends Container {
alpha: 1, alpha: 1,
y: emoteY, y: emoteY,
}, },
ease: 'Power2', ease: "Power2",
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
this.startPulseTransition(emoteY, scalingFactor); this.startPulseTransition(emoteY, scalingFactor);
} },
}); });
} }
@ -300,7 +326,7 @@ export abstract class Character extends Container {
targets: this.emote, targets: this.emote,
props: { props: {
y: emoteY * 1.3, y: emoteY * 1.3,
scale: scalingFactor * 1.1 scale: scalingFactor * 1.1,
}, },
duration: 250, duration: 250,
yoyo: true, yoyo: true,
@ -308,7 +334,7 @@ export abstract class Character extends Container {
completeDelay: 200, completeDelay: 200,
onComplete: () => { onComplete: () => {
this.startExitTransition(emoteY); this.startExitTransition(emoteY);
} },
}); });
} }
@ -319,11 +345,11 @@ export abstract class Character extends Container {
alpha: 0, alpha: 0,
y: 2 * emoteY, y: 2 * emoteY,
}, },
ease: 'Power2', ease: "Power2",
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
this.destroyEmote(); this.destroyEmote();
} },
}); });
} }
@ -331,7 +357,7 @@ export abstract class Character extends Container {
if (!this.emote) return; if (!this.emote) return;
this.emoteTween?.remove(); this.emoteTween?.remove();
this.destroyEmote() this.destroyEmote();
} }
private destroyEmote() { private destroyEmote() {

View file

@ -1,5 +1,5 @@
import Container = Phaser.GameObjects.Container; import Container = Phaser.GameObjects.Container;
import type {Scene} from "phaser"; import type { Scene } from "phaser";
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
/** /**

View file

@ -1,333 +1,439 @@
//The list of all the player textures, both the default models and the partial textures used for customization //The list of all the player textures, both the default models and the partial textures used for customization
export interface BodyResourceDescriptionListInterface { export interface BodyResourceDescriptionListInterface {
[key: string]: BodyResourceDescriptionInterface [key: string]: BodyResourceDescriptionInterface;
} }
export interface BodyResourceDescriptionInterface { export interface BodyResourceDescriptionInterface {
name: string, name: string;
img: string, img: string;
level?: number level?: number;
} }
export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = { export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {
"male1": {name: "male1", img: "resources/characters/pipoya/Male 01-1.png"}, male1: { name: "male1", img: "resources/characters/pipoya/Male 01-1.png" },
"male2": {name: "male2", img: "resources/characters/pipoya/Male 02-2.png"}, male2: { name: "male2", img: "resources/characters/pipoya/Male 02-2.png" },
"male3": {name: "male3", img: "resources/characters/pipoya/Male 03-4.png"}, male3: { name: "male3", img: "resources/characters/pipoya/Male 03-4.png" },
"male4": {name: "male4", img: "resources/characters/pipoya/Male 09-1.png"}, male4: { name: "male4", img: "resources/characters/pipoya/Male 09-1.png" },
"male5": {name: "male5", img: "resources/characters/pipoya/Male 10-3.png"}, male5: { name: "male5", img: "resources/characters/pipoya/Male 10-3.png" },
"male6": {name: "male6", img: "resources/characters/pipoya/Male 17-2.png"}, male6: { name: "male6", img: "resources/characters/pipoya/Male 17-2.png" },
"male7": {name: "male7", img: "resources/characters/pipoya/Male 18-1.png"}, male7: { name: "male7", img: "resources/characters/pipoya/Male 18-1.png" },
"male8": {name: "male8", img: "resources/characters/pipoya/Male 16-4.png"}, male8: { name: "male8", img: "resources/characters/pipoya/Male 16-4.png" },
"male9": {name: "male9", img: "resources/characters/pipoya/Male 07-2.png"}, male9: { name: "male9", img: "resources/characters/pipoya/Male 07-2.png" },
"male10": {name: "male10", img: "resources/characters/pipoya/Male 05-3.png"}, male10: { name: "male10", img: "resources/characters/pipoya/Male 05-3.png" },
"male11": {name: "male11", img: "resources/characters/pipoya/Teacher male 02.png"}, male11: { name: "male11", img: "resources/characters/pipoya/Teacher male 02.png" },
"male12": {name: "male12", img: "resources/characters/pipoya/su4 Student male 12.png"}, male12: { name: "male12", img: "resources/characters/pipoya/su4 Student male 12.png" },
"Female1": {name: "Female1", img: "resources/characters/pipoya/Female 01-1.png"}, Female1: { name: "Female1", img: "resources/characters/pipoya/Female 01-1.png" },
"Female2": {name: "Female2", img: "resources/characters/pipoya/Female 02-2.png"}, Female2: { name: "Female2", img: "resources/characters/pipoya/Female 02-2.png" },
"Female3": {name: "Female3", img: "resources/characters/pipoya/Female 03-4.png"}, Female3: { name: "Female3", img: "resources/characters/pipoya/Female 03-4.png" },
"Female4": {name: "Female4", img: "resources/characters/pipoya/Female 09-1.png"}, Female4: { name: "Female4", img: "resources/characters/pipoya/Female 09-1.png" },
"Female5": {name: "Female5", img: "resources/characters/pipoya/Female 10-3.png"}, Female5: { name: "Female5", img: "resources/characters/pipoya/Female 10-3.png" },
"Female6": {name: "Female6", img: "resources/characters/pipoya/Female 17-2.png"}, Female6: { name: "Female6", img: "resources/characters/pipoya/Female 17-2.png" },
"Female7": {name: "Female7", img: "resources/characters/pipoya/Female 18-1.png"}, Female7: { name: "Female7", img: "resources/characters/pipoya/Female 18-1.png" },
"Female8": {name: "Female8", img: "resources/characters/pipoya/Female 16-4.png"}, Female8: { name: "Female8", img: "resources/characters/pipoya/Female 16-4.png" },
"Female9": {name: "Female9", img: "resources/characters/pipoya/Female 07-2.png"}, Female9: { name: "Female9", img: "resources/characters/pipoya/Female 07-2.png" },
"Female10": {name: "Female10", img: "resources/characters/pipoya/Female 05-3.png"}, Female10: { name: "Female10", img: "resources/characters/pipoya/Female 05-3.png" },
"Female11": {name: "Female11", img: "resources/characters/pipoya/Teacher fmale 02.png"}, Female11: { name: "Female11", img: "resources/characters/pipoya/Teacher fmale 02.png" },
"Female12": {name: "Female12", img: "resources/characters/pipoya/su4 Student fmale 12.png"}, Female12: { name: "Female12", img: "resources/characters/pipoya/su4 Student fmale 12.png" },
}; };
export const COLOR_RESOURCES: BodyResourceDescriptionListInterface = { export const COLOR_RESOURCES: BodyResourceDescriptionListInterface = {
"color_1": {name: "color_1", img: "resources/customisation/character_color/character_color0.png"}, color_1: { name: "color_1", img: "resources/customisation/character_color/character_color0.png" },
"color_2": {name: "color_2", img: "resources/customisation/character_color/character_color1.png"}, color_2: { name: "color_2", img: "resources/customisation/character_color/character_color1.png" },
"color_3": {name: "color_3", img: "resources/customisation/character_color/character_color2.png"}, color_3: { name: "color_3", img: "resources/customisation/character_color/character_color2.png" },
"color_4": {name: "color_4", img: "resources/customisation/character_color/character_color3.png"}, color_4: { name: "color_4", img: "resources/customisation/character_color/character_color3.png" },
"color_5": {name: "color_5", img: "resources/customisation/character_color/character_color4.png"}, color_5: { name: "color_5", img: "resources/customisation/character_color/character_color4.png" },
"color_6": {name: "color_6", img: "resources/customisation/character_color/character_color5.png"}, color_6: { name: "color_6", img: "resources/customisation/character_color/character_color5.png" },
"color_7": {name: "color_7", img: "resources/customisation/character_color/character_color6.png"}, color_7: { name: "color_7", img: "resources/customisation/character_color/character_color6.png" },
"color_8": {name: "color_8", img: "resources/customisation/character_color/character_color7.png"}, color_8: { name: "color_8", img: "resources/customisation/character_color/character_color7.png" },
"color_9": {name: "color_9", img: "resources/customisation/character_color/character_color8.png"}, color_9: { name: "color_9", img: "resources/customisation/character_color/character_color8.png" },
"color_10": {name: "color_10", img: "resources/customisation/character_color/character_color9.png"}, color_10: { name: "color_10", img: "resources/customisation/character_color/character_color9.png" },
"color_11": {name: "color_11", img: "resources/customisation/character_color/character_color10.png"}, color_11: { name: "color_11", img: "resources/customisation/character_color/character_color10.png" },
"color_12": {name: "color_12", img: "resources/customisation/character_color/character_color11.png"}, color_12: { name: "color_12", img: "resources/customisation/character_color/character_color11.png" },
"color_13": {name: "color_13", img: "resources/customisation/character_color/character_color12.png"}, color_13: { name: "color_13", img: "resources/customisation/character_color/character_color12.png" },
"color_14": {name: "color_14", img: "resources/customisation/character_color/character_color13.png"}, color_14: { name: "color_14", img: "resources/customisation/character_color/character_color13.png" },
"color_15": {name: "color_15", img: "resources/customisation/character_color/character_color14.png"}, color_15: { name: "color_15", img: "resources/customisation/character_color/character_color14.png" },
"color_16": {name: "color_16", img: "resources/customisation/character_color/character_color15.png"}, color_16: { name: "color_16", img: "resources/customisation/character_color/character_color15.png" },
"color_17": {name: "color_17", img: "resources/customisation/character_color/character_color16.png"}, color_17: { name: "color_17", img: "resources/customisation/character_color/character_color16.png" },
"color_18": {name: "color_18", img: "resources/customisation/character_color/character_color17.png"}, color_18: { name: "color_18", img: "resources/customisation/character_color/character_color17.png" },
"color_19": {name: "color_19", img: "resources/customisation/character_color/character_color18.png"}, color_19: { name: "color_19", img: "resources/customisation/character_color/character_color18.png" },
"color_20": {name: "color_20", img: "resources/customisation/character_color/character_color19.png"}, color_20: { name: "color_20", img: "resources/customisation/character_color/character_color19.png" },
"color_21": {name: "color_21", img: "resources/customisation/character_color/character_color20.png"}, color_21: { name: "color_21", img: "resources/customisation/character_color/character_color20.png" },
"color_22": {name: "color_22", img: "resources/customisation/character_color/character_color21.png"}, color_22: { name: "color_22", img: "resources/customisation/character_color/character_color21.png" },
"color_23": {name: "color_23", img: "resources/customisation/character_color/character_color22.png"}, color_23: { name: "color_23", img: "resources/customisation/character_color/character_color22.png" },
"color_24": {name: "color_24", img: "resources/customisation/character_color/character_color23.png"}, color_24: { name: "color_24", img: "resources/customisation/character_color/character_color23.png" },
"color_25": {name: "color_25", img: "resources/customisation/character_color/character_color24.png"}, color_25: { name: "color_25", img: "resources/customisation/character_color/character_color24.png" },
"color_26": {name: "color_26", img: "resources/customisation/character_color/character_color25.png"}, color_26: { name: "color_26", img: "resources/customisation/character_color/character_color25.png" },
"color_27": {name: "color_27", img: "resources/customisation/character_color/character_color26.png"}, color_27: { name: "color_27", img: "resources/customisation/character_color/character_color26.png" },
"color_28": {name: "color_28", img: "resources/customisation/character_color/character_color27.png"}, color_28: { name: "color_28", img: "resources/customisation/character_color/character_color27.png" },
"color_29": {name: "color_29", img: "resources/customisation/character_color/character_color28.png"}, color_29: { name: "color_29", img: "resources/customisation/character_color/character_color28.png" },
"color_30": {name: "color_30", img: "resources/customisation/character_color/character_color29.png"}, color_30: { name: "color_30", img: "resources/customisation/character_color/character_color29.png" },
"color_31": {name: "color_31", img: "resources/customisation/character_color/character_color30.png"}, color_31: { name: "color_31", img: "resources/customisation/character_color/character_color30.png" },
"color_32": {name: "color_32", img: "resources/customisation/character_color/character_color31.png"}, color_32: { name: "color_32", img: "resources/customisation/character_color/character_color31.png" },
"color_33": {name: "color_33", img: "resources/customisation/character_color/character_color32.png"} color_33: { name: "color_33", img: "resources/customisation/character_color/character_color32.png" },
}; };
export const EYES_RESOURCES: BodyResourceDescriptionListInterface = { export const EYES_RESOURCES: BodyResourceDescriptionListInterface = {
"eyes_1": {name: "eyes_1", img: "resources/customisation/character_eyes/character_eyes1.png"}, eyes_1: { name: "eyes_1", img: "resources/customisation/character_eyes/character_eyes1.png" },
"eyes_2": {name: "eyes_2", img: "resources/customisation/character_eyes/character_eyes2.png"}, eyes_2: { name: "eyes_2", img: "resources/customisation/character_eyes/character_eyes2.png" },
"eyes_3": {name: "eyes_3", img: "resources/customisation/character_eyes/character_eyes3.png"}, eyes_3: { name: "eyes_3", img: "resources/customisation/character_eyes/character_eyes3.png" },
"eyes_4": {name: "eyes_4", img: "resources/customisation/character_eyes/character_eyes4.png"}, eyes_4: { name: "eyes_4", img: "resources/customisation/character_eyes/character_eyes4.png" },
"eyes_5": {name: "eyes_5", img: "resources/customisation/character_eyes/character_eyes5.png"}, eyes_5: { name: "eyes_5", img: "resources/customisation/character_eyes/character_eyes5.png" },
"eyes_6": {name: "eyes_6", img: "resources/customisation/character_eyes/character_eyes6.png"}, eyes_6: { name: "eyes_6", img: "resources/customisation/character_eyes/character_eyes6.png" },
"eyes_7": {name: "eyes_7", img: "resources/customisation/character_eyes/character_eyes7.png"}, eyes_7: { name: "eyes_7", img: "resources/customisation/character_eyes/character_eyes7.png" },
"eyes_8": {name: "eyes_8", img: "resources/customisation/character_eyes/character_eyes8.png"}, eyes_8: { name: "eyes_8", img: "resources/customisation/character_eyes/character_eyes8.png" },
"eyes_9": {name: "eyes_9", img: "resources/customisation/character_eyes/character_eyes9.png"}, eyes_9: { name: "eyes_9", img: "resources/customisation/character_eyes/character_eyes9.png" },
"eyes_10": {name: "eyes_10", img: "resources/customisation/character_eyes/character_eyes10.png"}, eyes_10: { name: "eyes_10", img: "resources/customisation/character_eyes/character_eyes10.png" },
"eyes_11": {name: "eyes_11", img: "resources/customisation/character_eyes/character_eyes11.png"}, eyes_11: { name: "eyes_11", img: "resources/customisation/character_eyes/character_eyes11.png" },
"eyes_12": {name: "eyes_12", img: "resources/customisation/character_eyes/character_eyes12.png"}, eyes_12: { name: "eyes_12", img: "resources/customisation/character_eyes/character_eyes12.png" },
"eyes_13": {name: "eyes_13", img: "resources/customisation/character_eyes/character_eyes13.png"}, eyes_13: { name: "eyes_13", img: "resources/customisation/character_eyes/character_eyes13.png" },
"eyes_14": {name: "eyes_14", img: "resources/customisation/character_eyes/character_eyes14.png"}, eyes_14: { name: "eyes_14", img: "resources/customisation/character_eyes/character_eyes14.png" },
"eyes_15": {name: "eyes_15", img: "resources/customisation/character_eyes/character_eyes15.png"}, eyes_15: { name: "eyes_15", img: "resources/customisation/character_eyes/character_eyes15.png" },
"eyes_16": {name: "eyes_16", img: "resources/customisation/character_eyes/character_eyes16.png"}, eyes_16: { name: "eyes_16", img: "resources/customisation/character_eyes/character_eyes16.png" },
"eyes_17": {name: "eyes_17", img: "resources/customisation/character_eyes/character_eyes17.png"}, eyes_17: { name: "eyes_17", img: "resources/customisation/character_eyes/character_eyes17.png" },
"eyes_18": {name: "eyes_18", img: "resources/customisation/character_eyes/character_eyes18.png"}, eyes_18: { name: "eyes_18", img: "resources/customisation/character_eyes/character_eyes18.png" },
"eyes_19": {name: "eyes_19", img: "resources/customisation/character_eyes/character_eyes19.png"}, eyes_19: { name: "eyes_19", img: "resources/customisation/character_eyes/character_eyes19.png" },
"eyes_20": {name: "eyes_20", img: "resources/customisation/character_eyes/character_eyes20.png"}, eyes_20: { name: "eyes_20", img: "resources/customisation/character_eyes/character_eyes20.png" },
"eyes_21": {name: "eyes_21", img: "resources/customisation/character_eyes/character_eyes21.png"}, eyes_21: { name: "eyes_21", img: "resources/customisation/character_eyes/character_eyes21.png" },
"eyes_22": {name: "eyes_22", img: "resources/customisation/character_eyes/character_eyes22.png"}, eyes_22: { name: "eyes_22", img: "resources/customisation/character_eyes/character_eyes22.png" },
"eyes_23": {name: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png"}, eyes_23: { name: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png" },
"eyes_24": {name: "eyes_24", img: "resources/customisation/character_eyes/character_eyes24.png"}, eyes_24: { name: "eyes_24", img: "resources/customisation/character_eyes/character_eyes24.png" },
"eyes_25": {name: "eyes_25", img: "resources/customisation/character_eyes/character_eyes25.png"}, eyes_25: { name: "eyes_25", img: "resources/customisation/character_eyes/character_eyes25.png" },
"eyes_26": {name: "eyes_26", img: "resources/customisation/character_eyes/character_eyes26.png"}, eyes_26: { name: "eyes_26", img: "resources/customisation/character_eyes/character_eyes26.png" },
"eyes_27": {name: "eyes_27", img: "resources/customisation/character_eyes/character_eyes27.png"}, eyes_27: { name: "eyes_27", img: "resources/customisation/character_eyes/character_eyes27.png" },
"eyes_28": {name: "eyes_28", img: "resources/customisation/character_eyes/character_eyes28.png"}, eyes_28: { name: "eyes_28", img: "resources/customisation/character_eyes/character_eyes28.png" },
"eyes_29": {name: "eyes_29", img: "resources/customisation/character_eyes/character_eyes29.png"}, eyes_29: { name: "eyes_29", img: "resources/customisation/character_eyes/character_eyes29.png" },
"eyes_30": {name: "eyes_30", img: "resources/customisation/character_eyes/character_eyes30.png"} eyes_30: { name: "eyes_30", img: "resources/customisation/character_eyes/character_eyes30.png" },
}; };
export const HAIR_RESOURCES: BodyResourceDescriptionListInterface = { export const HAIR_RESOURCES: BodyResourceDescriptionListInterface = {
"hair_1": {name:"hair_1", img: "resources/customisation/character_hairs/character_hairs0.png"}, hair_1: { name: "hair_1", img: "resources/customisation/character_hairs/character_hairs0.png" },
"hair_2": {name:"hair_2", img: "resources/customisation/character_hairs/character_hairs1.png"}, hair_2: { name: "hair_2", img: "resources/customisation/character_hairs/character_hairs1.png" },
"hair_3": {name:"hair_3", img: "resources/customisation/character_hairs/character_hairs2.png"}, hair_3: { name: "hair_3", img: "resources/customisation/character_hairs/character_hairs2.png" },
"hair_4": {name:"hair_4", img: "resources/customisation/character_hairs/character_hairs3.png"}, hair_4: { name: "hair_4", img: "resources/customisation/character_hairs/character_hairs3.png" },
"hair_5": {name:"hair_5", img: "resources/customisation/character_hairs/character_hairs4.png"}, hair_5: { name: "hair_5", img: "resources/customisation/character_hairs/character_hairs4.png" },
"hair_6": {name:"hair_6", img: "resources/customisation/character_hairs/character_hairs5.png"}, hair_6: { name: "hair_6", img: "resources/customisation/character_hairs/character_hairs5.png" },
"hair_7": {name:"hair_7", img: "resources/customisation/character_hairs/character_hairs6.png"}, hair_7: { name: "hair_7", img: "resources/customisation/character_hairs/character_hairs6.png" },
"hair_8": {name:"hair_8", img: "resources/customisation/character_hairs/character_hairs7.png"}, hair_8: { name: "hair_8", img: "resources/customisation/character_hairs/character_hairs7.png" },
"hair_9": {name:"hair_9", img: "resources/customisation/character_hairs/character_hairs8.png"}, hair_9: { name: "hair_9", img: "resources/customisation/character_hairs/character_hairs8.png" },
"hair_10": {name:"hair_10",img: "resources/customisation/character_hairs/character_hairs9.png"}, hair_10: { name: "hair_10", img: "resources/customisation/character_hairs/character_hairs9.png" },
"hair_11": {name:"hair_11",img: "resources/customisation/character_hairs/character_hairs10.png"}, hair_11: { name: "hair_11", img: "resources/customisation/character_hairs/character_hairs10.png" },
"hair_12": {name:"hair_12",img: "resources/customisation/character_hairs/character_hairs11.png"}, hair_12: { name: "hair_12", img: "resources/customisation/character_hairs/character_hairs11.png" },
"hair_13": {name:"hair_13",img: "resources/customisation/character_hairs/character_hairs12.png"}, hair_13: { name: "hair_13", img: "resources/customisation/character_hairs/character_hairs12.png" },
"hair_14": {name:"hair_14",img: "resources/customisation/character_hairs/character_hairs13.png"}, hair_14: { name: "hair_14", img: "resources/customisation/character_hairs/character_hairs13.png" },
"hair_15": {name:"hair_15",img: "resources/customisation/character_hairs/character_hairs14.png"}, hair_15: { name: "hair_15", img: "resources/customisation/character_hairs/character_hairs14.png" },
"hair_16": {name:"hair_16",img: "resources/customisation/character_hairs/character_hairs15.png"}, hair_16: { name: "hair_16", img: "resources/customisation/character_hairs/character_hairs15.png" },
"hair_17": {name:"hair_17",img: "resources/customisation/character_hairs/character_hairs16.png"}, hair_17: { name: "hair_17", img: "resources/customisation/character_hairs/character_hairs16.png" },
"hair_18": {name:"hair_18",img: "resources/customisation/character_hairs/character_hairs17.png"}, hair_18: { name: "hair_18", img: "resources/customisation/character_hairs/character_hairs17.png" },
"hair_19": {name:"hair_19",img: "resources/customisation/character_hairs/character_hairs18.png"}, hair_19: { name: "hair_19", img: "resources/customisation/character_hairs/character_hairs18.png" },
"hair_20": {name:"hair_20",img: "resources/customisation/character_hairs/character_hairs19.png"}, hair_20: { name: "hair_20", img: "resources/customisation/character_hairs/character_hairs19.png" },
"hair_21": {name:"hair_21",img: "resources/customisation/character_hairs/character_hairs20.png"}, hair_21: { name: "hair_21", img: "resources/customisation/character_hairs/character_hairs20.png" },
"hair_22": {name:"hair_22",img: "resources/customisation/character_hairs/character_hairs21.png"}, hair_22: { name: "hair_22", img: "resources/customisation/character_hairs/character_hairs21.png" },
"hair_23": {name:"hair_23",img: "resources/customisation/character_hairs/character_hairs22.png"}, hair_23: { name: "hair_23", img: "resources/customisation/character_hairs/character_hairs22.png" },
"hair_24": {name:"hair_24",img: "resources/customisation/character_hairs/character_hairs23.png"}, hair_24: { name: "hair_24", img: "resources/customisation/character_hairs/character_hairs23.png" },
"hair_25": {name:"hair_25",img: "resources/customisation/character_hairs/character_hairs24.png"}, hair_25: { name: "hair_25", img: "resources/customisation/character_hairs/character_hairs24.png" },
"hair_26": {name:"hair_26",img: "resources/customisation/character_hairs/character_hairs25.png"}, hair_26: { name: "hair_26", img: "resources/customisation/character_hairs/character_hairs25.png" },
"hair_27": {name:"hair_27",img: "resources/customisation/character_hairs/character_hairs26.png"}, hair_27: { name: "hair_27", img: "resources/customisation/character_hairs/character_hairs26.png" },
"hair_28": {name:"hair_28",img: "resources/customisation/character_hairs/character_hairs27.png"}, hair_28: { name: "hair_28", img: "resources/customisation/character_hairs/character_hairs27.png" },
"hair_29": {name:"hair_29",img: "resources/customisation/character_hairs/character_hairs28.png"}, hair_29: { name: "hair_29", img: "resources/customisation/character_hairs/character_hairs28.png" },
"hair_30": {name:"hair_30",img: "resources/customisation/character_hairs/character_hairs29.png"}, hair_30: { name: "hair_30", img: "resources/customisation/character_hairs/character_hairs29.png" },
"hair_31": {name:"hair_31",img: "resources/customisation/character_hairs/character_hairs30.png"}, hair_31: { name: "hair_31", img: "resources/customisation/character_hairs/character_hairs30.png" },
"hair_32": {name:"hair_32",img: "resources/customisation/character_hairs/character_hairs31.png"}, hair_32: { name: "hair_32", img: "resources/customisation/character_hairs/character_hairs31.png" },
"hair_33": {name:"hair_33",img: "resources/customisation/character_hairs/character_hairs32.png"}, hair_33: { name: "hair_33", img: "resources/customisation/character_hairs/character_hairs32.png" },
"hair_34": {name:"hair_34",img: "resources/customisation/character_hairs/character_hairs33.png"}, hair_34: { name: "hair_34", img: "resources/customisation/character_hairs/character_hairs33.png" },
"hair_35": {name:"hair_35",img: "resources/customisation/character_hairs/character_hairs34.png"}, hair_35: { name: "hair_35", img: "resources/customisation/character_hairs/character_hairs34.png" },
"hair_36": {name:"hair_36",img: "resources/customisation/character_hairs/character_hairs35.png"}, hair_36: { name: "hair_36", img: "resources/customisation/character_hairs/character_hairs35.png" },
"hair_37": {name:"hair_37",img: "resources/customisation/character_hairs/character_hairs36.png"}, hair_37: { name: "hair_37", img: "resources/customisation/character_hairs/character_hairs36.png" },
"hair_38": {name:"hair_38",img: "resources/customisation/character_hairs/character_hairs37.png"}, hair_38: { name: "hair_38", img: "resources/customisation/character_hairs/character_hairs37.png" },
"hair_39": {name:"hair_39",img: "resources/customisation/character_hairs/character_hairs38.png"}, hair_39: { name: "hair_39", img: "resources/customisation/character_hairs/character_hairs38.png" },
"hair_40": {name:"hair_40",img: "resources/customisation/character_hairs/character_hairs39.png"}, hair_40: { name: "hair_40", img: "resources/customisation/character_hairs/character_hairs39.png" },
"hair_41": {name:"hair_41",img: "resources/customisation/character_hairs/character_hairs40.png"}, hair_41: { name: "hair_41", img: "resources/customisation/character_hairs/character_hairs40.png" },
"hair_42": {name:"hair_42",img: "resources/customisation/character_hairs/character_hairs41.png"}, hair_42: { name: "hair_42", img: "resources/customisation/character_hairs/character_hairs41.png" },
"hair_43": {name:"hair_43",img: "resources/customisation/character_hairs/character_hairs42.png"}, hair_43: { name: "hair_43", img: "resources/customisation/character_hairs/character_hairs42.png" },
"hair_44": {name:"hair_44",img: "resources/customisation/character_hairs/character_hairs43.png"}, hair_44: { name: "hair_44", img: "resources/customisation/character_hairs/character_hairs43.png" },
"hair_45": {name:"hair_45",img: "resources/customisation/character_hairs/character_hairs44.png"}, hair_45: { name: "hair_45", img: "resources/customisation/character_hairs/character_hairs44.png" },
"hair_46": {name:"hair_46",img: "resources/customisation/character_hairs/character_hairs45.png"}, hair_46: { name: "hair_46", img: "resources/customisation/character_hairs/character_hairs45.png" },
"hair_47": {name:"hair_47",img: "resources/customisation/character_hairs/character_hairs46.png"}, hair_47: { name: "hair_47", img: "resources/customisation/character_hairs/character_hairs46.png" },
"hair_48": {name:"hair_48",img: "resources/customisation/character_hairs/character_hairs47.png"}, hair_48: { name: "hair_48", img: "resources/customisation/character_hairs/character_hairs47.png" },
"hair_49": {name:"hair_49",img: "resources/customisation/character_hairs/character_hairs48.png"}, hair_49: { name: "hair_49", img: "resources/customisation/character_hairs/character_hairs48.png" },
"hair_50": {name:"hair_50",img: "resources/customisation/character_hairs/character_hairs49.png"}, hair_50: { name: "hair_50", img: "resources/customisation/character_hairs/character_hairs49.png" },
"hair_51": {name:"hair_51",img: "resources/customisation/character_hairs/character_hairs50.png"}, hair_51: { name: "hair_51", img: "resources/customisation/character_hairs/character_hairs50.png" },
"hair_52": {name:"hair_52",img: "resources/customisation/character_hairs/character_hairs51.png"}, hair_52: { name: "hair_52", img: "resources/customisation/character_hairs/character_hairs51.png" },
"hair_53": {name:"hair_53",img: "resources/customisation/character_hairs/character_hairs52.png"}, hair_53: { name: "hair_53", img: "resources/customisation/character_hairs/character_hairs52.png" },
"hair_54": {name:"hair_54",img: "resources/customisation/character_hairs/character_hairs53.png"}, hair_54: { name: "hair_54", img: "resources/customisation/character_hairs/character_hairs53.png" },
"hair_55": {name:"hair_55",img: "resources/customisation/character_hairs/character_hairs54.png"}, hair_55: { name: "hair_55", img: "resources/customisation/character_hairs/character_hairs54.png" },
"hair_56": {name:"hair_56",img: "resources/customisation/character_hairs/character_hairs55.png"}, hair_56: { name: "hair_56", img: "resources/customisation/character_hairs/character_hairs55.png" },
"hair_57": {name:"hair_57",img: "resources/customisation/character_hairs/character_hairs56.png"}, hair_57: { name: "hair_57", img: "resources/customisation/character_hairs/character_hairs56.png" },
"hair_58": {name:"hair_58",img: "resources/customisation/character_hairs/character_hairs57.png"}, hair_58: { name: "hair_58", img: "resources/customisation/character_hairs/character_hairs57.png" },
"hair_59": {name:"hair_59",img: "resources/customisation/character_hairs/character_hairs58.png"}, hair_59: { name: "hair_59", img: "resources/customisation/character_hairs/character_hairs58.png" },
"hair_60": {name:"hair_60",img: "resources/customisation/character_hairs/character_hairs59.png"}, hair_60: { name: "hair_60", img: "resources/customisation/character_hairs/character_hairs59.png" },
"hair_61": {name:"hair_61",img: "resources/customisation/character_hairs/character_hairs60.png"}, hair_61: { name: "hair_61", img: "resources/customisation/character_hairs/character_hairs60.png" },
"hair_62": {name:"hair_62",img: "resources/customisation/character_hairs/character_hairs61.png"}, hair_62: { name: "hair_62", img: "resources/customisation/character_hairs/character_hairs61.png" },
"hair_63": {name:"hair_63",img: "resources/customisation/character_hairs/character_hairs62.png"}, hair_63: { name: "hair_63", img: "resources/customisation/character_hairs/character_hairs62.png" },
"hair_64": {name:"hair_64",img: "resources/customisation/character_hairs/character_hairs63.png"}, hair_64: { name: "hair_64", img: "resources/customisation/character_hairs/character_hairs63.png" },
"hair_65": {name:"hair_65",img: "resources/customisation/character_hairs/character_hairs64.png"}, hair_65: { name: "hair_65", img: "resources/customisation/character_hairs/character_hairs64.png" },
"hair_66": {name:"hair_66",img: "resources/customisation/character_hairs/character_hairs65.png"}, hair_66: { name: "hair_66", img: "resources/customisation/character_hairs/character_hairs65.png" },
"hair_67": {name:"hair_67",img: "resources/customisation/character_hairs/character_hairs66.png"}, hair_67: { name: "hair_67", img: "resources/customisation/character_hairs/character_hairs66.png" },
"hair_68": {name:"hair_68",img: "resources/customisation/character_hairs/character_hairs67.png"}, hair_68: { name: "hair_68", img: "resources/customisation/character_hairs/character_hairs67.png" },
"hair_69": {name:"hair_69",img: "resources/customisation/character_hairs/character_hairs68.png"}, hair_69: { name: "hair_69", img: "resources/customisation/character_hairs/character_hairs68.png" },
"hair_70": {name:"hair_70",img: "resources/customisation/character_hairs/character_hairs69.png"}, hair_70: { name: "hair_70", img: "resources/customisation/character_hairs/character_hairs69.png" },
"hair_71": {name:"hair_71",img: "resources/customisation/character_hairs/character_hairs70.png"}, hair_71: { name: "hair_71", img: "resources/customisation/character_hairs/character_hairs70.png" },
"hair_72": {name:"hair_72",img: "resources/customisation/character_hairs/character_hairs71.png"}, hair_72: { name: "hair_72", img: "resources/customisation/character_hairs/character_hairs71.png" },
"hair_73": {name:"hair_73",img: "resources/customisation/character_hairs/character_hairs72.png"}, hair_73: { name: "hair_73", img: "resources/customisation/character_hairs/character_hairs72.png" },
"hair_74": {name:"hair_74",img: "resources/customisation/character_hairs/character_hairs73.png"} hair_74: { name: "hair_74", img: "resources/customisation/character_hairs/character_hairs73.png" },
}; };
export const CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = { export const CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {
"clothes_1": {name:"clothes_1", img: "resources/customisation/character_clothes/character_clothes0.png"}, clothes_1: { name: "clothes_1", img: "resources/customisation/character_clothes/character_clothes0.png" },
"clothes_2": {name:"clothes_2", img: "resources/customisation/character_clothes/character_clothes1.png"}, clothes_2: { name: "clothes_2", img: "resources/customisation/character_clothes/character_clothes1.png" },
"clothes_3": {name:"clothes_3", img: "resources/customisation/character_clothes/character_clothes2.png"}, clothes_3: { name: "clothes_3", img: "resources/customisation/character_clothes/character_clothes2.png" },
"clothes_4": {name:"clothes_4", img: "resources/customisation/character_clothes/character_clothes3.png"}, clothes_4: { name: "clothes_4", img: "resources/customisation/character_clothes/character_clothes3.png" },
"clothes_5": {name:"clothes_5", img: "resources/customisation/character_clothes/character_clothes4.png"}, clothes_5: { name: "clothes_5", img: "resources/customisation/character_clothes/character_clothes4.png" },
"clothes_6": {name:"clothes_6", img: "resources/customisation/character_clothes/character_clothes5.png"}, clothes_6: { name: "clothes_6", img: "resources/customisation/character_clothes/character_clothes5.png" },
"clothes_7": {name:"clothes_7", img: "resources/customisation/character_clothes/character_clothes6.png"}, clothes_7: { name: "clothes_7", img: "resources/customisation/character_clothes/character_clothes6.png" },
"clothes_8": {name:"clothes_8", img: "resources/customisation/character_clothes/character_clothes7.png"}, clothes_8: { name: "clothes_8", img: "resources/customisation/character_clothes/character_clothes7.png" },
"clothes_9": {name:"clothes_9", img: "resources/customisation/character_clothes/character_clothes8.png"}, clothes_9: { name: "clothes_9", img: "resources/customisation/character_clothes/character_clothes8.png" },
"clothes_10": {name:"clothes_10",img: "resources/customisation/character_clothes/character_clothes9.png"}, clothes_10: { name: "clothes_10", img: "resources/customisation/character_clothes/character_clothes9.png" },
"clothes_11": {name:"clothes_11",img: "resources/customisation/character_clothes/character_clothes10.png"}, clothes_11: { name: "clothes_11", img: "resources/customisation/character_clothes/character_clothes10.png" },
"clothes_12": {name:"clothes_12",img: "resources/customisation/character_clothes/character_clothes11.png"}, clothes_12: { name: "clothes_12", img: "resources/customisation/character_clothes/character_clothes11.png" },
"clothes_13": {name:"clothes_13",img: "resources/customisation/character_clothes/character_clothes12.png"}, clothes_13: { name: "clothes_13", img: "resources/customisation/character_clothes/character_clothes12.png" },
"clothes_14": {name:"clothes_14",img: "resources/customisation/character_clothes/character_clothes13.png"}, clothes_14: { name: "clothes_14", img: "resources/customisation/character_clothes/character_clothes13.png" },
"clothes_15": {name:"clothes_15",img: "resources/customisation/character_clothes/character_clothes14.png"}, clothes_15: { name: "clothes_15", img: "resources/customisation/character_clothes/character_clothes14.png" },
"clothes_16": {name:"clothes_16",img: "resources/customisation/character_clothes/character_clothes15.png"}, clothes_16: { name: "clothes_16", img: "resources/customisation/character_clothes/character_clothes15.png" },
"clothes_17": {name:"clothes_17",img: "resources/customisation/character_clothes/character_clothes16.png"}, clothes_17: { name: "clothes_17", img: "resources/customisation/character_clothes/character_clothes16.png" },
"clothes_18": {name:"clothes_18",img: "resources/customisation/character_clothes/character_clothes17.png"}, clothes_18: { name: "clothes_18", img: "resources/customisation/character_clothes/character_clothes17.png" },
"clothes_19": {name:"clothes_19",img: "resources/customisation/character_clothes/character_clothes18.png"}, clothes_19: { name: "clothes_19", img: "resources/customisation/character_clothes/character_clothes18.png" },
"clothes_20": {name:"clothes_20",img: "resources/customisation/character_clothes/character_clothes19.png"}, clothes_20: { name: "clothes_20", img: "resources/customisation/character_clothes/character_clothes19.png" },
"clothes_21": {name:"clothes_21",img: "resources/customisation/character_clothes/character_clothes20.png"}, clothes_21: { name: "clothes_21", img: "resources/customisation/character_clothes/character_clothes20.png" },
"clothes_22": {name:"clothes_22",img: "resources/customisation/character_clothes/character_clothes21.png"}, clothes_22: { name: "clothes_22", img: "resources/customisation/character_clothes/character_clothes21.png" },
"clothes_23": {name:"clothes_23",img: "resources/customisation/character_clothes/character_clothes22.png"}, clothes_23: { name: "clothes_23", img: "resources/customisation/character_clothes/character_clothes22.png" },
"clothes_24": {name:"clothes_24",img: "resources/customisation/character_clothes/character_clothes23.png"}, clothes_24: { name: "clothes_24", img: "resources/customisation/character_clothes/character_clothes23.png" },
"clothes_25": {name:"clothes_25",img: "resources/customisation/character_clothes/character_clothes24.png"}, clothes_25: { name: "clothes_25", img: "resources/customisation/character_clothes/character_clothes24.png" },
"clothes_26": {name:"clothes_26",img: "resources/customisation/character_clothes/character_clothes25.png"}, clothes_26: { name: "clothes_26", img: "resources/customisation/character_clothes/character_clothes25.png" },
"clothes_27": {name:"clothes_27",img: "resources/customisation/character_clothes/character_clothes26.png"}, clothes_27: { name: "clothes_27", img: "resources/customisation/character_clothes/character_clothes26.png" },
"clothes_28": {name:"clothes_28",img: "resources/customisation/character_clothes/character_clothes27.png"}, clothes_28: { name: "clothes_28", img: "resources/customisation/character_clothes/character_clothes27.png" },
"clothes_29": {name:"clothes_29",img: "resources/customisation/character_clothes/character_clothes28.png"}, clothes_29: { name: "clothes_29", img: "resources/customisation/character_clothes/character_clothes28.png" },
"clothes_30": {name:"clothes_30",img: "resources/customisation/character_clothes/character_clothes29.png"}, clothes_30: { name: "clothes_30", img: "resources/customisation/character_clothes/character_clothes29.png" },
"clothes_31": {name:"clothes_31",img: "resources/customisation/character_clothes/character_clothes30.png"}, clothes_31: { name: "clothes_31", img: "resources/customisation/character_clothes/character_clothes30.png" },
"clothes_32": {name:"clothes_32",img: "resources/customisation/character_clothes/character_clothes31.png"}, clothes_32: { name: "clothes_32", img: "resources/customisation/character_clothes/character_clothes31.png" },
"clothes_33": {name:"clothes_33",img: "resources/customisation/character_clothes/character_clothes32.png"}, clothes_33: { name: "clothes_33", img: "resources/customisation/character_clothes/character_clothes32.png" },
"clothes_34": {name:"clothes_34",img: "resources/customisation/character_clothes/character_clothes33.png"}, clothes_34: { name: "clothes_34", img: "resources/customisation/character_clothes/character_clothes33.png" },
"clothes_35": {name:"clothes_35",img: "resources/customisation/character_clothes/character_clothes34.png"}, clothes_35: { name: "clothes_35", img: "resources/customisation/character_clothes/character_clothes34.png" },
"clothes_36": {name:"clothes_36",img: "resources/customisation/character_clothes/character_clothes35.png"}, clothes_36: { name: "clothes_36", img: "resources/customisation/character_clothes/character_clothes35.png" },
"clothes_37": {name:"clothes_37",img: "resources/customisation/character_clothes/character_clothes36.png"}, clothes_37: { name: "clothes_37", img: "resources/customisation/character_clothes/character_clothes36.png" },
"clothes_38": {name:"clothes_38",img: "resources/customisation/character_clothes/character_clothes37.png"}, clothes_38: { name: "clothes_38", img: "resources/customisation/character_clothes/character_clothes37.png" },
"clothes_39": {name:"clothes_39",img: "resources/customisation/character_clothes/character_clothes38.png"}, clothes_39: { name: "clothes_39", img: "resources/customisation/character_clothes/character_clothes38.png" },
"clothes_40": {name:"clothes_40",img: "resources/customisation/character_clothes/character_clothes39.png"}, clothes_40: { name: "clothes_40", img: "resources/customisation/character_clothes/character_clothes39.png" },
"clothes_41": {name:"clothes_41",img: "resources/customisation/character_clothes/character_clothes40.png"}, clothes_41: { name: "clothes_41", img: "resources/customisation/character_clothes/character_clothes40.png" },
"clothes_42": {name:"clothes_42",img: "resources/customisation/character_clothes/character_clothes41.png"}, clothes_42: { name: "clothes_42", img: "resources/customisation/character_clothes/character_clothes41.png" },
"clothes_43": {name:"clothes_43",img: "resources/customisation/character_clothes/character_clothes42.png"}, clothes_43: { name: "clothes_43", img: "resources/customisation/character_clothes/character_clothes42.png" },
"clothes_44": {name:"clothes_44",img: "resources/customisation/character_clothes/character_clothes43.png"}, clothes_44: { name: "clothes_44", img: "resources/customisation/character_clothes/character_clothes43.png" },
"clothes_45": {name:"clothes_45",img: "resources/customisation/character_clothes/character_clothes44.png"}, clothes_45: { name: "clothes_45", img: "resources/customisation/character_clothes/character_clothes44.png" },
"clothes_46": {name:"clothes_46",img: "resources/customisation/character_clothes/character_clothes45.png"}, clothes_46: { name: "clothes_46", img: "resources/customisation/character_clothes/character_clothes45.png" },
"clothes_47": {name:"clothes_47",img: "resources/customisation/character_clothes/character_clothes46.png"}, clothes_47: { name: "clothes_47", img: "resources/customisation/character_clothes/character_clothes46.png" },
"clothes_48": {name:"clothes_48",img: "resources/customisation/character_clothes/character_clothes47.png"}, clothes_48: { name: "clothes_48", img: "resources/customisation/character_clothes/character_clothes47.png" },
"clothes_49": {name:"clothes_49",img: "resources/customisation/character_clothes/character_clothes48.png"}, clothes_49: { name: "clothes_49", img: "resources/customisation/character_clothes/character_clothes48.png" },
"clothes_50": {name:"clothes_50",img: "resources/customisation/character_clothes/character_clothes49.png"}, clothes_50: { name: "clothes_50", img: "resources/customisation/character_clothes/character_clothes49.png" },
"clothes_51": {name:"clothes_51",img: "resources/customisation/character_clothes/character_clothes50.png"}, clothes_51: { name: "clothes_51", img: "resources/customisation/character_clothes/character_clothes50.png" },
"clothes_52": {name:"clothes_52",img: "resources/customisation/character_clothes/character_clothes51.png"}, clothes_52: { name: "clothes_52", img: "resources/customisation/character_clothes/character_clothes51.png" },
"clothes_53": {name:"clothes_53",img: "resources/customisation/character_clothes/character_clothes52.png"}, clothes_53: { name: "clothes_53", img: "resources/customisation/character_clothes/character_clothes52.png" },
"clothes_54": {name:"clothes_54",img: "resources/customisation/character_clothes/character_clothes53.png"}, clothes_54: { name: "clothes_54", img: "resources/customisation/character_clothes/character_clothes53.png" },
"clothes_55": {name:"clothes_55",img: "resources/customisation/character_clothes/character_clothes54.png"}, clothes_55: { name: "clothes_55", img: "resources/customisation/character_clothes/character_clothes54.png" },
"clothes_56": {name:"clothes_56",img: "resources/customisation/character_clothes/character_clothes55.png"}, clothes_56: { name: "clothes_56", img: "resources/customisation/character_clothes/character_clothes55.png" },
"clothes_57": {name:"clothes_57",img: "resources/customisation/character_clothes/character_clothes56.png"}, clothes_57: { name: "clothes_57", img: "resources/customisation/character_clothes/character_clothes56.png" },
"clothes_58": {name:"clothes_58",img: "resources/customisation/character_clothes/character_clothes57.png"}, clothes_58: { name: "clothes_58", img: "resources/customisation/character_clothes/character_clothes57.png" },
"clothes_59": {name:"clothes_59",img: "resources/customisation/character_clothes/character_clothes58.png"}, clothes_59: { name: "clothes_59", img: "resources/customisation/character_clothes/character_clothes58.png" },
"clothes_60": {name:"clothes_60",img: "resources/customisation/character_clothes/character_clothes59.png"}, clothes_60: { name: "clothes_60", img: "resources/customisation/character_clothes/character_clothes59.png" },
"clothes_61": {name:"clothes_61",img: "resources/customisation/character_clothes/character_clothes60.png"}, clothes_61: { name: "clothes_61", img: "resources/customisation/character_clothes/character_clothes60.png" },
"clothes_62": {name:"clothes_62",img: "resources/customisation/character_clothes/character_clothes61.png"}, clothes_62: { name: "clothes_62", img: "resources/customisation/character_clothes/character_clothes61.png" },
"clothes_63": {name:"clothes_63",img: "resources/customisation/character_clothes/character_clothes62.png"}, clothes_63: { name: "clothes_63", img: "resources/customisation/character_clothes/character_clothes62.png" },
"clothes_64": {name:"clothes_64",img: "resources/customisation/character_clothes/character_clothes63.png"}, clothes_64: { name: "clothes_64", img: "resources/customisation/character_clothes/character_clothes63.png" },
"clothes_65": {name:"clothes_65",img: "resources/customisation/character_clothes/character_clothes64.png"}, clothes_65: { name: "clothes_65", img: "resources/customisation/character_clothes/character_clothes64.png" },
"clothes_66": {name:"clothes_66",img: "resources/customisation/character_clothes/character_clothes65.png"}, clothes_66: { name: "clothes_66", img: "resources/customisation/character_clothes/character_clothes65.png" },
"clothes_67": {name:"clothes_67",img: "resources/customisation/character_clothes/character_clothes66.png"}, clothes_67: { name: "clothes_67", img: "resources/customisation/character_clothes/character_clothes66.png" },
"clothes_68": {name:"clothes_68",img: "resources/customisation/character_clothes/character_clothes67.png"}, clothes_68: { name: "clothes_68", img: "resources/customisation/character_clothes/character_clothes67.png" },
"clothes_69": {name:"clothes_69",img: "resources/customisation/character_clothes/character_clothes68.png"}, clothes_69: { name: "clothes_69", img: "resources/customisation/character_clothes/character_clothes68.png" },
"clothes_70": {name:"clothes_70",img: "resources/customisation/character_clothes/character_clothes69.png"}, clothes_70: { name: "clothes_70", img: "resources/customisation/character_clothes/character_clothes69.png" },
"clothes_pride_shirt": {name:"clothes_pride_shirt",img: "resources/customisation/character_clothes/pride_shirt.png"}, clothes_pride_shirt: {
"clothes_black_hoodie": {name:"clothes_black_hoodie",img: "resources/customisation/character_clothes/black_hoodie.png"}, name: "clothes_pride_shirt",
"clothes_white_hoodie": {name:"clothes_white_hoodie",img: "resources/customisation/character_clothes/white_hoodie.png"}, img: "resources/customisation/character_clothes/pride_shirt.png",
"clothes_engelbert": {name:"clothes_engelbert",img: "resources/customisation/character_clothes/engelbert.png"} },
clothes_black_hoodie: {
name: "clothes_black_hoodie",
img: "resources/customisation/character_clothes/black_hoodie.png",
},
clothes_white_hoodie: {
name: "clothes_white_hoodie",
img: "resources/customisation/character_clothes/white_hoodie.png",
},
clothes_engelbert: { name: "clothes_engelbert", img: "resources/customisation/character_clothes/engelbert.png" },
}; };
export const HATS_RESOURCES: BodyResourceDescriptionListInterface = { export const HATS_RESOURCES: BodyResourceDescriptionListInterface = {
"hats_1": {name: "hats_1", img: "resources/customisation/character_hats/character_hats1.png"}, hats_1: { name: "hats_1", img: "resources/customisation/character_hats/character_hats1.png" },
"hats_2": {name: "hats_2", img: "resources/customisation/character_hats/character_hats2.png"}, hats_2: { name: "hats_2", img: "resources/customisation/character_hats/character_hats2.png" },
"hats_3": {name: "hats_3", img: "resources/customisation/character_hats/character_hats3.png"}, hats_3: { name: "hats_3", img: "resources/customisation/character_hats/character_hats3.png" },
"hats_4": {name: "hats_4", img: "resources/customisation/character_hats/character_hats4.png"}, hats_4: { name: "hats_4", img: "resources/customisation/character_hats/character_hats4.png" },
"hats_5": {name: "hats_5", img: "resources/customisation/character_hats/character_hats5.png"}, hats_5: { name: "hats_5", img: "resources/customisation/character_hats/character_hats5.png" },
"hats_6": {name: "hats_6", img: "resources/customisation/character_hats/character_hats6.png"}, hats_6: { name: "hats_6", img: "resources/customisation/character_hats/character_hats6.png" },
"hats_7": {name: "hats_7", img: "resources/customisation/character_hats/character_hats7.png"}, hats_7: { name: "hats_7", img: "resources/customisation/character_hats/character_hats7.png" },
"hats_8": {name: "hats_8", img: "resources/customisation/character_hats/character_hats8.png"}, hats_8: { name: "hats_8", img: "resources/customisation/character_hats/character_hats8.png" },
"hats_9": {name: "hats_9", img: "resources/customisation/character_hats/character_hats9.png"}, hats_9: { name: "hats_9", img: "resources/customisation/character_hats/character_hats9.png" },
"hats_10": {name: "hats_10", img: "resources/customisation/character_hats/character_hats10.png"}, hats_10: { name: "hats_10", img: "resources/customisation/character_hats/character_hats10.png" },
"hats_11": {name: "hats_11", img: "resources/customisation/character_hats/character_hats11.png"}, hats_11: { name: "hats_11", img: "resources/customisation/character_hats/character_hats11.png" },
"hats_12": {name: "hats_12", img: "resources/customisation/character_hats/character_hats12.png"}, hats_12: { name: "hats_12", img: "resources/customisation/character_hats/character_hats12.png" },
"hats_13": {name: "hats_13", img: "resources/customisation/character_hats/character_hats13.png"}, hats_13: { name: "hats_13", img: "resources/customisation/character_hats/character_hats13.png" },
"hats_14": {name: "hats_14", img: "resources/customisation/character_hats/character_hats14.png"}, hats_14: { name: "hats_14", img: "resources/customisation/character_hats/character_hats14.png" },
"hats_15": {name: "hats_15", img: "resources/customisation/character_hats/character_hats15.png"}, hats_15: { name: "hats_15", img: "resources/customisation/character_hats/character_hats15.png" },
"hats_16": {name: "hats_16", img: "resources/customisation/character_hats/character_hats16.png"}, hats_16: { name: "hats_16", img: "resources/customisation/character_hats/character_hats16.png" },
"hats_17": {name: "hats_17", img: "resources/customisation/character_hats/character_hats17.png"}, hats_17: { name: "hats_17", img: "resources/customisation/character_hats/character_hats17.png" },
"hats_18": {name: "hats_18", img: "resources/customisation/character_hats/character_hats18.png"}, hats_18: { name: "hats_18", img: "resources/customisation/character_hats/character_hats18.png" },
"hats_19": {name: "hats_19", img: "resources/customisation/character_hats/character_hats19.png"}, hats_19: { name: "hats_19", img: "resources/customisation/character_hats/character_hats19.png" },
"hats_20": {name: "hats_20", img: "resources/customisation/character_hats/character_hats20.png"}, hats_20: { name: "hats_20", img: "resources/customisation/character_hats/character_hats20.png" },
"hats_21": {name: "hats_21", img: "resources/customisation/character_hats/character_hats21.png"}, hats_21: { name: "hats_21", img: "resources/customisation/character_hats/character_hats21.png" },
"hats_22": {name: "hats_22", img: "resources/customisation/character_hats/character_hats22.png"}, hats_22: { name: "hats_22", img: "resources/customisation/character_hats/character_hats22.png" },
"hats_23": {name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png"}, hats_23: { name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png" },
"hats_24": {name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png"}, hats_24: { name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png" },
"hats_25": {name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png"}, hats_25: { name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png" },
"hats_26": {name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png"}, hats_26: { name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png" },
"tinfoil_hat1": {name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png"} tinfoil_hat1: { name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png" },
}; };
export const ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = { export const ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {
"accessory_1": {name: "accessory_1", img: "resources/customisation/character_accessories/character_accessories1.png"}, accessory_1: {
"accessory_2": {name: "accessory_2", img: "resources/customisation/character_accessories/character_accessories2.png"}, name: "accessory_1",
"accessory_3": {name: "accessory_3", img: "resources/customisation/character_accessories/character_accessories3.png"}, img: "resources/customisation/character_accessories/character_accessories1.png",
"accessory_4": {name: "accessory_4", img: "resources/customisation/character_accessories/character_accessories4.png"}, },
"accessory_5": {name: "accessory_5", img: "resources/customisation/character_accessories/character_accessories5.png"}, accessory_2: {
"accessory_6": {name: "accessory_6", img: "resources/customisation/character_accessories/character_accessories6.png"}, name: "accessory_2",
"accessory_7": {name: "accessory_7", img: "resources/customisation/character_accessories/character_accessories7.png"}, img: "resources/customisation/character_accessories/character_accessories2.png",
"accessory_8": {name: "accessory_8", img: "resources/customisation/character_accessories/character_accessories8.png"}, },
"accessory_9": {name: "accessory_9", img: "resources/customisation/character_accessories/character_accessories9.png"}, accessory_3: {
"accessory_10": {name: "accessory_10", img: "resources/customisation/character_accessories/character_accessories10.png"}, name: "accessory_3",
"accessory_11": {name: "accessory_11", img: "resources/customisation/character_accessories/character_accessories11.png"}, img: "resources/customisation/character_accessories/character_accessories3.png",
"accessory_12": {name: "accessory_12", img: "resources/customisation/character_accessories/character_accessories12.png"}, },
"accessory_13": {name: "accessory_13", img: "resources/customisation/character_accessories/character_accessories13.png"}, accessory_4: {
"accessory_14": {name: "accessory_14", img: "resources/customisation/character_accessories/character_accessories14.png"}, name: "accessory_4",
"accessory_15": {name: "accessory_15", img: "resources/customisation/character_accessories/character_accessories15.png"}, img: "resources/customisation/character_accessories/character_accessories4.png",
"accessory_16": {name: "accessory_16", img: "resources/customisation/character_accessories/character_accessories16.png"}, },
"accessory_17": {name: "accessory_17", img: "resources/customisation/character_accessories/character_accessories17.png"}, accessory_5: {
"accessory_18": {name: "accessory_18", img: "resources/customisation/character_accessories/character_accessories18.png"}, name: "accessory_5",
"accessory_19": {name: "accessory_19", img: "resources/customisation/character_accessories/character_accessories19.png"}, img: "resources/customisation/character_accessories/character_accessories5.png",
"accessory_20": {name: "accessory_20", img: "resources/customisation/character_accessories/character_accessories20.png"}, },
"accessory_21": {name: "accessory_21", img: "resources/customisation/character_accessories/character_accessories21.png"}, accessory_6: {
"accessory_22": {name: "accessory_22", img: "resources/customisation/character_accessories/character_accessories22.png"}, name: "accessory_6",
"accessory_23": {name: "accessory_23", img: "resources/customisation/character_accessories/character_accessories23.png"}, img: "resources/customisation/character_accessories/character_accessories6.png",
"accessory_24": {name: "accessory_24", img: "resources/customisation/character_accessories/character_accessories24.png"}, },
"accessory_25": {name: "accessory_25", img: "resources/customisation/character_accessories/character_accessories25.png"}, accessory_7: {
"accessory_26": {name: "accessory_26", img: "resources/customisation/character_accessories/character_accessories26.png"}, name: "accessory_7",
"accessory_27": {name: "accessory_27", img: "resources/customisation/character_accessories/character_accessories27.png"}, img: "resources/customisation/character_accessories/character_accessories7.png",
"accessory_28": {name: "accessory_28", img: "resources/customisation/character_accessories/character_accessories28.png"}, },
"accessory_29": {name: "accessory_29", img: "resources/customisation/character_accessories/character_accessories29.png"}, accessory_8: {
"accessory_30": {name: "accessory_30", img: "resources/customisation/character_accessories/character_accessories30.png"}, name: "accessory_8",
"accessory_31": {name: "accessory_31", img: "resources/customisation/character_accessories/character_accessories31.png"}, img: "resources/customisation/character_accessories/character_accessories8.png",
"accessory_32": {name: "accessory_32", img: "resources/customisation/character_accessories/character_accessories32.png"}, },
"accessory_mate_bottle": {name: "accessory_mate_bottle", img: "resources/customisation/character_accessories/mate_bottle1.png"}, accessory_9: {
"accessory_mask": {name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png"} name: "accessory_9",
img: "resources/customisation/character_accessories/character_accessories9.png",
},
accessory_10: {
name: "accessory_10",
img: "resources/customisation/character_accessories/character_accessories10.png",
},
accessory_11: {
name: "accessory_11",
img: "resources/customisation/character_accessories/character_accessories11.png",
},
accessory_12: {
name: "accessory_12",
img: "resources/customisation/character_accessories/character_accessories12.png",
},
accessory_13: {
name: "accessory_13",
img: "resources/customisation/character_accessories/character_accessories13.png",
},
accessory_14: {
name: "accessory_14",
img: "resources/customisation/character_accessories/character_accessories14.png",
},
accessory_15: {
name: "accessory_15",
img: "resources/customisation/character_accessories/character_accessories15.png",
},
accessory_16: {
name: "accessory_16",
img: "resources/customisation/character_accessories/character_accessories16.png",
},
accessory_17: {
name: "accessory_17",
img: "resources/customisation/character_accessories/character_accessories17.png",
},
accessory_18: {
name: "accessory_18",
img: "resources/customisation/character_accessories/character_accessories18.png",
},
accessory_19: {
name: "accessory_19",
img: "resources/customisation/character_accessories/character_accessories19.png",
},
accessory_20: {
name: "accessory_20",
img: "resources/customisation/character_accessories/character_accessories20.png",
},
accessory_21: {
name: "accessory_21",
img: "resources/customisation/character_accessories/character_accessories21.png",
},
accessory_22: {
name: "accessory_22",
img: "resources/customisation/character_accessories/character_accessories22.png",
},
accessory_23: {
name: "accessory_23",
img: "resources/customisation/character_accessories/character_accessories23.png",
},
accessory_24: {
name: "accessory_24",
img: "resources/customisation/character_accessories/character_accessories24.png",
},
accessory_25: {
name: "accessory_25",
img: "resources/customisation/character_accessories/character_accessories25.png",
},
accessory_26: {
name: "accessory_26",
img: "resources/customisation/character_accessories/character_accessories26.png",
},
accessory_27: {
name: "accessory_27",
img: "resources/customisation/character_accessories/character_accessories27.png",
},
accessory_28: {
name: "accessory_28",
img: "resources/customisation/character_accessories/character_accessories28.png",
},
accessory_29: {
name: "accessory_29",
img: "resources/customisation/character_accessories/character_accessories29.png",
},
accessory_30: {
name: "accessory_30",
img: "resources/customisation/character_accessories/character_accessories30.png",
},
accessory_31: {
name: "accessory_31",
img: "resources/customisation/character_accessories/character_accessories31.png",
},
accessory_32: {
name: "accessory_32",
img: "resources/customisation/character_accessories/character_accessories32.png",
},
accessory_mate_bottle: {
name: "accessory_mate_bottle",
img: "resources/customisation/character_accessories/mate_bottle1.png",
},
accessory_mask: { name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png" },
}; };
export const LAYERS: BodyResourceDescriptionListInterface[] = [ export const LAYERS: BodyResourceDescriptionListInterface[] = [
@ -336,9 +442,9 @@ export const LAYERS: BodyResourceDescriptionListInterface[] = [
HAIR_RESOURCES, HAIR_RESOURCES,
CLOTHES_RESOURCES, CLOTHES_RESOURCES,
HATS_RESOURCES, HATS_RESOURCES,
ACCESSORIES_RESOURCES ACCESSORIES_RESOURCES,
]; ];
export const OBJECTS: BodyResourceDescriptionInterface[] = [ export const OBJECTS: BodyResourceDescriptionInterface[] = [
{name:'teleportation', img:'resources/objects/teleportation.png'}, { name: "teleportation", img: "resources/objects/teleportation.png" },
]; ];

View file

@ -1,14 +1,12 @@
import Scene = Phaser.Scene; import Scene = Phaser.Scene;
import type {Character} from "./Character"; import type { Character } from "./Character";
//todo: improve this WIP //todo: improve this WIP
export class SpeechBubble { export class SpeechBubble {
private bubble: Phaser.GameObjects.Graphics; private bubble: Phaser.GameObjects.Graphics;
private content: Phaser.GameObjects.Text; private content: Phaser.GameObjects.Text;
constructor(scene: Scene, player: Character, text: string = "") { constructor(scene: Scene, player: Character, text: string = "") {
const bubbleHeight = 50; const bubbleHeight = 50;
const bubblePadding = 10; const bubblePadding = 10;
const bubbleWidth = bubblePadding * 2 + text.length * 10; const bubbleWidth = bubblePadding * 2 + text.length * 10;
@ -49,15 +47,24 @@ export class SpeechBubble {
this.bubble.lineBetween(point2X, point2Y, point3X, point3Y); this.bubble.lineBetween(point2X, point2Y, point3X, point3Y);
this.bubble.lineBetween(point1X, point1Y, point3X, point3Y); this.bubble.lineBetween(point1X, point1Y, point3X, point3Y);
this.content = scene.add.text(0, 0, text, { fontFamily: 'Arial', fontSize: '20', color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } }); this.content = scene.add.text(0, 0, text, {
fontFamily: "Arial",
fontSize: "20",
color: "#000000",
align: "center",
wordWrap: { width: bubbleWidth - bubblePadding * 2 },
});
player.add(this.content); player.add(this.content);
const bounds = this.content.getBounds(); const bounds = this.content.getBounds();
this.content.setPosition(this.bubble.x + (bubbleWidth / 2) - (bounds.width / 2), this.bubble.y + (bubbleHeight / 2) - (bounds.height / 2)); this.content.setPosition(
this.bubble.x + bubbleWidth / 2 - bounds.width / 2,
this.bubble.y + bubbleHeight / 2 - bounds.height / 2
);
} }
destroy(): void { destroy(): void {
this.bubble.setVisible(false) //todo find a better way this.bubble.setVisible(false); //todo find a better way
this.bubble.destroy(); this.bubble.destroy();
this.content.destroy(); this.content.destroy();
} }

View file

@ -1,8 +1,7 @@
export class Sprite extends Phaser.GameObjects.Sprite { export class Sprite extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number, texture: string, frame?: number | string) { constructor(scene: Phaser.Scene, x: number, y: number, texture: string, frame?: number | string) {
super(scene, x, y, texture, frame); super(scene, x, y, texture, frame);
scene.sys.updateList.add(this); scene.sys.updateList.add(this);
scene.sys.displayList.add(this); scene.sys.displayList.add(this);
} }
} }

View file

@ -1,37 +1,36 @@
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import {emoteEventStream} from "../../Connexion/EmoteEventStream"; import { emoteEventStream } from "../../Connexion/EmoteEventStream";
import type {GameScene} from "./GameScene"; import type { GameScene } from "./GameScene";
import type {RadialMenuItem} from "../Components/RadialMenu"; import type { RadialMenuItem } from "../Components/RadialMenu";
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type {Subscription} from "rxjs"; import type { Subscription } from "rxjs";
interface RegisteredEmote extends BodyResourceDescriptionInterface { interface RegisteredEmote extends BodyResourceDescriptionInterface {
name: string; name: string;
img: string; img: string;
} }
export const emotes: {[key: string]: RegisteredEmote} = { export const emotes: { [key: string]: RegisteredEmote } = {
'emote-heart': {name: 'emote-heart', img: 'resources/emotes/heart-emote.png'}, "emote-heart": { name: "emote-heart", img: "resources/emotes/heart-emote.png" },
'emote-clap': {name: 'emote-clap', img: 'resources/emotes/clap-emote.png'}, "emote-clap": { name: "emote-clap", img: "resources/emotes/clap-emote.png" },
'emote-hand': {name: 'emote-hand', img: 'resources/emotes/hand-emote.png'}, "emote-hand": { name: "emote-hand", img: "resources/emotes/hand-emote.png" },
'emote-thanks': {name: 'emote-thanks', img: 'resources/emotes/thanks-emote.png'}, "emote-thanks": { name: "emote-thanks", img: "resources/emotes/thanks-emote.png" },
'emote-thumb-up': {name: 'emote-thumb-up', img: 'resources/emotes/thumb-up-emote.png'}, "emote-thumb-up": { name: "emote-thumb-up", img: "resources/emotes/thumb-up-emote.png" },
'emote-thumb-down': {name: 'emote-thumb-down', img: 'resources/emotes/thumb-down-emote.png'}, "emote-thumb-down": { name: "emote-thumb-down", img: "resources/emotes/thumb-down-emote.png" },
}; };
export class EmoteManager { export class EmoteManager {
private subscription: Subscription; private subscription: Subscription;
constructor(private scene: GameScene) { constructor(private scene: GameScene) {
this.subscription = emoteEventStream.stream.subscribe((event) => { this.subscription = emoteEventStream.stream.subscribe((event) => {
const actor = this.scene.MapPlayersByKey.get(event.userId); const actor = this.scene.MapPlayersByKey.get(event.userId);
if (actor) { if (actor) {
this.lazyLoadEmoteTexture(event.emoteName).then(emoteKey => { this.lazyLoadEmoteTexture(event.emoteName).then((emoteKey) => {
actor.playEmote(emoteKey); actor.playEmote(emoteKey);
}) });
} }
}) });
} }
createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) { createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) {
return new Promise<string>((res) => { return new Promise<string>((res) => {
@ -39,20 +38,22 @@ export class EmoteManager {
return res(playerResourceDescriptor.name); return res(playerResourceDescriptor.name);
} }
loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img); loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img);
loadPlugin.once('filecomplete-image-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor.name)); loadPlugin.once("filecomplete-image-" + playerResourceDescriptor.name, () =>
res(playerResourceDescriptor.name)
);
}); });
} }
lazyLoadEmoteTexture(textureKey: string): Promise<string> { lazyLoadEmoteTexture(textureKey: string): Promise<string> {
const emoteDescriptor = emotes[textureKey]; const emoteDescriptor = emotes[textureKey];
if (emoteDescriptor === undefined) { if (emoteDescriptor === undefined) {
throw 'Emote not found!'; throw "Emote not found!";
} }
const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor); const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor);
this.scene.load.start(); this.scene.load.start();
return loadPromise return loadPromise;
} }
getMenuImages(): Promise<RadialMenuItem[]> { getMenuImages(): Promise<RadialMenuItem[]> {
const promises = []; const promises = [];
for (const key in emotes) { for (const key in emotes) {
@ -60,14 +61,14 @@ export class EmoteManager {
return { return {
image: textureKey, image: textureKey,
name: textureKey, name: textureKey,
} };
}); });
promises.push(promise); promises.push(promise);
} }
return Promise.all(promises); return Promise.all(promises);
} }
destroy() { destroy() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
} }

View file

@ -18,6 +18,7 @@ export class GameManager {
private characterLayers: string[] | null; private characterLayers: string[] | null;
private companion: string | null; private companion: string | null;
private startRoom!: Room; private startRoom!: Room;
private cameraSetup?: { video: unknown; audio: unknown };
currentGameSceneName: string | null = null; currentGameSceneName: string | null = null;
// Note: this scenePlugin is the scenePlugin of the EntryScene. We should always provide a key in methods called on this scenePlugin. // Note: this scenePlugin is the scenePlugin of the EntryScene. We should always provide a key in methods called on this scenePlugin.
private scenePlugin!: Phaser.Scenes.ScenePlugin; private scenePlugin!: Phaser.Scenes.ScenePlugin;
@ -26,6 +27,7 @@ export class GameManager {
this.playerName = localUserStore.getName(); this.playerName = localUserStore.getName();
this.characterLayers = localUserStore.getCharacterLayers(); this.characterLayers = localUserStore.getCharacterLayers();
this.companion = localUserStore.getCompanion(); this.companion = localUserStore.getCompanion();
this.cameraSetup = localUserStore.getCameraSetup();
} }
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
@ -33,12 +35,17 @@ export class GameManager {
this.startRoom = await connectionManager.initGameConnexion(); this.startRoom = await connectionManager.initGameConnexion();
this.loadMap(this.startRoom); this.loadMap(this.startRoom);
if (!this.playerName) { //If player name was not set show login scene with player name
//If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous)
if (!this.playerName || (this.startRoom.authenticationMandatory && !localUserStore.getAuthToken())) {
return LoginSceneName; return LoginSceneName;
} else if (!this.characterLayers || !this.characterLayers.length) { } else if (!this.characterLayers || !this.characterLayers.length) {
return SelectCharacterSceneName; return SelectCharacterSceneName;
} else { } else if (this.cameraSetup == undefined) {
return EnableCameraSceneName; return EnableCameraSceneName;
} else {
this.activeMenuSceneAndHelpCameraSettings();
return this.startRoom.key;
} }
} }
@ -84,7 +91,14 @@ export class GameManager {
public goToStartingMap(): void { public goToStartingMap(): void {
console.log("starting " + (this.currentGameSceneName || this.startRoom.key)); console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
this.scenePlugin.start(this.currentGameSceneName || this.startRoom.key); this.scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
this.activeMenuSceneAndHelpCameraSettings();
}
/**
* @private
* @return void
*/
private activeMenuSceneAndHelpCameraSettings(): void {
if ( if (
!localUserStore.getHelpCameraSettingsShown() && !localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState)) (!get(requestedMicrophoneState) || !get(requestedCameraState))
@ -131,6 +145,10 @@ export class GameManager {
if (this.currentGameSceneName === null) throw "No current scene id set!"; if (this.currentGameSceneName === null) throw "No current scene id set!";
return this.scenePlugin.get(this.currentGameSceneName) as GameScene; return this.scenePlugin.get(this.currentGameSceneName) as GameScene;
} }
public get currentStartedRoom() {
return this.startRoom;
}
} }
export const gameManager = new GameManager(); export const gameManager = new GameManager();

View file

@ -0,0 +1,55 @@
import type { GameScene } from "./GameScene";
import type { GameMap } from "./GameMap";
import { scriptUtils } from "../../Api/ScriptUtils";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import {
ON_ACTION_TRIGGER_BUTTON,
TRIGGER_WEBSITE_PROPERTIES,
WEBSITE_MESSAGE_PROPERTIES,
} from "../../WebRtc/LayoutManager";
export class GameMapPropertiesListener {
constructor(private scene: GameScene, private gameMap: GameMap) {}
register() {
this.gameMap.onPropertyChange("openTab", (newValue) => {
if (typeof newValue == "string" && newValue.length) {
scriptUtils.openTab(newValue);
}
});
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("openWebsite");
coWebsiteManager.closeCoWebsite();
} else {
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(
newValue as string,
this.scene.MapUrlFile,
allProps.get("openWebsiteAllowApi") as boolean | undefined,
allProps.get("openWebsitePolicy") as string | undefined,
allProps.get("openWebsiteWidth") as number | undefined
);
layoutManagerActionStore.removeAction("openWebsite");
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES);
if (message === undefined) {
message = "Press SPACE or touch here to open web site";
}
layoutManagerActionStore.addAction({
uuid: "openWebsite",
type: "message",
message: message,
callback: () => openWebsiteFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openWebsiteFunction();
}
}
});
}
}

View file

@ -37,7 +37,7 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer"; import { SimplePeer } from "../../WebRtc/SimplePeer";
import { addLoader } from "../Components/Loader"; import { addLoader, removeLoader } from "../Components/Loader";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer"; import { RemotePlayer } from "../Entity/RemotePlayer";
@ -92,6 +92,8 @@ import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore"; import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import type { RadialMenuItem } from "../Components/RadialMenu";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -290,8 +292,11 @@ export class GameScene extends DirtyScene {
} }
//once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading. //once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading.
console.error("Error when loading: ", file);
if (this.preloading) { if (this.preloading) {
//remove loader in progress
removeLoader(this);
//display an error scene
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: "Network error", title: "Network error",
subTitle: "An error occurred while loading resource:", subTitle: "An error occurred while loading resource:",
@ -580,6 +585,7 @@ export class GameScene extends DirtyScene {
this.updateCameraOffset(box) this.updateCameraOffset(box)
); );
new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange(); this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) { if (!this.room.isDisconnected()) {
@ -713,25 +719,8 @@ export class GameScene extends DirtyScene {
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection); this.simplePeer = new SimplePeer(this.connection);
peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(peer) {
//self.openChatIcon.setVisible(true);
audioManagerVolumeStore.setTalking(true);
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
//self.openChatIcon.setVisible(false);
audioManagerVolumeStore.setTalking(false);
}
},
});
//listen event to share position of user //listen event to share position of user
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)); this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this));
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)); this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this));
@ -825,40 +814,7 @@ export class GameScene extends DirtyScene {
}, 2000); }, 2000);
} }
}); });
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("openWebsite");
coWebsiteManager.closeCoWebsite();
} else {
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(
newValue as string,
this.MapUrlFile,
allProps.get("openWebsiteAllowApi") as boolean | undefined,
allProps.get("openWebsitePolicy") as string | undefined,
allProps.get("openWebsiteWidth") as number | undefined
);
layoutManagerActionStore.removeAction("openWebsite");
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES);
if (message === undefined) {
message = "Press SPACE or touch here to open web site";
}
layoutManagerActionStore.addAction({
uuid: "openWebsite",
type: "message",
message: message,
callback: () => openWebsiteFunction(),
userInputManager: this.userInputManager,
});
} else {
openWebsiteFunction();
}
}
});
this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi"); layoutManagerActionStore.removeAction("jitsi");
@ -898,8 +854,10 @@ export class GameScene extends DirtyScene {
this.gameMap.onPropertyChange("silent", (newValue, oldValue) => { this.gameMap.onPropertyChange("silent", (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") { if (newValue === undefined || newValue === false || newValue === "") {
this.connection?.setSilent(false); this.connection?.setSilent(false);
this.CurrentPlayer.noSilent();
} else { } else {
this.connection?.setSilent(true); this.connection?.setSilent(true);
this.CurrentPlayer.isSilent();
} }
}); });
this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => {
@ -1306,7 +1264,9 @@ ${escapedMessage}
if (!targetRoom.isEqual(this.room)) { if (!targetRoom.isEqual(this.room)) {
if (this.scene.get(targetRoom.key) === null) { if (this.scene.get(targetRoom.key) === null) {
console.error("next room not loaded", targetRoom.key); console.error("next room not loaded", targetRoom.key);
return; // Try to load next dame room from exit URL
// The policy of room can to be updated during a session and not load before
await this.loadNextGameFromExitUrl(targetRoom.key);
} }
this.cleanupClosingScene(); this.cleanupClosingScene();
this.scene.stop(); this.scene.stop();

View file

@ -1,10 +1,7 @@
import type { RoomConnection } from "../../Connexion/RoomConnection"; import type { RoomConnection } from "../../Connexion/RoomConnection";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import type { ITile, ITiledMapObject } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap";
import type { Var } from "svelte/types/compiler/interfaces";
import { init } from "svelte/internal";
interface Variable { interface Variable {
defaultValue: unknown; defaultValue: unknown;
@ -100,24 +97,33 @@ export class SharedVariablesManager {
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> { private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
const objects = new Map<string, Variable>(); const objects = new Map<string, Variable>();
for (const layer of gameMap.getMap().layers) { for (const layer of gameMap.getMap().layers) {
if (layer.type === "objectgroup") { this.recursiveFindVariablesInLayer(layer, objects);
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
} }
return objects; return objects;
} }
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
continue;
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
} else if (layer.type === "group") {
for (const innerLayer of layer.layers) {
this.recursiveFindVariablesInLayer(innerLayer, objects);
}
}
}
private static iTiledObjectToVariable(object: ITiledMapObject): Variable { private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = { const variable: Variable = {
defaultValue: undefined, defaultValue: undefined,

View file

@ -4,35 +4,39 @@ import BaseSound = Phaser.Sound.BaseSound;
import SoundConfig = Phaser.Types.Sound.SoundConfig; import SoundConfig = Phaser.Types.Sound.SoundConfig;
class SoundManager { class SoundManager {
private soundPromises : Map<string,Promise<BaseSound>> = new Map<string, Promise<Phaser.Sound.BaseSound>>(); private soundPromises: Map<string, Promise<BaseSound>> = new Map<string, Promise<Phaser.Sound.BaseSound>>();
public loadSound (loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string) : Promise<BaseSound> { public loadSound(loadPlugin: LoaderPlugin, soundManager: BaseSoundManager, soundUrl: string): Promise<BaseSound> {
let soundPromise = this.soundPromises.get(soundUrl); let soundPromise = this.soundPromises.get(soundUrl);
if (soundPromise !== undefined) { if (soundPromise !== undefined) {
return soundPromise; return soundPromise;
} }
soundPromise = new Promise<BaseSound>((res) => { soundPromise = new Promise<BaseSound>((res) => {
const sound = soundManager.get(soundUrl); const sound = soundManager.get(soundUrl);
if (sound !== null) { if (sound !== null) {
return res(sound); return res(sound);
} }
loadPlugin.audio(soundUrl, soundUrl); loadPlugin.audio(soundUrl, soundUrl);
loadPlugin.once('filecomplete-audio-' + soundUrl, () => { loadPlugin.once("filecomplete-audio-" + soundUrl, () => {
res(soundManager.add(soundUrl)); res(soundManager.add(soundUrl));
}); });
loadPlugin.start(); loadPlugin.start();
}); });
this.soundPromises.set(soundUrl,soundPromise); this.soundPromises.set(soundUrl, soundPromise);
return soundPromise; return soundPromise;
} }
public async playSound(loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string, config: SoundConfig|undefined) : Promise<void> { public async playSound(
const sound = await this.loadSound(loadPlugin,soundManager,soundUrl); loadPlugin: LoaderPlugin,
soundManager: BaseSoundManager,
soundUrl: string,
config: SoundConfig | undefined
): Promise<void> {
const sound = await this.loadSound(loadPlugin, soundManager, soundUrl);
if (config === undefined) sound.play(); if (config === undefined) sound.play();
else sound.play(config); else sound.play(config);
} }
public stopSound(soundManager : BaseSoundManager,soundUrl : string){ public stopSound(soundManager: BaseSoundManager, soundUrl: string) {
soundManager.get(soundUrl).stop(); soundManager.get(soundUrl).stop();
} }
} }

View file

@ -3,17 +3,23 @@
* It has coordinates and an "activation radius" * It has coordinates and an "activation radius"
*/ */
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
type EventCallback = (state: unknown, parameters: unknown) => void; type EventCallback = (state: unknown, parameters: unknown) => void;
export class ActionableItem { export class ActionableItem {
private readonly activationRadiusSquared : number; private readonly activationRadiusSquared: number;
private isSelectable: boolean = false; private isSelectable: boolean = false;
private callbacks: Map<string, Array<EventCallback>> = new Map<string, Array<EventCallback>>(); private callbacks: Map<string, Array<EventCallback>> = new Map<string, Array<EventCallback>>();
public constructor(private id: number, private sprite: Sprite, private eventHandler: GameScene, private activationRadius: number, private onActivateCallback: (item: ActionableItem) => void) { public constructor(
private id: number,
private sprite: Sprite,
private eventHandler: GameScene,
private activationRadius: number,
private onActivateCallback: (item: ActionableItem) => void
) {
this.activationRadiusSquared = activationRadius * activationRadius; this.activationRadiusSquared = activationRadius * activationRadius;
} }
@ -25,8 +31,8 @@ export class ActionableItem {
* Returns the square of the distance to the object center IF we are in item action range * Returns the square of the distance to the object center IF we are in item action range
* OR null if we are out of range. * OR null if we are out of range.
*/ */
public actionableDistance(x: number, y: number): number|null { public actionableDistance(x: number, y: number): number | null {
const distanceSquared = (x - this.sprite.x)*(x - this.sprite.x) + (y - this.sprite.y)*(y - this.sprite.y); const distanceSquared = (x - this.sprite.x) * (x - this.sprite.x) + (y - this.sprite.y) * (y - this.sprite.y);
if (distanceSquared < this.activationRadiusSquared) { if (distanceSquared < this.activationRadiusSquared) {
return distanceSquared; return distanceSquared;
} else { } else {
@ -45,7 +51,7 @@ export class ActionableItem {
this.getOutlinePlugin()?.add(this.sprite, { this.getOutlinePlugin()?.add(this.sprite, {
thickness: 2, thickness: 2,
outlineColor: 0xffff00 outlineColor: 0xffff00,
}); });
} }
@ -60,8 +66,8 @@ export class ActionableItem {
this.getOutlinePlugin()?.remove(this.sprite); this.getOutlinePlugin()?.remove(this.sprite);
} }
private getOutlinePlugin(): OutlinePipelinePlugin|undefined { private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.sprite.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined; return this.sprite.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
} }
/** /**
@ -78,7 +84,7 @@ export class ActionableItem {
} }
public on(eventName: string, callback: EventCallback): void { public on(eventName: string, callback: EventCallback): void {
let callbacksArray: Array<EventCallback>|undefined = this.callbacks.get(eventName); let callbacksArray: Array<EventCallback> | undefined = this.callbacks.get(eventName);
if (callbacksArray === undefined) { if (callbacksArray === undefined) {
callbacksArray = new Array<EventCallback>(); callbacksArray = new Array<EventCallback>();
this.callbacks.set(eventName, callbacksArray); this.callbacks.set(eventName, callbacksArray);

View file

@ -1,86 +1,91 @@
import * as Phaser from 'phaser'; import * as Phaser from "phaser";
import {Scene} from "phaser"; import { Scene } from "phaser";
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import type {ITiledMapObject} from "../../Map/ITiledMap"; import type { ITiledMapObject } from "../../Map/ITiledMap";
import type {ItemFactoryInterface} from "../ItemFactoryInterface"; import type { ItemFactoryInterface } from "../ItemFactoryInterface";
import type {GameScene} from "../../Game/GameScene"; import type { GameScene } from "../../Game/GameScene";
import {ActionableItem} from "../ActionableItem"; import { ActionableItem } from "../ActionableItem";
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
const isComputerState = const isComputerState = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
status: tg.isString, status: tg.isString,
}).get(); })
.get();
type ComputerState = tg.GuardedType<typeof isComputerState>; type ComputerState = tg.GuardedType<typeof isComputerState>;
let state: ComputerState = { let state: ComputerState = {
'status': 'off' status: "off",
}; };
export default { export default {
preload: (loader: Phaser.Loader.LoaderPlugin): void => { preload: (loader: Phaser.Loader.LoaderPlugin): void => {
loader.atlas('computer', '/resources/items/computer/computer.png', '/resources/items/computer/computer_atlas.json'); loader.atlas(
"computer",
"/resources/items/computer/computer.png",
"/resources/items/computer/computer_atlas.json"
);
}, },
create: (scene: GameScene): void => { create: (scene: GameScene): void => {
scene.anims.create({ scene.anims.create({
key: 'computer_off', key: "computer_off",
frames: [ frames: [
{ {
key: 'computer', key: "computer",
frame: 'computer_off' frame: "computer_off",
} },
], ],
frameRate: 10, frameRate: 10,
repeat: -1 repeat: -1,
}); });
scene.anims.create({ scene.anims.create({
key: 'computer_run', key: "computer_run",
frames: [ frames: [
{ {
key: 'computer', key: "computer",
frame: 'computer_on1' frame: "computer_on1",
}, },
{ {
key: 'computer', key: "computer",
frame: 'computer_on2' frame: "computer_on2",
} },
], ],
frameRate: 5, frameRate: 5,
repeat: -1 repeat: -1,
}); });
}, },
factory: (scene: GameScene, object: ITiledMapObject, initState: unknown): ActionableItem => { factory: (scene: GameScene, object: ITiledMapObject, initState: unknown): ActionableItem => {
if (initState !== undefined) { if (initState !== undefined) {
if (!isComputerState(initState)) { if (!isComputerState(initState)) {
throw new Error('Invalid state received for computer object'); throw new Error("Invalid state received for computer object");
} }
state = initState; state = initState;
} }
// Idée: ESSAYER WebPack? https://paultavares.wordpress.com/2018/07/02/webpack-how-to-generate-an-es-module-bundle/ // Idée: ESSAYER WebPack? https://paultavares.wordpress.com/2018/07/02/webpack-how-to-generate-an-es-module-bundle/
const computer = new Sprite(scene, object.x, object.y, 'computer'); const computer = new Sprite(scene, object.x, object.y, "computer");
scene.add.existing(computer); scene.add.existing(computer);
if (state.status === 'on') { if (state.status === "on") {
computer.anims.play('computer_run'); computer.anims.play("computer_run");
} }
const item = new ActionableItem(object.id, computer, scene, 32, (item: ActionableItem) => { const item = new ActionableItem(object.id, computer, scene, 32, (item: ActionableItem) => {
if (state.status === 'off') { if (state.status === "off") {
state.status = 'on'; state.status = "on";
item.emit('TURN_ON', state); item.emit("TURN_ON", state);
} else { } else {
state.status = 'off'; state.status = "off";
item.emit('TURN_OFF', state); item.emit("TURN_OFF", state);
} }
}); });
item.on('TURN_ON', () => { item.on("TURN_ON", () => {
computer.anims.play('computer_run'); computer.anims.play("computer_run");
}); });
item.on('TURN_OFF', () => { item.on("TURN_OFF", () => {
computer.anims.play('computer_off'); computer.anims.play("computer_off");
}); });
return item; return item;
//scene.add.sprite(object.x, object.y, 'computer'); //scene.add.sprite(object.x, object.y, 'computer');
} },
} as ItemFactoryInterface; } as ItemFactoryInterface;

View file

@ -1,6 +1,6 @@
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type {ITiledMapObject} from "../Map/ITiledMap"; import type { ITiledMapObject } from "../Map/ITiledMap";
import type {ActionableItem} from "./ActionableItem"; import type { ActionableItem } from "./ActionableItem";
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
export interface ItemFactoryInterface { export interface ItemFactoryInterface {

View file

@ -1,14 +1,13 @@
import {ResizableScene} from "./ResizableScene"; import { ResizableScene } from "./ResizableScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import {loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; import { loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import type {CharacterTexture} from "../../Connexion/LocalUser"; import type { CharacterTexture } from "../../Connexion/LocalUser";
export abstract class AbstractCharacterScene extends ResizableScene { export abstract class AbstractCharacterScene extends ResizableScene {
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
loadCustomSceneSelectCharacters() : Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures(); const textures = this.getTextures();
const promises : Promise<BodyResourceDescriptionInterface>[] = []; const promises: Promise<BodyResourceDescriptionInterface>[] = [];
if (textures) { if (textures) {
for (const texture of textures) { for (const texture of textures) {
if (texture.level === -1) { if (texture.level === -1) {
@ -17,10 +16,10 @@ export abstract class AbstractCharacterScene extends ResizableScene {
promises.push(loadCustomTexture(this.load, texture)); promises.push(loadCustomTexture(this.load, texture));
} }
} }
return Promise.all(promises) return Promise.all(promises);
} }
loadSelectSceneCharacters() : Promise<BodyResourceDescriptionInterface[]> { loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures(); const textures = this.getTextures();
const promises: Promise<BodyResourceDescriptionInterface>[] = []; const promises: Promise<BodyResourceDescriptionInterface>[] = [];
if (textures) { if (textures) {
@ -31,10 +30,10 @@ export abstract class AbstractCharacterScene extends ResizableScene {
promises.push(loadCustomTexture(this.load, texture)); promises.push(loadCustomTexture(this.load, texture));
} }
} }
return Promise.all(promises) return Promise.all(promises);
} }
private getTextures() : CharacterTexture[]|undefined{ private getTextures(): CharacterTexture[] | undefined {
const localUser = localUserStore.getLocalUser(); const localUser = localUserStore.getLocalUser();
return localUser?.textures; return localUser?.textures;
} }

View file

@ -1,6 +1,7 @@
import { gameManager } from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene"; import { ResizableScene } from "./ResizableScene";
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore"; import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
export const EnableCameraSceneName = "EnableCameraScene"; export const EnableCameraSceneName = "EnableCameraScene";

View file

@ -1,7 +1,9 @@
import { gameManager } from "../Game/GameManager";
import { SelectCharacterSceneName } from "./SelectCharacterScene"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
import { ResizableScene } from "./ResizableScene"; import { ResizableScene } from "./ResizableScene";
import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore"; import { loginSceneVisibleIframeStore, loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { gameManager } from "../Game/GameManager";
export const LoginSceneName = "LoginScene"; export const LoginSceneName = "LoginScene";
@ -18,6 +20,16 @@ export class LoginScene extends ResizableScene {
preload() {} preload() {}
create() { create() {
loginSceneVisibleIframeStore.set(false);
//If authentication is mandatory, push authentication iframe
if (
localUserStore.getAuthToken() == undefined &&
gameManager.currentStartedRoom &&
gameManager.currentStartedRoom?.authenticationMandatory
) {
connectionManager.loadOpenIDScreen();
loginSceneVisibleIframeStore.set(true);
}
loginSceneVisibleStore.set(true); loginSceneVisibleStore.set(true);
} }

View file

@ -1,4 +1,4 @@
import {Scene} from "phaser"; import { Scene } from "phaser";
import DOMElement = Phaser.GameObjects.DOMElement; import DOMElement = Phaser.GameObjects.DOMElement;
export abstract class ResizableScene extends Scene { export abstract class ResizableScene extends Scene {
@ -11,13 +11,10 @@ export abstract class ResizableScene extends Scene {
* @param defaultWidth The width of the DOM element. We try to compute it but it may not be available if called from "create". * @param defaultWidth The width of the DOM element. We try to compute it but it may not be available if called from "create".
*/ */
public centerXDomElement(object: DOMElement, defaultWidth: number): void { public centerXDomElement(object: DOMElement, defaultWidth: number): void {
object.x = (this.scale.width / 2) - object.x =
( this.scale.width / 2 -
object (object && object.node && object.node.getBoundingClientRect().width > 0
&& object.node ? object.node.getBoundingClientRect().width / 2 / this.scale.zoom
&& object.node.getBoundingClientRect().width > 0 : defaultWidth / this.scale.zoom);
? (object.node.getBoundingClientRect().width / 2 / this.scale.zoom)
: (defaultWidth / this.scale.zoom)
);
} }
} }

View file

@ -1,14 +1,13 @@
import { SelectCharacterScene } from "./SelectCharacterScene"; import { SelectCharacterScene } from "./SelectCharacterScene";
export class SelectCharacterMobileScene extends SelectCharacterScene { export class SelectCharacterMobileScene extends SelectCharacterScene {
create() {
create(){
super.create(); super.create();
this.onResize(); this.onResize();
this.selectedRectangle.destroy(); this.selectedRectangle.destroy();
} }
protected defineSetupPlayer(num: number){ protected defineSetupPlayer(num: number) {
const deltaX = 30; const deltaX = 30;
const deltaY = 2; const deltaY = 2;
let [playerX, playerY] = this.getCharacterPosition(); let [playerX, playerY] = this.getCharacterPosition();
@ -16,48 +15,44 @@ export class SelectCharacterMobileScene extends SelectCharacterScene {
let playerScale = 1.5; let playerScale = 1.5;
let playerOpacity = 1; let playerOpacity = 1;
if( this.currentSelectUser !== num ){ if (this.currentSelectUser !== num) {
playerVisible = false; playerVisible = false;
} }
if( num === (this.currentSelectUser + 1) ){ if (num === this.currentSelectUser + 1) {
playerY -= deltaY; playerY -= deltaY;
playerX += deltaX; playerX += deltaX;
playerScale = 0.8; playerScale = 0.8;
playerOpacity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
if( num === (this.currentSelectUser + 2) ){ if (num === this.currentSelectUser + 2) {
playerY -= deltaY; playerY -= deltaY;
playerX += (deltaX * 2); playerX += deltaX * 2;
playerScale = 0.8; playerScale = 0.8;
playerOpacity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
if( num === (this.currentSelectUser - 1) ){ if (num === this.currentSelectUser - 1) {
playerY -= deltaY; playerY -= deltaY;
playerX -= deltaX; playerX -= deltaX;
playerScale = 0.8; playerScale = 0.8;
playerOpacity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
if( num === (this.currentSelectUser - 2) ){ if (num === this.currentSelectUser - 2) {
playerY -= deltaY; playerY -= deltaY;
playerX -= (deltaX * 2); playerX -= deltaX * 2;
playerScale = 0.8; playerScale = 0.8;
playerOpacity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
return {playerX, playerY, playerScale, playerOpacity, playerVisible} return { playerX, playerY, playerScale, playerOpacity, playerVisible };
} }
/** /**
* Returns pixel position by on column and row number * Returns pixel position by on column and row number
*/ */
protected getCharacterPosition(): [number, number] { protected getCharacterPosition(): [number, number] {
return [ return [this.game.renderer.width / 2, this.game.renderer.height / 3];
this.game.renderer.width / 2, }
this.game.renderer.height / 3
];
}
} }

View file

@ -1,5 +1,3 @@
import Image = Phaser.GameObjects.Image;
import Rectangle = Phaser.GameObjects.Rectangle;
import { addLoader } from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import { gameManager } from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene"; import { ResizableScene } from "./ResizableScene";

View file

@ -1,13 +1,10 @@
export enum PlayerAnimationDirections { export enum PlayerAnimationDirections {
Down = 'down', Down = "down",
Left = 'left', Left = "left",
Up = 'up', Up = "up",
Right = 'right', Right = "right",
} }
export enum PlayerAnimationTypes { export enum PlayerAnimationTypes {
Walk = 'walk', Walk = "walk",
Idle = 'idle', Idle = "idle",
} }

View file

@ -1,9 +1,9 @@
import {PlayerAnimationDirections} from "./Animation"; import { PlayerAnimationDirections } from "./Animation";
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character"; import { Character } from "../Entity/Character";
import {userMovingStore} from "../../Stores/GameStore"; import { userMovingStore } from "../../Stores/GameStore";
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu"; import { RadialMenu, RadialMenuClickEvent, RadialMenuItem } from "../Components/RadialMenu";
export const hasMovedEventName = "hasMoved"; export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote"; export const requestEmoteEventName = "requestEmote";
@ -11,7 +11,7 @@ export const requestEmoteEventName = "requestEmote";
export class Player extends Character { export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down; private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false; private wasMoving: boolean = false;
private emoteMenu: RadialMenu|null = null; private emoteMenu: RadialMenu | null = null;
private updateListener: () => void; private updateListener: () => void;
constructor( constructor(
@ -23,7 +23,7 @@ export class Player extends Character {
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
private userInputManager: UserInputManager, private userInputManager: UserInputManager,
companion: string|null, companion: string | null,
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise); super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
@ -37,7 +37,7 @@ export class Player extends Character {
this.emoteMenu.y = this.y; this.emoteMenu.y = this.y;
} }
}; };
this.scene.events.addListener('postupdate', this.updateListener); this.scene.events.addListener("postupdate", this.updateListener);
} }
moveUser(delta: number): void { moveUser(delta: number): void {
@ -73,14 +73,14 @@ export class Player extends Character {
if (x !== 0 || y !== 0) { if (x !== 0 || y !== 0) {
this.move(x, y); this.move(x, y);
this.emit(hasMovedEventName, {moving, direction, x: this.x, y: this.y}); this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y });
} else if (this.wasMoving && moving) { } else if (this.wasMoving && moving) {
// slow joystick movement // slow joystick movement
this.move(0, 0); this.move(0, 0);
this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
} else if (this.wasMoving && !moving) { } else if (this.wasMoving && !moving) {
this.stop(); this.stop();
this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
} }
if (direction !== null) { if (direction !== null) {
@ -94,17 +94,17 @@ export class Player extends Character {
return this.wasMoving; return this.wasMoving;
} }
openOrCloseEmoteMenu(emotes:RadialMenuItem[]) { openOrCloseEmoteMenu(emotes: RadialMenuItem[]) {
if(this.emoteMenu) { if (this.emoteMenu) {
this.closeEmoteMenu(); this.closeEmoteMenu();
} else { } else {
this.openEmoteMenu(emotes); this.openEmoteMenu(emotes);
} }
} }
openEmoteMenu(emotes:RadialMenuItem[]): void { openEmoteMenu(emotes: RadialMenuItem[]): void {
this.cancelPreviousEmote(); this.cancelPreviousEmote();
this.emoteMenu = new RadialMenu(this.scene, this.x, this.y, emotes) this.emoteMenu = new RadialMenu(this.scene, this.x, this.y, emotes);
this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => { this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => {
this.closeEmoteMenu(); this.closeEmoteMenu();
this.emit(requestEmoteEventName, item.name); this.emit(requestEmoteEventName, item.name);
@ -112,6 +112,13 @@ export class Player extends Character {
}); });
} }
isSilent() {
super.isSilent();
}
noSilent() {
super.noSilent();
}
closeEmoteMenu(): void { closeEmoteMenu(): void {
if (!this.emoteMenu) return; if (!this.emoteMenu) return;
this.emoteMenu.destroy(); this.emoteMenu.destroy();
@ -119,7 +126,7 @@ export class Player extends Character {
} }
destroy() { destroy() {
this.scene.events.removeListener('postupdate', this.updateListener); this.scene.events.removeListener("postupdate", this.updateListener);
super.destroy(); super.destroy();
} }
} }

View file

@ -3,13 +3,13 @@ export class WAError extends Error {
private _subTitle: string; private _subTitle: string;
private _details: string; private _details: string;
constructor (title: string, subTitle: string, details: string) { constructor(title: string, subTitle: string, details: string) {
super(title+' - '+subTitle+' - '+details); super(title + " - " + subTitle + " - " + details);
this._title = title; this._title = title;
this._subTitle = subTitle; this._subTitle = subTitle;
this._details = details; this._details = details;
// Set the prototype explicitly. // Set the prototype explicitly.
Object.setPrototypeOf (this, WAError.prototype); Object.setPrototypeOf(this, WAError.prototype);
} }
get title(): string { get title(): string {

View file

@ -1,4 +1,3 @@
interface Size { interface Size {
width: number; width: number;
height: number; height: number;
@ -23,14 +22,14 @@ export class HdpiManager {
* *
* @param realPixelScreenSize * @param realPixelScreenSize
*/ */
public getOptimalGameSize(realPixelScreenSize: Size): { game: Size, real: Size } { public getOptimalGameSize(realPixelScreenSize: Size): { game: Size; real: Size } {
const realPixelNumber = realPixelScreenSize.width * realPixelScreenSize.height; const realPixelNumber = realPixelScreenSize.width * realPixelScreenSize.height;
// If the screen has not a definition small enough to match the minimum number of pixels we want to display, // If the screen has not a definition small enough to match the minimum number of pixels we want to display,
// let's make the canvas the size of the screen (in real pixels) // let's make the canvas the size of the screen (in real pixels)
if (realPixelNumber <= this.minRecommendedGamePixelsNumber) { if (realPixelNumber <= this.minRecommendedGamePixelsNumber) {
return { return {
game: realPixelScreenSize, game: realPixelScreenSize,
real: realPixelScreenSize real: realPixelScreenSize,
}; };
} }
@ -49,8 +48,8 @@ export class HdpiManager {
real: { real: {
width: realPixelScreenSize.width, width: realPixelScreenSize.width,
height: realPixelScreenSize.height, height: realPixelScreenSize.height,
} },
} };
} }
const gameWidth = Math.ceil(realPixelScreenSize.width / optimalZoomLevel / this._zoomModifier); const gameWidth = Math.ceil(realPixelScreenSize.width / optimalZoomLevel / this._zoomModifier);
@ -58,8 +57,12 @@ export class HdpiManager {
// Let's ensure we display a minimum of pixels, even if crazily zoomed in. // Let's ensure we display a minimum of pixels, even if crazily zoomed in.
if (gameWidth * gameHeight < this.absoluteMinPixelNumber) { if (gameWidth * gameHeight < this.absoluteMinPixelNumber) {
const minGameHeight = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.height / realPixelScreenSize.width); const minGameHeight = Math.sqrt(
const minGameWidth = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.width / realPixelScreenSize.height); (this.absoluteMinPixelNumber * realPixelScreenSize.height) / realPixelScreenSize.width
);
const minGameWidth = Math.sqrt(
(this.absoluteMinPixelNumber * realPixelScreenSize.width) / realPixelScreenSize.height
);
// Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter) // Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter)
this._zoomModifier = realPixelScreenSize.width / minGameWidth / optimalZoomLevel; this._zoomModifier = realPixelScreenSize.width / minGameWidth / optimalZoomLevel;
@ -72,9 +75,8 @@ export class HdpiManager {
real: { real: {
width: realPixelScreenSize.width, width: realPixelScreenSize.width,
height: realPixelScreenSize.height, height: realPixelScreenSize.height,
} },
} };
} }
return { return {
@ -85,8 +87,8 @@ export class HdpiManager {
real: { real: {
width: Math.ceil(realPixelScreenSize.width / optimalZoomLevel) * optimalZoomLevel, width: Math.ceil(realPixelScreenSize.width / optimalZoomLevel) * optimalZoomLevel,
height: Math.ceil(realPixelScreenSize.height / optimalZoomLevel) * optimalZoomLevel, height: Math.ceil(realPixelScreenSize.height / optimalZoomLevel) * optimalZoomLevel,
} },
} };
} }
/** /**
@ -95,7 +97,7 @@ export class HdpiManager {
private getOptimalZoomLevel(realPixelNumber: number): number { private getOptimalZoomLevel(realPixelNumber: number): number {
const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber); const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber);
if (1.5 <= result && result < 2) { if (1.5 <= result && result < 2) {
return 1.5 return 1.5;
} else { } else {
return Math.floor(result); return Math.floor(result);
} }

View file

@ -1,10 +1,9 @@
import {HdpiManager} from "./HdpiManager"; import { HdpiManager } from "./HdpiManager";
import ScaleManager = Phaser.Scale.ScaleManager; import ScaleManager = Phaser.Scale.ScaleManager;
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type {Game} from "../Game/Game"; import type { Game } from "../Game/Game";
import {ResizableScene} from "../Login/ResizableScene"; import { ResizableScene } from "../Login/ResizableScene";
import {HtmlUtils} from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
class WaScaleManager { class WaScaleManager {
private hdpiManager: HdpiManager; private hdpiManager: HdpiManager;
@ -23,26 +22,29 @@ class WaScaleManager {
} }
public applyNewSize() { public applyNewSize() {
const {width, height} = coWebsiteManager.getGameSize(); const { width, height } = coWebsiteManager.getGameSize();
let devicePixelRatio = 1; let devicePixelRatio = 1;
if (window.devicePixelRatio) { if (window.devicePixelRatio) {
devicePixelRatio = window.devicePixelRatio; devicePixelRatio = window.devicePixelRatio;
} }
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio}); const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({
width: width * devicePixelRatio,
height: height * devicePixelRatio,
});
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio; this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio) this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height); this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves // Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style; const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px'; style.width = Math.ceil(realSize.width / devicePixelRatio) + "px";
style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px'; style.height = Math.ceil(realSize.height / devicePixelRatio) + "px";
// Resize the game element at the same size at the canvas // Resize the game element at the same size at the canvas
const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game').style; const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game").style;
gameStyle.width = style.width; gameStyle.width = style.width;
gameStyle.height = style.height; gameStyle.height = style.height;
@ -70,7 +72,7 @@ class WaScaleManager {
this._saveZoom = this.hdpiManager.zoomModifier; this._saveZoom = this.hdpiManager.zoomModifier;
} }
public restoreZoom(): void{ public restoreZoom(): void {
this.hdpiManager.zoomModifier = this._saveZoom; this.hdpiManager.zoomModifier = this._saveZoom;
this.applyNewSize(); this.applyNewSize();
} }
@ -81,7 +83,6 @@ class WaScaleManager {
public get uiScalingFactor(): number { public get uiScalingFactor(): number {
return this.actualZoom > 1 ? 1 : 1.2; return this.actualZoom > 1 ? 1 : 1.2;
} }
} }
export const waScaleManager = new WaScaleManager(640*480, 196*196); export const waScaleManager = new WaScaleManager(640 * 480, 196 * 196);

View file

@ -1,6 +1,6 @@
import {Pinch} from "phaser3-rex-plugins/plugins/gestures.js"; import { Pinch } from "phaser3-rex-plugins/plugins/gestures.js";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import {GameScene} from "../Game/GameScene"; import { GameScene } from "../Game/GameScene";
export class PinchManager { export class PinchManager {
private scene: Phaser.Scene; private scene: Phaser.Scene;
@ -15,18 +15,18 @@ export class PinchManager {
// We are smoothing its value with previous values to prevent the flicking. // We are smoothing its value with previous values to prevent the flicking.
let smoothPinch = 1; let smoothPinch = 1;
this.pinch.on('pinchstart', () => { this.pinch.on("pinchstart", () => {
smoothPinch = 1; smoothPinch = 1;
}); });
// eslint-disable-next-line
this.pinch.on('pinch', (pinch:any) => { // eslint-disable-line this.pinch.on("pinch", (pinch: any) => {
if (pinch.scaleFactor > 1.2 || pinch.scaleFactor < 0.8) { if (pinch.scaleFactor > 1.2 || pinch.scaleFactor < 0.8) {
// Pinch too fast! Probably a bad measure. // Pinch too fast! Probably a bad measure.
return; return;
} }
smoothPinch = 3/5*smoothPinch + 2/5*pinch.scaleFactor; smoothPinch = (3 / 5) * smoothPinch + (2 / 5) * pinch.scaleFactor;
if (this.scene instanceof GameScene) { if (this.scene instanceof GameScene) {
this.scene.zoomByFactor(smoothPinch); this.scene.zoomByFactor(smoothPinch);
} else { } else {

View file

@ -1,4 +1,5 @@
import { get, writable } from "svelte/store"; import { get, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
export interface audioManagerVolume { export interface audioManagerVolume {
muted: boolean; muted: boolean;
@ -103,3 +104,7 @@ export const audioManagerVisibilityStore = writable(false);
export const audioManagerVolumeStore = createAudioManagerVolumeStore(); export const audioManagerVolumeStore = createAudioManagerVolumeStore();
export const audioManagerFileStore = createAudioManagerFileStore(); export const audioManagerFileStore = createAudioManagerFileStore();
peerStore.subscribe((peers) => {
audioManagerVolumeStore.setTalking(peers.size > 0);
});

View file

@ -2,4 +2,4 @@ import { writable } from "svelte/store";
export const consoleGlobalMessageManagerVisibleStore = writable(false); export const consoleGlobalMessageManagerVisibleStore = writable(false);
export const consoleGlobalMessageManagerFocusStore = writable(false); export const consoleGlobalMessageManagerFocusStore = writable(false);

View file

@ -1,4 +1,4 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
/** /**
* A store that contains a list of error messages to be displayed. * A store that contains a list of error messages to be displayed.
@ -8,7 +8,7 @@ function createErrorStore() {
return { return {
subscribe, subscribe,
addErrorMessage: (e: string|Error): void => { addErrorMessage: (e: string | Error): void => {
update((messages: string[]) => { update((messages: string[]) => {
let message: string; let message: string;
if (e instanceof Error) { if (e instanceof Error) {
@ -26,7 +26,7 @@ function createErrorStore() {
}, },
clearMessages: (): void => { clearMessages: (): void => {
set([]); set([]);
} },
}; };
} }

View file

@ -1,8 +1,10 @@
export class BrowserTooOldError extends Error { export class BrowserTooOldError extends Error {
static NAME = 'BrowserTooOldError'; static NAME = "BrowserTooOldError";
constructor() { constructor() {
super('Unable to access your camera or microphone. Your browser is too old. Please consider upgrading your browser or try using a recent version of Chrome.'); super(
"Unable to access your camera or microphone. Your browser is too old. Please consider upgrading your browser or try using a recent version of Chrome."
);
this.name = BrowserTooOldError.NAME; this.name = BrowserTooOldError.NAME;
} }
} }

View file

@ -0,0 +1,10 @@
export class MediaStreamConstraintsError extends Error {
static NAME = "MediaStreamConstraintsError";
constructor() {
super(
"Unable to access your camera or microphone. Your browser is too old. Please consider upgrading your browser or try using a recent version of Chrome."
);
this.name = MediaStreamConstraintsError.NAME;
}
}

View file

@ -1,8 +1,10 @@
export class WebviewOnOldIOS extends Error { export class WebviewOnOldIOS extends Error {
static NAME = 'WebviewOnOldIOS'; static NAME = "WebviewOnOldIOS";
constructor() { constructor() {
super('Your iOS version cannot use video/audio in the browser unless you are using Safari. Please switch to Safari or upgrade iOS to 14.3 or above.'); super(
"Your iOS version cannot use video/audio in the browser unless you are using Safari. Please switch to Safari or upgrade iOS to 14.3 or above."
);
this.name = WebviewOnOldIOS.NAME; this.name = WebviewOnOldIOS.NAME;
} }
} }

View file

@ -1,3 +1,4 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export const loginSceneVisibleStore = writable(false); export const loginSceneVisibleStore = writable(false);
export const loginSceneVisibleIframeStore = writable(false);

View file

@ -1,4 +1,4 @@
import { derived, get, Readable, readable, writable, Writable } from "svelte/store"; import { derived, get, Readable, readable, writable } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import { userMovingStore } from "./GameStore"; import { userMovingStore } from "./GameStore";
import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
@ -9,6 +9,7 @@ import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import { peerStore } from "./PeerStore"; import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -255,6 +256,12 @@ export const mediaStreamConstraintsStore = derived(
video: currentVideoConstraint, video: currentVideoConstraint,
audio: currentAudioConstraint, audio: currentAudioConstraint,
}); });
localUserStore.setCameraSetup(
JSON.stringify({
video: currentVideoConstraint,
audio: currentAudioConstraint,
})
);
return; return;
} }
@ -413,7 +420,7 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
}); });
return; return;
} catch (e) { } catch (e) {
if (constraints.video !== false) { if (constraints.video !== false || constraints.audio !== false) {
console.info( console.info(
"Error. Unable to get microphone and/or camera access. Trying audio only.", "Error. Unable to get microphone and/or camera access. Trying audio only.",
$mediaStreamConstraintsStore, $mediaStreamConstraintsStore,
@ -425,7 +432,17 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
error: e, error: e,
}); });
// Let's try without video constraints // Let's try without video constraints
requestedCameraState.disableWebcam(); if (constraints.video !== false) {
requestedCameraState.disableWebcam();
}
if (constraints.audio !== false) {
requestedMicrophoneState.disableMicrophone();
}
} else if (!constraints.video && !constraints.audio) {
set({
type: "error",
error: new MediaStreamConstraintsError(),
});
} else { } else {
console.info( console.info(
"Error. Unable to get microphone and/or camera access.", "Error. Unable to get microphone and/or camera access.",
@ -561,3 +578,8 @@ localStreamStore.subscribe((streamResult) => {
* A store containing the real active media is mobile * A store containing the real active media is mobile
*/ */
export const obtainedMediaConstraintIsMobileStore = writable(false); export const obtainedMediaConstraintIsMobileStore = writable(false);
/**
* A store containing if user is silent, so if he is in silent zone. This permit to show et hide camera of user
*/
export const isSilentStore = writable(false);

View file

@ -6,6 +6,8 @@ import { CONTACT_URL } from "../Enum/EnvironmentVariable";
export const menuIconVisiblilityStore = writable(false); export const menuIconVisiblilityStore = writable(false);
export const menuVisiblilityStore = writable(false); export const menuVisiblilityStore = writable(false);
export const menuInputFocusStore = writable(false); export const menuInputFocusStore = writable(false);
export const loginUrlStore = writable(false);
export const userIsConnected = writable(false);
let warningContainerTimeout: Timeout | null = null; let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() { function createWarningContainerStore() {

View file

@ -1,38 +1,30 @@
import { readable, writable } from "svelte/store"; import { readable, writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer"; import type { VideoPeer } from "../WebRtc/VideoPeer";
import { VideoPeer } from "../WebRtc/VideoPeer"; import type { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
/** /**
* A store that contains the list of (video) peers we are connected to. * A store that contains the list of (video) peers we are connected to.
*/ */
function createPeerStore() { function createPeerStore() {
let peers = new Map<number, VideoPeer>(); const { subscribe, set, update } = writable(new Map<number, VideoPeer>());
const { subscribe, set, update } = writable(peers);
return { return {
subscribe, subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => { pushNewPeer(peer: VideoPeer) {
peers = new Map<number, VideoPeer>(); update((users) => {
set(peers); users.set(peer.userId, peer);
simplePeer.registerPeerConnectionListener({ return users;
onConnect(peer: RemotePeer) {
if (peer instanceof VideoPeer) {
update((users) => {
users.set(peer.userId, peer);
return users;
});
}
},
onDisconnect(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
}); });
}, },
removePeer(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
cleanupStore() {
set(new Map<number, VideoPeer>());
},
}; };
} }
@ -40,32 +32,25 @@ function createPeerStore() {
* A store that contains the list of screen sharing peers we are connected to. * A store that contains the list of screen sharing peers we are connected to.
*/ */
function createScreenSharingPeerStore() { function createScreenSharingPeerStore() {
let peers = new Map<number, ScreenSharingPeer>(); const { subscribe, set, update } = writable(new Map<number, ScreenSharingPeer>());
const { subscribe, set, update } = writable(peers);
return { return {
subscribe, subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => { pushNewPeer(peer: ScreenSharingPeer) {
peers = new Map<number, ScreenSharingPeer>(); update((users) => {
set(peers); users.set(peer.userId, peer);
simplePeer.registerPeerConnectionListener({ return users;
onConnect(peer: RemotePeer) {
if (peer instanceof ScreenSharingPeer) {
update((users) => {
users.set(peer.userId, peer);
return users;
});
}
},
onDisconnect(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
}); });
}, },
removePeer(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
cleanupStore() {
set(new Map<number, ScreenSharingPeer>());
},
}; };
} }

View file

@ -2,6 +2,7 @@ import { writable } from "svelte/store";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection"; import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator"; import { getRandomColor } from "../WebRtc/ColorGenerator";
import { localUserStore } from "../Connexion/LocalUserStore";
let idCount = 0; let idCount = 0;

View file

@ -175,6 +175,7 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
export interface ScreenSharingLocalMedia { export interface ScreenSharingLocalMedia {
uniqueId: string; uniqueId: string;
stream: MediaStream | null; stream: MediaStream | null;
userId?: undefined;
} }
/** /**

View file

@ -1,3 +1,3 @@
import { derived, writable, Writable } from "svelte/store"; import { derived, writable, Writable } from "svelte/store";
export const selectCharacterSceneVisibleStore = writable(false); export const selectCharacterSceneVisibleStore = writable(false);

View file

@ -4,7 +4,7 @@ import { writable } from "svelte/store";
* A store that contains the URL of the sound currently playing * A store that contains the URL of the sound currently playing
*/ */
function createSoundPlayingStore() { function createSoundPlayingStore() {
const { subscribe, set, update } = writable<string|null>(null); const { subscribe, set, update } = writable<string | null>(null);
return { return {
subscribe, subscribe,
@ -13,9 +13,7 @@ function createSoundPlayingStore() {
}, },
soundEnded: () => { soundEnded: () => {
set(null); set(null);
} },
}; };
} }

View file

@ -1,14 +1,12 @@
import { writable } from "svelte/store"; import { get, writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import { VideoPeer } from "../WebRtc/VideoPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import type { Streamable } from "./StreamableCollectionStore"; import type { Streamable } from "./StreamableCollectionStore";
import { peerStore } from "./PeerStore";
/** /**
* A store that contains the peer / media that has currently the "importance" focus. * A store that contains the peer / media that has currently the "importance" focus.
*/ */
function createVideoFocusStore() { function createVideoFocusStore() {
const { subscribe, set, update } = writable<Streamable | null>(null); const { subscribe, set } = writable<Streamable | null>(null);
let focusedMedia: Streamable | null = null; let focusedMedia: Streamable | null = null;
@ -23,27 +21,17 @@ function createVideoFocusStore() {
set(null); set(null);
}, },
toggleFocus: (media: Streamable) => { toggleFocus: (media: Streamable) => {
if (media !== focusedMedia) { focusedMedia = media !== focusedMedia ? media : null;
focusedMedia = media;
} else {
focusedMedia = null;
}
set(focusedMedia); set(focusedMedia);
}, },
connectToSimplePeer: (simplePeer: SimplePeer) => {
simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) {},
onDisconnect(userId: number) {
if (
(focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) &&
focusedMedia.userId === userId
) {
set(null);
}
},
});
},
}; };
} }
export const videoFocusStore = createVideoFocusStore(); export const videoFocusStore = createVideoFocusStore();
peerStore.subscribe((peers) => {
const focusedMedia: Streamable | null = get(videoFocusStore);
if (focusedMedia && focusedMedia.userId !== undefined && !peers.get(focusedMedia.userId)) {
videoFocusStore.removeFocus();
}
});

View file

@ -1,16 +1,14 @@
class TouchScreenManager { class TouchScreenManager {
readonly supportTouchScreen: boolean;
readonly supportTouchScreen:boolean;
constructor() { constructor() {
this.supportTouchScreen = this.detectTouchscreen(); this.supportTouchScreen = this.detectTouchscreen();
} }
//found here: https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript#4819886 //found here: https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript#4819886
detectTouchscreen(): boolean { detectTouchscreen(): boolean {
return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
} }
} }
export const touchScreenManager = new TouchScreenManager(); export const touchScreenManager = new TouchScreenManager();

View file

@ -1,4 +1,5 @@
import type { Room } from "../Connexion/Room"; import type { Room } from "../Connexion/Room";
import { localUserStore } from "../Connexion/LocalUserStore";
export enum GameConnexionTypes { export enum GameConnexionTypes {
anonymous = 1, anonymous = 1,
@ -7,13 +8,16 @@ export enum GameConnexionTypes {
empty, empty,
unknown, unknown,
jwt, jwt,
login,
} }
//this class is responsible with analysing and editing the game's url //this class is responsible with analysing and editing the game's url
class UrlManager { class UrlManager {
public getGameConnexionType(): GameConnexionTypes { public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString(); const url = window.location.pathname.toString();
if (url === "/jwt") { if (url === "/login") {
return GameConnexionTypes.login;
} else if (url === "/jwt") {
return GameConnexionTypes.jwt; return GameConnexionTypes.jwt;
} else if (url.includes("_/")) { } else if (url.includes("_/")) {
return GameConnexionTypes.anonymous; return GameConnexionTypes.anonymous;
@ -35,6 +39,8 @@ class UrlManager {
public pushRoomIdToUrl(room: Room): void { public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id) return; if (window.location.pathname === room.id) return;
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
localUserStore.setLastRoomUrl(room.key);
const hash = window.location.hash; const hash = window.location.hash;
const search = room.search.toString(); const search = room.search.toString();
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash); history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);

View file

@ -166,7 +166,13 @@ class CoWebsiteManager {
return iframe; return iframe;
} }
public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string, widthPercent?: number): void { public loadCoWebsite(
url: string,
base: string,
allowApi?: boolean,
allowPolicy?: string,
widthPercent?: number
): void {
this.load(); this.load();
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;

View file

@ -1,12 +1,9 @@
export function isIOS(): boolean { export function isIOS(): boolean {
return [ return (
'iPad Simulator', ["iPad Simulator", "iPhone Simulator", "iPod Simulator", "iPad", "iPhone", "iPod"].includes(
'iPhone Simulator', navigator.platform
'iPod Simulator', ) ||
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection // iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document) (navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
} }

View file

@ -1,37 +1,67 @@
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import { JITSI_URL } from "../Enum/EnvironmentVariable";
import {mediaManager} from "./MediaManager"; import { coWebsiteManager } from "./CoWebsiteManager";
import {coWebsiteManager} from "./CoWebsiteManager"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore"; import { get } from "svelte/store";
import {get} from "svelte/store";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
interface jitsiConfigInterface { interface jitsiConfigInterface {
startWithAudioMuted: boolean startWithAudioMuted: boolean;
startWithVideoMuted: boolean startWithVideoMuted: boolean;
prejoinPageEnabled: boolean prejoinPageEnabled: boolean;
} }
const getDefaultConfig = () : jitsiConfigInterface => { interface JitsiOptions {
return { jwt?: string;
startWithAudioMuted: !get(requestedMicrophoneState), roomName: string;
startWithVideoMuted: !get(requestedCameraState), width: string;
prejoinPageEnabled: false height: string;
parentNode: HTMLElement;
configOverwrite: jitsiConfigInterface;
interfaceConfigOverwrite: typeof defaultInterfaceConfig;
onload?: Function;
}
interface JitsiApi {
executeCommand: (command: string, ...args: Array<unknown>) => void;
addListener: (type: string, callback: Function) => void;
removeListener: (type: string, callback: Function) => void;
dispose: () => void;
}
declare global {
interface Window {
JitsiMeetExternalAPI: new (domain: string, options: JitsiOptions) => JitsiApi;
} }
} }
const getDefaultConfig = (): jitsiConfigInterface => {
return {
startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false,
};
};
const mergeConfig = (config?: object) => { const mergeConfig = (config?: object) => {
const currentDefaultConfig = getDefaultConfig(); const currentDefaultConfig = getDefaultConfig();
if(!config){ if (!config) {
return currentDefaultConfig; return currentDefaultConfig;
} }
return { return {
...currentDefaultConfig, ...currentDefaultConfig,
...config, ...config,
startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted ? true : currentDefaultConfig.startWithAudioMuted, startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted
startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted ? true : currentDefaultConfig.startWithVideoMuted, ? true
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled ? true : currentDefaultConfig.prejoinPageEnabled : currentDefaultConfig.startWithAudioMuted,
} startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted
} ? true
: currentDefaultConfig.startWithVideoMuted,
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled
? true
: currentDefaultConfig.prejoinPageEnabled,
};
};
const defaultInterfaceConfig = { const defaultInterfaceConfig = {
SHOW_CHROME_EXTENSION_BANNER: false, SHOW_CHROME_EXTENSION_BANNER: false,
@ -49,28 +79,48 @@ const defaultInterfaceConfig = {
SHOW_WATERMARK_FOR_GUESTS: false, SHOW_WATERMARK_FOR_GUESTS: false,
TOOLBAR_BUTTONS: [ TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', /*'embedmeeting',*/ 'fullscreen', "microphone",
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', "camera",
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', "closedcaptions",
'videoquality', 'filmstrip', /*'invite',*/ 'feedback', 'stats', 'shortcuts', "desktop",
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', /*'security'*/ /*'embedmeeting',*/ "fullscreen",
"fodeviceselection",
"hangup",
"profile",
"chat",
"recording",
"livestreaming",
"etherpad",
"sharedvideo",
"settings",
"raisehand",
"videoquality",
"filmstrip",
/*'invite',*/ "feedback",
"stats",
"shortcuts",
"tileview",
"videobackgroundblur",
"download",
"help",
"mute-everyone" /*'security'*/,
], ],
}; };
const slugify = (...args: (string | number)[]): string => { const slugify = (...args: (string | number)[]): string => {
const value = args.join(' ') const value = args.join(" ");
return value return value
.normalize('NFD') // split an accented letter in the base letter and the accent .normalize("NFD") // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced) .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator .replace(/\s+/g, "-"); // separator
} };
class JitsiFactory { class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any private jitsiApi?: JitsiApi;
private audioCallback = this.onAudioChange.bind(this); private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this); private videoCallback = this.onVideoChange.bind(this);
private jitsiScriptLoaded: boolean = false; private jitsiScriptLoaded: boolean = false;
@ -79,11 +129,19 @@ class JitsiFactory {
* Slugifies the room name and prepends the room name with the instance * Slugifies the room name and prepends the room name with the instance
*/ */
public getRoomName(roomName: string, instance: string): string { public getRoomName(roomName: string, instance: string): string {
return slugify(instance.replace('/', '-') + "-" + roomName); return slugify(instance.replace("/", "-") + "-" + roomName);
} }
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string, jitsiWidth?: number): void { public start(
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => { roomName: string,
playerName: string,
jwt?: string,
config?: object,
interfaceConfig?: object,
jitsiUrl?: string,
jitsiWidth?: number
): void {
coWebsiteManager.insertCoWebsite(async (cowebsiteDiv) => {
// Jitsi meet external API maintains some data in local storage // Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a // which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus // conference. Problem is that this data grows indefinitely. Thus
@ -94,18 +152,18 @@ class JitsiFactory {
const domain = jitsiUrl || JITSI_URL; const domain = jitsiUrl || JITSI_URL;
if (domain === undefined) { if (domain === undefined) {
throw new Error('Missing JITSI_URL environment variable or jitsiUrl parameter in the map.') throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
} }
await this.loadJitsiScript(domain); await this.loadJitsiScript(domain);
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any const options: JitsiOptions = {
roomName: roomName, roomName: roomName,
jwt: jwt, jwt: jwt,
width: "100%", width: "100%",
height: "100%", height: "100%",
parentNode: cowebsiteDiv, parentNode: cowebsiteDiv,
configOverwrite: mergeConfig(config), configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig} interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
}; };
if (!options.jwt) { if (!options.jwt) {
delete options.jwt; delete options.jwt;
@ -115,25 +173,25 @@ class JitsiFactory {
options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations. options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand('displayName', playerName); this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback); this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback); this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
}); });
}), jitsiWidth); }, jitsiWidth);
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
if(!this.jitsiApi){ if (!this.jitsiApi) {
return; return;
} }
await coWebsiteManager.closeCoWebsite(); await coWebsiteManager.closeCoWebsite();
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback); this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback); this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback);
this.jitsiApi?.dispose(); this.jitsiApi?.dispose();
} }
private onAudioChange({muted}: {muted: boolean}): void { private onAudioChange({ muted }: { muted: boolean }): void {
if (muted) { if (muted) {
requestedMicrophoneState.disableMicrophone(); requestedMicrophoneState.disableMicrophone();
} else { } else {
@ -141,7 +199,7 @@ class JitsiFactory {
} }
} }
private onVideoChange({muted}: {muted: boolean}): void { private onVideoChange({ muted }: { muted: boolean }): void {
if (muted) { if (muted) {
requestedCameraState.disableWebcam(); requestedCameraState.disableWebcam();
} else { } else {
@ -159,20 +217,17 @@ class JitsiFactory {
this.jitsiScriptLoaded = true; this.jitsiScriptLoaded = true;
// Load Jitsi if the environment variable is set. // Load Jitsi if the environment variable is set.
const jitsiScript = document.createElement('script'); const jitsiScript = document.createElement("script");
jitsiScript.src = 'https://' + domain + '/external_api.js'; jitsiScript.src = "https://" + domain + "/external_api.js";
jitsiScript.onload = () => { jitsiScript.onload = () => {
resolve(); resolve();
} };
jitsiScript.onerror = () => { jitsiScript.onerror = () => {
reject(); reject();
} };
document.head.appendChild(jitsiScript); document.head.appendChild(jitsiScript);
});
})
} }
} }

View file

@ -12,6 +12,7 @@ import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore"; import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError";
export class MediaManager { export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
@ -24,16 +25,17 @@ export class MediaManager {
constructor() { constructor() {
localStreamStore.subscribe((result) => { localStreamStore.subscribe((result) => {
if (result.type === "error") { if (result.type === "error") {
console.error(result.error); if (result.error.name !== MediaStreamConstraintsError.NAME) {
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: "cameraAccessDenied", uuid: "cameraAccessDenied",
type: "warning", type: "warning",
message: "Camera access denied. Click here and check your browser permissions.", message: "Camera access denied. Click here and check your browser permissions.",
callback: () => { callback: () => {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
}, },
userInputManager: this.userInputManager, userInputManager: this.userInputManager,
}); });
}
//remove it after 10 sec //remove it after 10 sec
setTimeout(() => { setTimeout(() => {
layoutManagerActionStore.removeAction("cameraAccessDenied"); layoutManagerActionStore.removeAction("cameraAccessDenied");

View file

@ -10,6 +10,7 @@ import { blackListManager } from "./BlackListManager";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { peerStore, screenSharingPeerStore } from "../Stores/PeerStore";
export interface UserSimplePeerInterface { export interface UserSimplePeerInterface {
userId: number; userId: number;
@ -20,12 +21,6 @@ export interface UserSimplePeerInterface {
export type RemotePeer = VideoPeer | ScreenSharingPeer; export type RemotePeer = VideoPeer | ScreenSharingPeer;
export interface PeerConnectionListener {
onConnect(user: RemotePeer): void;
onDisconnect(userId: number): void;
}
/** /**
* This class manages connections to all the peers in the same group as me. * This class manages connections to all the peers in the same group as me.
*/ */
@ -37,12 +32,14 @@ export class SimplePeer {
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback; private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback; private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly unsubscribers: (() => void)[] = []; private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number; private readonly userId: number;
private lastWebrtcUserName: string | undefined; private lastWebrtcUserName: string | undefined;
private lastWebrtcPassword: string | undefined; private lastWebrtcPassword: string | undefined;
constructor(private Connection: RoomConnection) { constructor(private Connection: RoomConnection) {
//we make sure we don't get any old peer.
peerStore.cleanupStore();
screenSharingPeerStore.cleanupStore();
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this); this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
@ -73,14 +70,6 @@ export class SimplePeer {
this.initialise(); this.initialise();
} }
public registerPeerConnectionListener(peerConnectionListener: PeerConnectionListener) {
this.peerConnectionListeners.push(peerConnectionListener);
}
public getNbConnections(): number {
return this.Users.length;
}
/** /**
* permit to listen when user could start visio * permit to listen when user could start visio
*/ */
@ -164,9 +153,7 @@ export class SimplePeer {
} }
this.PeerConnectionArray.set(user.userId, peer); this.PeerConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { peerStore.pushNewPeer(peer);
peerConnectionListener.onConnect(peer);
}
return peer; return peer;
} }
@ -214,9 +201,7 @@ export class SimplePeer {
); );
this.PeerScreenSharingConnectionArray.set(user.userId, peer); this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { screenSharingPeerStore.pushNewPeer(peer);
peerConnectionListener.onConnect(peer);
}
return peer; return peer;
} }
@ -255,12 +240,11 @@ export class SimplePeer {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) { for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId); this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId); this.PeerScreenSharingConnectionArray.delete(userId);
screenSharingPeerStore.removePeer(userId);
} }
} }
for (const peerConnectionListener of this.peerConnectionListeners) { peerStore.removePeer(userId);
peerConnectionListener.onDisconnect(userId);
}
} }
/** /**
@ -302,6 +286,8 @@ export class SimplePeer {
for (const unsubscriber of this.unsubscribers) { for (const unsubscriber of this.unsubscribers) {
unsubscriber(); unsubscriber();
} }
peerStore.cleanupStore();
screenSharingPeerStore.cleanupStore();
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -325,7 +311,6 @@ export class SimplePeer {
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) { private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
const uuid = playersStore.getPlayerById(data.userId)?.userUuid || ""; const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return; if (blackListManager.isBlackListed(uuid)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore); const streamResult = get(screenSharingLocalStreamStore);
let stream: MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream !== null) { if (streamResult.type === "success" && streamResult.stream !== null) {

View file

@ -4,7 +4,7 @@ import type { RoomConnection } from "../Connexion/RoomConnection";
import { blackListManager } from "./BlackListManager"; import { blackListManager } from "./BlackListManager";
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import type { UserSimplePeerInterface } from "./SimplePeer"; import type { UserSimplePeerInterface } from "./SimplePeer";
import { get, readable, Readable, Unsubscriber } from "svelte/store"; import { readable, Readable, Unsubscriber } from "svelte/store";
import { import {
localStreamStore, localStreamStore,
obtainedMediaConstraintIsMobileStore, obtainedMediaConstraintIsMobileStore,
@ -12,7 +12,7 @@ import {
ObtainedMediaStreamConstraints, ObtainedMediaStreamConstraints,
} from "../Stores/MediaStore"; } from "../Stores/MediaStore";
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore"; import { chatMessagesStore, newChatMessageStore } from "../Stores/ChatStore";
import { getIceServersConfig } from "../Components/Video/utils"; import { getIceServersConfig } from "../Components/Video/utils";
import { isMobile } from "../Enum/EnvironmentVariable"; import { isMobile } from "../Enum/EnvironmentVariable";

View file

@ -1,16 +1,16 @@
declare module 'phaser3-rex-plugins/plugins/virtualjoystick.js' { declare module "phaser3-rex-plugins/plugins/virtualjoystick.js" {
const content: any; // eslint-disable-line const content: any; // eslint-disable-line
export default content; export default content;
} }
declare module 'phaser3-rex-plugins/plugins/gestures-plugin.js' { declare module "phaser3-rex-plugins/plugins/gestures-plugin.js" {
const content: any; // eslint-disable-line const content: any; // eslint-disable-line
export default content; export default content;
} }
declare module 'phaser3-rex-plugins/plugins/webfontloader-plugin.js' { declare module "phaser3-rex-plugins/plugins/webfontloader-plugin.js" {
const content: any; // eslint-disable-line const content: any; // eslint-disable-line
export default content; export default content;
} }
declare module 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js' { declare module "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js" {
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
class OutlinePipelinePlugin { class OutlinePipelinePlugin {
@ -21,6 +21,6 @@ declare module 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js' {
export default OutlinePipelinePlugin; export default OutlinePipelinePlugin;
} }
declare module 'phaser3-rex-plugins/plugins/gestures.js' { declare module "phaser3-rex-plugins/plugins/gestures.js" {
export const Pinch: any; // eslint-disable-line export const Pinch: any; // eslint-disable-line
} }

View file

@ -399,62 +399,6 @@ body {
} }
} }
.audioplayer:first-child {
display: grid;
grid: 2rem / 4rem 10rem;
}
.audioplayer > button, .audioplayer > div, .audioplayer > label {
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.audioplayer > div {
padding-right: 1.2rem;
}
#audioplayerctrl {
position: fixed;
top: 0;
right: calc(50% - 120px);
padding: 0.3rem 0.5rem;
color: white;
transition: transform 0.5s;
}
#audioplayer_mute {
max-width: 5rem;
border: none;
}
#audioplayer_mute:focus, #audioplayer_mute:active {
outline: none;
}
#audioplayer_mute > svg {
width: 100%;
height: 100%;
pointer-events: none;
}
#audioplayer_volume_icon_playing.muted {
visibility: hidden;
display: none;
}
#audioplayerctrl > #audioplayer_volume {
width: 100%;
background-color: rgba(0,0,0,0.5);
}
/*
* sollte eigentlich in den aspect-ratio teil ..
*/
#audioplayerctrl.loading {
transform: translateY(-90%);
}
#audioplayerctrl.hidden {
transform: translateY(-100%);
}
/* /*
* Style Input Range * Style Input Range
* https://www.cssportal.com/style-input-range/ * https://www.cssportal.com/style-input-range/
@ -1139,3 +1083,19 @@ div.action.danger p.action-body{
} }
} }
} }
div.is-silent {
position: absolute;
bottom: 40px;
border-radius: 15px 15px 15px 15px;
max-height: 20%;
transition: right 350ms;
right: -20vw;
background-color: black;
font-size: 20px;
color: white;
padding: 30px 20px;
}
div.is-silent.hide {
right: 15px;
}

View file

@ -7,6 +7,7 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
import sveltePreprocess from "svelte-preprocess"; import sveltePreprocess from "svelte-preprocess";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import { PROFILE_URL } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development"; const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -191,6 +192,7 @@ module.exports = {
UPLOADER_URL: null, UPLOADER_URL: null,
ADMIN_URL: undefined, ADMIN_URL: undefined,
CONTACT_URL: null, CONTACT_URL: null,
PROFILE_URL: null,
DEBUG_MODE: null, DEBUG_MODE: null,
STUN_SERVER: null, STUN_SERVER: null,
TURN_SERVER: null, TURN_SERVER: null,

View file

@ -2,7 +2,7 @@ import { v4 } from "uuid";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController"; import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { jwtTokenManager } from "../Services/JWTTokenManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string"; import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient"; import { openIDClient } from "../Services/OpenIDClient";
@ -17,6 +17,7 @@ export class AuthenticateController extends BaseController {
this.openIDCallback(); this.openIDCallback();
this.register(); this.register();
this.anonymLogin(); this.anonymLogin();
this.profileCallback();
} }
openIDLogin() { openIDLogin() {
@ -48,14 +49,31 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => { res.onAborted(() => {
console.warn("/message request was aborted"); console.warn("/message request was aborted");
}); });
const { code, nonce } = parse(req.getQuery()); const { code, nonce, token } = parse(req.getQuery());
try { try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken: token }));
} catch (err) {
console.info("User was not connected", err);
}
}
//user have not token created, check data on hydra and create token
const userInfo = await openIDClient.getUserInfo(code as string, nonce as string); const userInfo = await openIDClient.getUserInfo(code as string, nonce as string);
const email = userInfo.email || userInfo.sub; const email = userInfo.email || userInfo.sub;
if (!email) { if (!email) {
throw new Error("No email in the response"); throw new Error("No email in the response");
} }
const authToken = jwtTokenManager.createAuthToken(email); const authToken = jwtTokenManager.createAuthToken(email, userInfo.access_token);
res.writeStatus("200"); res.writeStatus("200");
this.addCorsHeaders(res); this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken })); return res.end(JSON.stringify({ authToken }));
@ -63,6 +81,30 @@ export class AuthenticateController extends BaseController {
return this.errorToResponse(e, res); return this.errorToResponse(e, res);
} }
}); });
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/logout-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { token } = parse(req.getQuery());
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
throw Error("Token cannot to be logout on Hydra");
}
await openIDClient.logoutUser(authTokenData.hydraAccessToken);
} catch (error) {
console.error("openIDCallback => logout-callback", error);
} finally {
res.writeStatus("200");
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
}
});
} }
//Try to login with an admin token //Try to login with an admin token
@ -136,4 +178,39 @@ export class AuthenticateController extends BaseController {
); );
}); });
} }
profileCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/profile-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { userIdentify, token } = parse(req.getQuery());
try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
//get login profile
res.writeStatus("302");
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.hydraAccessToken));
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
} catch (error) {
return this.errorToResponse(error, res);
}
}
} catch (error) {
this.errorToResponse(error, res);
}
});
}
} }

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