Merge branch 'develop' of github.com:thecodingmachine/workadventure into upgrade_typescript_4.5
This commit is contained in:
commit
b2bcfde5b1
64 changed files with 1650 additions and 145 deletions
11
front/src/Api/Events/ChangeZoneEvent.ts
Normal file
11
front/src/Api/Events/ChangeZoneEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isChangeZoneEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
name: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user enters or leaves a zone.
|
||||
*/
|
||||
export type ChangeZoneEvent = tg.GuardedType<typeof isChangeZoneEvent>;
|
|
@ -9,6 +9,7 @@ export const isGameStateEvent = new tg.IsInterface()
|
|||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
variables: tg.isObject,
|
||||
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
|
|
|
@ -28,6 +28,7 @@ import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent";
|
|||
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
|
||||
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
|
||||
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
|
||||
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
|
@ -76,6 +77,8 @@ export interface IframeResponseEventMap {
|
|||
leaveEvent: EnterLeaveEvent;
|
||||
enterLayerEvent: ChangeLayerEvent;
|
||||
leaveLayerEvent: ChangeLayerEvent;
|
||||
enterZoneEvent: ChangeZoneEvent;
|
||||
leaveZoneEvent: ChangeZoneEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
|
|
|
@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent";
|
|||
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
|
||||
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
|
||||
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
|
||||
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"],
|
||||
|
@ -414,6 +415,24 @@ class IframeListener {
|
|||
});
|
||||
}
|
||||
|
||||
sendEnterZoneEvent(zoneName: string) {
|
||||
this.postMessage({
|
||||
type: "enterZoneEvent",
|
||||
data: {
|
||||
name: zoneName,
|
||||
} as ChangeZoneEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveZoneEvent(zoneName: string) {
|
||||
this.postMessage({
|
||||
type: "leaveZoneEvent",
|
||||
data: {
|
||||
name: zoneName,
|
||||
} as ChangeZoneEvent,
|
||||
});
|
||||
}
|
||||
|
||||
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
||||
if (this.sendPlayerMove) {
|
||||
this.postMessage({
|
||||
|
|
|
@ -20,6 +20,12 @@ export const setTags = (_tags: string[]) => {
|
|||
|
||||
let uuid: string | undefined;
|
||||
|
||||
let userRoomToken: string | undefined;
|
||||
|
||||
export const setUserRoomToken = (token: string | undefined) => {
|
||||
userRoomToken = token;
|
||||
};
|
||||
|
||||
export const setUuid = (_uuid: string | undefined) => {
|
||||
uuid = _uuid;
|
||||
};
|
||||
|
@ -67,6 +73,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
get userRoomToken(): string | undefined {
|
||||
if (userRoomToken === undefined) {
|
||||
throw new Error(
|
||||
"User-room token not initialized yet. You should call WA.player.userRoomToken within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return userRoomToken;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventurePlayerCommands();
|
||||
|
|
50
front/src/Components/Companion/Companion.svelte
Normal file
50
front/src/Components/Companion/Companion.svelte
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script lang="typescript">
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import type { PictureStore } from "../../Stores/PictureStore";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let userId: number;
|
||||
export let placeholderSrc: string;
|
||||
export let width: string = "62px";
|
||||
export let height: string = "62px";
|
||||
|
||||
const gameScene = gameManager.getCurrentGameScene();
|
||||
let companionWokaPictureStore: PictureStore | undefined;
|
||||
if (userId === -1) {
|
||||
companionWokaPictureStore = gameScene.CurrentPlayer.companion?.pictureStore;
|
||||
} else {
|
||||
companionWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(
|
||||
userId,
|
||||
(item) => item.companion?.pictureStore
|
||||
);
|
||||
}
|
||||
|
||||
let src = placeholderSrc;
|
||||
|
||||
if (companionWokaPictureStore) {
|
||||
const unsubscribe = companionWokaPictureStore.subscribe((source) => {
|
||||
src = source ?? placeholderSrc;
|
||||
});
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
}
|
||||
</script>
|
||||
|
||||
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
display: inline-block;
|
||||
pointer-events: auto;
|
||||
width: var(--theme-width);
|
||||
height: var(--theme-height);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: static;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
|
@ -6,11 +6,14 @@
|
|||
|
||||
let expandedMapCopyright = false;
|
||||
let expandedTilesetCopyright = false;
|
||||
let expandedAudioCopyright = false;
|
||||
|
||||
let mapName: string = "";
|
||||
let mapLink: string = "";
|
||||
let mapDescription: string = "";
|
||||
let mapCopyright: string = "The map creator did not declare a copyright for the map.";
|
||||
let tilesetCopyright: string[] = [];
|
||||
let audioCopyright: string[] = [];
|
||||
|
||||
onMount(() => {
|
||||
if (gameScene.mapFile.properties !== undefined) {
|
||||
|
@ -18,6 +21,10 @@
|
|||
if (propertyName !== undefined && typeof propertyName.value === "string") {
|
||||
mapName = propertyName.value;
|
||||
}
|
||||
const propertyLink = gameScene.mapFile.properties.find((property) => property.name === "mapLink");
|
||||
if (propertyLink !== undefined && typeof propertyLink.value === "string") {
|
||||
mapLink = propertyLink.value;
|
||||
}
|
||||
const propertyDescription = gameScene.mapFile.properties.find(
|
||||
(property) => property.name === "mapDescription"
|
||||
);
|
||||
|
@ -36,7 +43,18 @@
|
|||
(property) => property.name === "tilesetCopyright"
|
||||
);
|
||||
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") {
|
||||
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
|
||||
// Assignment needed to trigger Svelte's reactivity
|
||||
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of gameScene.mapFile.layers) {
|
||||
if (layer.type && layer.type === "tilelayer" && layer.properties) {
|
||||
const propertyAudioCopyright = layer.properties.find((property) => property.name === "audioCopyright");
|
||||
if (propertyAudioCopyright !== undefined && typeof propertyAudioCopyright.value === "string") {
|
||||
// Assignment needed to trigger Svelte's reactivity
|
||||
audioCopyright = [...audioCopyright, propertyAudioCopyright.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +66,9 @@
|
|||
<section class="container-overflow">
|
||||
<h3>{mapName}</h3>
|
||||
<p class="string-HTML">{mapDescription}</p>
|
||||
{#if mapLink}
|
||||
<p class="string-HTML">> <a href={mapLink} target="_blank">link to this map</a> <</p>
|
||||
{/if}
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
|
||||
Copyrights of the map
|
||||
</h3>
|
||||
|
@ -60,8 +81,21 @@
|
|||
<p class="string-HTML">{copyright}</p>
|
||||
{:else}
|
||||
<p>
|
||||
The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those
|
||||
tilesets have no license.
|
||||
The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets
|
||||
have no license.
|
||||
</p>
|
||||
{/each}
|
||||
</section>
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
|
||||
Copyrights of audio files
|
||||
</h3>
|
||||
<section hidden={!expandedAudioCopyright}>
|
||||
{#each audioCopyright as copyright}
|
||||
<p class="string-HTML">{copyright}</p>
|
||||
{:else}
|
||||
<p>
|
||||
The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
|
||||
have no license.
|
||||
</p>
|
||||
{/each}
|
||||
</section>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import logoWA from "../images/logo-WA-pixel.png";
|
||||
import logoTalk from "../images/logo-message-pixel.png";
|
||||
import logoWA from "../images/logo-WA-pixel.png";
|
||||
import { menuVisiblilityStore } from "../../Stores/MenuStore";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { get } from "svelte/store";
|
||||
|
@ -31,6 +31,7 @@
|
|||
width: 60px;
|
||||
padding-top: 0;
|
||||
margin: 3px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
.menuIcon img:hover {
|
||||
|
@ -38,9 +39,26 @@
|
|||
}
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
.menuIcon {
|
||||
margin: 3px;
|
||||
display: inline-grid;
|
||||
z-index: 90;
|
||||
position: relative;
|
||||
margin: 25px;
|
||||
img {
|
||||
width: 50px;
|
||||
pointer-events: auto;
|
||||
width: 60px;
|
||||
padding-top: 0;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
.menuIcon img:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
.menuIcon {
|
||||
margin: 3px;
|
||||
img {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
|
||||
import btnProfileSubMenuIdentity from "../images/btn-menu-profile-identity.svg";
|
||||
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
|
||||
import btnProfileSubMenuWoka from "../images/btn-menu-profile-woka.svg";
|
||||
import Woka from "../Woka/Woka.svelte";
|
||||
import Companion from "../Companion/Companion.svelte";
|
||||
|
||||
function disableMenuStores() {
|
||||
menuVisiblilityStore.set(false);
|
||||
|
@ -65,11 +66,11 @@
|
|||
<span class="btn-hover">Edit your name</span>
|
||||
</button>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
|
||||
<img src={btnProfileSubMenuWoka} alt="Edit your WOKA" />
|
||||
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
|
||||
<span class="btn-hover">Edit your WOKA</span>
|
||||
</button>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
|
||||
<img src={btnProfileSubMenuCompanion} alt="Edit your companion" />
|
||||
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
|
||||
<span class="btn-hover">Edit your companion</span>
|
||||
</button>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
|
||||
import { getColorByString, srcObject } from "./utils";
|
||||
|
||||
import Woka from "../Woka/Woka.svelte";
|
||||
|
||||
export let peer: VideoPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
let name = peer.userName;
|
||||
|
@ -26,9 +28,15 @@
|
|||
{#if $statusStore === "error"}
|
||||
<div class="rtc-error" />
|
||||
{/if}
|
||||
{#if !$constraintStore || $constraintStore.video === false}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{/if}
|
||||
<!-- {#if !$constraintStore || $constraintStore.video === false} -->
|
||||
<i
|
||||
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}"
|
||||
style="background-color: {getColorByString(name)};"
|
||||
>
|
||||
<span>{peer.userName}</span>
|
||||
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
|
||||
</i>
|
||||
<!-- {/if} -->
|
||||
{#if $constraintStore && $constraintStore.audio === false}
|
||||
<img src={microphoneCloseImg} class="active" alt="Muted" />
|
||||
{/if}
|
||||
|
@ -43,3 +51,21 @@
|
|||
<SoundMeterWidget stream={$streamStore} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.minimized {
|
||||
left: auto;
|
||||
transform: scale(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.woka-icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
45
front/src/Components/Woka/Woka.svelte
Normal file
45
front/src/Components/Woka/Woka.svelte
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script lang="typescript">
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
|
||||
export let userId: number;
|
||||
export let placeholderSrc: string;
|
||||
export let width: string = "62px";
|
||||
export let height: string = "62px";
|
||||
|
||||
const gameScene = gameManager.getCurrentGameScene();
|
||||
let playerWokaPictureStore;
|
||||
if (userId === -1) {
|
||||
playerWokaPictureStore = gameScene.CurrentPlayer.pictureStore;
|
||||
} else {
|
||||
playerWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(userId, (item) => item.pictureStore);
|
||||
}
|
||||
|
||||
let src = placeholderSrc;
|
||||
|
||||
const unsubscribe = playerWokaPictureStore.subscribe((source) => {
|
||||
src = source ?? placeholderSrc;
|
||||
});
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
</script>
|
||||
|
||||
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
display: inline-block;
|
||||
pointer-events: auto;
|
||||
width: var(--theme-width);
|
||||
height: var(--theme-height);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: static;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
|
@ -68,6 +68,7 @@ export class RoomConnection implements RoomConnection {
|
|||
private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
private closed: boolean = false;
|
||||
private tags: string[] = [];
|
||||
private _userRoomToken: string | undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
|
||||
|
@ -211,6 +212,7 @@ export class RoomConnection implements RoomConnection {
|
|||
|
||||
this.userId = roomJoinedMessage.getCurrentuserid();
|
||||
this.tags = roomJoinedMessage.getTagList();
|
||||
this._userRoomToken = roomJoinedMessage.getUserroomtoken();
|
||||
|
||||
this.dispatch(EventMessage.CONNECT, {
|
||||
connection: this,
|
||||
|
@ -713,4 +715,8 @@ export class RoomConnection implements RoomConnection {
|
|||
public getAllTags(): string[] {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
public get userRoomToken(): string | undefined {
|
||||
return this._userRoomToken;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import Container = Phaser.GameObjects.Container;
|
||||
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
|
||||
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
||||
import { Writable, writable } from "svelte/store";
|
||||
import type { PictureStore } from "../../Stores/PictureStore";
|
||||
|
||||
export interface CompanionStatus {
|
||||
x: number;
|
||||
|
@ -21,6 +24,7 @@ export class Companion extends Container {
|
|||
private companionName: string;
|
||||
private direction: PlayerAnimationDirections;
|
||||
private animationType: PlayerAnimationTypes;
|
||||
private readonly _pictureStore: Writable<string | undefined>;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
|
||||
super(scene, x + 14, y + 4);
|
||||
|
@ -35,10 +39,14 @@ export class Companion extends Container {
|
|||
this.animationType = PlayerAnimationTypes.Idle;
|
||||
|
||||
this.companionName = name;
|
||||
this._pictureStore = writable(undefined);
|
||||
|
||||
texturePromise.then((resource) => {
|
||||
this.addResource(resource);
|
||||
this.invisible = false;
|
||||
return this.getSnapshot().then((htmlImageElementSrc) => {
|
||||
this._pictureStore.set(htmlImageElementSrc);
|
||||
});
|
||||
});
|
||||
|
||||
this.scene.physics.world.enableBody(this);
|
||||
|
@ -123,6 +131,22 @@ export class Companion extends Container {
|
|||
};
|
||||
}
|
||||
|
||||
public async getSnapshot(): Promise<string> {
|
||||
const sprites = Array.from(this.sprites.values()).map((sprite) => {
|
||||
return { sprite, frame: 1 };
|
||||
});
|
||||
return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => {
|
||||
console.warn(reason);
|
||||
for (const sprite of this.sprites.values()) {
|
||||
// it can be either cat or dog prefix
|
||||
if (sprite.texture.key.includes("cat") || sprite.texture.key.includes("dog")) {
|
||||
return this.scene.textures.getBase64(sprite.texture.key);
|
||||
}
|
||||
}
|
||||
return "cat1";
|
||||
});
|
||||
}
|
||||
|
||||
private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void {
|
||||
if (this.invisible) return;
|
||||
|
||||
|
@ -220,4 +244,8 @@ export class Companion extends Container {
|
|||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
public get pictureStore(): PictureStore {
|
||||
return this._pictureStore;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,12 @@ import { TextureError } from "../../Exception/TextureError";
|
|||
import { Companion } from "../Companion/Companion";
|
||||
import type { GameScene } from "../Game/GameScene";
|
||||
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
|
||||
import { isSilentStore } from "../../Stores/MediaStore";
|
||||
import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager";
|
||||
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
|
||||
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
||||
import type { PictureStore } from "../../Stores/PictureStore";
|
||||
import { Writable, writable } from "svelte/store";
|
||||
|
||||
const playerNameY = -25;
|
||||
|
||||
|
@ -37,6 +39,7 @@ export abstract class Character extends Container {
|
|||
private emote: Phaser.GameObjects.DOMElement | null = null;
|
||||
private emoteTween: Phaser.Tweens.Tween | null = null;
|
||||
scene: GameScene;
|
||||
private readonly _pictureStore: Writable<string | undefined>;
|
||||
|
||||
constructor(
|
||||
scene: GameScene,
|
||||
|
@ -57,6 +60,7 @@ export abstract class Character extends Container {
|
|||
this.invisible = true;
|
||||
|
||||
this.sprites = new Map<string, Sprite>();
|
||||
this._pictureStore = writable(undefined);
|
||||
|
||||
//textures are inside a Promise in case they need to be lazyloaded before use.
|
||||
texturesPromise
|
||||
|
@ -64,6 +68,9 @@ export abstract class Character extends Container {
|
|||
this.addTextures(textures, frame);
|
||||
this.invisible = false;
|
||||
this.playAnimation(direction, moving);
|
||||
return this.getSnapshot().then((htmlImageElementSrc) => {
|
||||
this._pictureStore.set(htmlImageElementSrc);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => {
|
||||
|
@ -117,8 +124,20 @@ export abstract class Character extends Container {
|
|||
}
|
||||
}
|
||||
|
||||
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
|
||||
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
|
||||
private async getSnapshot(): Promise<string> {
|
||||
const sprites = Array.from(this.sprites.values()).map((sprite) => {
|
||||
return { sprite, frame: 1 };
|
||||
});
|
||||
return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => {
|
||||
console.warn(reason);
|
||||
for (const sprite of this.sprites.values()) {
|
||||
// we can be sure that either predefined woka or body texture is at this point loaded
|
||||
if (sprite.texture.key.includes("color") || sprite.texture.key.includes("male")) {
|
||||
return this.scene.textures.getBase64(sprite.texture.key);
|
||||
}
|
||||
}
|
||||
return "male1";
|
||||
});
|
||||
}
|
||||
|
||||
public addCompanion(name: string, texturePromise?: Promise<string>): void {
|
||||
|
@ -154,6 +173,10 @@ export abstract class Character extends Container {
|
|||
}
|
||||
}
|
||||
|
||||
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
|
||||
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
|
||||
}
|
||||
|
||||
private getPlayerAnimations(name: string): AnimationData[] {
|
||||
return [
|
||||
{
|
||||
|
@ -374,4 +397,8 @@ export abstract class Character extends Container {
|
|||
this.emote = null;
|
||||
this.playerName.setVisible(true);
|
||||
}
|
||||
|
||||
public get pictureStore(): PictureStore {
|
||||
return this._pictureStore;
|
||||
}
|
||||
}
|
||||
|
|
178
front/src/Phaser/Game/CameraManager.ts
Normal file
178
front/src/Phaser/Game/CameraManager.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { Easing } from "../../types";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import type { Box } from "../../WebRtc/LayoutManager";
|
||||
import type { Player } from "../Player/Player";
|
||||
import type { WaScaleManager } from "../Services/WaScaleManager";
|
||||
import type { GameScene } from "./GameScene";
|
||||
|
||||
export enum CameraMode {
|
||||
Free = "Free",
|
||||
Follow = "Follow",
|
||||
Focus = "Focus",
|
||||
}
|
||||
|
||||
export class CameraManager extends Phaser.Events.EventEmitter {
|
||||
private scene: GameScene;
|
||||
private camera: Phaser.Cameras.Scene2D.Camera;
|
||||
private cameraBounds: { x: number; y: number };
|
||||
private waScaleManager: WaScaleManager;
|
||||
|
||||
private cameraMode: CameraMode = CameraMode.Free;
|
||||
|
||||
private restoreZoomTween?: Phaser.Tweens.Tween;
|
||||
private startFollowTween?: Phaser.Tweens.Tween;
|
||||
|
||||
private cameraFollowTarget?: { x: number; y: number };
|
||||
|
||||
constructor(scene: GameScene, cameraBounds: { x: number; y: number }, waScaleManager: WaScaleManager) {
|
||||
super();
|
||||
this.scene = scene;
|
||||
|
||||
this.camera = scene.cameras.main;
|
||||
this.cameraBounds = cameraBounds;
|
||||
|
||||
this.waScaleManager = waScaleManager;
|
||||
|
||||
this.initCamera();
|
||||
|
||||
this.bindEventHandlers();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.scene.game.events.off("wa-scale-manager:refresh-focus-on-target");
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
public getCamera(): Phaser.Cameras.Scene2D.Camera {
|
||||
return this.camera;
|
||||
}
|
||||
|
||||
public enterFocusMode(
|
||||
focusOn: { x: number; y: number; width: number; height: number },
|
||||
margin: number = 0,
|
||||
duration: number = 1000
|
||||
): void {
|
||||
this.setCameraMode(CameraMode.Focus);
|
||||
this.waScaleManager.saveZoom();
|
||||
this.waScaleManager.setFocusTarget(focusOn);
|
||||
|
||||
this.restoreZoomTween?.stop();
|
||||
this.startFollowTween?.stop();
|
||||
const marginMult = 1 + margin;
|
||||
const targetZoomModifier = this.waScaleManager.getTargetZoomModifierFor(
|
||||
focusOn.width * marginMult,
|
||||
focusOn.height * marginMult
|
||||
);
|
||||
const currentZoomModifier = this.waScaleManager.zoomModifier;
|
||||
const zoomModifierChange = targetZoomModifier - currentZoomModifier;
|
||||
this.camera.stopFollow();
|
||||
this.cameraFollowTarget = undefined;
|
||||
this.camera.pan(
|
||||
focusOn.x + focusOn.width * 0.5 * marginMult,
|
||||
focusOn.y + focusOn.height * 0.5 * marginMult,
|
||||
duration,
|
||||
Easing.SineEaseOut,
|
||||
true,
|
||||
(camera, progress, x, y) => {
|
||||
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public leaveFocusMode(player: Player): void {
|
||||
this.waScaleManager.setFocusTarget();
|
||||
this.startFollow(player, 1000);
|
||||
this.restoreZoom(1000);
|
||||
}
|
||||
|
||||
public startFollow(target: object | Phaser.GameObjects.GameObject, duration: number = 0): void {
|
||||
this.cameraFollowTarget = target as { x: number; y: number };
|
||||
this.setCameraMode(CameraMode.Follow);
|
||||
if (duration === 0) {
|
||||
this.camera.startFollow(target, true);
|
||||
return;
|
||||
}
|
||||
const oldPos = { x: this.camera.scrollX, y: this.camera.scrollY };
|
||||
this.startFollowTween = this.scene.tweens.addCounter({
|
||||
from: 0,
|
||||
to: 1,
|
||||
duration,
|
||||
ease: Easing.SineEaseOut,
|
||||
onUpdate: (tween: Phaser.Tweens.Tween) => {
|
||||
if (!this.cameraFollowTarget) {
|
||||
return;
|
||||
}
|
||||
const shiftX =
|
||||
(this.cameraFollowTarget.x - this.camera.worldView.width * 0.5 - oldPos.x) * tween.getValue();
|
||||
const shiftY =
|
||||
(this.cameraFollowTarget.y - this.camera.worldView.height * 0.5 - oldPos.y) * tween.getValue();
|
||||
this.camera.setScroll(oldPos.x + shiftX, oldPos.y + shiftY);
|
||||
},
|
||||
onComplete: () => {
|
||||
this.camera.startFollow(target, true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the offset of the character compared to the center of the screen according to the layout manager
|
||||
* (tries to put the character in the center of the remaining space if there is a discussion going on.
|
||||
*/
|
||||
public updateCameraOffset(array: Box): void {
|
||||
const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
|
||||
const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
|
||||
|
||||
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
|
||||
// Let's put this in Game coordinates by applying the zoom level:
|
||||
|
||||
this.camera.setFollowOffset(
|
||||
((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scene.scale.zoom,
|
||||
((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scene.scale.zoom
|
||||
);
|
||||
}
|
||||
|
||||
public isCameraLocked(): boolean {
|
||||
return this.cameraMode === CameraMode.Focus;
|
||||
}
|
||||
|
||||
private setCameraMode(mode: CameraMode): void {
|
||||
if (this.cameraMode === mode) {
|
||||
return;
|
||||
}
|
||||
this.cameraMode = mode;
|
||||
}
|
||||
|
||||
private restoreZoom(duration: number = 0): void {
|
||||
if (duration === 0) {
|
||||
this.waScaleManager.zoomModifier = this.waScaleManager.getSaveZoom();
|
||||
return;
|
||||
}
|
||||
this.restoreZoomTween?.stop();
|
||||
this.restoreZoomTween = this.scene.tweens.addCounter({
|
||||
from: this.waScaleManager.zoomModifier,
|
||||
to: this.waScaleManager.getSaveZoom(),
|
||||
duration,
|
||||
ease: Easing.SineEaseOut,
|
||||
onUpdate: (tween: Phaser.Tweens.Tween) => {
|
||||
this.waScaleManager.zoomModifier = tween.getValue();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private initCamera() {
|
||||
this.camera = this.scene.cameras.main;
|
||||
this.camera.setBounds(0, 0, this.cameraBounds.x, this.cameraBounds.y);
|
||||
}
|
||||
|
||||
private bindEventHandlers(): void {
|
||||
this.scene.game.events.on(
|
||||
"wa-scale-manager:refresh-focus-on-target",
|
||||
(focusOn: { x: number; y: number; width: number; height: number }) => {
|
||||
if (!focusOn) {
|
||||
return;
|
||||
}
|
||||
this.camera.centerOn(focusOn.x + focusOn.width * 0.5, focusOn.y + focusOn.height * 0.5);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,15 @@
|
|||
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap";
|
||||
import type {
|
||||
ITiledMap,
|
||||
ITiledMapLayer,
|
||||
ITiledMapObject,
|
||||
ITiledMapObjectLayer,
|
||||
ITiledMapProperty,
|
||||
} from "../Map/ITiledMap";
|
||||
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||
import { GameMapProperties } from "./GameMapProperties";
|
||||
import { MathUtils } from "../../Utils/MathUtils";
|
||||
|
||||
export type PropertyChangeCallback = (
|
||||
newValue: string | number | boolean | undefined,
|
||||
|
@ -15,24 +22,48 @@ export type layerChangeCallback = (
|
|||
allLayersOnNewPosition: Array<ITiledMapLayer>
|
||||
) => void;
|
||||
|
||||
export type zoneChangeCallback = (
|
||||
zonesChangedByAction: Array<ITiledMapObject>,
|
||||
allZonesOnNewPosition: Array<ITiledMapObject>
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
||||
* It is used to handle layer properties.
|
||||
*/
|
||||
export class GameMap {
|
||||
// oldKey is the index of the previous tile.
|
||||
/**
|
||||
* oldKey is the index of the previous tile.
|
||||
*/
|
||||
private oldKey: number | undefined;
|
||||
// key is the index of the current tile.
|
||||
/**
|
||||
* key is the index of the current tile.
|
||||
*/
|
||||
private key: number | undefined;
|
||||
/**
|
||||
* oldPosition is the previous position of the player.
|
||||
*/
|
||||
private oldPosition: { x: number; y: number } | undefined;
|
||||
/**
|
||||
* position is the current position of the player.
|
||||
*/
|
||||
private position: { x: number; y: number } | undefined;
|
||||
|
||||
private lastProperties = new Map<string, string | boolean | number>();
|
||||
private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
|
||||
private enterLayerCallbacks = Array<layerChangeCallback>();
|
||||
private leaveLayerCallbacks = Array<layerChangeCallback>();
|
||||
private enterZoneCallbacks = Array<zoneChangeCallback>();
|
||||
private leaveZoneCallbacks = Array<zoneChangeCallback>();
|
||||
|
||||
private tileNameMap = new Map<string, number>();
|
||||
|
||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
|
||||
public readonly flatLayers: ITiledMapLayer[];
|
||||
public readonly tiledObjects: ITiledMapObject[];
|
||||
public readonly phaserLayers: TilemapLayer[] = [];
|
||||
public readonly zones: ITiledMapObject[] = [];
|
||||
|
||||
public exitUrls: Array<string> = [];
|
||||
|
||||
|
@ -44,6 +75,9 @@ export class GameMap {
|
|||
terrains: Array<Phaser.Tilemaps.Tileset>
|
||||
) {
|
||||
this.flatLayers = flattenGroupLayersMap(map);
|
||||
this.tiledObjects = this.getObjectsFromLayers(this.flatLayers);
|
||||
this.zones = this.tiledObjects.filter((object) => object.type === "zone");
|
||||
|
||||
let depth = -2;
|
||||
for (const layer of this.flatLayers) {
|
||||
if (layer.type === "tilelayer") {
|
||||
|
@ -88,6 +122,10 @@ export class GameMap {
|
|||
* This will trigger events if properties are changing.
|
||||
*/
|
||||
public setPosition(x: number, y: number) {
|
||||
this.oldPosition = this.position;
|
||||
this.position = { x, y };
|
||||
this.triggerZonesChange();
|
||||
|
||||
this.oldKey = this.key;
|
||||
|
||||
const xMap = Math.floor(x / this.map.tilewidth);
|
||||
|
@ -126,7 +164,7 @@ export class GameMap {
|
|||
}
|
||||
}
|
||||
|
||||
private triggerLayersChange() {
|
||||
private triggerLayersChange(): void {
|
||||
const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : [];
|
||||
const layersByNewKey = this.key ? this.getLayersByKey(this.key) : [];
|
||||
|
||||
|
@ -155,6 +193,53 @@ export class GameMap {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We use Tiled Objects with type "zone" as zones with defined x, y, width and height for easier event triggering.
|
||||
*/
|
||||
private triggerZonesChange(): void {
|
||||
const zonesByOldPosition = this.oldPosition
|
||||
? this.zones.filter((zone) => {
|
||||
if (!this.oldPosition) {
|
||||
return false;
|
||||
}
|
||||
return MathUtils.isOverlappingWithRectangle(this.oldPosition, zone);
|
||||
})
|
||||
: [];
|
||||
|
||||
const zonesByNewPosition = this.position
|
||||
? this.zones.filter((zone) => {
|
||||
if (!this.position) {
|
||||
return false;
|
||||
}
|
||||
return MathUtils.isOverlappingWithRectangle(this.position, zone);
|
||||
})
|
||||
: [];
|
||||
|
||||
const enterZones = new Set(zonesByNewPosition);
|
||||
const leaveZones = new Set(zonesByOldPosition);
|
||||
|
||||
enterZones.forEach((zone) => {
|
||||
if (leaveZones.has(zone)) {
|
||||
leaveZones.delete(zone);
|
||||
enterZones.delete(zone);
|
||||
}
|
||||
});
|
||||
|
||||
if (enterZones.size > 0) {
|
||||
const zonesArray = Array.from(enterZones);
|
||||
for (const callback of this.enterZoneCallbacks) {
|
||||
callback(zonesArray, zonesByNewPosition);
|
||||
}
|
||||
}
|
||||
|
||||
if (leaveZones.size > 0) {
|
||||
const zonesArray = Array.from(leaveZones);
|
||||
for (const callback of this.leaveZoneCallbacks) {
|
||||
callback(zonesArray, zonesByNewPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentProperties(): Map<string, string | boolean | number> {
|
||||
return this.lastProperties;
|
||||
}
|
||||
|
@ -251,6 +336,20 @@ export class GameMap {
|
|||
this.leaveLayerCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback called when the user moves inside another zone.
|
||||
*/
|
||||
public onEnterZone(callback: zoneChangeCallback) {
|
||||
this.enterZoneCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback called when the user moves outside another zone.
|
||||
*/
|
||||
public onLeaveZone(callback: zoneChangeCallback) {
|
||||
this.leaveZoneCallbacks.push(callback);
|
||||
}
|
||||
|
||||
public findLayer(layerName: string): ITiledMapLayer | undefined {
|
||||
return this.flatLayers.find((layer) => layer.name === layerName);
|
||||
}
|
||||
|
@ -362,4 +461,17 @@ export class GameMap {
|
|||
this.trigger(oldPropName, oldPropValue, undefined, emptyProps);
|
||||
}
|
||||
}
|
||||
|
||||
private getObjectsFromLayers(layers: ITiledMapLayer[]): ITiledMapObject[] {
|
||||
const objects: ITiledMapObject[] = [];
|
||||
|
||||
const objectLayers = layers.filter((layer) => layer.type === "objectgroup");
|
||||
for (const objectLayer of objectLayers) {
|
||||
if (objectLayer.type === "objectgroup") {
|
||||
objects.push(...objectLayer.objects);
|
||||
}
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,54 @@
|
|||
import type { Subscription } from "rxjs";
|
||||
import AnimatedTiles from "phaser-animated-tiles";
|
||||
import { Queue } from "queue-typescript";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { userMessageManager } from "../../Administration/UserMessageManager";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
|
||||
import { urlManager } from "../../Url/UrlManager";
|
||||
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||
import { UserInputManager } from "../UserInput/UserInputManager";
|
||||
import { gameManager } from "./GameManager";
|
||||
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||
import { PinchManager } from "../UserInput/PinchManager";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import { EmoteManager } from "./EmoteManager";
|
||||
import { soundManager } from "./SoundManager";
|
||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
|
||||
|
||||
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
|
||||
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
|
||||
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
||||
import { Room } from "../../Connexion/Room";
|
||||
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
||||
import { TextureError } from "../../Exception/TextureError";
|
||||
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 { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||
import { PlayerAnimationDirections } from "../Player/Animation";
|
||||
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
||||
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
|
||||
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
|
||||
import { GameMap } from "./GameMap";
|
||||
import { PlayerMovement } from "./PlayerMovement";
|
||||
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
|
||||
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
|
||||
import { DirtyScene } from "./DirtyScene";
|
||||
import { TextUtils } from "../Components/TextUtils";
|
||||
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
|
||||
import { StartPositionCalculator } from "./StartPositionCalculator";
|
||||
import { PropertyUtils } from "../Map/PropertyUtils";
|
||||
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
import { GameMapProperties } from "./GameMapProperties";
|
||||
import type {
|
||||
GroupCreatedUpdatedMessageInterface,
|
||||
MessageUserJoined,
|
||||
|
@ -12,84 +59,35 @@ import type {
|
|||
PositionInterface,
|
||||
RoomJoinedMessageInterface,
|
||||
} from "../../Connexion/ConnexionModels";
|
||||
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
import { Queue } from "queue-typescript";
|
||||
import { Box, ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
|
||||
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
|
||||
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
|
||||
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
||||
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
||||
import { Room } from "../../Connexion/Room";
|
||||
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
||||
import { urlManager } from "../../Url/UrlManager";
|
||||
import { TextureError } from "../../Exception/TextureError";
|
||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||
import { SimplePeer } from "../../WebRtc/SimplePeer";
|
||||
import { Loader } from "../Components/Loader";
|
||||
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||
import { RemotePlayer } from "../Entity/RemotePlayer";
|
||||
import type { ActionableItem } from "../Items/ActionableItem";
|
||||
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
|
||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
|
||||
import { PlayerAnimationDirections } from "../Player/Animation";
|
||||
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
||||
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
|
||||
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
|
||||
import { UserInputManager } from "../UserInput/UserInputManager";
|
||||
import type { AddPlayerInterface } from "./AddPlayerInterface";
|
||||
import { gameManager } from "./GameManager";
|
||||
import { GameMap } from "./GameMap";
|
||||
import { PlayerMovement } from "./PlayerMovement";
|
||||
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
|
||||
import { CameraManager } from "./CameraManager";
|
||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||
import type { Character } from "../Entity/Character";
|
||||
|
||||
import { peerStore } from "../../Stores/PeerStore";
|
||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
|
||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||
import { contactPageStore } from "../../Stores/MenuStore";
|
||||
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
|
||||
|
||||
import EVENT_TYPE = Phaser.Scenes.Events;
|
||||
import Texture = Phaser.Textures.Texture;
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import CanvasTexture = Phaser.Textures.CanvasTexture;
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||
import DOMElement = Phaser.GameObjects.DOMElement;
|
||||
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
|
||||
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
|
||||
import { DirtyScene } from "./DirtyScene";
|
||||
import { TextUtils } from "../Components/TextUtils";
|
||||
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||
import { PinchManager } from "../UserInput/PinchManager";
|
||||
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import { EmoteManager } from "./EmoteManager";
|
||||
import EVENT_TYPE = Phaser.Scenes.Events;
|
||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||
|
||||
import AnimatedTiles from "phaser-animated-tiles";
|
||||
import { StartPositionCalculator } from "./StartPositionCalculator";
|
||||
import { soundManager } from "./SoundManager";
|
||||
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
|
||||
import {
|
||||
audioManagerFileStore,
|
||||
audioManagerVisibilityStore,
|
||||
audioManagerVolumeStore,
|
||||
} from "../../Stores/AudioManagerStore";
|
||||
import { PropertyUtils } from "../Map/PropertyUtils";
|
||||
import Tileset = Phaser.Tilemaps.Tileset;
|
||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
|
||||
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
|
||||
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
import { get } from "svelte/store";
|
||||
import { contactPageStore } from "../../Stores/MenuStore";
|
||||
import { GameMapProperties } from "./GameMapProperties";
|
||||
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
|
||||
|
||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||
import { MapStore } from "../../Stores/Utils/MapStore";
|
||||
export interface GameSceneInitInterface {
|
||||
initPosition: PointInterface | null;
|
||||
reconnecting: boolean;
|
||||
|
@ -129,7 +127,7 @@ export class GameScene extends DirtyScene {
|
|||
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
||||
CurrentPlayer!: Player;
|
||||
MapPlayers!: Phaser.Physics.Arcade.Group;
|
||||
MapPlayersByKey: Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
|
||||
MapPlayersByKey: MapStore<number, RemotePlayer> = new MapStore<number, RemotePlayer>();
|
||||
Map!: Phaser.Tilemaps.Tilemap;
|
||||
Objects!: Array<Phaser.Physics.Arcade.Sprite>;
|
||||
mapFile!: ITiledMap;
|
||||
|
@ -198,6 +196,7 @@ export class GameScene extends DirtyScene {
|
|||
private pinchManager: PinchManager | undefined;
|
||||
private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time.
|
||||
private emoteManager!: EmoteManager;
|
||||
private cameraManager!: CameraManager;
|
||||
private preloading: boolean = true;
|
||||
private startPositionCalculator!: StartPositionCalculator;
|
||||
private sharedVariablesManager!: SharedVariablesManager;
|
||||
|
@ -550,7 +549,13 @@ export class GameScene extends DirtyScene {
|
|||
this.createCurrentPlayer();
|
||||
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
|
||||
|
||||
this.initCamera();
|
||||
this.cameraManager = new CameraManager(
|
||||
this,
|
||||
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels },
|
||||
waScaleManager
|
||||
);
|
||||
biggestAvailableAreaStore.recompute();
|
||||
this.cameraManager.startFollow(this.CurrentPlayer);
|
||||
|
||||
this.animatedTiles.init(this.Map);
|
||||
this.events.on("tileanimationupdate", () => (this.dirty = true));
|
||||
|
@ -591,7 +596,7 @@ export class GameScene extends DirtyScene {
|
|||
|
||||
// From now, this game scene will be notified of reposition events
|
||||
this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) =>
|
||||
this.updateCameraOffset(box)
|
||||
this.cameraManager.updateCameraOffset(box)
|
||||
);
|
||||
|
||||
new GameMapPropertiesListener(this, this.gameMap).register();
|
||||
|
@ -644,7 +649,7 @@ export class GameScene extends DirtyScene {
|
|||
* Initializes the connection to Pusher.
|
||||
*/
|
||||
private connect(): void {
|
||||
const camera = this.cameras.main;
|
||||
const camera = this.cameraManager.getCamera();
|
||||
|
||||
connectionManager
|
||||
.connectToRoomSocket(
|
||||
|
@ -666,7 +671,6 @@ export class GameScene extends DirtyScene {
|
|||
this.connection = onConnect.connection;
|
||||
|
||||
playersStore.connectToRoomConnection(this.connection);
|
||||
|
||||
userIsAdminStore.set(this.connection.hasTag("admin"));
|
||||
|
||||
this.connection.onUserJoins((message: MessageUserJoined) => {
|
||||
|
@ -779,6 +783,42 @@ export class GameScene extends DirtyScene {
|
|||
iframeListener.sendLeaveLayerEvent(layer.name);
|
||||
});
|
||||
});
|
||||
|
||||
this.gameMap.onEnterZone((zones) => {
|
||||
for (const zone of zones) {
|
||||
const focusable = zone.properties?.find((property) => property.name === "focusable");
|
||||
if (focusable && focusable.value === true) {
|
||||
const zoomMargin = zone.properties?.find((property) => property.name === "zoom_margin");
|
||||
this.cameraManager.enterFocusMode(
|
||||
zone,
|
||||
zoomMargin ? Math.max(0, Number(zoomMargin.value)) : undefined
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
zones.forEach((zone) => {
|
||||
iframeListener.sendEnterZoneEvent(zone.name);
|
||||
});
|
||||
});
|
||||
|
||||
this.gameMap.onLeaveZone((zones) => {
|
||||
for (const zone of zones) {
|
||||
const focusable = zone.properties?.find((property) => property.name === "focusable");
|
||||
if (focusable && focusable.value === true) {
|
||||
this.cameraManager.leaveFocusMode(this.CurrentPlayer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
zones.forEach((zone) => {
|
||||
iframeListener.sendLeaveZoneEvent(zone.name);
|
||||
});
|
||||
});
|
||||
|
||||
// this.gameMap.onLeaveLayer((layers) => {
|
||||
// layers.forEach((layer) => {
|
||||
// iframeListener.sendLeaveLayerEvent(layer.name);
|
||||
// });
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1167,6 +1207,7 @@ ${escapedMessage}
|
|||
roomId: this.roomUrl,
|
||||
tags: this.connection ? this.connection.getAllTags() : [],
|
||||
variables: this.sharedVariablesManager.variables,
|
||||
userRoomToken: this.connection ? this.connection.userRoomToken : "",
|
||||
};
|
||||
});
|
||||
this.iframeSubscriptionList.push(
|
||||
|
@ -1369,6 +1410,7 @@ ${escapedMessage}
|
|||
this.userInputManager.destroy();
|
||||
this.pinchManager?.destroy();
|
||||
this.emoteManager.destroy();
|
||||
this.cameraManager.destroy();
|
||||
this.peerStoreUnsubscribe();
|
||||
this.emoteUnsubscribe();
|
||||
this.emoteMenuUnsubscribe();
|
||||
|
@ -1400,7 +1442,7 @@ ${escapedMessage}
|
|||
|
||||
this.MapPlayers.remove(player);
|
||||
});
|
||||
this.MapPlayersByKey = new Map<number, RemotePlayer>();
|
||||
this.MapPlayersByKey.clear();
|
||||
}
|
||||
|
||||
private getExitUrl(layer: ITiledMapLayer): string | undefined {
|
||||
|
@ -1458,13 +1500,6 @@ ${escapedMessage}
|
|||
}
|
||||
}
|
||||
|
||||
//todo: in a dedicated class/function?
|
||||
initCamera() {
|
||||
this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
|
||||
this.cameras.main.startFollow(this.CurrentPlayer, true);
|
||||
biggestAvailableAreaStore.recompute();
|
||||
}
|
||||
|
||||
createCollisionWithPlayer() {
|
||||
//add collision layer
|
||||
for (const phaserLayer of this.gameMap.phaserLayers) {
|
||||
|
@ -1856,23 +1891,6 @@ ${escapedMessage}
|
|||
biggestAvailableAreaStore.recompute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the offset of the character compared to the center of the screen according to the layout manager
|
||||
* (tries to put the character in the center of the remaining space if there is a discussion going on.
|
||||
*/
|
||||
private updateCameraOffset(array: Box): void {
|
||||
const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
|
||||
const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
|
||||
|
||||
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
|
||||
// Let's put this in Game coordinates by applying the zoom level:
|
||||
|
||||
this.cameras.main.setFollowOffset(
|
||||
((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scale.zoom,
|
||||
((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scale.zoom
|
||||
);
|
||||
}
|
||||
|
||||
public startJitsi(roomName: string, jwt?: string): void {
|
||||
const allProps = this.gameMap.getCurrentProperties();
|
||||
const jitsiConfig = this.safeParseJSONstring(
|
||||
|
@ -1941,6 +1959,9 @@ ${escapedMessage}
|
|||
}
|
||||
|
||||
zoomByFactor(zoomFactor: number) {
|
||||
if (this.cameraManager.isCameraLocked()) {
|
||||
return;
|
||||
}
|
||||
waScaleManager.zoomModifier *= zoomFactor;
|
||||
biggestAvailableAreaStore.recompute();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import type { GameMap } from "./GameMap";
|
||||
import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap";
|
||||
import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
|
||||
import { GameMapProperties } from "./GameMapProperties";
|
||||
|
||||
interface Variable {
|
||||
|
|
34
front/src/Phaser/Helpers/TexturesHelper.ts
Normal file
34
front/src/Phaser/Helpers/TexturesHelper.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
export class TexturesHelper {
|
||||
public static async getSnapshot(
|
||||
scene: Phaser.Scene,
|
||||
...sprites: { sprite: Phaser.GameObjects.Sprite; frame?: string | number }[]
|
||||
): Promise<string> {
|
||||
const rt = scene.make.renderTexture({}, false);
|
||||
try {
|
||||
for (const { sprite, frame } of sprites) {
|
||||
if (frame) {
|
||||
sprite.setFrame(frame);
|
||||
}
|
||||
rt.draw(sprite, sprite.displayWidth * 0.5, sprite.displayHeight * 0.5);
|
||||
}
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
rt.snapshot(
|
||||
(url) => {
|
||||
resolve((url as HTMLImageElement).src);
|
||||
rt.destroy();
|
||||
},
|
||||
"image/png",
|
||||
1
|
||||
);
|
||||
} catch (error) {
|
||||
rt.destroy();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
rt.destroy();
|
||||
throw new Error("Could not get the snapshot");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import * as Phaser from "phaser";
|
||||
import { Scene } from "phaser";
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import type { ITiledMapObject } from "../../Map/ITiledMap";
|
||||
import type { ItemFactoryInterface } from "../ItemFactoryInterface";
|
||||
|
|
|
@ -94,7 +94,7 @@ export class HdpiManager {
|
|||
/**
|
||||
* We only accept integer but we make an exception for 1.5
|
||||
*/
|
||||
private getOptimalZoomLevel(realPixelNumber: number): number {
|
||||
public getOptimalZoomLevel(realPixelNumber: number): number {
|
||||
const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber);
|
||||
if (1.5 <= result && result < 2) {
|
||||
return 1.5;
|
||||
|
|
|
@ -5,13 +5,15 @@ import type { Game } from "../Game/Game";
|
|||
import { ResizableScene } from "../Login/ResizableScene";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
|
||||
class WaScaleManager {
|
||||
export class WaScaleManager {
|
||||
private hdpiManager: HdpiManager;
|
||||
private scaleManager!: ScaleManager;
|
||||
private game!: Game;
|
||||
private actualZoom: number = 1;
|
||||
private _saveZoom: number = 1;
|
||||
|
||||
private focusTarget?: { x: number; y: number; width: number; height: number };
|
||||
|
||||
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
|
||||
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
|
||||
}
|
||||
|
@ -23,18 +25,14 @@ class WaScaleManager {
|
|||
|
||||
public applyNewSize() {
|
||||
const { width, height } = coWebsiteManager.getGameSize();
|
||||
|
||||
let devicePixelRatio = 1;
|
||||
if (window.devicePixelRatio) {
|
||||
devicePixelRatio = window.devicePixelRatio;
|
||||
}
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio ?? 1;
|
||||
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({
|
||||
width: width * devicePixelRatio,
|
||||
height: height * devicePixelRatio,
|
||||
});
|
||||
|
||||
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
|
||||
|
||||
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
|
||||
this.scaleManager.resize(gameSize.width, gameSize.height);
|
||||
|
||||
|
@ -59,6 +57,34 @@ class WaScaleManager {
|
|||
this.game.markDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this in case of resizing while focusing on something
|
||||
*/
|
||||
public refreshFocusOnTarget(): void {
|
||||
if (!this.focusTarget) {
|
||||
return;
|
||||
}
|
||||
this.zoomModifier = this.getTargetZoomModifierFor(this.focusTarget.width, this.focusTarget.height);
|
||||
this.game.events.emit("wa-scale-manager:refresh-focus-on-target", this.focusTarget);
|
||||
}
|
||||
|
||||
public setFocusTarget(targetDimensions?: { x: number; y: number; width: number; height: number }): void {
|
||||
this.focusTarget = targetDimensions;
|
||||
}
|
||||
|
||||
public getTargetZoomModifierFor(viewportWidth: number, viewportHeight: number) {
|
||||
const { width: gameWidth, height: gameHeight } = coWebsiteManager.getGameSize();
|
||||
const devicePixelRatio = window.devicePixelRatio ?? 1;
|
||||
|
||||
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({
|
||||
width: gameWidth * devicePixelRatio,
|
||||
height: gameHeight * devicePixelRatio,
|
||||
});
|
||||
const desiredZoom = Math.min(realSize.width / viewportWidth, realSize.height / viewportHeight);
|
||||
const realPixelNumber = gameWidth * devicePixelRatio * gameHeight * devicePixelRatio;
|
||||
return desiredZoom / (this.hdpiManager.getOptimalZoomLevel(realPixelNumber) || 1);
|
||||
}
|
||||
|
||||
public get zoomModifier(): number {
|
||||
return this.hdpiManager.zoomModifier;
|
||||
}
|
||||
|
@ -72,6 +98,10 @@ class WaScaleManager {
|
|||
this._saveZoom = this.hdpiManager.zoomModifier;
|
||||
}
|
||||
|
||||
public getSaveZoom(): number {
|
||||
return this._saveZoom;
|
||||
}
|
||||
|
||||
public restoreZoom(): void {
|
||||
this.hdpiManager.zoomModifier = this._saveZoom;
|
||||
this.applyNewSize();
|
||||
|
|
6
front/src/Stores/PictureStore.ts
Normal file
6
front/src/Stores/PictureStore.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import type { Readable } from "svelte/store";
|
||||
|
||||
/**
|
||||
* A store that contains the player/companion avatar picture
|
||||
*/
|
||||
export type PictureStore = Readable<string | undefined>;
|
|
@ -12,7 +12,7 @@ let idCount = 0;
|
|||
function createPlayersStore() {
|
||||
let players = new Map<number, PlayerInterface>();
|
||||
|
||||
const { subscribe, set, update } = writable(players);
|
||||
const { subscribe, set, update } = writable<Map<number, PlayerInterface>>(players);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
|
122
front/src/Stores/Utils/MapStore.ts
Normal file
122
front/src/Stores/Utils/MapStore.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import type { Readable, Subscriber, Unsubscriber, Writable } from "svelte/store";
|
||||
import { get, readable, writable } from "svelte/store";
|
||||
|
||||
/**
|
||||
* Is it a Map? Is it a Store? No! It's a MapStore!
|
||||
*
|
||||
* The MapStore behaves just like a regular JS Map, but... it is also a regular Svelte store.
|
||||
*
|
||||
* As a bonus, you can also get a store on any given key of the map.
|
||||
*
|
||||
* For instance:
|
||||
*
|
||||
* const mapStore = new MapStore<string, string>();
|
||||
* mapStore.getStore('foo').subscribe((value) => {
|
||||
* console.log('Foo key has been written to the store. New value: ', value);
|
||||
* });
|
||||
* mapStore.set('foo', 'bar');
|
||||
*
|
||||
*
|
||||
* Even better, if the items stored in map contain stores, you can directly get the store to those values:
|
||||
*
|
||||
* const mapStore = new MapStore<string, {
|
||||
* nestedStore: Readable<string>
|
||||
* }>();
|
||||
*
|
||||
* mapStore.getNestedStore('foo', item => item.nestedStore).subscribe((value) => {
|
||||
* console.log('Foo key has been written to the store or the nested store has been updated. New value: ', value);
|
||||
* });
|
||||
* mapStore.set('foo', {
|
||||
* nestedStore: writable('bar')
|
||||
* });
|
||||
* // Whenever the nested store is updated OR the 'foo' key is overwritten, the store returned by mapStore.getNestedStore
|
||||
* // will be triggered.
|
||||
*/
|
||||
export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
|
||||
private readonly store = writable(this);
|
||||
private readonly storesByKey = new Map<K, Writable<V | undefined>>();
|
||||
|
||||
subscribe(run: Subscriber<Map<K, V>>, invalidate?: (value?: Map<K, V>) => void): Unsubscriber {
|
||||
return this.store.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
clear() {
|
||||
super.clear();
|
||||
this.store.set(this);
|
||||
this.storesByKey.forEach((store) => {
|
||||
store.set(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const result = super.delete(key);
|
||||
if (result) {
|
||||
this.store.set(this);
|
||||
this.storesByKey.get(key)?.set(undefined);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
set(key: K, value: V): this {
|
||||
super.set(key, value);
|
||||
this.store.set(this);
|
||||
this.storesByKey.get(key)?.set(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
getStore(key: K): Readable<V | undefined> {
|
||||
const store = writable(this.get(key), () => {
|
||||
return () => {
|
||||
// No more subscribers!
|
||||
this.storesByKey.delete(key);
|
||||
};
|
||||
});
|
||||
this.storesByKey.set(key, store);
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an "inner" store inside a value stored in the map.
|
||||
*/
|
||||
getNestedStore<T>(key: K, accessor: (value: V) => Readable<T> | undefined): Readable<T | undefined> {
|
||||
const initVal = this.get(key);
|
||||
let initStore: Readable<T> | undefined;
|
||||
let initStoreValue: T | undefined;
|
||||
if (initVal) {
|
||||
initStore = accessor(initVal);
|
||||
if (initStore !== undefined) {
|
||||
initStoreValue = get(initStore);
|
||||
}
|
||||
}
|
||||
|
||||
return readable(initStoreValue, (set) => {
|
||||
const storeByKey = this.getStore(key);
|
||||
|
||||
let unsubscribeDeepStore: Unsubscriber | undefined;
|
||||
const unsubscribe = storeByKey.subscribe((newMapValue) => {
|
||||
if (unsubscribeDeepStore) {
|
||||
unsubscribeDeepStore();
|
||||
}
|
||||
if (newMapValue === undefined) {
|
||||
set(undefined);
|
||||
} else {
|
||||
const deepValueStore = accessor(newMapValue);
|
||||
if (deepValueStore !== undefined) {
|
||||
set(get(deepValueStore));
|
||||
|
||||
unsubscribeDeepStore = deepValueStore.subscribe((value) => {
|
||||
set(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (unsubscribeDeepStore) {
|
||||
unsubscribeDeepStore();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
25
front/src/Utils/MathUtils.ts
Normal file
25
front/src/Utils/MathUtils.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
export class MathUtils {
|
||||
/**
|
||||
*
|
||||
* @param p Position to check.
|
||||
* @param r Rectangle to check the overlap against.
|
||||
* @returns true is overlapping
|
||||
*/
|
||||
public static isOverlappingWithRectangle(
|
||||
p: { x: number; y: number },
|
||||
r: { x: number; y: number; width: number; height: number }
|
||||
): boolean {
|
||||
return this.isBetween(p.x, r.x, r.x + r.width) && this.isBetween(p.y, r.y, r.y + r.height);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value Value to check
|
||||
* @param min inclusive min value
|
||||
* @param max inclusive max value
|
||||
* @returns true if value is in <min, max>
|
||||
*/
|
||||
public static isBetween(value: number, min: number, max: number): boolean {
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
}
|
|
@ -642,6 +642,7 @@ class CoWebsiteManager {
|
|||
private fire(): void {
|
||||
this._onResize.next();
|
||||
waScaleManager.applyNewSize();
|
||||
waScaleManager.refreshFocusOnTarget();
|
||||
}
|
||||
|
||||
private fullscreen(): void {
|
||||
|
|
|
@ -15,11 +15,11 @@ import ui from "./Api/iframe/ui";
|
|||
import sound from "./Api/iframe/sound";
|
||||
import room, { setMapURL, setRoomId } from "./Api/iframe/room";
|
||||
import state, { initVariables } from "./Api/iframe/state";
|
||||
import player, { setPlayerName, setTags, setUuid } from "./Api/iframe/player";
|
||||
import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
|
||||
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
|
||||
import type { Popup } from "./Api/iframe/Ui/Popup";
|
||||
import type { Sound } from "./Api/iframe/Sound/Sound";
|
||||
import { answerPromises, queryWorkadventure, sendToWorkadventure } from "./Api/iframe/IframeApiContribution";
|
||||
import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution";
|
||||
|
||||
// Notify WorkAdventure that we are ready to receive data
|
||||
const initPromise = queryWorkadventure({
|
||||
|
@ -32,6 +32,7 @@ const initPromise = queryWorkadventure({
|
|||
setTags(state.tags);
|
||||
setUuid(state.uuid);
|
||||
initVariables(state.variables as Map<string, unknown>);
|
||||
setUserRoomToken(state.userRoomToken);
|
||||
});
|
||||
|
||||
const wa = {
|
||||
|
|
|
@ -144,10 +144,12 @@ window.addEventListener("resize", function (event) {
|
|||
coWebsiteManager.resetStyleMain();
|
||||
|
||||
waScaleManager.applyNewSize();
|
||||
waScaleManager.refreshFocusOnTarget();
|
||||
});
|
||||
|
||||
coWebsiteManager.onResize.subscribe(() => {
|
||||
waScaleManager.applyNewSize();
|
||||
waScaleManager.refreshFocusOnTarget();
|
||||
});
|
||||
|
||||
iframeListener.init();
|
||||
|
|
30
front/src/types.ts
Normal file
30
front/src/types.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export enum Easing {
|
||||
Linear = "Linear",
|
||||
QuadEaseIn = "Quad.easeIn",
|
||||
CubicEaseIn = "Cubic.easeIn",
|
||||
QuartEaseIn = "Quart.easeIn",
|
||||
QuintEaseIn = "Quint.easeIn",
|
||||
SineEaseIn = "Sine.easeIn",
|
||||
ExpoEaseIn = "Expo.easeIn",
|
||||
CircEaseIn = "Circ.easeIn",
|
||||
BackEaseIn = "Back.easeIn",
|
||||
BounceEaseIn = "Bounce.easeIn",
|
||||
QuadEaseOut = "Quad.easeOut",
|
||||
CubicEaseOut = "Cubic.easeOut",
|
||||
QuartEaseOut = "Quart.easeOut",
|
||||
QuintEaseOut = "Quint.easeOut",
|
||||
SineEaseOut = "Sine.easeOut",
|
||||
ExpoEaseOut = "Expo.easeOut",
|
||||
CircEaseOut = "Circ.easeOut",
|
||||
BackEaseOut = "Back.easeOut",
|
||||
BounceEaseOut = "Bounce.easeOut",
|
||||
QuadEaseInOut = "Quad.easeInOut",
|
||||
CubicEaseInOut = "Cubic.easeInOut",
|
||||
QuartEaseInOut = "Quart.easeInOut",
|
||||
QuintEaseInOut = "Quint.easeInOut",
|
||||
SineEaseInOut = "Sine.easeInOut",
|
||||
ExpoEaseInOut = "Expo.easeInOut",
|
||||
CircEaseInOut = "Circ.easeInOut",
|
||||
BackEaseInOut = "Back.easeInOut",
|
||||
BounceEaseInOut = "Bounce.easeInOut",
|
||||
}
|
|
@ -62,8 +62,7 @@ body .message-info.warning{
|
|||
background-color: black;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
padding-top: 32px;
|
||||
font-size: 28px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
98
front/tests/Stores/Utils/MapStoreTest.ts
Normal file
98
front/tests/Stores/Utils/MapStoreTest.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import "jasmine";
|
||||
import { MapStore } from "../../../src/Stores/Utils/MapStore";
|
||||
import type { Readable, Writable } from "svelte/store";
|
||||
import { get, writable } from "svelte/store";
|
||||
|
||||
describe("Main store", () => {
|
||||
it("Set / delete / clear triggers main store updates", () => {
|
||||
const mapStore = new MapStore<string, string>();
|
||||
|
||||
let triggered = false;
|
||||
|
||||
mapStore.subscribe((map) => {
|
||||
triggered = true;
|
||||
expect(map).toBe(mapStore);
|
||||
});
|
||||
|
||||
expect(triggered).toBeTrue();
|
||||
triggered = false;
|
||||
mapStore.set("foo", "bar");
|
||||
expect(triggered).toBeTrue();
|
||||
|
||||
triggered = false;
|
||||
mapStore.delete("baz");
|
||||
expect(triggered).toBe(false);
|
||||
mapStore.delete("foo");
|
||||
expect(triggered).toBe(true);
|
||||
|
||||
triggered = false;
|
||||
mapStore.clear();
|
||||
expect(triggered).toBe(true);
|
||||
});
|
||||
|
||||
it("generates stores for keys with getStore", () => {
|
||||
const mapStore = new MapStore<string, string>();
|
||||
|
||||
let valueReceivedInStoreForFoo: string | undefined;
|
||||
let valueReceivedInStoreForBar: string | undefined;
|
||||
|
||||
mapStore.set("foo", "someValue");
|
||||
|
||||
mapStore.getStore("foo").subscribe((value) => {
|
||||
valueReceivedInStoreForFoo = value;
|
||||
});
|
||||
const unsubscribeBar = mapStore.getStore("bar").subscribe((value) => {
|
||||
valueReceivedInStoreForBar = value;
|
||||
});
|
||||
|
||||
expect(valueReceivedInStoreForFoo).toBe("someValue");
|
||||
expect(valueReceivedInStoreForBar).toBe(undefined);
|
||||
mapStore.set("foo", "someOtherValue");
|
||||
expect(valueReceivedInStoreForFoo).toBe("someOtherValue");
|
||||
mapStore.delete("foo");
|
||||
expect(valueReceivedInStoreForFoo).toBe(undefined);
|
||||
mapStore.set("bar", "baz");
|
||||
expect(valueReceivedInStoreForBar).toBe("baz");
|
||||
mapStore.clear();
|
||||
expect(valueReceivedInStoreForBar).toBe(undefined);
|
||||
unsubscribeBar();
|
||||
mapStore.set("bar", "fiz");
|
||||
expect(valueReceivedInStoreForBar).toBe(undefined);
|
||||
});
|
||||
|
||||
it("generates stores with getStoreByAccessor", () => {
|
||||
const mapStore = new MapStore<
|
||||
string,
|
||||
{
|
||||
foo: string;
|
||||
store: Writable<string>;
|
||||
}
|
||||
>();
|
||||
|
||||
const fooStore = mapStore.getNestedStore("foo", (value) => {
|
||||
return value.store;
|
||||
});
|
||||
|
||||
mapStore.set("foo", {
|
||||
foo: "bar",
|
||||
store: writable("init"),
|
||||
});
|
||||
|
||||
expect(get(fooStore)).toBe("init");
|
||||
|
||||
mapStore.get("foo")?.store.set("newVal");
|
||||
|
||||
expect(get(fooStore)).toBe("newVal");
|
||||
|
||||
mapStore.set("foo", {
|
||||
foo: "bar",
|
||||
store: writable("anotherVal"),
|
||||
});
|
||||
|
||||
expect(get(fooStore)).toBe("anotherVal");
|
||||
|
||||
mapStore.delete("foo");
|
||||
|
||||
expect(get(fooStore)).toBeUndefined();
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue