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
working-directory: "front"
- name: "Pretty"
run: yarn run pretty
working-directory: "front"
- name: "Jasmine"
run: yarn test
working-directory: "front"

View file

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

View file

@ -1,7 +1,12 @@
/**
* 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 { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient";
@ -83,25 +88,33 @@ export class VariablesManager {
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
const objects = new Map<string, Variable>();
for (const layer of map.layers) {
if (layer.type === "objectgroup") {
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));
}
}
}
this.recursiveFindVariablesInLayer(layer, 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 {
const variable: Variable = {};

View file

@ -194,10 +194,10 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@workadventure/tiled-map-type-guard@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba"
integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A==
"@workadventure/tiled-map-type-guard@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.2.tgz#4171550f6cd71be19791faef48360d65d698bcb0"
integrity sha512-RCtygGV5y9cb7QoyGMINBE9arM5pyXjkxvXgA5uXEv4GDbXKorhFim/rHgwbVR+eFnVF3rDgWbRnk3DIaHt+lQ==
dependencies:
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
```
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.
@ -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).
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.
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.
Out of the box, you cannot edit *any* variable. Variables MUST be declared in the map.
Check the [dedicated variables page](variables.md) to learn how to declare a variable in a map.
## 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";
export const isButtonClickedEvent =
new tg.IsInterface().withProperties({
export const isButtonClickedEvent = new tg.IsInterface()
.withProperties({
popupId: 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.
*/

View file

@ -1,10 +1,11 @@
import * as tg from "generic-type-guard";
export const isChatEvent =
new tg.IsInterface().withProperties({
export const isChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
author: tg.isString,
}).get();
})
.get();
/**
* 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";
export const isClosePopupEvent =
new tg.IsInterface().withProperties({
export const isClosePopupEvent = new tg.IsInterface()
.withProperties({
popupId: tg.isNumber,
}).get();
})
.get();
/**
* 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";
export const isEnterLeaveEvent =
new tg.IsInterface().withProperties({
export const isEnterLeaveEvent = new tg.IsInterface()
.withProperties({
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.
*/

View file

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard";
export const isGoToPageEvent =
new tg.IsInterface().withProperties({
export const isGoToPageEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
}).get();
})
.get();
/**
* 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";
export const isLoadSoundEvent =
new tg.IsInterface().withProperties({
export const isLoadSoundEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
}).get();
})
.get();
/**
* 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";
const isButtonDescriptor =
new tg.IsInterface().withProperties({
const isButtonDescriptor = new tg.IsInterface()
.withProperties({
label: tg.isString,
className: tg.isOptional(tg.isString)
}).get();
className: tg.isOptional(tg.isString),
})
.get();
export const isOpenPopupEvent =
new tg.IsInterface().withProperties({
export const isOpenPopupEvent = new tg.IsInterface()
.withProperties({
popupId: tg.isNumber,
targetObject: tg.isString,
message: tg.isString,
buttons: tg.isArray(isButtonDescriptor)
}).get();
buttons: tg.isArray(isButtonDescriptor),
})
.get();
/**
* 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";
export const isOpenTabEvent =
new tg.IsInterface().withProperties({
export const isOpenTabEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
}).get();
})
.get();
/**
* 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";
const isSoundConfig =
new tg.IsInterface().withProperties({
const isSoundConfig = new tg.IsInterface()
.withProperties({
volume: tg.isOptional(tg.isNumber),
loop: tg.isOptional(tg.isBoolean),
mute: tg.isOptional(tg.isBoolean),
rate: tg.isOptional(tg.isNumber),
detune: tg.isOptional(tg.isNumber),
seek: tg.isOptional(tg.isNumber),
delay: tg.isOptional(tg.isNumber)
}).get();
delay: tg.isOptional(tg.isNumber),
})
.get();
export const isPlaySoundEvent =
new tg.IsInterface().withProperties({
export const isPlaySoundEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
config : tg.isOptional(isSoundConfig),
}).get();
config: tg.isOptional(isSoundConfig),
})
.get();
/**
* 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";
export const isStopSoundEvent =
new tg.IsInterface().withProperties({
export const isStopSoundEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
}).get();
})
.get();
/**
* 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";
export const isUserInputChatEvent =
new tg.IsInterface().withProperties({
export const isUserInputChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
}).get();
})
.get();
/**
* 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;
@ -6,13 +6,13 @@ export interface ButtonDescriptor {
/**
* The label of the button
*/
label: string,
label: string;
/**
* 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: ButtonClickedCallback,
callback: ButtonClickedCallback;
}

View file

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

View file

@ -1,16 +1,18 @@
import type {IframeResponseEventMap} from "../../Api/Events/IframeEvent";
import type {IframeCallback} from "../../Api/iframe/IframeApiContribution";
import type {IframeCallbackContribution} from "../../Api/iframe/IframeApiContribution";
import type { IframeResponseEventMap } from "../../Api/Events/IframeEvent";
import type { IframeCallback } 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 = {
typeChecker: callbackData.typeChecker,
callback: callbackData.callback
callback: callbackData.callback,
} as IframeCallback<T>;
const newCallback = { [callbackData.type]: iframeCallback };
Object.assign(registeredCallbacks, newCallback)
Object.assign(registeredCallbacks, newCallback);
return callbackData as unknown as IframeCallbackContribution<keyof IframeResponseEventMap>;
}

View file

@ -1,6 +1,4 @@
<script lang="ts">
import audioImg from "../images/audio.svg";
import audioMuteImg from "../images/audio-mute.svg";
import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import {
@ -12,6 +10,8 @@
import {onDestroy, onMount} from "svelte";
let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement;
let audioPlayerVol: HTMLInputElement;
let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null;
@ -19,6 +19,10 @@
let decreaseWhileTalking: boolean = true;
onMount(() => {
volume = localUserStore.getAudioPlayerVolume();
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
setVolume();
unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{
HTMLAudioPlayer.pause();
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
@ -52,9 +56,26 @@
function onMute() {
audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted);
localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted);
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)
localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume);
}
@ -67,8 +88,28 @@
<div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume">
<img src={$audioManagerVolumeStore.muted ? audioMuteImg : audioImg} alt="player volume" on:click={onMute}>
<input type="range" min="0" max="1" step="0.025" bind:value={volume} on:change={setVolume}>
<span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon}
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 class="audio-manager-reduce-conversation">
<label>
@ -86,34 +127,66 @@
<style lang="scss">
div.main-audio-manager.nes-container.is-rounded {
position: relative;
top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw);
padding: 3px 3px;
margin-left: auto;
margin-right: auto;
position: relative;
top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw);
padding: 3px 3px;
margin-left: auto;
margin-right: auto;
background-color: rgb(0,0,0,0.5);
display: grid;
grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
div.audio-manager-player-volume {
background-color: rgb(0,0,0,0.5);
display: grid;
grid-template-columns: 50px 1fr;
grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
img {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
div.audio-manager-player-volume {
display: grid;
grid-template-columns: 50px 1fr;
span svg {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
}
}
}
section.audio-manager-file {
display: none;
}
section.audio-manager-file {
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">
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 monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg";
@ -12,6 +12,7 @@
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
import {LayoutMode} from "../WebRtc/LayoutManager";
import {peerStore} from "../Stores/PeerStore";
import {onDestroy} from "svelte";
function screenSharingClick(): void {
if ($requestedScreenSharingState === true) {
@ -44,6 +45,12 @@
$layoutModeStore = LayoutMode.Presentation;
}
}
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script>
<div>
@ -55,22 +62,22 @@
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
{/if}
</div>
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState}
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore || isSilent} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing">
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing">
{/if}
</div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}>
{#if $requestedCameraState}
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{#if $requestedCameraState && !isSilent}
<img src={cinemaImg} alt="Turn on webcam">
{:else}
<img src={cinemaCloseImg} alt="Turn off webcam">
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
{#if $requestedMicrophoneState}
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone">
{:else}
<img src={microphoneCloseImg} alt="Turn off microphone">

View file

@ -1,13 +1,17 @@
<script lang="typescript">
import {gameManager} from "../../Phaser/Game/GameManager";
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 {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
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(){
@ -33,22 +37,50 @@
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
/*function clickLogin() {
connectionManager.loadOpenIDScreen();
}*/
</script>
<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>
<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" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
</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>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={clickLogin}>Login</button>
@ -63,6 +95,12 @@
align-items: center;
margin-bottom: 20px;
iframe{
width: 100%;
height: 50vh;
border: none;
}
button {
height: 50px;
width: 250px;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,19 +1,17 @@
import {Subject} from "rxjs";
import { Subject } from "rxjs";
interface EmoteEvent {
userId: number,
emoteName: string,
userId: number;
emoteName: string;
}
class EmoteEventStream {
private _stream:Subject<EmoteEvent> = new Subject();
private _stream: Subject<EmoteEvent> = new Subject();
public stream = this._stream.asObservable();
fire(userId: number, emoteName:string) {
this._stream.next({userId, emoteName});
fire(userId: number, emoteName: string) {
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 { v4 as uuidv4 } from "uuid";
import { START_ROOM_URL } from "../Enum/EnvironmentVariable";
const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer";
@ -17,6 +18,8 @@ const authToken = "authToken";
const state = "state";
const nonce = "nonce";
const notification = "notificationPermission";
const code = "code";
const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache";
@ -125,7 +128,9 @@ class LocalUserStore {
});
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
}
getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => {
@ -160,19 +165,32 @@ class LocalUserStore {
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value;
}
getState(): string | null {
return localStorage.getItem(state);
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
const oldValue = localStorage.getItem(nonce);
localStorage.removeItem(nonce);
return oldValue;
return localStorage.getItem(nonce);
}
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 {
public readonly id: string;
public readonly isPublic: boolean;
private _authenticationMandatory: boolean = false;
private _iframeAuthentication?: string;
private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined;
private instance: string | undefined;
@ -101,6 +103,8 @@ export class Room {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
this._authenticationMandatory = data.authenticationMandatory || false;
this._iframeAuthentication = data.iframeAuthentication;
return new MapDetail(data.mapUrl, data.textures);
}
@ -186,4 +190,12 @@ export class Room {
}
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 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(
token: string | null,
@ -218,7 +223,7 @@ export class RoomConnection implements RoomConnection {
worldFullMessageStream.onMessage();
this.closed = true;
} 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
} else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());

View file

@ -1,14 +1,12 @@
import {Subject} from "rxjs";
import { Subject } from "rxjs";
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();
onMessage(message? :string) {
onMessage(message?: string) {
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 NODE_ENV = process.env.NODE_ENV || "development";
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;

View file

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

View file

@ -1,19 +1,21 @@
export class MessageUI {
static warningMessage(text: string){
static warningMessage(text: string) {
this.removeMessage();
const body = document.getElementById("body");
body?.insertAdjacentHTML('afterbegin', `
body?.insertAdjacentHTML(
"afterbegin",
`
<div id="message-reconnect" class="message-info warning">
${text}
</div>
`);
`
);
}
static removeMessage(id : string|null = null) {
if(!id){
static removeMessage(id: string | null = null) {
if (!id) {
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();
}
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 type {PointInterface} from "../Connexion/ConnexionModels";
import type { PointInterface } from "../Connexion/ConnexionModels";
export class ProtobufClientUtils {
public static toPointInterface(position: PositionMessage): PointInterface {
let direction: string;
switch (position.getDirection()) {
case Direction.UP:
direction = 'up';
direction = "up";
break;
case Direction.DOWN:
direction = 'down';
direction = "down";
break;
case Direction.LEFT:
direction = 'left';
direction = "left";
break;
case Direction.RIGHT:
direction = 'right';
direction = "right";
break;
default:
throw new Error("Unexpected direction");

View file

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

View file

@ -1,7 +1,7 @@
export interface CompanionResourceDescriptionInterface {
name: string,
img: string,
behaviour: "dog" | "cat"
name: string;
img: string;
behaviour: "dog" | "cat";
}
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: "cat2", img: "resources/characters/pipoya/Cat 01-2.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;
}
};
export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise<string> => {
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!`);
}
if (loader.textureManager.exists(resource.name)) {
return resolve(resource.name);
}
loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 });
loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name));
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 {
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);
this.setScrollFactor(0, 0);
this.setOrigin(0, 1);
@ -10,4 +10,4 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite {
this.setVisible(false);
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) {
super(scene, x, y, textureName);
this.scene.add.existing(this);
this.setInteractive();
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 {
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);
this.setScrollFactor(0, 0);
this.setOrigin(0, 1);
@ -10,4 +10,4 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
this.setVisible(false);
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
}
}
}

View file

@ -1,20 +1,20 @@
import Sprite = Phaser.GameObjects.Sprite;
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
import { DEPTH_UI_INDEX } from "../Game/DepthIndexes";
import { waScaleManager } from "../Services/WaScaleManager";
export interface RadialMenuItem {
image: string,
name: string,
image: string;
name: string;
}
export const RadialMenuClickEvent = 'radialClick';
export const RadialMenuClickEvent = "radialClick";
export class RadialMenu extends Phaser.GameObjects.Container {
private resizeCallback: OmitThisParameter<() => void>;
constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) {
super(scene, x, y);
this.setDepth(DEPTH_UI_INDEX)
this.setDepth(DEPTH_UI_INDEX);
this.scene.add.existing(this);
this.initItems();
@ -22,45 +22,45 @@ export class RadialMenu extends Phaser.GameObjects.Container {
this.resizeCallback = this.resize.bind(this);
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
}
private initItems() {
const itemsNumber = this.items.length;
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) {
const image = new Sprite(this.scene, 0, menuRadius, item.image);
const image = new Sprite(this.scene, 0, menuRadius, item.image);
this.add(image);
this.scene.sys.updateList.add(image);
const scalingFactor = waScaleManager.uiScalingFactor * 0.075;
image.setScale(scalingFactor)
image.setScale(scalingFactor);
image.setInteractive({
useHandCursor: true,
});
image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item));
image.on('pointerover', () => {
image.on("pointerdown", () => this.emit(RadialMenuClickEvent, item));
image.on("pointerover", () => {
this.scene.tweens.add({
targets: image,
props: {
scale: 2 * scalingFactor,
},
duration: 500,
ease: 'Power3',
})
ease: "Power3",
});
});
image.on('pointerout', () => {
image.on("pointerout", () => {
this.scene.tweens.add({
targets: image,
props: {
scale: scalingFactor,
},
duration: 500,
ease: 'Power3',
})
ease: "Power3",
});
});
const angle = 2 * Math.PI * index / itemsNumber;
Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius);
const angle = (2 * Math.PI * index) / itemsNumber;
Phaser.Actions.RotateAroundDistance([image], { x: 0, y: 0 }, angle, menuRadius);
}
private resize() {
@ -71,4 +71,4 @@ export class RadialMenu extends Phaser.GameObjects.Container {
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
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
@ -7,10 +7,10 @@ export class SoundMeter {
private instant: number;
private clip: number;
//private script: ScriptProcessorNode;
private analyser: IAnalyserNode<IAudioContext>|undefined;
private dataArray: Uint8Array|undefined;
private context: IAudioContext|undefined;
private source: IMediaStreamAudioSourceNode<IAudioContext>|undefined;
private analyser: IAnalyserNode<IAudioContext> | undefined;
private dataArray: Uint8Array | undefined;
private context: IAudioContext | undefined;
private source: IMediaStreamAudioSourceNode<IAudioContext> | undefined;
constructor() {
this.instant = 0.0;
@ -27,8 +27,7 @@ export class SoundMeter {
this.dataArray = new Uint8Array(bufferLength);
}
public connectToSource(stream: MediaStream, context: IAudioContext): void
{
public connectToSource(stream: MediaStream, context: IAudioContext): void {
if (this.source !== undefined) {
this.stop();
}
@ -42,8 +41,6 @@ export class SoundMeter {
//analyser.connect(distortion);
//distortion.connect(this.context.destination);
//this.analyser.connect(this.context.destination);
}
public getVolume(): number {
@ -52,16 +49,15 @@ export class SoundMeter {
}
this.analyser.getByteFrequencyData(this.dataArray);
const input = this.dataArray;
let i;
let sum = 0.0;
//let clipcount = 0;
for (i = 0; i < input.length; ++i) {
sum += input[i] * input[i];
// if (Math.abs(input[i]) > 0.99) {
// clipcount += 1;
// }
// if (Math.abs(input[i]) > 0.99) {
// clipcount += 1;
// }
}
this.instant = Math.sqrt(sum / input.length);
//this.slow = 0.95 * that.slow + 0.05 * that.instant;
@ -84,6 +80,4 @@ export class SoundMeter {
this.dataArray = undefined;
this.source = undefined;
}
}

View file

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

View file

@ -1,5 +1,5 @@
import Container = Phaser.GameObjects.Container;
import type {Scene} from "phaser";
import type { Scene } from "phaser";
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
export interface BodyResourceDescriptionListInterface {
[key: string]: BodyResourceDescriptionInterface
[key: string]: BodyResourceDescriptionInterface;
}
export interface BodyResourceDescriptionInterface {
name: string,
img: string,
level?: number
name: string;
img: string;
level?: number;
}
export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {
"male1": {name: "male1", img: "resources/characters/pipoya/Male 01-1.png"},
"male2": {name: "male2", img: "resources/characters/pipoya/Male 02-2.png"},
"male3": {name: "male3", img: "resources/characters/pipoya/Male 03-4.png"},
"male4": {name: "male4", img: "resources/characters/pipoya/Male 09-1.png"},
"male5": {name: "male5", img: "resources/characters/pipoya/Male 10-3.png"},
"male6": {name: "male6", img: "resources/characters/pipoya/Male 17-2.png"},
"male7": {name: "male7", img: "resources/characters/pipoya/Male 18-1.png"},
"male8": {name: "male8", img: "resources/characters/pipoya/Male 16-4.png"},
"male9": {name: "male9", img: "resources/characters/pipoya/Male 07-2.png"},
"male10": {name: "male10", img: "resources/characters/pipoya/Male 05-3.png"},
"male11": {name: "male11", img: "resources/characters/pipoya/Teacher male 02.png"},
"male12": {name: "male12", img: "resources/characters/pipoya/su4 Student male 12.png"},
male1: { name: "male1", img: "resources/characters/pipoya/Male 01-1.png" },
male2: { name: "male2", img: "resources/characters/pipoya/Male 02-2.png" },
male3: { name: "male3", img: "resources/characters/pipoya/Male 03-4.png" },
male4: { name: "male4", img: "resources/characters/pipoya/Male 09-1.png" },
male5: { name: "male5", img: "resources/characters/pipoya/Male 10-3.png" },
male6: { name: "male6", img: "resources/characters/pipoya/Male 17-2.png" },
male7: { name: "male7", img: "resources/characters/pipoya/Male 18-1.png" },
male8: { name: "male8", img: "resources/characters/pipoya/Male 16-4.png" },
male9: { name: "male9", img: "resources/characters/pipoya/Male 07-2.png" },
male10: { name: "male10", img: "resources/characters/pipoya/Male 05-3.png" },
male11: { name: "male11", img: "resources/characters/pipoya/Teacher male 02.png" },
male12: { name: "male12", img: "resources/characters/pipoya/su4 Student male 12.png" },
"Female1": {name: "Female1", img: "resources/characters/pipoya/Female 01-1.png"},
"Female2": {name: "Female2", img: "resources/characters/pipoya/Female 02-2.png"},
"Female3": {name: "Female3", img: "resources/characters/pipoya/Female 03-4.png"},
"Female4": {name: "Female4", img: "resources/characters/pipoya/Female 09-1.png"},
"Female5": {name: "Female5", img: "resources/characters/pipoya/Female 10-3.png"},
"Female6": {name: "Female6", img: "resources/characters/pipoya/Female 17-2.png"},
"Female7": {name: "Female7", img: "resources/characters/pipoya/Female 18-1.png"},
"Female8": {name: "Female8", img: "resources/characters/pipoya/Female 16-4.png"},
"Female9": {name: "Female9", img: "resources/characters/pipoya/Female 07-2.png"},
"Female10": {name: "Female10", img: "resources/characters/pipoya/Female 05-3.png"},
"Female11": {name: "Female11", img: "resources/characters/pipoya/Teacher fmale 02.png"},
"Female12": {name: "Female12", img: "resources/characters/pipoya/su4 Student fmale 12.png"},
Female1: { name: "Female1", img: "resources/characters/pipoya/Female 01-1.png" },
Female2: { name: "Female2", img: "resources/characters/pipoya/Female 02-2.png" },
Female3: { name: "Female3", img: "resources/characters/pipoya/Female 03-4.png" },
Female4: { name: "Female4", img: "resources/characters/pipoya/Female 09-1.png" },
Female5: { name: "Female5", img: "resources/characters/pipoya/Female 10-3.png" },
Female6: { name: "Female6", img: "resources/characters/pipoya/Female 17-2.png" },
Female7: { name: "Female7", img: "resources/characters/pipoya/Female 18-1.png" },
Female8: { name: "Female8", img: "resources/characters/pipoya/Female 16-4.png" },
Female9: { name: "Female9", img: "resources/characters/pipoya/Female 07-2.png" },
Female10: { name: "Female10", img: "resources/characters/pipoya/Female 05-3.png" },
Female11: { name: "Female11", img: "resources/characters/pipoya/Teacher fmale 02.png" },
Female12: { name: "Female12", img: "resources/characters/pipoya/su4 Student fmale 12.png" },
};
export const COLOR_RESOURCES: BodyResourceDescriptionListInterface = {
"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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_33": {name: "color_33", img: "resources/customisation/character_color/character_color32.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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_33: { name: "color_33", img: "resources/customisation/character_color/character_color32.png" },
};
export const EYES_RESOURCES: BodyResourceDescriptionListInterface = {
"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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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" },
};
export const HAIR_RESOURCES: BodyResourceDescriptionListInterface = {
"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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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" },
};
export const CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {
"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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_pride_shirt": {name:"clothes_pride_shirt",img: "resources/customisation/character_clothes/pride_shirt.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"}
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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_pride_shirt: {
name: "clothes_pride_shirt",
img: "resources/customisation/character_clothes/pride_shirt.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 = {
"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_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_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_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_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_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_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_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_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_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_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_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_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"},
"tinfoil_hat1": {name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.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_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_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_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_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_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_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_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_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_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_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_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_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" },
tinfoil_hat1: { name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png" },
};
export const ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {
"accessory_1": {name: "accessory_1", img: "resources/customisation/character_accessories/character_accessories1.png"},
"accessory_2": {name: "accessory_2", img: "resources/customisation/character_accessories/character_accessories2.png"},
"accessory_3": {name: "accessory_3", img: "resources/customisation/character_accessories/character_accessories3.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_6": {name: "accessory_6", img: "resources/customisation/character_accessories/character_accessories6.png"},
"accessory_7": {name: "accessory_7", img: "resources/customisation/character_accessories/character_accessories7.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_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"}
accessory_1: {
name: "accessory_1",
img: "resources/customisation/character_accessories/character_accessories1.png",
},
accessory_2: {
name: "accessory_2",
img: "resources/customisation/character_accessories/character_accessories2.png",
},
accessory_3: {
name: "accessory_3",
img: "resources/customisation/character_accessories/character_accessories3.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_6: {
name: "accessory_6",
img: "resources/customisation/character_accessories/character_accessories6.png",
},
accessory_7: {
name: "accessory_7",
img: "resources/customisation/character_accessories/character_accessories7.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_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[] = [
@ -336,9 +442,9 @@ export const LAYERS: BodyResourceDescriptionListInterface[] = [
HAIR_RESOURCES,
CLOTHES_RESOURCES,
HATS_RESOURCES,
ACCESSORIES_RESOURCES
ACCESSORIES_RESOURCES,
];
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 type {Character} from "./Character";
import type { Character } from "./Character";
//todo: improve this WIP
export class SpeechBubble {
private bubble: Phaser.GameObjects.Graphics;
private content: Phaser.GameObjects.Text;
constructor(scene: Scene, player: Character, text: string = "") {
const bubbleHeight = 50;
const bubblePadding = 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(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);
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 {
this.bubble.setVisible(false) //todo find a better way
this.bubble.setVisible(false); //todo find a better way
this.bubble.destroy();
this.content.destroy();
}

View file

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

View file

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

View file

@ -18,6 +18,7 @@ export class GameManager {
private characterLayers: string[] | null;
private companion: string | null;
private startRoom!: Room;
private cameraSetup?: { video: unknown; audio: unknown };
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.
private scenePlugin!: Phaser.Scenes.ScenePlugin;
@ -26,6 +27,7 @@ export class GameManager {
this.playerName = localUserStore.getName();
this.characterLayers = localUserStore.getCharacterLayers();
this.companion = localUserStore.getCompanion();
this.cameraSetup = localUserStore.getCameraSetup();
}
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
@ -33,12 +35,17 @@ export class GameManager {
this.startRoom = await connectionManager.initGameConnexion();
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;
} else if (!this.characterLayers || !this.characterLayers.length) {
return SelectCharacterSceneName;
} else {
} else if (this.cameraSetup == undefined) {
return EnableCameraSceneName;
} else {
this.activeMenuSceneAndHelpCameraSettings();
return this.startRoom.key;
}
}
@ -84,7 +91,14 @@ export class GameManager {
public goToStartingMap(): void {
console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
this.scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
this.activeMenuSceneAndHelpCameraSettings();
}
/**
* @private
* @return void
*/
private activeMenuSceneAndHelpCameraSettings(): void {
if (
!localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState))
@ -131,6 +145,10 @@ export class GameManager {
if (this.currentGameSceneName === null) throw "No current scene id set!";
return this.scenePlugin.get(this.currentGameSceneName) as GameScene;
}
public get currentStartedRoom() {
return this.startRoom;
}
}
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 { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { addLoader } from "../Components/Loader";
import { addLoader, removeLoader } from "../Components/Loader";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer";
@ -92,6 +92,8 @@ import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import type { RadialMenuItem } from "../Components/RadialMenu";
export interface GameSceneInitInterface {
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.
console.error("Error when loading: ", file);
if (this.preloading) {
//remove loader in progress
removeLoader(this);
//display an error scene
this.scene.start(ErrorSceneName, {
title: "Network error",
subTitle: "An error occurred while loading resource:",
@ -580,6 +585,7 @@ export class GameScene extends DirtyScene {
this.updateCameraOffset(box)
);
new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) {
@ -713,25 +719,8 @@ export class GameScene extends DirtyScene {
// When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection);
peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer);
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
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this));
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this));
@ -825,40 +814,7 @@ export class GameScene extends DirtyScene {
}, 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) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
@ -898,8 +854,10 @@ export class GameScene extends DirtyScene {
this.gameMap.onPropertyChange("silent", (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.connection?.setSilent(false);
this.CurrentPlayer.noSilent();
} else {
this.connection?.setSilent(true);
this.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => {
@ -1306,7 +1264,9 @@ ${escapedMessage}
if (!targetRoom.isEqual(this.room)) {
if (this.scene.get(targetRoom.key) === null) {
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.scene.stop();

View file

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

View file

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

View file

@ -3,17 +3,23 @@
* It has coordinates and an "activation radius"
*/
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";
type EventCallback = (state: unknown, parameters: unknown) => void;
export class ActionableItem {
private readonly activationRadiusSquared : number;
private readonly activationRadiusSquared: number;
private isSelectable: boolean = false;
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;
}
@ -25,8 +31,8 @@ export class ActionableItem {
* 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.
*/
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);
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);
if (distanceSquared < this.activationRadiusSquared) {
return distanceSquared;
} else {
@ -45,7 +51,7 @@ export class ActionableItem {
this.getOutlinePlugin()?.add(this.sprite, {
thickness: 2,
outlineColor: 0xffff00
outlineColor: 0xffff00,
});
}
@ -60,8 +66,8 @@ export class ActionableItem {
this.getOutlinePlugin()?.remove(this.sprite);
}
private getOutlinePlugin(): OutlinePipelinePlugin|undefined {
return this.sprite.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined;
private getOutlinePlugin(): 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 {
let callbacksArray: Array<EventCallback>|undefined = this.callbacks.get(eventName);
let callbacksArray: Array<EventCallback> | undefined = this.callbacks.get(eventName);
if (callbacksArray === undefined) {
callbacksArray = new Array<EventCallback>();
this.callbacks.set(eventName, callbacksArray);

View file

@ -1,86 +1,91 @@
import * as Phaser from 'phaser';
import {Scene} from "phaser";
import * as Phaser from "phaser";
import { Scene } from "phaser";
import Sprite = Phaser.GameObjects.Sprite;
import type {ITiledMapObject} from "../../Map/ITiledMap";
import type {ItemFactoryInterface} from "../ItemFactoryInterface";
import type {GameScene} from "../../Game/GameScene";
import {ActionableItem} from "../ActionableItem";
import type { ITiledMapObject } from "../../Map/ITiledMap";
import type { ItemFactoryInterface } from "../ItemFactoryInterface";
import type { GameScene } from "../../Game/GameScene";
import { ActionableItem } from "../ActionableItem";
import * as tg from "generic-type-guard";
const isComputerState =
new tg.IsInterface().withProperties({
const isComputerState = new tg.IsInterface()
.withProperties({
status: tg.isString,
}).get();
})
.get();
type ComputerState = tg.GuardedType<typeof isComputerState>;
let state: ComputerState = {
'status': 'off'
status: "off",
};
export default {
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 => {
scene.anims.create({
key: 'computer_off',
key: "computer_off",
frames: [
{
key: 'computer',
frame: 'computer_off'
}
key: "computer",
frame: "computer_off",
},
],
frameRate: 10,
repeat: -1
repeat: -1,
});
scene.anims.create({
key: 'computer_run',
key: "computer_run",
frames: [
{
key: 'computer',
frame: 'computer_on1'
},
{
key: 'computer',
frame: 'computer_on2'
}
],
{
key: "computer",
frame: "computer_on1",
},
{
key: "computer",
frame: "computer_on2",
},
],
frameRate: 5,
repeat: -1
repeat: -1,
});
},
factory: (scene: GameScene, object: ITiledMapObject, initState: unknown): ActionableItem => {
if (initState !== undefined) {
if (!isComputerState(initState)) {
throw new Error('Invalid state received for computer object');
throw new Error("Invalid state received for computer object");
}
state = initState;
}
// 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);
if (state.status === 'on') {
computer.anims.play('computer_run');
if (state.status === "on") {
computer.anims.play("computer_run");
}
const item = new ActionableItem(object.id, computer, scene, 32, (item: ActionableItem) => {
if (state.status === 'off') {
state.status = 'on';
item.emit('TURN_ON', state);
if (state.status === "off") {
state.status = "on";
item.emit("TURN_ON", state);
} else {
state.status = 'off';
item.emit('TURN_OFF', state);
state.status = "off";
item.emit("TURN_OFF", state);
}
});
item.on('TURN_ON', () => {
computer.anims.play('computer_run');
item.on("TURN_ON", () => {
computer.anims.play("computer_run");
});
item.on('TURN_OFF', () => {
computer.anims.play('computer_off');
item.on("TURN_OFF", () => {
computer.anims.play("computer_off");
});
return item;
//scene.add.sprite(object.x, object.y, 'computer');
}
},
} as ItemFactoryInterface;

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
import { gameManager } from "../Game/GameManager";
import { SelectCharacterSceneName } from "./SelectCharacterScene";
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";
@ -18,6 +20,16 @@ export class LoginScene extends ResizableScene {
preload() {}
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);
}

View file

@ -1,4 +1,4 @@
import {Scene} from "phaser";
import { Scene } from "phaser";
import DOMElement = Phaser.GameObjects.DOMElement;
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".
*/
public centerXDomElement(object: DOMElement, defaultWidth: number): void {
object.x = (this.scale.width / 2) -
(
object
&& object.node
&& object.node.getBoundingClientRect().width > 0
? (object.node.getBoundingClientRect().width / 2 / this.scale.zoom)
: (defaultWidth / this.scale.zoom)
);
object.x =
this.scale.width / 2 -
(object && object.node && object.node.getBoundingClientRect().width > 0
? object.node.getBoundingClientRect().width / 2 / this.scale.zoom
: defaultWidth / this.scale.zoom);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
interface Size {
width: number;
height: number;
@ -23,14 +22,14 @@ export class HdpiManager {
*
* @param realPixelScreenSize
*/
public getOptimalGameSize(realPixelScreenSize: Size): { game: Size, real: Size } {
public getOptimalGameSize(realPixelScreenSize: Size): { game: Size; real: Size } {
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,
// let's make the canvas the size of the screen (in real pixels)
if (realPixelNumber <= this.minRecommendedGamePixelsNumber) {
return {
game: realPixelScreenSize,
real: realPixelScreenSize
real: realPixelScreenSize,
};
}
@ -49,8 +48,8 @@ export class HdpiManager {
real: {
width: realPixelScreenSize.width,
height: realPixelScreenSize.height,
}
}
},
};
}
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.
if (gameWidth * gameHeight < this.absoluteMinPixelNumber) {
const minGameHeight = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.height / realPixelScreenSize.width);
const minGameWidth = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.width / realPixelScreenSize.height);
const minGameHeight = Math.sqrt(
(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)
this._zoomModifier = realPixelScreenSize.width / minGameWidth / optimalZoomLevel;
@ -72,9 +75,8 @@ export class HdpiManager {
real: {
width: realPixelScreenSize.width,
height: realPixelScreenSize.height,
}
}
},
};
}
return {
@ -85,8 +87,8 @@ export class HdpiManager {
real: {
width: Math.ceil(realPixelScreenSize.width / optimalZoomLevel) * optimalZoomLevel,
height: Math.ceil(realPixelScreenSize.height / optimalZoomLevel) * optimalZoomLevel,
}
}
},
};
}
/**
@ -95,7 +97,7 @@ export class HdpiManager {
private getOptimalZoomLevel(realPixelNumber: number): number {
const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber);
if (1.5 <= result && result < 2) {
return 1.5
return 1.5;
} else {
return Math.floor(result);
}

View file

@ -1,10 +1,9 @@
import {HdpiManager} from "./HdpiManager";
import { HdpiManager } from "./HdpiManager";
import ScaleManager = Phaser.Scale.ScaleManager;
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import type {Game} from "../Game/Game";
import {ResizableScene} from "../Login/ResizableScene";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type { Game } from "../Game/Game";
import { ResizableScene } from "../Login/ResizableScene";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
class WaScaleManager {
private hdpiManager: HdpiManager;
@ -23,26 +22,29 @@ class WaScaleManager {
}
public applyNewSize() {
const {width, height} = coWebsiteManager.getGameSize();
const { width, height } = coWebsiteManager.getGameSize();
let devicePixelRatio = 1;
if (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.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio)
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px';
style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px';
style.width = Math.ceil(realSize.width / devicePixelRatio) + "px";
style.height = Math.ceil(realSize.height / devicePixelRatio) + "px";
// 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.height = style.height;
@ -70,7 +72,7 @@ class WaScaleManager {
this._saveZoom = this.hdpiManager.zoomModifier;
}
public restoreZoom(): void{
public restoreZoom(): void {
this.hdpiManager.zoomModifier = this._saveZoom;
this.applyNewSize();
}
@ -81,7 +83,6 @@ class WaScaleManager {
public get uiScalingFactor(): number {
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 {waScaleManager} from "../Services/WaScaleManager";
import {GameScene} from "../Game/GameScene";
import { Pinch } from "phaser3-rex-plugins/plugins/gestures.js";
import { waScaleManager } from "../Services/WaScaleManager";
import { GameScene } from "../Game/GameScene";
export class PinchManager {
private scene: Phaser.Scene;
@ -15,18 +15,18 @@ export class PinchManager {
// We are smoothing its value with previous values to prevent the flicking.
let smoothPinch = 1;
this.pinch.on('pinchstart', () => {
this.pinch.on("pinchstart", () => {
smoothPinch = 1;
});
this.pinch.on('pinch', (pinch:any) => { // eslint-disable-line
// eslint-disable-next-line
this.pinch.on("pinch", (pinch: any) => {
if (pinch.scaleFactor > 1.2 || pinch.scaleFactor < 0.8) {
// Pinch too fast! Probably a bad measure.
return;
}
smoothPinch = 3/5*smoothPinch + 2/5*pinch.scaleFactor;
smoothPinch = (3 / 5) * smoothPinch + (2 / 5) * pinch.scaleFactor;
if (this.scene instanceof GameScene) {
this.scene.zoomByFactor(smoothPinch);
} else {

View file

@ -1,4 +1,5 @@
import { get, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
export interface audioManagerVolume {
muted: boolean;
@ -103,3 +104,7 @@ export const audioManagerVisibilityStore = writable(false);
export const audioManagerVolumeStore = createAudioManagerVolumeStore();
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 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.
@ -8,7 +8,7 @@ function createErrorStore() {
return {
subscribe,
addErrorMessage: (e: string|Error): void => {
addErrorMessage: (e: string | Error): void => {
update((messages: string[]) => {
let message: string;
if (e instanceof Error) {
@ -26,7 +26,7 @@ function createErrorStore() {
},
clearMessages: (): void => {
set([]);
}
},
};
}

View file

@ -1,8 +1,10 @@
export class BrowserTooOldError extends Error {
static NAME = 'BrowserTooOldError';
static NAME = "BrowserTooOldError";
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;
}
}

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 {
static NAME = 'WebviewOnOldIOS';
static NAME = "WebviewOnOldIOS";
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;
}
}

View file

@ -1,3 +1,4 @@
import { writable } from "svelte/store";
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 { userMovingStore } from "./GameStore";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
@ -9,6 +9,7 @@ import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
/**
* A store that contains the camera state requested by the user (on or off).
@ -255,6 +256,12 @@ export const mediaStreamConstraintsStore = derived(
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
localUserStore.setCameraSetup(
JSON.stringify({
video: currentVideoConstraint,
audio: currentAudioConstraint,
})
);
return;
}
@ -413,7 +420,7 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
});
return;
} catch (e) {
if (constraints.video !== false) {
if (constraints.video !== false || constraints.audio !== false) {
console.info(
"Error. Unable to get microphone and/or camera access. Trying audio only.",
$mediaStreamConstraintsStore,
@ -425,7 +432,17 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
error: e,
});
// 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 {
console.info(
"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
*/
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 menuVisiblilityStore = writable(false);
export const menuInputFocusStore = writable(false);
export const loginUrlStore = writable(false);
export const userIsConnected = writable(false);
let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() {

View file

@ -1,38 +1,30 @@
import { readable, writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import { VideoPeer } from "../WebRtc/VideoPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import type { VideoPeer } from "../WebRtc/VideoPeer";
import type { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
/**
* A store that contains the list of (video) peers we are connected to.
*/
function createPeerStore() {
let peers = new Map<number, VideoPeer>();
const { subscribe, set, update } = writable(peers);
const { subscribe, set, update } = writable(new Map<number, VideoPeer>());
return {
subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => {
peers = new Map<number, VideoPeer>();
set(peers);
simplePeer.registerPeerConnectionListener({
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;
});
},
pushNewPeer(peer: VideoPeer) {
update((users) => {
users.set(peer.userId, peer);
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.
*/
function createScreenSharingPeerStore() {
let peers = new Map<number, ScreenSharingPeer>();
const { subscribe, set, update } = writable(peers);
const { subscribe, set, update } = writable(new Map<number, ScreenSharingPeer>());
return {
subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => {
peers = new Map<number, ScreenSharingPeer>();
set(peers);
simplePeer.registerPeerConnectionListener({
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;
});
},
pushNewPeer(peer: ScreenSharingPeer) {
update((users) => {
users.set(peer.userId, peer);
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 { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator";
import { localUserStore } from "../Connexion/LocalUserStore";
let idCount = 0;

View file

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

View file

@ -1,3 +1,3 @@
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
*/
function createSoundPlayingStore() {
const { subscribe, set, update } = writable<string|null>(null);
const { subscribe, set, update } = writable<string | null>(null);
return {
subscribe,
@ -13,9 +13,7 @@ function createSoundPlayingStore() {
},
soundEnded: () => {
set(null);
}
},
};
}

View file

@ -1,14 +1,12 @@
import { writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import { VideoPeer } from "../WebRtc/VideoPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import { get, writable } from "svelte/store";
import type { Streamable } from "./StreamableCollectionStore";
import { peerStore } from "./PeerStore";
/**
* A store that contains the peer / media that has currently the "importance" focus.
*/
function createVideoFocusStore() {
const { subscribe, set, update } = writable<Streamable | null>(null);
const { subscribe, set } = writable<Streamable | null>(null);
let focusedMedia: Streamable | null = null;
@ -23,27 +21,17 @@ function createVideoFocusStore() {
set(null);
},
toggleFocus: (media: Streamable) => {
if (media !== focusedMedia) {
focusedMedia = media;
} else {
focusedMedia = null;
}
focusedMedia = media !== focusedMedia ? media : null;
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();
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 {
readonly supportTouchScreen:boolean;
readonly supportTouchScreen: boolean;
constructor() {
this.supportTouchScreen = this.detectTouchscreen();
}
//found here: https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript#4819886
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 { localUserStore } from "../Connexion/LocalUserStore";
export enum GameConnexionTypes {
anonymous = 1,
@ -7,13 +8,16 @@ export enum GameConnexionTypes {
empty,
unknown,
jwt,
login,
}
//this class is responsible with analysing and editing the game's url
class UrlManager {
public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString();
if (url === "/jwt") {
if (url === "/login") {
return GameConnexionTypes.login;
} else if (url === "/jwt") {
return GameConnexionTypes.jwt;
} else if (url.includes("_/")) {
return GameConnexionTypes.anonymous;
@ -35,6 +39,8 @@ class UrlManager {
public pushRoomIdToUrl(room: Room): void {
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 search = room.search.toString();
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);

View file

@ -166,7 +166,13 @@ class CoWebsiteManager {
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.cowebsiteMainDom.innerHTML = ``;

View file

@ -1,12 +1,9 @@
export function isIOS(): boolean {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
return (
["iPad Simulator", "iPhone Simulator", "iPod Simulator", "iPad", "iPhone", "iPod"].includes(
navigator.platform
) ||
// 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 {mediaManager} from "./MediaManager";
import {coWebsiteManager} from "./CoWebsiteManager";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import {get} from "svelte/store";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { coWebsiteManager } from "./CoWebsiteManager";
import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import { get } from "svelte/store";
interface jitsiConfigInterface {
startWithAudioMuted: boolean
startWithVideoMuted: boolean
prejoinPageEnabled: boolean
startWithAudioMuted: boolean;
startWithVideoMuted: boolean;
prejoinPageEnabled: boolean;
}
const getDefaultConfig = () : jitsiConfigInterface => {
return {
startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false
interface JitsiOptions {
jwt?: string;
roomName: string;
width: string;
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 currentDefaultConfig = getDefaultConfig();
if(!config){
if (!config) {
return currentDefaultConfig;
}
return {
...currentDefaultConfig,
...config,
startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted ? true : currentDefaultConfig.startWithAudioMuted,
startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted ? true : currentDefaultConfig.startWithVideoMuted,
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled ? true : currentDefaultConfig.prejoinPageEnabled
}
}
startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted
? true
: currentDefaultConfig.startWithAudioMuted,
startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted
? true
: currentDefaultConfig.startWithVideoMuted,
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled
? true
: currentDefaultConfig.prejoinPageEnabled,
};
};
const defaultInterfaceConfig = {
SHOW_CHROME_EXTENSION_BANNER: false,
@ -49,28 +79,48 @@ const defaultInterfaceConfig = {
SHOW_WATERMARK_FOR_GUESTS: false,
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', /*'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'*/
"microphone",
"camera",
"closedcaptions",
"desktop",
/*'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 value = args.join(' ')
const value = args.join(" ");
return value
.normalize('NFD') // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.normalize("NFD") // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator
}
.replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, "-"); // separator
};
class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
private jitsiApi?: JitsiApi;
private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this);
private jitsiScriptLoaded: boolean = false;
@ -79,11 +129,19 @@ class JitsiFactory {
* Slugifies the room name and prepends the room name with the instance
*/
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 {
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
public start(
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
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
@ -94,18 +152,18 @@ class JitsiFactory {
const domain = jitsiUrl || JITSI_URL;
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);
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
const options: JitsiOptions = {
roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: cowebsiteDiv,
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig}
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!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.
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.executeCommand('displayName', playerName);
this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
});
}), jitsiWidth);
}, jitsiWidth);
}
public async stop(): Promise<void> {
if(!this.jitsiApi){
if (!this.jitsiApi) {
return;
}
await coWebsiteManager.closeCoWebsite();
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);
this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback);
this.jitsiApi?.dispose();
}
private onAudioChange({muted}: {muted: boolean}): void {
private onAudioChange({ muted }: { muted: boolean }): void {
if (muted) {
requestedMicrophoneState.disableMicrophone();
} else {
@ -141,7 +199,7 @@ class JitsiFactory {
}
}
private onVideoChange({muted}: {muted: boolean}): void {
private onVideoChange({ muted }: { muted: boolean }): void {
if (muted) {
requestedCameraState.disableWebcam();
} else {
@ -159,20 +217,17 @@ class JitsiFactory {
this.jitsiScriptLoaded = true;
// Load Jitsi if the environment variable is set.
const jitsiScript = document.createElement('script');
jitsiScript.src = 'https://' + domain + '/external_api.js';
const jitsiScript = document.createElement("script");
jitsiScript.src = "https://" + domain + "/external_api.js";
jitsiScript.onload = () => {
resolve();
}
};
jitsiScript.onerror = () => {
reject();
}
};
document.head.appendChild(jitsiScript);
})
});
}
}

View file

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

View file

@ -10,6 +10,7 @@ import { blackListManager } from "./BlackListManager";
import { get } from "svelte/store";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { playersStore } from "../Stores/PlayersStore";
import { peerStore, screenSharingPeerStore } from "../Stores/PeerStore";
export interface UserSimplePeerInterface {
userId: number;
@ -20,12 +21,6 @@ export interface UserSimplePeerInterface {
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.
*/
@ -37,12 +32,14 @@ export class SimplePeer {
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number;
private lastWebrtcUserName: string | undefined;
private lastWebrtcPassword: string | undefined;
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.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
@ -73,14 +70,6 @@ export class SimplePeer {
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
*/
@ -164,9 +153,7 @@ export class SimplePeer {
}
this.PeerConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(peer);
}
peerStore.pushNewPeer(peer);
return peer;
}
@ -214,9 +201,7 @@ export class SimplePeer {
);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(peer);
}
screenSharingPeerStore.pushNewPeer(peer);
return peer;
}
@ -255,12 +240,11 @@ export class SimplePeer {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId);
screenSharingPeerStore.removePeer(userId);
}
}
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId);
}
peerStore.removePeer(userId);
}
/**
@ -302,6 +286,8 @@ export class SimplePeer {
for (const unsubscriber of this.unsubscribers) {
unsubscriber();
}
peerStore.cleanupStore();
screenSharingPeerStore.cleanupStore();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -325,7 +311,6 @@ export class SimplePeer {
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore);
let stream: MediaStream | null = 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 type { Subscription } from "rxjs";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { get, readable, Readable, Unsubscriber } from "svelte/store";
import { readable, Readable, Unsubscriber } from "svelte/store";
import {
localStreamStore,
obtainedMediaConstraintIsMobileStore,
@ -12,7 +12,7 @@ import {
ObtainedMediaStreamConstraints,
} from "../Stores/MediaStore";
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 { 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
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
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
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;
class OutlinePipelinePlugin {
@ -21,6 +21,6 @@ declare module 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js' {
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
}

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
* 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 ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import { PROFILE_URL } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -191,6 +192,7 @@ module.exports = {
UPLOADER_URL: null,
ADMIN_URL: undefined,
CONTACT_URL: null,
PROFILE_URL: null,
DEBUG_MODE: null,
STUN_SERVER: null,
TURN_SERVER: null,

View file

@ -2,7 +2,7 @@ import { v4 } from "uuid";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi";
import { jwtTokenManager } from "../Services/JWTTokenManager";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
@ -17,6 +17,7 @@ export class AuthenticateController extends BaseController {
this.openIDCallback();
this.register();
this.anonymLogin();
this.profileCallback();
}
openIDLogin() {
@ -48,14 +49,31 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { code, nonce } = parse(req.getQuery());
const { code, nonce, 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);
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 email = userInfo.email || userInfo.sub;
if (!email) {
throw new Error("No email in the response");
}
const authToken = jwtTokenManager.createAuthToken(email);
const authToken = jwtTokenManager.createAuthToken(email, userInfo.access_token);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken }));
@ -63,6 +81,30 @@ export class AuthenticateController extends BaseController {
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
@ -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