merged develop
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isActionsMenuActionClickedEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isNumber,
|
||||
actionName: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type ActionsMenuActionClickedEvent = tg.GuardedType<typeof isActionsMenuActionClickedEvent>;
|
||||
|
||||
export type ActionsMenuActionClickedEventCallback = (event: ActionsMenuActionClickedEvent) => void;
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isAddActionsMenuKeyToRemotePlayerEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isNumber,
|
||||
actionKey: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type AddActionsMenuKeyToRemotePlayerEvent = tg.GuardedType<typeof isAddActionsMenuKeyToRemotePlayerEvent>;
|
||||
|
||||
export type AddActionsMenuKeyToRemotePlayerEventCallback = (event: AddActionsMenuKeyToRemotePlayerEvent) => void;
|
||||
@@ -36,6 +36,10 @@ import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
|
||||
import { isColorEvent } from "./ColorEvent";
|
||||
import { isMovePlayerToEventConfig } from "./MovePlayerToEvent";
|
||||
import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer";
|
||||
import type { RemotePlayerClickedEvent } from "./RemotePlayerClickedEvent";
|
||||
import type { AddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent";
|
||||
import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent";
|
||||
import type { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
@@ -45,6 +49,8 @@ export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
* List event types sent from an iFrame to WorkAdventure
|
||||
*/
|
||||
export type IframeEventMap = {
|
||||
addActionsMenuKeyToRemotePlayer: AddActionsMenuKeyToRemotePlayerEvent;
|
||||
removeActionsMenuKeyFromRemotePlayer: RemoveActionsMenuKeyFromRemotePlayerEvent;
|
||||
loadPage: LoadPageEvent;
|
||||
chat: ChatEvent;
|
||||
cameraFollowPlayer: CameraFollowPlayerEvent;
|
||||
@@ -58,6 +64,7 @@ export type IframeEventMap = {
|
||||
displayBubble: null;
|
||||
removeBubble: null;
|
||||
onPlayerMove: undefined;
|
||||
onOpenActionMenu: undefined;
|
||||
onCameraUpdate: undefined;
|
||||
showLayer: LayerEvent;
|
||||
hideLayer: LayerEvent;
|
||||
@@ -90,6 +97,8 @@ export interface IframeResponseEventMap {
|
||||
enterZoneEvent: ChangeZoneEvent;
|
||||
leaveZoneEvent: ChangeZoneEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
remotePlayerClickedEvent: RemotePlayerClickedEvent;
|
||||
actionsMenuActionClickedEvent: ActionsMenuActionClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
wasCameraUpdated: WasCameraUpdatedEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
// TODO: Change for player Clicked, add all neccessary data
|
||||
export const isRemotePlayerClickedEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the game to the iFrame when RemotePlayer is clicked.
|
||||
*/
|
||||
export type RemotePlayerClickedEvent = tg.GuardedType<typeof isRemotePlayerClickedEvent>;
|
||||
|
||||
export type RemotePlayerClickedEventCallback = (event: RemotePlayerClickedEvent) => void;
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isRemoveActionsMenuKeyFromRemotePlayerEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isNumber,
|
||||
actionKey: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type RemoveActionsMenuKeyFromRemotePlayerEvent = tg.GuardedType<
|
||||
typeof isRemoveActionsMenuKeyFromRemotePlayerEvent
|
||||
>;
|
||||
|
||||
export type RemoveActionsMenuKeyFromRemotePlayerEventCallback = (
|
||||
event: RemoveActionsMenuKeyFromRemotePlayerEvent
|
||||
) => void;
|
||||
@@ -34,6 +34,16 @@ import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
|
||||
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
|
||||
import { CameraSetEvent, isCameraSetEvent } from "./Events/CameraSetEvent";
|
||||
import { CameraFollowPlayerEvent, isCameraFollowPlayerEvent } from "./Events/CameraFollowPlayerEvent";
|
||||
import type { RemotePlayerClickedEvent } from "./Events/RemotePlayerClickedEvent";
|
||||
import {
|
||||
AddActionsMenuKeyToRemotePlayerEvent,
|
||||
isAddActionsMenuKeyToRemotePlayerEvent,
|
||||
} from "./Events/AddActionsMenuKeyToRemotePlayerEvent";
|
||||
import type { ActionsMenuActionClickedEvent } from "./Events/ActionsMenuActionClickedEvent";
|
||||
import {
|
||||
isRemoveActionsMenuKeyFromRemotePlayerEvent,
|
||||
RemoveActionsMenuKeyFromRemotePlayerEvent,
|
||||
} from "./Events/RemoveActionsMenuKeyFromRemotePlayerEvent";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"],
|
||||
@@ -63,6 +73,15 @@ class IframeListener {
|
||||
private readonly _cameraFollowPlayerStream: Subject<CameraFollowPlayerEvent> = new Subject();
|
||||
public readonly cameraFollowPlayerStream = this._cameraFollowPlayerStream.asObservable();
|
||||
|
||||
private readonly _addActionsMenuKeyToRemotePlayerStream: Subject<AddActionsMenuKeyToRemotePlayerEvent> =
|
||||
new Subject();
|
||||
public readonly addActionsMenuKeyToRemotePlayerStream = this._addActionsMenuKeyToRemotePlayerStream.asObservable();
|
||||
|
||||
private readonly _removeActionsMenuKeyFromRemotePlayerEvent: Subject<RemoveActionsMenuKeyFromRemotePlayerEvent> =
|
||||
new Subject();
|
||||
public readonly removeActionsMenuKeyFromRemotePlayerEvent =
|
||||
this._removeActionsMenuKeyFromRemotePlayerEvent.asObservable();
|
||||
|
||||
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
|
||||
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
|
||||
|
||||
@@ -241,6 +260,16 @@ class IframeListener {
|
||||
this._removeBubbleStream.next();
|
||||
} else if (payload.type == "onPlayerMove") {
|
||||
this.sendPlayerMove = true;
|
||||
} else if (
|
||||
payload.type == "addActionsMenuKeyToRemotePlayer" &&
|
||||
isAddActionsMenuKeyToRemotePlayerEvent(payload.data)
|
||||
) {
|
||||
this._addActionsMenuKeyToRemotePlayerStream.next(payload.data);
|
||||
} else if (
|
||||
payload.type == "removeActionsMenuKeyFromRemotePlayer" &&
|
||||
isRemoveActionsMenuKeyFromRemotePlayerEvent(payload.data)
|
||||
) {
|
||||
this._removeActionsMenuKeyFromRemotePlayerEvent.next(payload.data);
|
||||
} else if (payload.type == "onCameraUpdate") {
|
||||
this._trackCameraUpdateStream.next();
|
||||
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
||||
@@ -439,6 +468,20 @@ class IframeListener {
|
||||
}
|
||||
}
|
||||
|
||||
sendRemotePlayerClickedEvent(event: RemotePlayerClickedEvent) {
|
||||
this.postMessage({
|
||||
type: "remotePlayerClickedEvent",
|
||||
data: event,
|
||||
});
|
||||
}
|
||||
|
||||
sendActionsMenuActionClickedEvent(event: ActionsMenuActionClickedEvent) {
|
||||
this.postMessage({
|
||||
type: "actionsMenuActionClickedEvent",
|
||||
data: event,
|
||||
});
|
||||
}
|
||||
|
||||
sendCameraUpdated(event: WasCameraUpdatedEvent) {
|
||||
this.postMessage({
|
||||
type: "wasCameraUpdated",
|
||||
|
||||
+113
-5
@@ -8,6 +8,12 @@ import { ActionMessage } from "./Ui/ActionMessage";
|
||||
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
|
||||
import { Menu } from "./Ui/Menu";
|
||||
import type { RequireOnlyOne } from "../types";
|
||||
import { isRemotePlayerClickedEvent, RemotePlayerClickedEvent } from "../Events/RemotePlayerClickedEvent";
|
||||
import {
|
||||
ActionsMenuActionClickedEvent,
|
||||
isActionsMenuActionClickedEvent,
|
||||
} from "../Events/ActionsMenuActionClickedEvent";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
let popupId = 0;
|
||||
const popups: Map<number, Popup> = new Map<number, Popup>();
|
||||
@@ -42,7 +48,77 @@ export interface ActionMessageOptions {
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
export interface RemotePlayerInterface {
|
||||
addAction(key: string, callback: Function): void;
|
||||
}
|
||||
|
||||
export class RemotePlayer implements RemotePlayerInterface {
|
||||
private id: number;
|
||||
|
||||
private actions: Map<string, ActionsMenuAction> = new Map<string, ActionsMenuAction>();
|
||||
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public addAction(key: string, callback: Function): ActionsMenuAction {
|
||||
const newAction = new ActionsMenuAction(this, key, callback);
|
||||
this.actions.set(key, newAction);
|
||||
sendToWorkadventure({
|
||||
type: "addActionsMenuKeyToRemotePlayer",
|
||||
data: { id: this.id, actionKey: key },
|
||||
});
|
||||
return newAction;
|
||||
}
|
||||
|
||||
public callAction(key: string): void {
|
||||
const action = this.actions.get(key);
|
||||
if (action) {
|
||||
action.call();
|
||||
}
|
||||
}
|
||||
|
||||
public removeAction(key: string): void {
|
||||
this.actions.delete(key);
|
||||
sendToWorkadventure({
|
||||
type: "removeActionsMenuKeyFromRemotePlayer",
|
||||
data: { id: this.id, actionKey: key },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsMenuAction {
|
||||
private remotePlayer: RemotePlayer;
|
||||
private key: string;
|
||||
private callback: Function;
|
||||
|
||||
constructor(remotePlayer: RemotePlayer, key: string, callback: Function) {
|
||||
this.remotePlayer = remotePlayer;
|
||||
this.key = key;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public call(): void {
|
||||
this.callback();
|
||||
}
|
||||
|
||||
public remove(): void {
|
||||
this.remotePlayer.removeAction(this.key);
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||
public readonly _onRemotePlayerClicked: Subject<RemotePlayerInterface>;
|
||||
public readonly onRemotePlayerClicked: Observable<RemotePlayerInterface>;
|
||||
|
||||
private currentlyClickedRemotePlayer?: RemotePlayer;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._onRemotePlayerClicked = new Subject<RemotePlayerInterface>();
|
||||
this.onRemotePlayerClicked = this._onRemotePlayerClicked.asObservable();
|
||||
}
|
||||
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "buttonClickedEvent",
|
||||
@@ -82,9 +158,38 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
|
||||
}
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "remotePlayerClickedEvent",
|
||||
typeChecker: isRemotePlayerClickedEvent,
|
||||
callback: (payloadData: RemotePlayerClickedEvent) => {
|
||||
this.currentlyClickedRemotePlayer = new RemotePlayer(payloadData.id);
|
||||
this._onRemotePlayerClicked.next(this.currentlyClickedRemotePlayer);
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "actionsMenuActionClickedEvent",
|
||||
typeChecker: isActionsMenuActionClickedEvent,
|
||||
callback: (payloadData: ActionsMenuActionClickedEvent) => {
|
||||
this.currentlyClickedRemotePlayer?.callAction(payloadData.actionName);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
public addActionsMenuKeyToRemotePlayer(id: number, actionKey: string): void {
|
||||
sendToWorkadventure({
|
||||
type: "addActionsMenuKeyToRemotePlayer",
|
||||
data: { id, actionKey },
|
||||
});
|
||||
}
|
||||
|
||||
public removeActionsMenuKeyFromRemotePlayer(id: number, actionKey: string): void {
|
||||
sendToWorkadventure({
|
||||
type: "removeActionsMenuKeyFromRemotePlayer",
|
||||
data: { id, actionKey },
|
||||
});
|
||||
}
|
||||
|
||||
public openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
popupId++;
|
||||
const popup = new Popup(popupId);
|
||||
const btnMap = new Map<number, () => void>();
|
||||
@@ -119,7 +224,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
|
||||
return popup;
|
||||
}
|
||||
|
||||
registerMenuCommand(commandDescriptor: string, options: MenuOptions | ((commandDescriptor: string) => void)): Menu {
|
||||
public registerMenuCommand(
|
||||
commandDescriptor: string,
|
||||
options: MenuOptions | ((commandDescriptor: string) => void)
|
||||
): Menu {
|
||||
const menu = new Menu(commandDescriptor);
|
||||
|
||||
if (typeof options === "function") {
|
||||
@@ -168,15 +276,15 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
|
||||
return menu;
|
||||
}
|
||||
|
||||
displayBubble(): void {
|
||||
public displayBubble(): void {
|
||||
sendToWorkadventure({ type: "displayBubble", data: null });
|
||||
}
|
||||
|
||||
removeBubble(): void {
|
||||
public removeBubble(): void {
|
||||
sendToWorkadventure({ type: "removeBubble", data: null });
|
||||
}
|
||||
|
||||
displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {
|
||||
public displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {
|
||||
const actionMessage = new ActionMessage(actionMessageOptions, () => {
|
||||
actionMessages.delete(actionMessage.uuid);
|
||||
});
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { actionsMenuStore } from "../../Stores/ActionsMenuStore";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
import type { ActionsMenuAction } from "../../Stores/ActionsMenuStore";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import type { ActionsMenuData } from "../../Stores/ActionsMenuStore";
|
||||
|
||||
let actionsMenuData: ActionsMenuData | undefined;
|
||||
let sortedActions: ActionsMenuAction[] | undefined;
|
||||
|
||||
let actionsMenuStoreUnsubscriber: Unsubscriber | null;
|
||||
|
||||
@@ -21,6 +23,20 @@
|
||||
|
||||
actionsMenuStoreUnsubscriber = actionsMenuStore.subscribe((value) => {
|
||||
actionsMenuData = value;
|
||||
if (actionsMenuData) {
|
||||
sortedActions = [...actionsMenuData.actions.values()].sort((a, b) => {
|
||||
const ap = a.priority ?? 0;
|
||||
const bp = b.priority ?? 0;
|
||||
if (ap > bp) {
|
||||
return -1;
|
||||
}
|
||||
if (ap < bp) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -37,15 +53,15 @@
|
||||
<button type="button" class="nes-btn is-error close" on:click={closeActionsMenu}>×</button>
|
||||
<h2>{actionsMenuData.playerName}</h2>
|
||||
<div class="actions">
|
||||
{#each [...actionsMenuData.actions] as { actionName, callback }}
|
||||
{#each sortedActions ?? [] as action}
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn"
|
||||
class="nes-btn {action.style ?? ''}"
|
||||
on:click|preventDefault={() => {
|
||||
callback();
|
||||
action.callback();
|
||||
}}
|
||||
>
|
||||
{actionName}
|
||||
{action.actionName}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -61,6 +77,7 @@
|
||||
height: max-content !important;
|
||||
max-height: 40vh;
|
||||
margin-top: 200px;
|
||||
z-index: 425;
|
||||
|
||||
pointer-events: auto;
|
||||
font-family: "Press Start 2P";
|
||||
@@ -68,7 +85,7 @@
|
||||
color: whitesmoke;
|
||||
|
||||
.actions {
|
||||
max-height: calc(100% - 50px);
|
||||
max-height: 30vh;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 |
@@ -330,9 +330,12 @@ class ConnectionManager {
|
||||
throw new Error("No Auth code provided");
|
||||
}
|
||||
}
|
||||
const { authToken, userUuid, email, username, locale } = await Axios.get(`${PUSHER_URL}/login-callback`, {
|
||||
params: { code, nonce, token, playUri: this.currentRoom?.key },
|
||||
}).then((res) => {
|
||||
const { authToken, userUuid, email, username, locale, textures } = await Axios.get(
|
||||
`${PUSHER_URL}/login-callback`,
|
||||
{
|
||||
params: { code, nonce, token, playUri: this.currentRoom?.key },
|
||||
}
|
||||
).then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
localUserStore.setAuthToken(authToken);
|
||||
@@ -361,6 +364,18 @@ class ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (textures) {
|
||||
const layers: string[] = [];
|
||||
for (const texture of textures) {
|
||||
if (texture !== undefined) {
|
||||
layers.push(texture.id);
|
||||
}
|
||||
}
|
||||
if (layers.length > 0) {
|
||||
gameManager.setCharacterLayers(layers);
|
||||
}
|
||||
}
|
||||
|
||||
//user connected, set connected store for menu at true
|
||||
userIsConnected.set(true);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +349,9 @@ export class RoomConnection implements RoomConnection {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.closed) {
|
||||
this.closeConnection();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,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;
|
||||
const interactiveRadius = 35;
|
||||
@@ -46,6 +47,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,
|
||||
@@ -74,12 +80,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",
|
||||
@@ -88,11 +95,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;
|
||||
@@ -453,4 +469,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requestVisitCardsStore } from "../../Stores/GameStore";
|
||||
import { ActionsMenuData, actionsMenuStore } from "../../Stores/ActionsMenuStore";
|
||||
import { ActionsMenuAction, ActionsMenuData, actionsMenuStore } from "../../Stores/ActionsMenuStore";
|
||||
import { Character } from "../Entity/Character";
|
||||
import type { GameScene } from "../Game/GameScene";
|
||||
import type { PointInterface } from "../../Connexion/ConnexionModels";
|
||||
@@ -8,21 +8,28 @@ import type { Unsubscriber } from "svelte/store";
|
||||
import type { ActivatableInterface } from "../Game/ActivatableInterface";
|
||||
import type CancelablePromise from "cancelable-promise";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { blackListManager } from "../../WebRtc/BlackListManager";
|
||||
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
|
||||
|
||||
export enum RemotePlayerEvent {
|
||||
Clicked = "Clicked",
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing the sprite of a remote player (a player that plays on another computer)
|
||||
*/
|
||||
export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
public userId: number;
|
||||
public readonly userId: number;
|
||||
public readonly userUuid: string;
|
||||
public readonly activationRadius: number;
|
||||
|
||||
private registeredActions: { actionName: string; callback: Function }[];
|
||||
private visitCardUrl: string | null;
|
||||
private isActionsMenuInitialized: boolean = false;
|
||||
private actionsMenuStoreUnsubscriber: Unsubscriber;
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
userUuid: string,
|
||||
Scene: GameScene,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -39,10 +46,9 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
|
||||
//set data
|
||||
this.userId = userId;
|
||||
this.userUuid = userUuid;
|
||||
this.visitCardUrl = visitCardUrl;
|
||||
this.registeredActions = [];
|
||||
this.registerDefaultActionsMenuActions();
|
||||
this.setClickable(this.registeredActions.length > 0);
|
||||
this.setClickable(this.getDefaultActionsMenuActions().length > 0);
|
||||
this.activationRadius = activationRadius ?? 96;
|
||||
this.actionsMenuStoreUnsubscriber = actionsMenuStore.subscribe((value: ActionsMenuData | undefined) => {
|
||||
this.isActionsMenuInitialized = value ? true : false;
|
||||
@@ -63,17 +69,19 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public registerActionsMenuAction(action: { actionName: string; callback: Function }): void {
|
||||
this.registeredActions.push(action);
|
||||
this.updateIsClickable();
|
||||
public registerActionsMenuAction(action: ActionsMenuAction): void {
|
||||
actionsMenuStore.addAction({
|
||||
...action,
|
||||
priority: action.priority ?? 0,
|
||||
callback: () => {
|
||||
action.callback();
|
||||
actionsMenuStore.clear();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public unregisterActionsMenuAction(actionName: string) {
|
||||
const index = this.registeredActions.findIndex((action) => action.actionName === actionName);
|
||||
if (index !== -1) {
|
||||
this.registeredActions.splice(index, 1);
|
||||
}
|
||||
this.updateIsClickable();
|
||||
actionsMenuStore.removeAction(actionName);
|
||||
}
|
||||
|
||||
public activate(): void {
|
||||
@@ -90,37 +98,52 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
return this.isClickable();
|
||||
}
|
||||
|
||||
private updateIsClickable(): void {
|
||||
this.setClickable(this.registeredActions.length > 0);
|
||||
}
|
||||
|
||||
private toggleActionsMenu(): void {
|
||||
if (this.isActionsMenuInitialized) {
|
||||
actionsMenuStore.clear();
|
||||
return;
|
||||
}
|
||||
actionsMenuStore.initialize(this.playerName);
|
||||
for (const action of this.registeredActions) {
|
||||
actionsMenuStore.addAction(action.actionName, action.callback);
|
||||
for (const action of this.getDefaultActionsMenuActions()) {
|
||||
actionsMenuStore.addAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
private registerDefaultActionsMenuActions(): void {
|
||||
private getDefaultActionsMenuActions(): ActionsMenuAction[] {
|
||||
const actions: ActionsMenuAction[] = [];
|
||||
if (this.visitCardUrl) {
|
||||
this.registeredActions.push({
|
||||
actions.push({
|
||||
actionName: LL.woka.menu.businessCard(),
|
||||
protected: true,
|
||||
priority: 1,
|
||||
callback: () => {
|
||||
requestVisitCardsStore.set(this.visitCardUrl);
|
||||
actionsMenuStore.clear();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
actionName: blackListManager.isBlackListed(this.userUuid)
|
||||
? LL.report.block.unblock()
|
||||
: LL.report.block.block(),
|
||||
protected: true,
|
||||
priority: -1,
|
||||
style: "is-error",
|
||||
callback: () => {
|
||||
showReportScreenStore.set({ userId: this.userId, userName: this.name });
|
||||
actionsMenuStore.clear();
|
||||
},
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private bindEventHandlers(): void {
|
||||
this.on(Phaser.Input.Events.POINTER_DOWN, (event: Phaser.Input.Pointer) => {
|
||||
if (event.downElement.nodeName === "CANVAS" && event.leftButtonDown()) {
|
||||
this.toggleActionsMenu();
|
||||
this.emit(RemotePlayerEvent.Clicked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -30,7 +30,7 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import { SimplePeer } from "../../WebRtc/SimplePeer";
|
||||
import { Loader } from "../Components/Loader";
|
||||
import { RemotePlayer } from "../Entity/RemotePlayer";
|
||||
import { RemotePlayer, RemotePlayerEvent } from "../Entity/RemotePlayer";
|
||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||
import { PlayerAnimationDirections } from "../Player/Animation";
|
||||
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
||||
@@ -97,8 +97,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 +166,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;
|
||||
@@ -220,6 +218,7 @@ export class GameScene extends DirtyScene {
|
||||
private loader: Loader;
|
||||
private lastCameraEvent: WasCameraUpdatedEvent | undefined;
|
||||
private firstCameraUpdateSent: boolean = false;
|
||||
public readonly superLoad: SuperLoaderPlugin;
|
||||
|
||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||
super({
|
||||
@@ -232,13 +231,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 +404,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 +605,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 +631,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 +648,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 +676,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 +708,11 @@ export class GameScene extends DirtyScene {
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises])
|
||||
Promise.all([
|
||||
this.connectionAnswerPromiseDeferred.promise as Promise<unknown>,
|
||||
...scriptPromises,
|
||||
this.CurrentPlayer.getTextureLoadedPromise() as Promise<unknown>,
|
||||
])
|
||||
.then(() => {
|
||||
this.scene.wake();
|
||||
})
|
||||
@@ -739,7 +749,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);
|
||||
})
|
||||
@@ -868,7 +878,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()) {
|
||||
@@ -1108,6 +1118,23 @@ ${escapedMessage}
|
||||
})
|
||||
);
|
||||
|
||||
this.iframeSubscriptionList.push(
|
||||
iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => {
|
||||
this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({
|
||||
actionName: data.actionKey,
|
||||
callback: () => {
|
||||
iframeListener.sendActionsMenuActionClickedEvent({ actionName: data.actionKey, id: data.id });
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.iframeSubscriptionList.push(
|
||||
iframeListener.removeActionsMenuKeyFromRemotePlayerEvent.subscribe((data) => {
|
||||
this.MapPlayersByKey.get(data.id)?.unregisterActionsMenuAction(data.actionKey);
|
||||
})
|
||||
);
|
||||
|
||||
this.iframeSubscriptionList.push(
|
||||
iframeListener.trackCameraUpdateStream.subscribe(() => {
|
||||
if (!this.firstCameraUpdateSent) {
|
||||
@@ -1266,7 +1293,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,
|
||||
@@ -1289,7 +1316,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;
|
||||
@@ -1890,9 +1917,10 @@ ${escapedMessage}
|
||||
return;
|
||||
}
|
||||
|
||||
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, addPlayerData.characterLayers);
|
||||
const texturesPromise = lazyLoadPlayerCharacterTextures(this.superLoad, addPlayerData.characterLayers);
|
||||
const player = new RemotePlayer(
|
||||
addPlayerData.userId,
|
||||
addPlayerData.userUuid,
|
||||
this,
|
||||
addPlayerData.position.x,
|
||||
addPlayerData.position.y,
|
||||
@@ -1920,6 +1948,10 @@ ${escapedMessage}
|
||||
this.activatablesManager.handlePointerOutActivatableObject();
|
||||
this.markDirty();
|
||||
});
|
||||
|
||||
player.on(RemotePlayerEvent.Clicked, () => {
|
||||
iframeListener.sendRemotePlayerClickedEvent({ id: player.userId });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2056,8 +2088,6 @@ ${escapedMessage}
|
||||
right: camera.scrollX + camera.width,
|
||||
bottom: camera.scrollY + camera.height,
|
||||
});
|
||||
|
||||
this.loader.resize();
|
||||
}
|
||||
|
||||
private getObjectLayerData(objectName: string): ITiledMapObject | undefined {
|
||||
|
||||
@@ -1,41 +1,14 @@
|
||||
import { ResizableScene } from "./ResizableScene";
|
||||
import { BodyResourceDescriptionInterface, PlayerTexturesKey } from "../Entity/PlayerTextures";
|
||||
import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||
import type CancelablePromise from "cancelable-promise";
|
||||
import { PlayerTextures } from "../Entity/PlayerTextures";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "../Components/CustomizeWoka/WokaBodyPartSlot";
|
||||
import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid";
|
||||
import { Button } from "../Components/Ui/Button";
|
||||
import { wokaList } from "../../Messages/JsonMessages/PlayerTextures";
|
||||
|
||||
export const CustomizeSceneName = "CustomizeScene";
|
||||
|
||||
@@ -63,45 +64,30 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
this.load.image("iconEyes", "/resources/icons/icon_eyes.png");
|
||||
this.load.image("iconBody", "/resources/icons/icon_body.png");
|
||||
|
||||
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/` + 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();
|
||||
}
|
||||
|
||||
public create(): void {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { localeDetector } from "../../i18n/locales";
|
||||
import { PlayerTextures } from "../Entity/PlayerTextures";
|
||||
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
export const EntrySceneName = "EntryScene";
|
||||
|
||||
|
||||
@@ -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/` + 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() {
|
||||
|
||||
@@ -76,7 +76,7 @@ export class Player extends Character {
|
||||
speed?: number
|
||||
): Promise<{ x: number; y: number; cancelled: boolean }> {
|
||||
const isPreviousPathInProgress = this.pathToFollow !== undefined && this.pathToFollow.length > 0;
|
||||
// take collider offset into consideraton
|
||||
// take collider offset into consideration
|
||||
this.pathToFollow = this.adjustPathToFollowToColliderBounds(path);
|
||||
this.pathWalkingSpeed = speed;
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type ActionsMenuAction = {
|
||||
actionName: string;
|
||||
callback: Function;
|
||||
protected?: boolean;
|
||||
priority?: number;
|
||||
style?: "is-success" | "is-error" | "is-primary";
|
||||
};
|
||||
export interface ActionsMenuData {
|
||||
playerName: string;
|
||||
actions: { actionName: string; callback: Function }[];
|
||||
actions: Map<string, ActionsMenuAction>;
|
||||
}
|
||||
|
||||
function createActionsMenuStore() {
|
||||
@@ -13,21 +20,18 @@ function createActionsMenuStore() {
|
||||
initialize: (playerName: string) => {
|
||||
set({
|
||||
playerName,
|
||||
actions: [],
|
||||
actions: new Map<string, ActionsMenuAction>(),
|
||||
});
|
||||
},
|
||||
addAction: (actionName: string, callback: Function) => {
|
||||
addAction: (action: ActionsMenuAction) => {
|
||||
update((data) => {
|
||||
data?.actions.push({ actionName, callback });
|
||||
data?.actions.set(action.actionName, action);
|
||||
return data;
|
||||
});
|
||||
},
|
||||
removeAction: (actionName: string) => {
|
||||
update((data) => {
|
||||
const actionIndex = data?.actions.findIndex((action) => action.actionName === actionName);
|
||||
if (actionIndex !== undefined && actionIndex != -1) {
|
||||
data?.actions.splice(actionIndex, 1);
|
||||
}
|
||||
data?.actions.delete(actionName);
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -545,8 +545,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 +561,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 +579,9 @@ export const localVolumeStore = readable<number | undefined>(undefined, (set) =>
|
||||
return () => {
|
||||
unsubscribe();
|
||||
clearInterval(timeout);
|
||||
if (soundMeter) {
|
||||
soundMeter.stop();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user