Merge branch 'develop' into changeRegisterAccess

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

# Conflicts:
#	front/src/Connexion/ConnectionManager.ts
#	front/vite.config.ts
This commit is contained in:
Gregoire Parant
2022-03-29 13:30:40 +02:00
108 changed files with 1637 additions and 1318 deletions
+7
View File
@@ -1,5 +1,12 @@
import { isSilentStore, requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import { get } from "svelte/store";
import { WorkAdventureDesktopApi } from "@wa-preload-app";
declare global {
interface Window {
WAD: WorkAdventureDesktopApi;
}
}
class DesktopApi {
isSilent: boolean = false;
@@ -77,6 +77,7 @@
height: max-content !important;
max-height: 40vh;
margin-top: 200px;
z-index: 425;
pointer-events: auto;
font-family: "Press Start 2P";
@@ -13,20 +13,20 @@
let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null;
let decreaseWhileTalking: boolean = true;
let isAudioAllowed: boolean = true;
onMount(() => {
let volume = Math.min(localUserStore.getAudioPlayerVolume(), get(audioManagerVolumeStore).volume);
audioManagerVolumeStore.setVolume(volume);
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
unsubscriberFileStore = audioManagerFileStore.subscribe((src) => {
unsubscriberFileStore = audioManagerFileStore.subscribe((src: string) => {
HTMLAudioPlayer.pause();
HTMLAudioPlayer.src = src;
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted;
void HTMLAudioPlayer.play();
tryPlay();
});
unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => {
const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking;
@@ -52,6 +52,16 @@
}
});
function tryPlay() {
void HTMLAudioPlayer.play()
.then(() => {
isAudioAllowed = true;
})
.catch(() => {
isAudioAllowed = false;
});
}
function updateVolumeUI() {
if (get(audioManagerVolumeStore).muted) {
audioPlayerVolumeIcon.classList.add("muted");
@@ -90,73 +100,67 @@
audioPlayerVol.blur();
return false;
}
function setDecrease() {
audioManagerVolumeStore.setDecreaseWhileTalking(decreaseWhileTalking);
}
</script>
<div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume">
<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">
<div class:hidden={!isAudioAllowed}>
<div class="audio-manager-player-volume">
<span id="audioplayer_volume_icon_playing" 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
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"
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>
<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:this={audioPlayerVol}
on:change={setVolume}
on:keydown={disallowKeys}
/>
</div>
<div class="audio-manager-reduce-conversation">
<label>
{$LL.audio.manager.reduce()}
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
</label>
<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:this={audioPlayerVol}
on:change={setVolume}
on:keydown={disallowKeys}
/>
</div>
<section class="audio-manager-file">
<!-- svelte-ignore a11y-media-has-caption -->
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer} />
</section>
</div>
<div class:hidden={isAudioAllowed}>
<button type="button" class="nes-btn" on:click={tryPlay}>{$LL.audio.manager.allow()}</button>
</div>
</div>
<style lang="scss">
div.main-audio-manager.nes-container.is-rounded {
.hidden {
display: none;
}
div.main-audio-manager {
position: absolute;
top: 1%;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
+24 -1
View File
@@ -10,12 +10,14 @@
import layoutPresentationImg from "./images/layout-presentation.svg";
import layoutChatImg from "./images/layout-chat.svg";
import followImg from "./images/follow.svg";
import lockImg from "./images/lock.svg";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { peerStore } from "../Stores/PeerStore";
import { onDestroy } from "svelte";
import { embedScreenLayout } from "../Stores/EmbedScreensStore";
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
import { gameManager } from "../Phaser/Game/GameManager";
import { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore";
const gameScene = gameManager.getCurrentGameScene();
@@ -70,6 +72,10 @@
}
}
function lockClick() {
gameScene.connection?.emitLockGroup(!$currentPlayerGroupLockStateStore);
}
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
isSilent = value;
@@ -95,6 +101,15 @@
<img class="noselect" src={followImg} alt="" />
</div>
<div
class="btn-lock"
class:hide={$peerStore.size === 0 || isSilent}
class:disabled={$currentPlayerGroupLockStateStore}
on:click={lockClick}
>
<img class="noselect" src={lockImg} alt="" />
</div>
<div
class="btn-monitor"
on:click={screenSharingClick}
@@ -162,7 +177,7 @@
transform: translateY(15px);
transition-timing-function: ease-in-out;
transition: all 0.3s;
margin: 0 4%;
margin: 0 2%;
&.hide {
transform: translateY(60px);
@@ -211,6 +226,14 @@
}
}
.btn-lock {
pointer-events: auto;
img {
filter: brightness(0) invert(1);
}
}
@media (hover: none) {
/**
* If we cannot hover over elements, let's display camera button in full.
@@ -9,6 +9,8 @@
import { iframeStates } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import uploadFile from "../images/jitsi.png";
export let index: number;
export let coWebsite: CoWebsite;
export let vertical: boolean;
@@ -21,7 +23,7 @@
onMount(() => {
icon.src = isJitsi
? "/resources/logos/jitsi.png"
? uploadFile
: `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = coWebsite.getUrl().hostname;
icon.onload = () => {
@@ -350,9 +352,14 @@
color: white;
padding: 4px;
border-radius: 4px;
p {
margin-bottom: 0;
}
&.hide {
display: none;
}
}
}
</style>
@@ -18,5 +18,7 @@
display: flex;
padding-top: 2%;
height: 100%;
position: relative;
z-index: 200;
}
</style>
@@ -1,8 +1,6 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
import LL from "../../i18n/i18n-svelte";
@@ -33,9 +31,9 @@
<p class="err">
{$LL.camera.help.firefoxContent()}
</p>
<img src={firefoxImg} alt="" />
<img src={$LL.camera.help.screen.firefox()} alt="" />
{:else if isChrome && !isAndroid}
<img src={chromeImg} alt="" />
<img src={$LL.camera.help.screen.chrome()} alt="" />
{/if}
</p>
</section>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

+14 -1
View File
@@ -3,6 +3,7 @@
import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png";
import poweredByWorkAdventureImg from "../images/Powered_By_WorkAdventure_Big.png";
import { gameManager } from "../../Phaser/Game/GameManager";
import LL from "../../i18n/i18n-svelte";
@@ -13,6 +14,8 @@
let name = gameManager.getPlayerName() || "";
let startValidating = false;
let logo = gameManager.currentStartedRoom.loginSceneLogo ?? logoImg;
function submit() {
startValidating = true;
@@ -25,7 +28,7 @@
<form class="loginScene" on:submit|preventDefault={submit}>
<section class="text-center">
<img src={logoImg} alt="WorkAdventure logo" />
<img src={logo} alt="" />
</section>
<section class="text-center">
<h2>{$LL.login.input.name.placeholder()}</h2>
@@ -60,6 +63,11 @@
<section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">{$LL.login.continue()}</button>
</section>
{#if logo !== logoImg}
<section class="text-center powered-by">
<img src={poweredByWorkAdventureImg} alt="Powered by WorkAdventure" />
</section>
{/if}
</form>
<style lang="scss">
@@ -132,6 +140,11 @@
width: 100%;
margin: 20px 0;
}
&.powered-by {
position: fixed;
bottom: 0;
}
}
}
</style>
+5 -4
View File
@@ -54,6 +54,7 @@
});
</script>
<!-- Components ordered by z-index -->
<div id="main-layout" bind:this={mainLayout}>
<aside id="main-layout-left-aside">
{#if $menuIconVisiblilityStore}
@@ -104,14 +105,14 @@
<ShareLinkMapModal />
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<FollowMenu />
{/if}
{#if $actionsMenuStore}
<ActionsMenu />
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<FollowMenu />
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
@@ -44,7 +44,6 @@
async function logOut() {
disableMenuStores();
loginSceneVisibleStore.set(true);
return connectionManager.logout();
}
@@ -7,17 +7,24 @@
import type { Locales } from "../../i18n/i18n-types";
import { displayableLocales, setCurrentLocale } from "../../i18n/locales";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
import { audioManagerVolumeStore } from "../../Stores/AudioManagerStore";
let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted";
let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger();
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
let decreaseAudioPlayerVolumeWhileTalking: boolean = localUserStore.getDecreaseAudioPlayerVolumeWhileTalking();
let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue();
let valueLocale: string = $locale;
let valueCameraPrivacySettings = localUserStore.getCameraPrivacySettings();
let valueMicrophonePrivacySettings = localUserStore.getMicrophonePrivacySettings();
let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
let previewValueLocale = valueLocale;
let previewCameraPrivacySettings = valueCameraPrivacySettings;
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
function saveSetting() {
let change = false;
@@ -38,6 +45,18 @@
change = true;
}
if (valueCameraPrivacySettings !== previewCameraPrivacySettings) {
previewCameraPrivacySettings = valueCameraPrivacySettings;
localUserStore.setCameraPrivacySettings(valueCameraPrivacySettings);
}
if (valueMicrophonePrivacySettings !== previewMicrophonePrivacySettings) {
previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
localUserStore.setMicrophonePrivacySettings(valueMicrophonePrivacySettings);
}
audioManagerVolumeStore.setDecreaseWhileTalking(decreaseAudioPlayerVolumeWhileTalking);
if (change) {
window.location.reload();
}
@@ -82,6 +101,10 @@
localUserStore.setIgnoreFollowRequests(ignoreFollowRequests);
}
function changeDecreaseAudioPlayerVolumeWhileTalking() {
localUserStore.setDecreaseAudioPlayerVolumeWhileTalking(decreaseAudioPlayerVolumeWhileTalking);
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
@@ -154,6 +177,19 @@
</select>
</div>
</section>
<section>
<h3>{$LL.menu.settings.privacySettings.title()}</h3>
<p>{$LL.menu.settings.privacySettings.explanation()}</p>
<label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={valueCameraPrivacySettings} />
<span>{$LL.menu.settings.privacySettings.cameraToggle()}</span>
</label>
<label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={valueMicrophonePrivacySettings} />
<span>{$LL.menu.settings.privacySettings.microphoneToggle()}</span>
</label>
</section>
<section class="settings-section-save">
<p>{$LL.menu.settings.save.warning()}</p>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}
@@ -196,6 +232,15 @@
on:change={changeIgnoreFollowRequests}
/>
<span>{$LL.menu.settings.ignoreFollowRequest()}</span>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={decreaseAudioPlayerVolumeWhileTalking}
on:change={changeDecreaseAudioPlayerVolumeWhileTalking}
/>
<span>{$LL.audio.manager.reduce()}</span>
</label>
</label>
</section>
</div>
@@ -217,12 +262,15 @@
outline: none;
}
}
section.settings-section-save {
text-align: center;
p {
margin: 16px 0;
}
}
section.settings-section-noSaveOption {
display: flex;
align-items: center;
@@ -76,6 +76,7 @@
transform: translate(-50%, 0);
margin-top: 200px;
max-width: 80vw;
z-index: 350;
iframe {
border: 0;
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 25 3 C 18.363281 3 13 8.363281 13 15 L 13 20 L 9 20 C 7.355469 20 6 21.355469 6 23 L 6 47 C 6 48.644531 7.355469 50 9 50 L 41 50 C 42.644531 50 44 48.644531 44 47 L 44 23 C 44 21.355469 42.644531 20 41 20 L 37 20 L 37 15 C 37 8.363281 31.636719 3 25 3 Z M 25 5 C 30.566406 5 35 9.433594 35 15 L 35 20 L 15 20 L 15 15 C 15 9.433594 19.433594 5 25 5 Z M 9 22 L 41 22 C 41.554688 22 42 22.445313 42 23 L 42 47 C 42 47.554688 41.554688 48 41 48 L 9 48 C 8.445313 48 8 47.554688 8 47 L 8 23 C 8 22.445313 8.445313 22 9 22 Z M 25 30 C 23.300781 30 22 31.300781 22 33 C 22 33.898438 22.398438 34.6875 23 35.1875 L 23 38 C 23 39.101563 23.898438 40 25 40 C 26.101563 40 27 39.101563 27 38 L 27 35.1875 C 27.601563 34.6875 28 33.898438 28 33 C 28 31.300781 26.699219 30 25 30 Z"/></svg>

After

Width:  |  Height:  |  Size: 891 B

+20 -10
View File
@@ -45,8 +45,10 @@ class ConnectionManager {
/**
* TODO fix me to be move in game manager
*
* Returns the URL that we need to redirect to to load the OpenID screen, or "null" if no redirection needs to happen.
*/
public loadOpenIDScreen() {
public loadOpenIDScreen(): URL | null {
const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null);
@@ -55,11 +57,10 @@ class ConnectionManager {
loginSceneVisibleIframeStore.set(false);
return null;
}
const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`);
const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`, window.location.href);
redirectUrl.searchParams.append("state", state);
redirectUrl.searchParams.append("nonce", nonce);
redirectUrl.searchParams.append("playUri", this._currentRoom.key);
window.location.assign(redirectUrl.toString());
return redirectUrl;
}
@@ -83,8 +84,10 @@ class ConnectionManager {
/**
* Tries to login to the node server and return the starting map url to be loaded
*
* @return returns a promise to the Room we are going to load OR a pointer to the URL we must redirect to if authentication is needed.
*/
public async initGameConnexion(): Promise<Room> {
public async initGameConnexion(): Promise<Room | URL> {
this.connexionType = urlManager.getGameConnexionType();
this._currentRoom = null;
@@ -100,8 +103,9 @@ class ConnectionManager {
if (this.connexionType === GameConnexionTypes.login) {
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
if (this.loadOpenIDScreen() !== null) {
return Promise.reject(new Error("You will be redirect on login page"));
const redirect = this.loadOpenIDScreen();
if (redirect !== null) {
return redirect;
}
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (this.connexionType === GameConnexionTypes.jwt) {
@@ -124,8 +128,11 @@ class ConnectionManager {
analyticsClient.loggedWithSso();
} catch (err) {
console.error(err);
this.loadOpenIDScreen();
return Promise.reject(new Error("You will be redirect on login page"));
const redirect = this.loadOpenIDScreen();
if (redirect === null) {
throw new Error("Unable to redirect on login page.");
}
return redirect;
}
urlManager.pushRoomIdToUrl(this._currentRoom);
}
@@ -213,8 +220,11 @@ class ConnectionManager {
err.response?.data &&
err.response.data !== "User cannot to be connected on openid provider")
) {
this.loadOpenIDScreen();
return Promise.reject(new Error("You will be redirect on login page"));
const redirect = this.loadOpenIDScreen();
if (redirect === null) {
throw new Error("Unable to redirect on login page.");
}
return redirect;
}
}
}
+6
View File
@@ -44,6 +44,12 @@ export interface GroupCreatedUpdatedMessageInterface {
position: PositionInterface;
groupId: number;
groupSize: number;
locked: boolean;
}
export interface GroupUsersUpdateMessageInterface {
groupId: number;
userIds: number[];
}
export interface WebRtcDisconnectMessageInterface {
+58
View File
@@ -15,6 +15,7 @@ const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen";
const forceCowebsiteTriggerKey = "forceCowebsiteTrigger";
const ignoreFollowRequests = "ignoreFollowRequests";
const decreaseAudioPlayerVolumeWhileTalking = "decreaseAudioPlayerVolumeWhileTalking";
const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
@@ -24,11 +25,14 @@ const code = "code";
const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache";
const userProperties = "user-properties";
const cameraPrivacySettings = "cameraPrivacySettings";
const microphonePrivacySettings = "microphonePrivacySettings";
class LocalUserStore {
saveUser(localUser: LocalUser) {
localStorage.setItem("localUser", JSON.stringify(localUser));
}
getLocalUser(): LocalUser | null {
const data = localStorage.getItem("localUser");
return data ? JSON.parse(data) : null;
@@ -37,6 +41,7 @@ class LocalUserStore {
setName(name: string): void {
localStorage.setItem(playerNameKey, name);
}
getName(): string | null {
const value = localStorage.getItem(playerNameKey) || "";
return isUserNameValid(value) ? value : null;
@@ -45,6 +50,7 @@ class LocalUserStore {
setPlayerCharacterIndex(playerCharacterIndex: number): void {
localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex);
}
getPlayerCharacterIndex(): number {
return parseInt(localStorage.getItem(selectedPlayerKey) || "");
}
@@ -52,6 +58,7 @@ class LocalUserStore {
setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void {
localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers }));
}
getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null {
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
}
@@ -59,6 +66,7 @@ class LocalUserStore {
setCharacterLayers(layers: string[]): void {
localStorage.setItem(characterLayersKey, JSON.stringify(layers));
}
getCharacterLayers(): string[] | null {
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
return areCharacterLayersValid(value) ? value : null;
@@ -67,6 +75,7 @@ class LocalUserStore {
setCompanion(companion: string | null): void {
return localStorage.setItem(companionKey, JSON.stringify(companion));
}
getCompanion(): string | null {
const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
@@ -76,6 +85,7 @@ class LocalUserStore {
return companion;
}
wasCompanionSet(): boolean {
return localStorage.getItem(companionKey) ? true : false;
}
@@ -83,6 +93,7 @@ class LocalUserStore {
setGameQualityValue(value: number): void {
localStorage.setItem(gameQualityKey, "" + value);
}
getGameQualityValue(): number {
return parseInt(localStorage.getItem(gameQualityKey) || "60");
}
@@ -90,6 +101,7 @@ class LocalUserStore {
setVideoQualityValue(value: number): void {
localStorage.setItem(videoQualityKey, "" + value);
}
getVideoQualityValue(): number {
return parseInt(localStorage.getItem(videoQualityKey) || "20");
}
@@ -97,6 +109,7 @@ class LocalUserStore {
setAudioPlayerVolume(value: number): void {
localStorage.setItem(audioPlayerVolumeKey, "" + value);
}
getAudioPlayerVolume(): number {
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1");
}
@@ -104,6 +117,7 @@ class LocalUserStore {
setAudioPlayerMuted(value: boolean): void {
localStorage.setItem(audioPlayerMuteKey, value.toString());
}
getAudioPlayerMuted(): boolean {
return localStorage.getItem(audioPlayerMuteKey) === "true";
}
@@ -111,6 +125,7 @@ class LocalUserStore {
setHelpCameraSettingsShown(): void {
localStorage.setItem(helpCameraSettingsShown, "1");
}
getHelpCameraSettingsShown(): boolean {
return localStorage.getItem(helpCameraSettingsShown) === "1";
}
@@ -118,6 +133,7 @@ class LocalUserStore {
setFullscreen(value: boolean): void {
localStorage.setItem(fullscreenKey, value.toString());
}
getFullscreen(): boolean {
return localStorage.getItem(fullscreenKey) === "true";
}
@@ -125,6 +141,7 @@ class LocalUserStore {
setForceCowebsiteTrigger(value: boolean): void {
localStorage.setItem(forceCowebsiteTriggerKey, value.toString());
}
getForceCowebsiteTrigger(): boolean {
return localStorage.getItem(forceCowebsiteTriggerKey) === "true";
}
@@ -132,9 +149,16 @@ class LocalUserStore {
setIgnoreFollowRequests(value: boolean): void {
localStorage.setItem(ignoreFollowRequests, value.toString());
}
getIgnoreFollowRequests(): boolean {
return localStorage.getItem(ignoreFollowRequests) === "true";
}
setDecreaseAudioPlayerVolumeWhileTalking(value: boolean): void {
localStorage.setItem(decreaseAudioPlayerVolumeWhileTalking, value.toString());
}
getDecreaseAudioPlayerVolumeWhileTalking(): boolean {
return localStorage.getItem(decreaseAudioPlayerVolumeWhileTalking) === "true";
}
async setLastRoomUrl(roomUrl: string): Promise<void> {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
@@ -148,11 +172,13 @@ class LocalUserStore {
}
}
}
getLastRoomUrl(): string {
return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
}
getLastRoomUrlCacheApi(): Promise<string | undefined> {
if (!("caches" in window)) {
return Promise.resolve(undefined);
@@ -169,6 +195,7 @@ class LocalUserStore {
setAuthToken(value: string | null) {
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
}
getAuthToken(): string | null {
return localStorage.getItem(authToken);
}
@@ -195,23 +222,29 @@ class LocalUserStore {
}
return oldValue === value;
}
setState(value: string) {
localStorage.setItem(state, value);
}
getState(): string | null {
return localStorage.getItem(state);
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
return localStorage.getItem(nonce);
}
setCode(value: string): void {
localStorage.setItem(code, value);
}
getCode(): string | null {
return localStorage.getItem(code);
}
@@ -219,11 +252,36 @@ class LocalUserStore {
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;
}
setCameraPrivacySettings(option: boolean) {
localStorage.setItem(cameraPrivacySettings, option.toString());
}
getCameraPrivacySettings() {
//if this setting doesn't exist in LocalUserStore, we set a default value
if (localStorage.getItem(cameraPrivacySettings) == null) {
localStorage.setItem(cameraPrivacySettings, "false");
}
return localStorage.getItem(cameraPrivacySettings) === "true";
}
setMicrophonePrivacySettings(option: boolean) {
localStorage.setItem(microphonePrivacySettings, option.toString());
}
getMicrophonePrivacySettings() {
//if this setting doesn't exist in LocalUserStore, we set a default value
if (localStorage.getItem(microphonePrivacySettings) == null) {
localStorage.setItem(microphonePrivacySettings, "true");
}
return localStorage.getItem(microphonePrivacySettings) === "true";
}
getAllUserProperties(): Map<string, unknown> {
const result = new Map<string, string>();
for (let i = 0; i < localStorage.length; i++) {
+12
View File
@@ -31,6 +31,8 @@ export class Room {
private _group: string | null = null;
private _expireOn: Date | undefined;
private _canReport: boolean = false;
private _loadingLogo: string | undefined;
private _loginSceneLogo: string | undefined;
private constructor(private roomUrl: URL) {
this.id = roomUrl.pathname;
@@ -126,6 +128,8 @@ export class Room {
this._expireOn = new Date(data.expireOn);
}
this._canReport = data.canReport ?? false;
this._loadingLogo = data.loadingLogo ?? undefined;
this._loginSceneLogo = data.loginSceneLogo ?? undefined;
return new MapDetail(data.mapUrl);
} else {
throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
@@ -233,4 +237,12 @@ export class Room {
get canReport(): boolean {
return this._canReport;
}
get loadingLogo(): string | undefined {
return this._loadingLogo;
}
get loginSceneLogo(): string | undefined {
return this._loginSceneLogo;
}
}
+32 -22
View File
@@ -5,47 +5,35 @@ import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
import type {
GroupCreatedUpdatedMessageInterface,
ItemEventMessageInterface,
GroupUsersUpdateMessageInterface,
MessageUserJoined,
OnConnectInterface,
PlayerDetailsUpdatedMessageInterface,
PlayGlobalMessageInterface,
PositionInterface,
RoomJoinedMessageInterface,
ViewportInterface,
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { adminMessagesService } from "./AdminMessagesService";
import { connectionManager } from "./ConnectionManager";
import { get } from "svelte/store";
import { followRoleStore, followUsersStore } from "../Stores/FollowStore";
import { menuIconVisiblilityStore, menuVisiblilityStore, warningContainerStore } from "../Stores/MenuStore";
import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore";
import { localUserStore } from "./LocalUserStore";
import {
RefreshRoomMessage,
ServerToClientMessage as ServerToClientMessageTsProto,
TokenExpiredMessage,
WorldConnexionMessage,
WorldFullMessage,
ErrorMessage as ErrorMessageTsProto,
UserMovedMessage as UserMovedMessageTsProto,
GroupUpdateMessage as GroupUpdateMessageTsProto,
GroupDeleteMessage as GroupDeleteMessageTsProto,
UserJoinedMessage as UserJoinedMessageTsProto,
UserLeftMessage as UserLeftMessageTsProto,
ItemEventMessage as ItemEventMessageTsProto,
EmoteEventMessage as EmoteEventMessageTsProto,
VariableMessage as VariableMessageTsProto,
PlayerDetailsUpdatedMessage as PlayerDetailsUpdatedMessageTsProto,
WorldFullWarningMessage,
WebRtcDisconnectMessage as WebRtcDisconnectMessageTsProto,
PlayGlobalMessage as PlayGlobalMessageTsProto,
StopGlobalMessage as StopGlobalMessageTsProto,
SendJitsiJwtMessage as SendJitsiJwtMessageTsProto,
SendUserMessage as SendUserMessageTsProto,
BanUserMessage as BanUserMessageTsProto,
ClientToServerMessage as ClientToServerMessageTsProto,
PositionMessage as PositionMessageTsProto,
ViewportMessage as ViewportMessageTsProto,
@@ -55,8 +43,6 @@ import {
CharacterLayerMessage,
} from "../Messages/ts-proto-generated/messages";
import { Subject } from "rxjs";
import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent";
import { match } from "assert";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import { gameManager } from "../Phaser/Game/GameManager";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Phaser/Login/SelectCharacterScene";
@@ -116,6 +102,9 @@ export class RoomConnection implements RoomConnection {
private readonly _groupUpdateMessageStream = new Subject<GroupCreatedUpdatedMessageInterface>();
public readonly groupUpdateMessageStream = this._groupUpdateMessageStream.asObservable();
private readonly _groupUsersUpdateMessageStream = new Subject<GroupUsersUpdateMessageInterface>();
public readonly groupUsersUpdateMessageStream = this._groupUsersUpdateMessageStream.asObservable();
private readonly _groupDeleteMessageStream = new Subject<GroupDeleteMessageTsProto>();
public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable();
@@ -220,6 +209,7 @@ export class RoomConnection implements RoomConnection {
this.socket.onmessage = (messageEvent) => {
const arrayBuffer: ArrayBuffer = messageEvent.data;
const initCharacterLayers = characterLayers;
const serverToClientMessage = ServerToClientMessageTsProto.decode(new Uint8Array(arrayBuffer));
//const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer));
@@ -342,14 +332,16 @@ export class RoomConnection implements RoomConnection {
this._userRoomToken = roomJoinedMessage.userRoomToken;
// If one of the URLs sent to us does not exist, let's go to the Woka selection screen.
for (const characterLayer of roomJoinedMessage.characterLayer) {
if (!characterLayer.url) {
this.goToSelectYourWokaScene();
this.closed = true;
break;
}
if (
roomJoinedMessage.characterLayer.length !== initCharacterLayers.length ||
roomJoinedMessage.characterLayer.find((layer) => !layer.url)
) {
this.goToSelectYourWokaScene();
this.closed = true;
}
if (this.closed) {
this.closeConnection();
break;
}
@@ -440,6 +432,10 @@ export class RoomConnection implements RoomConnection {
this._sendJitsiJwtMessageStream.next(message.sendJitsiJwtMessage);
break;
}
case "groupUsersUpdateMessage": {
this._groupUsersUpdateMessageStream.next(message.groupUsersUpdateMessage);
break;
}
case "sendUserMessage": {
adminMessagesService.onSendusermessage(message.sendUserMessage);
break;
@@ -672,6 +668,7 @@ export class RoomConnection implements RoomConnection {
groupId: message.groupId,
position: position,
groupSize: message.groupSize,
locked: message.locked,
};
}
@@ -887,6 +884,19 @@ export class RoomConnection implements RoomConnection {
this.socket.send(bytes);
}
public emitLockGroup(lock: boolean = true): void {
const bytes = ClientToServerMessageTsProto.encode({
message: {
$case: "lockGroupPromptMessage",
lockGroupPromptMessage: {
lock,
},
},
}).finish();
this.socket.send(bytes);
}
public getAllTags(): string[] {
return this.tags;
}
+71 -39
View File
@@ -1,10 +1,12 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
import { DirtyScene } from "../Game/DirtyScene";
import { gameManager } from "../Game/GameManager";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
import CancelablePromise from "cancelable-promise";
import Image = Phaser.GameObjects.Image;
import Texture = Phaser.Textures.Texture;
const LogoNameIndex: string = "logoLoading";
const TextName: string = "Loading...";
const LogoResource: string = "static/images/logo.png";
const LogoFrame: ImageFrameConfig = { frameWidth: 310, frameHeight: 60 };
const loadingBarHeight: number = 16;
const padding: number = 5;
@@ -14,9 +16,15 @@ export class Loader {
private progress!: Phaser.GameObjects.Graphics;
private progressAmount: number = 0;
private logo: Phaser.GameObjects.Image | undefined;
private logoPoweredBy: Phaser.GameObjects.Image | undefined;
private poweredByLogo: Phaser.GameObjects.Image | undefined;
private loadingText: Phaser.GameObjects.Text | null = null;
private logoNameIndex!: string;
private superLoad: SuperLoaderPlugin;
public constructor(private scene: Phaser.Scene) {}
public constructor(private scene: Phaser.Scene) {
this.superLoad = new SuperLoaderPlugin(scene);
}
public addLoader(): void {
// If there is nothing to load, do not display the loader.
@@ -24,43 +32,53 @@ export class Loader {
return;
}
const logoResource = gameManager.currentStartedRoom.loadingLogo ?? "static/images/logo.png";
this.logoNameIndex = "logoLoading" + logoResource;
const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3);
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
if (this.scene.load.textureManager.exists(LogoNameIndex)) {
return res(
(this.logo = this.scene.add.image(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 150,
LogoNameIndex
))
);
} else {
//add loading if logo image is not ready
this.loadingText = this.scene.add.text(
//add loading if logo image until logo image is ready
this.loadingText = this.scene.add.text(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 50,
TextName
);
const logoPromise = this.superLoad.image(this.logoNameIndex, logoResource);
logoPromise
.then((texture) => {
this.logo = this.scene.add.image(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 50,
TextName
this.scene.game.renderer.height / 2 - 150,
texture
);
}
this.scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
this.scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if (this.loadingText) {
this.loadingText.destroy();
}
return res(
(this.logo = this.scene.add.image(
this.loadingText?.destroy();
})
.catch((e) => console.warn("Could not load logo: ", logoResource, e));
let poweredByLogoPromise: CancelablePromise<Texture> | undefined;
if (gameManager.currentStartedRoom.loadingLogo) {
poweredByLogoPromise = this.superLoad.image(
"poweredByLogo",
"static/images/Powered_By_WorkAdventure_Small.png"
);
poweredByLogoPromise
.then((texture) => {
this.poweredByLogo = this.scene.add.image(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 150,
LogoNameIndex
))
this.scene.game.renderer.height - 50,
texture
);
})
.catch((e) =>
console.warn('Could not load image "static/images/Powered_By_WorkAdventure_Small.png"', e)
);
});
});
}
this.progressContainer = this.scene.add.graphics();
this.progress = this.scene.add.graphics();
this.progressContainer.fillStyle(0x444444, 0.8);
this.progress = this.scene.add.graphics();
this.resize();
@@ -68,26 +86,35 @@ export class Loader {
this.progressAmount = value;
this.drawProgress();
});
const resizeFunction = this.resize.bind(this);
this.scene.scale.on(Phaser.Scale.Events.RESIZE, resizeFunction);
this.scene.load.on("complete", () => {
if (this.loadingText) {
this.loadingText.destroy();
}
promiseLoadLogoTexture
.then((resLoadingImage: Phaser.GameObjects.Image) => {
resLoadingImage.destroy();
})
.catch((e) => console.error(e));
logoPromise.cancel();
poweredByLogoPromise?.cancel();
this.logo?.destroy();
this.poweredByLogo?.destroy();
this.progress.destroy();
this.progressContainer.destroy();
if (this.scene instanceof DirtyScene) {
this.scene.markDirty();
}
this.scene.scale.off(Phaser.Scale.Events.RESIZE, resizeFunction);
});
}
public removeLoader(): void {
if (this.scene.load.textureManager.exists(LogoNameIndex)) {
this.scene.load.textureManager.remove(LogoNameIndex);
if (this.scene.load.textureManager.exists(this.logoNameIndex)) {
this.scene.load.textureManager.remove(this.logoNameIndex);
}
if (this.scene.load.textureManager.exists("poweredByLogo")) {
this.scene.load.textureManager.remove("poweredByLogo");
}
}
@@ -114,6 +141,11 @@ export class Loader {
this.logo.x = this.scene.game.renderer.width / 2;
this.logo.y = this.scene.game.renderer.height / 2 - 150;
}
if (this.poweredByLogo) {
this.poweredByLogo.x = this.scene.game.renderer.width / 2;
this.poweredByLogo.y = this.scene.game.renderer.height - 40;
}
}
private drawProgress() {
+2 -2
View File
@@ -57,8 +57,8 @@ export class SoundMeter {
this.context = context;
this.analyser = this.context.createAnalyser();
this.analyser.fftSize = 2048;
const bufferLength = this.analyser.fftSize;
this.analyser.fftSize = 256;
const bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);
}
+31 -6
View File
@@ -18,6 +18,7 @@ import { createColorStore } from "../../Stores/OutlineColorStore";
import type { OutlineableInterface } from "../Game/OutlineableInterface";
import type CancelablePromise from "cancelable-promise";
import { TalkIcon } from "../Components/TalkIcon";
import { Deferred } from "ts-deferred";
const playerNameY = -25;
@@ -50,6 +51,11 @@ export abstract class Character extends Container implements OutlineableInterfac
private readonly outlineColorStoreUnsubscribe: Unsubscriber;
private texturePromise: CancelablePromise<string[] | void> | undefined;
/**
* A deferred promise that resolves when the texture of the character is actually displayed.
*/
private textureLoadedDeferred = new Deferred<void>();
constructor(
scene: GameScene,
x: number,
@@ -78,12 +84,13 @@ export abstract class Character extends Container implements OutlineableInterfac
this.addTextures(textures, frame);
this.invisible = false;
this.playAnimation(direction, moving);
this.textureLoadedDeferred.resolve();
return this.getSnapshot().then((htmlImageElementSrc) => {
this._pictureStore.set(htmlImageElementSrc);
});
})
.catch(() => {
return lazyLoadPlayerCharacterTextures(scene.load, [
return lazyLoadPlayerCharacterTextures(scene.superLoad, [
{
id: "color_22",
img: "resources/customisation/character_color/character_color21.png",
@@ -92,11 +99,20 @@ export abstract class Character extends Container implements OutlineableInterfac
id: "eyes_23",
img: "resources/customisation/character_eyes/character_eyes23.png",
},
]).then((textures) => {
this.addTextures(textures, frame);
this.invisible = false;
this.playAnimation(direction, moving);
});
])
.then((textures) => {
this.addTextures(textures, frame);
this.invisible = false;
this.playAnimation(direction, moving);
this.textureLoadedDeferred.resolve();
return this.getSnapshot().then((htmlImageElementSrc) => {
this._pictureStore.set(htmlImageElementSrc);
});
})
.catch((e) => {
this.textureLoadedDeferred.reject(e);
throw e;
});
})
.finally(() => {
this.texturePromise = undefined;
@@ -517,4 +533,13 @@ export abstract class Character extends Container implements OutlineableInterfac
public characterFarAwayOutline(): void {
this.outlineColorStore.characterFarAway();
}
/**
* Returns a promise that resolves as soon as a texture is displayed for the user.
* The promise will return when the required texture is loaded OR when the fallback texture is loaded (in case
* the required texture could not be loaded).
*/
public getTextureLoadedPromise(): PromiseLike<void> {
return this.textureLoadedDeferred.promise;
}
}
+5 -21
View File
@@ -1,5 +1,7 @@
//The list of all the player textures, both the default models and the partial textures used for customization
import { WokaList, WokaPartType } from "../../Messages/JsonMessages/PlayerTextures";
export interface BodyResourceDescriptionListInterface {
[key: string]: BodyResourceDescriptionInterface;
}
@@ -33,24 +35,6 @@ export enum PlayerTexturesKey {
Woka = "woka",
}
type PlayerTexturesMetadata = Record<PlayerTexturesKey, PlayerTexturesCategory>;
interface PlayerTexturesCategory {
collections: PlayerTexturesCollection[];
required?: boolean;
}
interface PlayerTexturesCollection {
name: string;
textures: PlayerTexturesRecord[];
}
interface PlayerTexturesRecord {
id: string;
name: string;
url: string;
}
export class PlayerTextures {
private PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {};
private COLOR_RESOURCES: BodyResourceDescriptionListInterface = {};
@@ -61,7 +45,7 @@ export class PlayerTextures {
private ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {};
private LAYERS: BodyResourceDescriptionListInterface[] = [];
public loadPlayerTexturesMetadata(metadata: PlayerTexturesMetadata): void {
public loadPlayerTexturesMetadata(metadata: WokaList): void {
this.mapTexturesMetadataIntoResources(metadata);
}
@@ -88,7 +72,7 @@ export class PlayerTextures {
return this.LAYERS;
}
private mapTexturesMetadataIntoResources(metadata: PlayerTexturesMetadata): void {
private mapTexturesMetadataIntoResources(metadata: WokaList): void {
this.PLAYER_RESOURCES = this.getMappedResources(metadata.woka);
this.COLOR_RESOURCES = this.getMappedResources(metadata.body);
this.EYES_RESOURCES = this.getMappedResources(metadata.eyes);
@@ -107,7 +91,7 @@ export class PlayerTextures {
];
}
private getMappedResources(category: PlayerTexturesCategory): BodyResourceDescriptionListInterface {
private getMappedResources(category: WokaPartType): BodyResourceDescriptionListInterface {
const resources: BodyResourceDescriptionListInterface = {};
if (!category) {
return {};
@@ -2,6 +2,8 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type { CharacterTexture } from "../../Connexion/LocalUser";
import { BodyResourceDescriptionInterface, mapLayerToLevel, PlayerTextures, PlayerTexturesKey } from "./PlayerTextures";
import CancelablePromise from "cancelable-promise";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
import Texture = Phaser.Textures.Texture;
export interface FrameConfig {
frameWidth: number;
@@ -35,81 +37,33 @@ export const loadAllDefaultModels = (
};
export const loadWokaTexture = (
loaderPlugin: LoaderPlugin,
superLoaderPlugin: SuperLoaderPlugin,
texture: BodyResourceDescriptionInterface
): CancelablePromise<BodyResourceDescriptionInterface> => {
return createLoadingPromise(loaderPlugin, texture, {
): CancelablePromise<Texture> => {
return superLoaderPlugin.spritesheet(texture.id, texture.img, {
frameWidth: 32,
frameHeight: 32,
});
};
export const lazyLoadPlayerCharacterTextures = (
loadPlugin: LoaderPlugin,
superLoaderPlugin: SuperLoaderPlugin,
textures: BodyResourceDescriptionInterface[]
): CancelablePromise<string[]> => {
const promisesList: CancelablePromise<unknown>[] = [];
textures.forEach((texture) => {
try {
//TODO refactor
if (!loadPlugin.textureManager.exists(texture.id)) {
promisesList.push(
createLoadingPromise(loadPlugin, texture, {
frameWidth: 32,
frameHeight: 32,
})
);
}
} catch (err) {
console.error(err);
}
});
let returnPromise: CancelablePromise<Array<string | BodyResourceDescriptionInterface>>;
if (promisesList.length > 0) {
loadPlugin.start();
returnPromise = CancelablePromise.all(promisesList).then(() => textures);
} else {
returnPromise = CancelablePromise.resolve(textures);
const promisesList: CancelablePromise<Texture>[] = [];
for (const texture of textures) {
promisesList.push(
superLoaderPlugin.spritesheet(texture.id, texture.img, {
frameWidth: 32,
frameHeight: 32,
})
);
}
const returnPromise: CancelablePromise<Texture[]> = CancelablePromise.all(promisesList);
//If the loading fail, we render the default model instead.
return returnPromise.then((keys) =>
keys.map((key) => {
return typeof key !== "string" ? key.id : key;
return returnPromise.then(() =>
textures.map((key) => {
return key.id;
})
);
};
export const createLoadingPromise = (
loadPlugin: LoaderPlugin,
playerResourceDescriptor: BodyResourceDescriptionInterface,
frameConfig: FrameConfig
) => {
return new CancelablePromise<BodyResourceDescriptionInterface>((res, rej, cancel) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.id)) {
return res(playerResourceDescriptor);
}
cancel(() => {
loadPlugin.off("loaderror");
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.id);
return;
});
loadPlugin.spritesheet(playerResourceDescriptor.id, playerResourceDescriptor.img, frameConfig);
const errorCallback = (file: { src: string }) => {
if (file.src !== playerResourceDescriptor.img) return;
console.error("failed loading player resource: ", playerResourceDescriptor);
rej(playerResourceDescriptor);
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.id, successCallback);
loadPlugin.off("loaderror", errorCallback);
};
const successCallback = () => {
loadPlugin.off("loaderror", errorCallback);
res(playerResourceDescriptor);
};
loadPlugin.once("filecomplete-spritesheet-" + playerResourceDescriptor.id, successCallback);
loadPlugin.on("loaderror", errorCallback);
});
};
+10 -2
View File
@@ -9,6 +9,7 @@ import { EnableCameraSceneName } from "../Login/EnableCameraScene";
import { LoginSceneName } from "../Login/LoginScene";
import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { GameScene } from "./GameScene";
import { EmptySceneName } from "../Login/EmptyScene";
/**
* This class should be responsible for any scene starting/stopping
@@ -19,7 +20,7 @@ export class GameManager {
private companion: string | null;
private startRoom!: Room;
private cameraSetup?: { video: unknown; audio: unknown };
currentGameSceneName: string | null = null;
private 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;
@@ -32,7 +33,14 @@ export class GameManager {
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
this.scenePlugin = scenePlugin;
this.startRoom = await connectionManager.initGameConnexion();
const result = await connectionManager.initGameConnexion();
if (result instanceof URL) {
window.location.assign(result.toString());
// window.location.assign is not immediate and Javascript keeps running after.
// so we need to redirect to an empty Phaser scene, waiting for the redirection to take place
return EmptySceneName;
}
this.startRoom = result;
this.loadMap(this.startRoom);
//If player name was not set show login scene with player name
+65 -33
View File
@@ -18,7 +18,7 @@ import { soundManager } from "./SoundManager";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { lazyLoadPlayerCharacterTextures, loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadPlayerCharacterTextures } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { iframeListener } from "../../Api/IframeListener";
import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
@@ -76,6 +76,7 @@ import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore } from "../../Stores/AudioManagerStore";
import { currentPlayerGroupIdStore, currentPlayerGroupLockStateStore } from "../../Stores/CurrentPlayerGroupStore";
import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture;
@@ -97,8 +98,10 @@ import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import type { VideoPeer } from "../../WebRtc/VideoPeer";
import CancelablePromise from "cancelable-promise";
import { Deferred } from "ts-deferred";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@@ -164,13 +167,9 @@ export class GameScene extends DirtyScene {
private playersPositionInterpolator = new PlayersPositionInterpolator();
public connection: RoomConnection | undefined;
private simplePeer!: SimplePeer;
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
private connectionAnswerPromiseResolve!: (
value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>
) => void;
private connectionAnswerPromiseDeferred: Deferred<RoomJoinedMessageInterface>;
// A promise that will resolve when the "create" method is called (signaling loading is ended)
private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private createPromiseDeferred: Deferred<void>;
private iframeSubscriptionList!: Array<Subscription>;
private peerStoreUnsubscribe!: Unsubscriber;
private emoteUnsubscribe!: Unsubscriber;
@@ -179,6 +178,7 @@ export class GameScene extends DirtyScene {
private volumeStoreUnsubscribers: Map<number, Unsubscriber> = new Map<number, Unsubscriber>();
private localVolumeStoreUnsubscriber: Unsubscriber | undefined;
private followUsersColorStoreUnsubscribe!: Unsubscriber;
private currentPlayerGroupIdStoreUnsubscribe!: Unsubscriber;
private biggestAvailableAreaStoreUnsubscribe!: () => void;
MapUrlFile: string;
@@ -220,6 +220,8 @@ export class GameScene extends DirtyScene {
private loader: Loader;
private lastCameraEvent: WasCameraUpdatedEvent | undefined;
private firstCameraUpdateSent: boolean = false;
private currentPlayerGroupId?: number;
public readonly superLoad: SuperLoaderPlugin;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({
@@ -232,13 +234,10 @@ export class GameScene extends DirtyScene {
this.MapUrlFile = MapUrlFile;
this.roomUrl = room.key;
this.createPromise = new Promise<void>((resolve, reject): void => {
this.createPromiseResolve = resolve;
});
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
this.connectionAnswerPromiseResolve = resolve;
});
this.createPromiseDeferred = new Deferred<void>();
this.connectionAnswerPromiseDeferred = new Deferred<RoomJoinedMessageInterface>();
this.loader = new Loader(this);
this.superLoad = new SuperLoaderPlugin(this);
}
//hook preload scene
@@ -408,11 +407,11 @@ export class GameScene extends DirtyScene {
this.load.on("complete", () => {
// FIXME: the factory might fail because the resources might not be loaded yet...
// We would need to add a loader ended event in addition to the createPromise
this.createPromise
this.createPromiseDeferred.promise
.then(async () => {
itemFactory.create(this);
const roomJoinedAnswer = await this.connectionAnswerPromise;
const roomJoinedAnswer = await this.connectionAnswerPromiseDeferred.promise;
for (const object of objectsOfType) {
// TODO: we should pass here a factory to create sprites (maybe?)
@@ -609,7 +608,7 @@ export class GameScene extends DirtyScene {
}
}
this.createPromiseResolve();
this.createPromiseDeferred.resolve();
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
const disableModuleMode = this.getProperty(this.mapFile, GameMapProperties.SCRIPT_DISABLE_MODULE_SUPPORT) as
@@ -635,7 +634,7 @@ export class GameScene extends DirtyScene {
}
const talkIconVolumeTreshold = 10;
let oldPeerNumber = 0;
const oldPeers = new Map<number, VideoPeer>();
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
this.volumeStoreUnsubscribers.forEach((unsubscribe) => unsubscribe());
this.volumeStoreUnsubscribers.clear();
@@ -652,10 +651,17 @@ export class GameScene extends DirtyScene {
}
const newPeerNumber = peers.size;
if (newPeerNumber > oldPeerNumber) {
if (newPeerNumber > oldPeers.size) {
this.playSound("audio-webrtc-in");
} else if (newPeerNumber < oldPeerNumber) {
} else if (newPeerNumber < oldPeers.size) {
this.playSound("audio-webrtc-out");
const oldPeersKeys = oldPeers.keys();
const newPeersKeys = Array.from(peers.keys());
for (const oldKey of oldPeersKeys) {
if (!newPeersKeys.includes(oldKey)) {
this.MapPlayersByKey.get(oldKey)?.showTalkIcon(false, true);
}
}
}
if (newPeerNumber > 0) {
if (!this.localVolumeStoreUnsubscriber) {
@@ -673,7 +679,10 @@ export class GameScene extends DirtyScene {
this.localVolumeStoreUnsubscriber = undefined;
}
}
oldPeerNumber = newPeerNumber;
oldPeers.clear();
for (const [key, val] of peers) {
oldPeers.set(key, val);
}
});
this.emoteUnsubscribe = emoteStore.subscribe((emote) => {
@@ -702,7 +711,15 @@ export class GameScene extends DirtyScene {
}
});
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises])
this.currentPlayerGroupIdStoreUnsubscribe = currentPlayerGroupIdStore.subscribe((groupId) => {
this.currentPlayerGroupId = groupId;
});
Promise.all([
this.connectionAnswerPromiseDeferred.promise as Promise<unknown>,
...scriptPromises,
this.CurrentPlayer.getTextureLoadedPromise() as Promise<unknown>,
])
.then(() => {
this.scene.wake();
})
@@ -739,7 +756,7 @@ export class GameScene extends DirtyScene {
.then((onConnect: OnConnectInterface) => {
this.connection = onConnect.connection;
lazyLoadPlayerCharacterTextures(this.load, onConnect.room.characterLayers)
lazyLoadPlayerCharacterTextures(this.superLoad, onConnect.room.characterLayers)
.then((layers) => {
this.currentPlayerTexturesResolve(layers);
})
@@ -829,6 +846,11 @@ export class GameScene extends DirtyScene {
});
});
this.connection.groupUsersUpdateMessageStream.subscribe((message) => {
// TODO: how else can we deduce our current group?
currentPlayerGroupIdStore.set(message.groupId);
});
/**
* Triggered when we receive the JWT token to connect to Jitsi
*/
@@ -868,7 +890,7 @@ export class GameScene extends DirtyScene {
);
//this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(onConnect.room);
this.connectionAnswerPromiseDeferred.resolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console.
if (this.scene.isSleeping()) {
@@ -959,7 +981,9 @@ export class GameScene extends DirtyScene {
context.arc(48, 48, 48, 0, 2 * Math.PI, false);
// context.lineWidth = 5;
context.strokeStyle = "#ffffff";
context.fillStyle = "#ffffff44";
context.stroke();
context.fill();
this.circleTexture.refresh();
//create red circle canvas use to create sprite
@@ -969,7 +993,9 @@ export class GameScene extends DirtyScene {
contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false);
//context.lineWidth = 5;
contextRed.strokeStyle = "#ff0000";
contextRed.fillStyle = "#ff000044";
contextRed.stroke();
contextRed.fill();
this.circleRedTexture.refresh();
}
@@ -1283,7 +1309,7 @@ ${escapedMessage}
iframeListener.registerAnswerer("getState", async () => {
// The sharedVariablesManager is not instantiated before the connection is established. So we need to wait
// for the connection to send back the answer.
await this.connectionAnswerPromise;
await this.connectionAnswerPromiseDeferred.promise;
return {
mapUrl: this.MapUrlFile,
startLayerName: this.startPositionCalculator.startLayerName,
@@ -1306,7 +1332,7 @@ ${escapedMessage}
})
);
iframeListener.registerAnswerer("loadTileset", (eventTileset) => {
return this.connectionAnswerPromise.then(() => {
return this.connectionAnswerPromiseDeferred.promise.then(() => {
const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/"));
//Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1
let newFirstgid = 1;
@@ -1839,12 +1865,15 @@ ${escapedMessage}
case "GroupCreatedUpdatedEvent":
this.doShareGroupPosition(event.event);
break;
case "DeleteGroupEvent":
this.doDeleteGroup(event.groupId);
break;
case "PlayerDetailsUpdated":
this.doUpdatePlayerDetails(event.details);
break;
case "DeleteGroupEvent": {
this.doDeleteGroup(event.groupId);
currentPlayerGroupIdStore.set(undefined);
currentPlayerGroupLockStateStore.set(undefined);
break;
}
default: {
const tmp: never = event;
}
@@ -1907,7 +1936,7 @@ ${escapedMessage}
return;
}
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, addPlayerData.characterLayers);
const texturesPromise = lazyLoadPlayerCharacterTextures(this.superLoad, addPlayerData.characterLayers);
const player = new RemotePlayer(
addPlayerData.userId,
addPlayerData.userUuid,
@@ -2018,11 +2047,16 @@ ${escapedMessage}
this,
Math.round(groupPositionMessage.position.x),
Math.round(groupPositionMessage.position.y),
groupPositionMessage.groupSize === MAX_PER_GROUP ? "circleSprite-red" : "circleSprite-white"
groupPositionMessage.groupSize === MAX_PER_GROUP || groupPositionMessage.locked
? "circleSprite-red"
: "circleSprite-white"
);
sprite.setDisplayOrigin(48, 48);
this.add.existing(sprite);
this.groups.set(groupPositionMessage.groupId, sprite);
if (this.currentPlayerGroupId === groupPositionMessage.groupId) {
currentPlayerGroupLockStateStore.set(groupPositionMessage.locked);
}
return sprite;
}
@@ -2078,8 +2112,6 @@ ${escapedMessage}
right: camera.scrollX + camera.width,
bottom: camera.scrollY + camera.height,
});
this.loader.resize();
}
private getObjectLayerData(objectName: string): ITiledMapObject | undefined {
@@ -3,39 +3,16 @@ import { BodyResourceDescriptionInterface, PlayerTexturesKey } from "../Entity/P
import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
import type CancelablePromise from "cancelable-promise";
import { PlayerTextures } from "../Entity/PlayerTextures";
import Texture = Phaser.Textures.Texture;
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
export abstract class AbstractCharacterScene extends ResizableScene {
protected playerTextures: PlayerTextures;
protected superLoad: SuperLoaderPlugin;
constructor(params: { key: string }) {
super(params);
this.playerTextures = new PlayerTextures();
}
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
const textures = this.playerTextures.getTexturesResources(PlayerTexturesKey.Woka);
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
if (textures) {
for (const texture of Object.values(textures)) {
if (texture.level === -1) {
continue;
}
promises.push(loadWokaTexture(this.load, texture));
}
}
return Promise.all(promises);
}
loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
for (const textures of this.playerTextures.getLayers()) {
for (const texture of Object.values(textures)) {
if (texture.level !== -1) {
continue;
}
promises.push(loadWokaTexture(this.load, texture));
}
}
return Promise.all(promises);
this.superLoad = new SuperLoaderPlugin(this);
}
}
+22 -36
View File
@@ -16,6 +16,7 @@ import { get } from "svelte/store";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
import { wokaList } from "../../Messages/JsonMessages/PlayerTextures";
export const CustomizeSceneName = "CustomizeScene";
@@ -41,45 +42,30 @@ export class CustomizeScene extends AbstractCharacterScene {
}
preload() {
const wokaMetadataKey = "woka-list";
const wokaMetadataKey = "woka-list" + gameManager.currentStartedRoom.href;
this.cache.json.remove(wokaMetadataKey);
// FIXME: window.location.href is wrong. We need the URL of the main room (so we need to apply any redirect before!)
this.load.json(
wokaMetadataKey,
`${PUSHER_URL}/woka/list?roomUrl=` + encodeURIComponent(window.location.href),
undefined,
{
responseType: "text",
headers: {
Authorization: localUserStore.getAuthToken() ?? "",
this.superLoad
.json(
wokaMetadataKey,
`${PUSHER_URL}/woka/list?roomUrl=` + encodeURIComponent(gameManager.currentStartedRoom.href),
undefined,
{
responseType: "text",
headers: {
Authorization: localUserStore.getAuthToken() ?? "",
},
withCredentials: true,
},
withCredentials: true,
}
);
this.load.once(`filecomplete-json-${wokaMetadataKey}`, () => {
this.playerTextures.loadPlayerTexturesMetadata(this.cache.json.get(wokaMetadataKey));
this.loadCustomSceneSelectCharacters()
.then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if (
bodyResourceDescription.level == undefined ||
bodyResourceDescription.level < 0 ||
bodyResourceDescription.level > 5
) {
throw new Error("Texture level is null");
}
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
});
this.lazyloadingAttempt = true;
})
.catch((e) => console.error(e));
(key, type, data) => {
this.playerTextures.loadPlayerTexturesMetadata(wokaList.parse(data));
this.layers = loadAllLayers(this.load, this.playerTextures);
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function
this.loader.addLoader();
});
this.layers = loadAllLayers(this.load, this.playerTextures);
this.lazyloadingAttempt = false;
}
)
.catch((e) => console.error(e));
//this function must stay at the end of preload function
this.loader.addLoader();
}
create() {
+17
View File
@@ -0,0 +1,17 @@
import { Scene } from "phaser";
export const EmptySceneName = "EmptyScene";
export class EmptyScene extends Scene {
constructor() {
super({
key: EmptySceneName,
});
}
preload() {}
create() {}
update(time: number, delta: number): void {}
}
+4 -1
View File
@@ -28,7 +28,10 @@ export class LoginScene extends ResizableScene {
gameManager.currentStartedRoom &&
gameManager.currentStartedRoom.authenticationMandatory
) {
connectionManager.loadOpenIDScreen();
const redirect = connectionManager.loadOpenIDScreen();
if (redirect !== null) {
window.location.assign(redirect.toString());
}
loginSceneVisibleIframeStore.set(true);
}
loginSceneVisibleStore.set(true);
+22 -28
View File
@@ -16,6 +16,7 @@ import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
import { customizeAvailableStore } from "../../Stores/SelectCharacterSceneStore";
import { wokaList } from "../../Messages/JsonMessages/PlayerTextures";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
@@ -44,38 +45,31 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
preload() {
const wokaMetadataKey = "woka-list";
const wokaMetadataKey = "woka-list" + gameManager.currentStartedRoom.href;
this.cache.json.remove(wokaMetadataKey);
// FIXME: window.location.href is wrong. We need the URL of the main room (so we need to apply any redirect before!)
this.load.json(
wokaMetadataKey,
`${PUSHER_URL}/woka/list?roomUrl=` + encodeURIComponent(window.location.href),
undefined,
{
responseType: "text",
headers: {
Authorization: localUserStore.getAuthToken() ?? "",
this.superLoad
.json(
wokaMetadataKey,
`${PUSHER_URL}/woka/list?roomUrl=` + encodeURIComponent(gameManager.currentStartedRoom.href),
undefined,
{
responseType: "text",
headers: {
Authorization: localUserStore.getAuthToken() ?? "",
},
withCredentials: true,
},
withCredentials: true,
}
);
this.load.once(`filecomplete-json-${wokaMetadataKey}`, () => {
this.playerTextures.loadPlayerTexturesMetadata(this.cache.json.get(wokaMetadataKey));
this.loadSelectSceneCharacters()
.then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription);
});
this.lazyloadingAttempt = true;
})
.catch((e) => console.error(e));
this.playerModels = loadAllDefaultModels(this.load, this.playerTextures);
this.lazyloadingAttempt = false;
(key, type, data) => {
this.playerTextures.loadPlayerTexturesMetadata(wokaList.parse(data));
this.playerModels = loadAllDefaultModels(this.load, this.playerTextures);
this.lazyloadingAttempt = false;
}
)
.catch((e) => console.error(e));
//this function must stay at the end of preload function
this.loader.addLoader();
});
//this function must stay at the end of preload function
this.loader.addLoader();
}
create() {
@@ -0,0 +1,176 @@
import CancelablePromise from "cancelable-promise";
import { Scene } from "phaser";
import Texture = Phaser.Textures.Texture;
/**
* A wrapper around Phaser LoaderPlugin. Each method returns a (cancelable) Promise that resolves as soon as
* the file is loaded.
*
* As a bonus, if the scene is destroyed BEFORE the promise resolves, the promise is canceled automatically.
* So there is no risk of trying to add a resource on a closed scene.
*/
export class SuperLoaderPlugin {
constructor(private scene: Scene) {}
public spritesheet(
key: string,
url: string,
frameConfig?: Phaser.Types.Loader.FileTypes.ImageFrameConfig,
xhrSettings?: Phaser.Types.Loader.XHRSettingsObject
) {
return this.loadResource<Texture>(
() => {
this.scene.load.spritesheet(key, url, frameConfig, xhrSettings);
},
key,
url,
() => {
if (this.scene.load.textureManager.exists(key)) {
return this.scene.load.textureManager.get(key);
}
return undefined;
},
"spritesheet"
);
}
public image(key: string, url: string, xhrSettings?: Phaser.Types.Loader.XHRSettingsObject) {
return this.loadResource<Texture>(
() => {
this.scene.load.image(key, url, xhrSettings);
},
key,
url,
() => {
if (this.scene.load.textureManager.exists(key)) {
return this.scene.load.textureManager.get(key);
}
return undefined;
},
"image"
);
}
/**
* @param key
* @param url
* @param dataKey
* @param xhrSettings
* @param immediateCallback The function returns a promise BUT the "then" promise will be triggered after the current Javascript thread finishes. In case of Phaser loader, this can be a problem if you want to add additional resources to the loader in the callback. The "immediateCallback" triggers directly in the
*/
public json(
key: string,
url: string,
dataKey?: string,
xhrSettings?: Phaser.Types.Loader.XHRSettingsObject,
immediateCallback?: (key: string, type: string, data: unknown) => void
) {
return this.loadResource<unknown>(
() => {
this.scene.load.json(key, url, dataKey, xhrSettings);
},
key,
url,
() => {
if (this.scene.load.cacheManager.json.exists(key)) {
return this.scene.load.cacheManager.json.get(key);
}
return undefined;
},
"json",
immediateCallback
);
}
/**
* @param callback The function that calls the loader to load a resource
* @param key The key of the resource to be loaded
* @param fromCache A function that checks in the cache if the resource is already available
* @param type The type of resource loaded
* @param immediateCallback The function returns a promise BUT the "then" promise will be triggered after the current Javascript thread finishes. In case of Phaser loader, this can be a problem if you want to add additional resources to the loader in the callback. The "immediateCallback" triggers directly in the
* @private
*/
private loadResource<T>(
callback: () => void,
key: string,
url: string,
fromCache: () => T | undefined,
type: string,
immediateCallback?: (key: string, type: string, data: unknown) => void
): CancelablePromise<T> {
// If for some reason, the "url" is empty, let's reject the promise.
if (!url) {
console.error("Tried to load an empty " + type + ". URL is missing.");
return CancelablePromise.reject(Error("Failed loading " + type + ": URL is empty"));
}
return new CancelablePromise<T>((res, rej, cancel) => {
if (this.scene.scene.settings.status === Phaser.Scenes.DESTROYED) {
rej(new Error("Trying to load a " + type + " in a Scene that is already destroyed."));
}
const resource = fromCache();
if (resource !== undefined) {
return res(resource);
}
let destroySceneEventRegistered = false;
const unloadCallbacks = () => {
this.scene.load.off("filecomplete-" + type + "-" + key, successCallback);
this.scene.load.off("loaderror", errorCallback);
if (destroySceneEventRegistered) {
this.scene.events.off(Phaser.Scenes.Events.DESTROY, unloadCallbacks);
}
};
const errorCallback = (file: { src: string }) => {
if (file.src !== url) return;
console.error("Failed loading " + type + ": ", url);
rej(new Error('Failed loading "+type+": "' + url + '"'));
unloadCallbacks();
};
const successCallback = (key: string, type: string, data: unknown) => {
this.scene.load.off("loaderror", errorCallback);
this.scene.events.off(Phaser.Scenes.Events.DESTROY, unloadCallbacks);
const resource = fromCache();
if (!resource) {
return rej(new Error("Newly loaded resource not available in cache"));
}
res(resource);
// The "then" callbacks registered on the promise are not executed immediately when "res" is called.
// Instead, they are called after the current JS thread finishes.
// In some cases, we want the callbacks to execute right now. In particular if we load a map / json file
// that contains references to other files that needs to be loaded.
if (immediateCallback) {
immediateCallback(key, type, data);
}
console.log("Resolve done for ", url);
};
cancel(() => {
unloadCallbacks();
});
callback();
this.scene.load.once("filecomplete-" + type + "-" + key, successCallback);
this.scene.load.on("loaderror", errorCallback);
if (this.scene.scene.settings.status > Phaser.Scenes.LOADING) {
// When the scene is destroyed, let's remove our callbacks.
// We only need to register this destroy event is the scene is not in loading state (otherwise, Phaser
// will take care of that for us).
destroySceneEventRegistered = true;
this.scene.events.once(Phaser.Scenes.Events.DESTROY, unloadCallbacks);
// Let's start the loader if we are no more in the scene loading state
this.scene.load.start();
// Due to a bug, if the loader is already started, additional items are not added.... unless we
// explicitly call the "update" method.
this.scene.load.update();
}
});
}
}
@@ -0,0 +1,4 @@
import { writable } from "svelte/store";
export const currentPlayerGroupIdStore = writable<number | undefined>(undefined);
export const currentPlayerGroupLockStateStore = writable<boolean | undefined>(undefined);
+19 -3
View File
@@ -11,7 +11,7 @@ import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { AudioContext } from "standardized-audio-context";
import { visibilityStore } from "./VisibilityStore";
/**
* A store that contains the camera state requested by the user (on or off).
@@ -242,6 +242,7 @@ export const mediaStreamConstraintsStore = derived(
privacyShutdownStore,
cameraEnergySavingStore,
isSilentStore,
visibilityStore,
],
(
[
@@ -254,6 +255,7 @@ export const mediaStreamConstraintsStore = derived(
$privacyShutdownStore,
$cameraEnergySavingStore,
$isSilentStore,
$visibilityStore,
],
set
) => {
@@ -292,7 +294,14 @@ export const mediaStreamConstraintsStore = derived(
// Disable webcam for privacy reasons (the game is not visible and we were talking to no one)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
const userMicrophonePrivacySetting = localUserStore.getMicrophonePrivacySettings();
const userCameraPrivacySetting = localUserStore.getCameraPrivacySettings();
if (!userMicrophonePrivacySetting) {
currentAudioConstraint = false;
}
if (!userCameraPrivacySetting) {
currentVideoConstraint = false;
}
}
// Disable webcam for energy reasons (the user is not moving and we are talking to no one)
@@ -545,8 +554,12 @@ export const obtainedMediaConstraintStore = derived<Readable<MediaStreamConstrai
export const localVolumeStore = readable<number | undefined>(undefined, (set) => {
let timeout: ReturnType<typeof setTimeout>;
let soundMeter: SoundMeter;
const unsubscribe = localStreamStore.subscribe((localStreamStoreValue) => {
clearInterval(timeout);
if (soundMeter) {
soundMeter.stop();
}
if (localStreamStoreValue.type === "error") {
set(undefined);
return;
@@ -557,7 +570,7 @@ export const localVolumeStore = readable<number | undefined>(undefined, (set) =>
set(undefined);
return;
}
const soundMeter = new SoundMeter(mediaStream);
soundMeter = new SoundMeter(mediaStream);
let error = false;
timeout = setInterval(() => {
@@ -575,6 +588,9 @@ export const localVolumeStore = readable<number | undefined>(undefined, (set) =>
return () => {
unsubscribe();
clearInterval(timeout);
if (soundMeter) {
soundMeter.stop();
}
};
});
+4 -1
View File
@@ -619,7 +619,10 @@ class CoWebsiteManager {
setTimeout(() => {
this.fire();
}, animationTime);
} else if (!highlightedEmbed) {
} else if (
!highlightedEmbed &&
this.getCoWebsites().find((searchCoWebsite) => searchCoWebsite.getId() === coWebsite.getId())
) {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
+4 -1
View File
@@ -197,7 +197,10 @@ class JitsiFactory {
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener("videoConferenceJoined", () => {
this.jitsiApi?.executeCommand("displayName", playerName);
});
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
+9 -1
View File
@@ -74,12 +74,17 @@ export class VideoPeer extends Peer {
this.volumeStore = readable<number | undefined>(undefined, (set) => {
let timeout: ReturnType<typeof setTimeout>;
let soundMeter: SoundMeter;
const unsubscribe = this.streamStore.subscribe((mediaStream) => {
clearInterval(timeout);
if (soundMeter) {
soundMeter.stop();
}
if (mediaStream === null || mediaStream.getAudioTracks().length <= 0) {
set(undefined);
return;
}
const soundMeter = new SoundMeter(mediaStream);
soundMeter = new SoundMeter(mediaStream);
let error = false;
timeout = setInterval(() => {
@@ -97,6 +102,9 @@ export class VideoPeer extends Peer {
return () => {
unsubscribe();
clearInterval(timeout);
if (soundMeter) {
soundMeter.stop();
}
};
});
+2 -1
View File
@@ -2,7 +2,8 @@ import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "Während Unterhaltungen verringern",
reduce: "Verringern Sie die Lautstärke des Audioplayers während des Sprechens",
allow: "Ton zulassen",
},
message: "Sprachnachricht",
};
+4
View File
@@ -13,6 +13,10 @@ const camera: NonNullable<Translation["camera"]> = {
'Bitte klicke auf "Diese Entscheidungen speichern" Schaltfläche um erneute Nachfragen nach der Freigabe in Firefox zu verhindern.',
refresh: "Aktualisieren",
continue: "Ohne Kamera fortfahren",
screen: {
firefox: "/resources/help-setting-camera-permission/de-DE-chrome.png",
chrome: "/resources/help-setting-camera-permission/de-DE-chrome.png",
},
},
my: {
silentZone: "Stiller Bereich",
+7
View File
@@ -57,6 +57,13 @@ const menu: NonNullable<Translation["menu"]> = {
language: {
title: "Sprache",
},
privacySettings: {
title: "Einstellungen Abwesenheitsmodus",
explanation:
"Falls der WorkAdventure Tab nicht aktiv ist wird in den Abwesenheitsmodus umgeschaltet. Für diesen Modus kann eingestellt werden, ob die Kamera und/oder das Mikrofon deaktiviert sind solange der Tab nicht sichtbar ist.",
cameraToggle: "Kamera",
microphoneToggle: "Mikrofon",
},
save: {
warning: "(Das Spiel wird nach dem Speichern neugestartet)",
button: "Speichern",
+2 -1
View File
@@ -2,7 +2,8 @@ import type { BaseTranslation } from "../i18n-types";
const audio: BaseTranslation = {
manager: {
reduce: "reduce in conversations",
reduce: "Decrease audio player volume while speaking",
allow: "Allow audio",
},
message: "Audio message",
};
+4
View File
@@ -13,6 +13,10 @@ const camera: BaseTranslation = {
'Please click the "Remember this decision" checkbox, if you don\'t want Firefox to keep asking you the authorization.',
refresh: "Refresh",
continue: "Continue without webcam",
screen: {
firefox: "/resources/help-setting-camera-permission/en-US-firefox.png",
chrome: "/resources/help-setting-camera-permission/en-US-firefox.png",
},
},
my: {
silentZone: "Silent zone",
+7
View File
@@ -57,6 +57,13 @@ const menu: BaseTranslation = {
language: {
title: "Language",
},
privacySettings: {
title: "Away mode settings",
explanation:
'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.',
cameraToggle: "Camera",
microphoneToggle: "Microphone",
},
save: {
warning: "(Saving these settings will restart the game)",
button: "Save",
+2 -1
View File
@@ -2,7 +2,8 @@ import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "réduit dans les conversations",
reduce: "Diminuer le volume du lecteur audio dans les conversations",
allow: "Autoriser l'audio",
},
message: "Message audio",
};
+4
View File
@@ -13,6 +13,10 @@ const camera: NonNullable<Translation["camera"]> = {
'Veuillez cocher la case "Se souvenir de cette décision" si vous ne voulez pas que Firefox vous demande sans cesse l\'autorisation.',
refresh: "Rafraîchir",
continue: "Continuer sans webcam",
screen: {
firefox: "/resources/help-setting-camera-permission/fr-FR-chrome.png",
chrome: "/resources/help-setting-camera-permission/fr-FR-chrome.png",
},
},
my: {
silentZone: "Zone silencieuse",
+7
View File
@@ -57,6 +57,13 @@ const menu: NonNullable<Translation["menu"]> = {
language: {
title: "Langage",
},
privacySettings: {
title: "Paramètres du mode absent",
explanation:
"Quand l'onglet WorkAdventure n'est pas visible, vous passez en \"mode absent\". Lorsque ce mode est actif, vous pouvez décider de garder vos webcam et/ou micro désactivés tant que vous ne revenez pas sur l'onglet",
cameraToggle: "Camera",
microphoneToggle: "Microphone",
},
save: {
warning: "(La sauvegarde de ces paramètres redémarre le jeu)",
button: "Sauvegarder",
+7 -1
View File
@@ -21,7 +21,6 @@ import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution";
import camera from "./Api/iframe/camera";
import {} from "./window";
const globalState = createState("global");
@@ -183,6 +182,13 @@ const wa = {
export type WorkAdventureApi = typeof wa;
declare global {
interface Window {
WA: WorkAdventureApi;
}
let WA: WorkAdventureApi;
}
window.WA = wa;
window.addEventListener(
-10
View File
@@ -1,10 +0,0 @@
import { WorkAdventureApi } from "./iframe_api";
import { WorkAdventureDesktopApi } from "@wa-preload-app";
declare global {
interface Window {
WA: WorkAdventureApi;
WAD: WorkAdventureDesktopApi;
}
let WA: WorkAdventureApi;
}