Merge branch 'develop' into avoid-exits-on-path

This commit is contained in:
Hanusiak Piotr
2022-02-16 14:03:48 +01:00
41 changed files with 707 additions and 382 deletions
-1
View File
@@ -29,7 +29,6 @@
<script src="/env-config.js"></script>
<base href="/">
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
<title>WorkAdventure</title>
</head>
+4 -2
View File
@@ -42,6 +42,7 @@
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
"@16bits/nes.css": "^2.3.2",
"@fontsource/press-start-2p": "^4.3.0",
"@joeattardi/emoji-button": "^4.6.2",
"@types/simple-peer": "^9.11.1",
@@ -71,7 +72,7 @@
"zod": "^3.11.6"
},
"scripts": {
"start": "run-p templater serve svelte-check-watch typesafe-i18n",
"start": "run-p templater serve svelte-check-watch typesafe-i18n-watch",
"templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
@@ -84,7 +85,8 @@
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'",
"typesafe-i18n": "typesafe-i18n --no-watch"
"typesafe-i18n": "typesafe-i18n --no-watch",
"typesafe-i18n-watch": "typesafe-i18n"
},
"lint-staged": {
"*.svelte": [
@@ -2,9 +2,10 @@
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { coWebsitesNotAsleep, mainCoWebsite, jitsiCoWebsite } from "../../Stores/CoWebsiteStore";
import { mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { iframeStates } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
@@ -15,10 +16,10 @@
let icon: HTMLImageElement;
let iconLoaded = false;
let state = coWebsite.getStateSubscriber();
let isJitsi: boolean = false;
let isJitsi: boolean = coWebsite instanceof JitsiCoWebsite;
const mainState = coWebsiteManager.getMainStateSubscriber();
onMount(() => {
isJitsi = Boolean($jitsiCoWebsite && $jitsiCoWebsite.getId() === coWebsite.getId());
icon.src = isJitsi
? "/resources/logos/meet.svg"
: `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`;
@@ -33,20 +34,23 @@
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
if ($mainCoWebsite.getId() === coWebsite.getId()) {
const coWebsites = $coWebsitesNotAsleep;
const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined;
if (newMain && newMain.getId() !== $mainCoWebsite.getId()) {
coWebsiteManager.goToMain(newMain);
} else if (coWebsiteManager.getMainState() === iframeStates.closed) {
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.displayMain();
} else if ($highlightedEmbedScreen?.type === "cowebsite") {
coWebsiteManager.goToMain($highlightedEmbedScreen.embed);
} else {
coWebsiteManager.hideMain();
}
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.goToMain(coWebsite);
coWebsiteManager.displayMain();
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
}
}
}
@@ -64,7 +68,10 @@
let isHighlight: boolean = false;
let isMain: boolean = false;
$: {
isMain = $mainCoWebsite !== undefined && $mainCoWebsite.getId() === coWebsite.getId();
isMain =
$mainState === iframeStates.opened &&
$mainCoWebsite !== undefined &&
$mainCoWebsite.getId() === coWebsite.getId();
isHighlight =
$highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" &&
@@ -212,7 +219,8 @@
}
&:not(.vertical) {
animation: bounce 0.35s ease 6 alternate;
transition: all 300ms;
transform: translateY(0px);
}
&.vertical {
@@ -233,7 +241,7 @@
&.displayed {
&:not(.vertical) {
animation: activeThumbnail 300ms ease-in 0s forwards;
transform: translateY(-15px);
}
}
@@ -262,16 +270,6 @@
}
}
@keyframes activeThumbnail {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-15px);
}
}
@keyframes bounce {
from {
transform: translateY(0);
+82 -22
View File
@@ -1,5 +1,12 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
let entryPoint: string = $startLayerNamesStore[0];
let walkAutomatically: boolean = false;
const currentPlayer = gameManager.getCurrentGameScene().CurrentPlayer;
const playerPos = { x: Math.floor(currentPlayer.x), y: Math.floor(currentPlayer.y) };
function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@@ -8,8 +15,23 @@
document.execCommand("copy");
}
function getLink() {
return `${location.origin}${location.pathname}#${entryPoint}${
walkAutomatically ? `&moveTo=${playerPos.x},${playerPos.y}` : ""
}`;
}
function updateInputFieldValue() {
const input = document.getElementById("input-share-link");
if (input) {
(input as HTMLInputElement).value = getLink();
}
}
let canShare = navigator.share !== undefined;
async function shareLink() {
const shareData = { url: location.toString() };
const shareData = { url: getLink() };
try {
await navigator.share(shareData);
@@ -22,16 +44,43 @@
<div class="guest-main">
<section class="container-overflow">
<section class="share-url not-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section>
<section class="is-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
{#if !canShare}
<section class="share-url not-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" class="link-url" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section>
{:else}
<section class="is-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section>
{/if}
<h3>Select an entry point</h3>
<section class="nes-select is-dark starting-points">
<select
bind:value={entryPoint}
on:blur={() => {
updateInputFieldValue();
}}
>
{#each $startLayerNamesStore as entryPointName}
<option value={entryPointName}>{entryPointName}</option>
{/each}
</select>
</section>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={walkAutomatically}
on:change={() => {
updateInputFieldValue();
}}
/>
<span>{$LL.menu.invite.walk_automatically_to_position()}</span>
</label>
</section>
</div>
@@ -39,14 +88,27 @@
@import "../../../style/breakpoints.scss";
div.guest-main {
width: 50%;
margin-left: auto;
margin-right: auto;
height: calc(100% - 56px);
text-align: center;
input.link-url {
width: calc(100% - 200px);
}
.starting-points {
width: 80%;
}
section {
margin-bottom: 50px;
}
section.nes-select select:focus {
outline: none;
}
section.container-overflow {
height: 100%;
margin: 0;
@@ -55,25 +117,23 @@
}
section.is-mobile {
display: none;
display: block;
text-align: center;
margin-bottom: 20px;
}
}
@include media-breakpoint-up(md) {
div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow {
height: calc(100% - 120px);
}
}
}
@include media-breakpoint-up(lg) {
div.guest-main {
width: 100%;
}
}
</style>
@@ -76,7 +76,3 @@
<section class="section-input-send-text">
<div class="input-send-text" bind:this={QUILL_EDITOR} />
</section>
<style lang="scss">
@import "https://cdn.quilljs.com/1.3.7/quill.snow.css";
</style>
+2 -1
View File
@@ -7,6 +7,7 @@ import type { PlayerAnimationDirections } from "../Player/Animation";
import type { Unsubscriber } from "svelte/store";
import type { ActivatableInterface } from "../Game/ActivatableInterface";
import type CancelablePromise from "cancelable-promise";
import LL from "../../i18n/i18n-svelte";
/**
* Class representing the sprite of a remote player (a player that plays on another computer)
@@ -107,7 +108,7 @@ export class RemotePlayer extends Character implements ActivatableInterface {
private registerDefaultActionsMenuActions(): void {
if (this.visitCardUrl) {
this.registeredActions.push({
actionName: "Visiting Card",
actionName: LL.woka.menu.businessCard(),
callback: () => {
requestVisitCardsStore.set(this.visitCardUrl);
actionsMenuStore.clear();
+4
View File
@@ -335,6 +335,10 @@ export class GameMap {
throw new Error("No possible position found");
}
public getObjectWithName(name: string): ITiledMapObject | undefined {
return this.tiledObjects.find((object) => object.name === name);
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
}
@@ -10,6 +10,13 @@ import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { JITSI_PRIVATE_MODE, JITSI_URL } from "../../Enum/EnvironmentVariable";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import { iframeListener } from "../../Api/IframeListener";
import { Room } from "../../Connexion/Room";
import LL from "../../i18n/i18n-svelte";
interface OpenCoWebsite {
actionId: string;
@@ -23,6 +30,7 @@ export class GameMapPropertiesListener {
constructor(private scene: GameScene, private gameMap: GameMap) {}
register() {
// Website on new tab
this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("openTab");
@@ -33,7 +41,7 @@ export class GameMapPropertiesListener {
if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to open web site in new tab";
message = get(LL).trigger.newTab();
}
layoutManagerActionStore.addAction({
uuid: "openTab",
@@ -48,6 +56,129 @@ export class GameMapPropertiesListener {
}
});
// Jitsi room
this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
coWebsiteManager.getCoWebsites().forEach((coWebsite) => {
if (coWebsite instanceof JitsiCoWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
});
} else {
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.scene.instance);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.scene.connection?.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
let domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") {
domain = `${location.protocol}//${domain}`;
}
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.scene.initialiseJitsi(coWebsite, roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = get(LL).trigger.jitsiRoom();
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openJitsiRoomFunction();
}
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => {
if (newValue) {
this.scene
.onMapExit(
Room.getRoomPathFromExitSceneUrl(
newValue as string,
window.location.toString(),
this.scene.MapUrlFile
)
)
.catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) {
this.scene
.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()))
.catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.scene.connection?.setSilent(false);
this.scene.CurrentPlayer.noSilent();
} else {
this.scene.connection?.setSilent(true);
this.scene.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => {
const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined;
const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined;
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => {
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: Legacy functionnality replace by layer change
this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
// Open a new co-website by the property.
this.gameMap.onEnterLayer((newLayers) => {
const handler = () => {
@@ -135,7 +266,7 @@ export class GameMapPropertiesListener {
websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON
) {
if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site";
websiteTriggerMessageProperty = get(LL).trigger.cowebsite();
}
this.coWebsitesActionTriggerByLayer.set(layer, actionId);
+48 -154
View File
@@ -20,15 +20,8 @@ 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,
JITSI_URL,
MAX_PER_GROUP,
POSITION_DELAY,
} from "../../Enum/EnvironmentVariable";
import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
@@ -82,7 +75,7 @@ import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import { audioManagerFileStore } from "../../Stores/AudioManagerStore";
import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture;
@@ -98,6 +91,8 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore";
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte";
import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
@@ -550,6 +545,8 @@ export class GameScene extends DirtyScene {
urlManager.getStartLayerNameFromUrl()
);
startLayerNamesStore.set(this.startPositionCalculator.getStartPositionNames());
//add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@@ -578,6 +575,8 @@ export class GameScene extends DirtyScene {
this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
this.tryMovePlayerWithMoveToParameter();
this.cameraManager = new CameraManager(
this,
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels },
@@ -631,7 +630,6 @@ export class GameScene extends DirtyScene {
);
new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) {
this.scene.sleep();
@@ -810,7 +808,7 @@ export class GameScene extends DirtyScene {
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.startJitsi(coWebsite, message.jitsiRoom, message.jwt);
this.initialiseJitsi(coWebsite, message.jitsiRoom, message.jwt);
});
this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => {
@@ -947,116 +945,6 @@ export class GameScene extends DirtyScene {
}
}
private triggerOnMapLayerPropertyChange() {
this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => {
if (newValue) {
this.onMapExit(
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
).catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) {
this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) =>
console.error(e)
);
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
this.stopJitsi();
} else {
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
let domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") {
domain = `${location.protocol}//${domain}`;
}
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.startJitsi(coWebsite, roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to enter Jitsi Meet room";
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.userInputManager,
});
} else {
openJitsiRoomFunction();
}
}
});
this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.connection?.setSilent(false);
this.CurrentPlayer.noSilent();
} else {
this.connection?.setSilent(true);
this.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => {
const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined;
const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined;
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => {
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: Legacy functionnality replace by layer change
this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
}
private listenToIframeEvents(): void {
this.iframeSubscriptionList = [];
this.iframeSubscriptionList.push(
@@ -1412,7 +1300,7 @@ ${escapedMessage}
//Create new colliders with the new GameMap
this.createCollisionWithPlayer();
//Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange();
new GameMapPropertiesListener(this, this.gameMap).register();
resolve(newFirstgid);
});
});
@@ -1483,9 +1371,9 @@ ${escapedMessage}
});
iframeListener.registerAnswerer("movePlayerTo", async (message) => {
const index = this.getGameMap().getTileIndexAt(message.x, message.y);
const startTile = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const path = await this.getPathfindingManager().findPath(startTile, index, true, true);
const startTileIndex = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const destinationTileIndex = this.getGameMap().getTileIndexAt(message.x, message.y);
const path = await this.getPathfindingManager().findPath(startTileIndex, destinationTileIndex, true, true);
path.shift();
if (path.length === 0) {
throw new Error("no path available");
@@ -1512,7 +1400,7 @@ ${escapedMessage}
phaserLayer.setCollisionByProperty({ collides: true }, visible);
} else {
const phaserLayers = this.gameMap.findPhaserLayers(layerName + "/");
if (phaserLayers === []) {
if (phaserLayers.length === 0) {
console.warn(
'Could not find layer with name that contains "' +
layerName +
@@ -1529,11 +1417,11 @@ ${escapedMessage}
this.markDirty();
}
private getMapDirUrl(): string {
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
public getMapDirUrl(): string {
return this.MapUrlFile.substring(0, this.MapUrlFile.lastIndexOf("/"));
}
private async onMapExit(roomUrl: URL) {
public async onMapExit(roomUrl: URL) {
if (this.mapTransitioning) return;
this.mapTransitioning = true;
@@ -1599,7 +1487,6 @@ ${escapedMessage}
iframeListener.unregisterScript(script);
}
this.stopJitsi();
audioManagerFileStore.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection?.closeConnection();
@@ -1650,6 +1537,36 @@ ${escapedMessage}
this.MapPlayersByKey.clear();
}
private tryMovePlayerWithMoveToParameter(): void {
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
let endPos;
const posFromParam = StringUtils.parsePointFromParam(moveToParam);
if (posFromParam) {
endPos = this.gameMap.getTileIndexAt(posFromParam.x, posFromParam.y);
} else {
const destinationObject = this.gameMap.getObjectWithName(moveToParam);
if (destinationObject) {
endPos = this.gameMap.getTileIndexAt(destinationObject.x, destinationObject.y);
} else {
endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
}
}
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
}
private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined;
}
@@ -1772,22 +1689,6 @@ ${escapedMessage}
this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey);
});
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) {
if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
@@ -2140,7 +2041,7 @@ ${escapedMessage}
mediaManager.hideMyCamera();
}
public startJitsi(coWebsite: JitsiCoWebsite, roomName: string, jwt?: string): void {
public initialiseJitsi(coWebsite: JitsiCoWebsite, roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
@@ -2163,13 +2064,6 @@ ${escapedMessage}
analyticsClient.enteredJitsi(roomName, this.room.id);
}
public stopJitsi(): void {
const coWebsite = coWebsiteManager.searchJitsi();
if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
}
//todo: put this into an 'orchestrator' scene (EntryScene?)
private bannedUser() {
this.cleanupClosingScene();
@@ -16,32 +16,6 @@ export class StartPositionCalculator {
) {
this.initStartXAndStartY();
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
/**
*
@@ -76,6 +50,47 @@ export class StartPositionCalculator {
}
}
public getStartPositionNames(): string[] {
const names: string[] = [];
for (const layer of this.gameMap.flatLayers) {
if (layer.name === "start") {
names.push(layer.name);
continue;
}
if (this.isStartLayer(layer)) {
names.push(layer.name);
}
}
return names;
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, GameMapProperties.START_LAYER) == true;
}
+1 -6
View File
@@ -1,6 +1,5 @@
import { derived, get, writable } from "svelte/store";
import { derived, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../WebRtc/CoWebsite/JitsiCoWebsite";
function createCoWebsiteStore() {
const { subscribe, set, update } = writable(Array<CoWebsite>());
@@ -50,7 +49,3 @@ export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) =>
export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => coWebsite.getState() !== "asleep")
);
export const jitsiCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => coWebsite instanceof JitsiCoWebsite)
);
+6
View File
@@ -0,0 +1,6 @@
import { Readable, writable } from "svelte/store";
/**
* A store that contains the map starting layers names
*/
export const startLayerNamesStore = writable<string[]>([]);
+12
View File
@@ -0,0 +1,12 @@
export class StringUtils {
public static parsePointFromParam(param: string, separator: string = ","): { x: number; y: number } | undefined {
const values = param.split(separator).map((val) => parseInt(val));
if (values.length !== 2) {
return;
}
if (isNaN(values[0]) || isNaN(values[1])) {
return;
}
return { x: values[0], y: values[1] };
}
}
@@ -1,22 +1,11 @@
import CancelablePromise from "cancelable-promise";
import { gameManager } from "../../Phaser/Game/GameManager";
import { coWebsiteManager } from "../CoWebsiteManager";
import { jitsiFactory } from "../JitsiFactory";
import { SimpleCoWebsite } from "./SimpleCoWebsite";
export class JitsiCoWebsite extends SimpleCoWebsite {
private jitsiLoadPromise?: () => CancelablePromise<HTMLIFrameElement>;
constructor(url: URL, allowApi?: boolean, allowPolicy?: string, widthPercent?: number, closable?: boolean) {
const coWebsite = coWebsiteManager.searchJitsi();
if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
super(url, allowApi, allowPolicy, widthPercent, closable);
}
setJitsiLoadPromise(promise: () => CancelablePromise<HTMLIFrameElement>): void {
this.jitsiLoadPromise = promise;
}
@@ -26,7 +15,6 @@ export class JitsiCoWebsite extends SimpleCoWebsite {
this.state.set("loading");
gameManager.getCurrentGameScene().disableMediaBehaviors();
jitsiFactory.restart();
if (!this.jitsiLoadPromise) {
return reject("Undefined Jitsi start callback");
+14 -17
View File
@@ -1,8 +1,8 @@
import { HtmlUtils } from "./HtmlUtils";
import { Subject } from "rxjs";
import { waScaleManager } from "../Phaser/Services/WaScaleManager";
import { coWebsites, coWebsitesNotAsleep, jitsiCoWebsite, mainCoWebsite } from "../Stores/CoWebsiteStore";
import { get } from "svelte/store";
import { coWebsites, coWebsitesNotAsleep, mainCoWebsite } from "../Stores/CoWebsiteStore";
import { get, Readable, Writable, writable } from "svelte/store";
import { embedScreenLayout, highlightedEmbedScreen } from "../Stores/EmbedScreensStore";
import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils";
import { LayoutMode } from "./LayoutManager";
@@ -34,7 +34,7 @@ interface TouchMoveCoordinates {
}
class CoWebsiteManager {
private openedMain: iframeStates = iframeStates.closed;
private openedMain: Writable<iframeStates> = writable(iframeStates.closed);
private _onResize: Subject<void> = new Subject();
public onResize = this._onResize.asObservable();
@@ -57,6 +57,10 @@ class CoWebsiteManager {
});
public getMainState() {
return get(this.openedMain);
}
public getMainStateSubscriber(): Readable<iframeStates> {
return this.openedMain;
}
@@ -324,7 +328,7 @@ class CoWebsiteManager {
}
this.cowebsiteDom.classList.add("closing");
this.cowebsiteDom.classList.remove("opened");
this.openedMain = iframeStates.closed;
this.openedMain.set(iframeStates.closed);
this.fire();
}
@@ -332,7 +336,7 @@ class CoWebsiteManager {
this.toggleFullScreenIcon(true);
this.cowebsiteDom.classList.add("closing");
this.cowebsiteDom.classList.remove("opened");
this.openedMain = iframeStates.closed;
this.openedMain.set(iframeStates.closed);
this.resetStyleMain();
this.fire();
}
@@ -386,14 +390,14 @@ class CoWebsiteManager {
}
this.cowebsiteDom.classList.add("opened");
this.openedMain = iframeStates.loading;
this.openedMain.set(iframeStates.loading);
}
private openMain(): void {
this.cowebsiteDom.addEventListener("transitionend", () => {
this.resizeAllIframes();
});
this.openedMain = iframeStates.opened;
this.openedMain.set(iframeStates.opened);
}
public resetStyleMain() {
@@ -556,19 +560,12 @@ class CoWebsiteManager {
mainCoWebsite.getId() !== coWebsite.getId() &&
mainCoWebsite.getState() !== "asleep"
) {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: mainCoWebsite,
});
highlightedEmbedScreen.removeHighlight();
}
this.resizeAllIframes();
}
public searchJitsi(): CoWebsite | undefined {
return get(jitsiCoWebsite);
}
public addCoWebsiteToStore(coWebsite: CoWebsite, position: number | undefined) {
const coWebsitePosition = position === undefined ? get(coWebsites).length : position;
coWebsites.add(coWebsite, coWebsitePosition);
@@ -591,7 +588,7 @@ class CoWebsiteManager {
}
// Check if the main is hide
if (this.getMainCoWebsite() && this.openedMain === iframeStates.closed) {
if (this.getMainCoWebsite() && this.getMainState() === iframeStates.closed) {
this.displayMain();
}
@@ -665,7 +662,7 @@ class CoWebsiteManager {
}
public getGameSize(): { width: number; height: number } {
if (this.openedMain === iframeStates.closed) {
if (this.getMainState() === iframeStates.closed) {
return {
width: window.innerWidth,
height: window.innerHeight,
+6 -29
View File
@@ -2,7 +2,6 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { coWebsiteManager } from "./CoWebsiteManager";
import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import { get } from "svelte/store";
import type { CoWebsite } from "./CoWebsite/CoWesbite";
import CancelablePromise from "cancelable-promise";
interface jitsiConfigInterface {
@@ -180,13 +179,14 @@ class JitsiFactory {
const iframe = coWebsiteManager
.getCoWebsiteBuffer()
.querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe && this.jitsiApi) {
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload();
this.closeOrUnload(iframe);
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload();
this.closeOrUnload(iframe);
});
return resolve(iframe);
@@ -209,8 +209,9 @@ class JitsiFactory {
});
}
private closeOrUnload = function () {
const coWebsite = coWebsiteManager.searchJitsi();
private closeOrUnload = function (iframe: HTMLIFrameElement) {
const coWebsite = coWebsiteManager.getCoWebsites().find((coWebsite) => coWebsite.getIframe() === iframe);
if (!coWebsite) {
return;
}
@@ -224,30 +225,6 @@ class JitsiFactory {
}
};
public restart() {
if (!this.jitsiApi) {
return;
}
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
const coWebsite = coWebsiteManager.searchJitsi();
if (!coWebsite) {
this.destroy();
return;
}
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload();
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload();
});
}
public stop() {
if (!this.jitsiApi) {
return;
+3 -1
View File
@@ -12,9 +12,10 @@ import menu from "./menu";
import report from "./report";
import warning from "./warning";
import woka from "./woka";
import trigger from "./trigger";
const de_DE: Translation = {
...en_US,
...(en_US as Translation),
language: "Deutsch",
country: "Deutschland",
audio,
@@ -29,6 +30,7 @@ const de_DE: Translation = {
report,
warning,
emoji,
trigger,
};
export default de_DE;
+1
View File
@@ -70,6 +70,7 @@ const menu: NonNullable<Translation["menu"]> = {
description: "Link zu diesem Raum teilen!",
copy: "Kopieren",
share: "Teilen",
walk_automatically_to_position: "Automatisch zu meiner Position gehen",
},
globalMessage: {
text: "Text",
+9
View File
@@ -0,0 +1,9 @@
import type { Translation } from "../i18n-types";
const trigger: NonNullable<Translation["trigger"]> = {
cowebsite: "Drücke LEERTASTE oder tippe hier um die Webseite zu öffnen",
newTab: "Drücke LEERTASTE oder tippe hier um die Webseite in einem neuen Tab zu öffnen",
jitsiRoom: "Drücke LEERTASTE oder tippe hier um dem Jitsi Meet Raum beizutreten",
};
export default trigger;
+3
View File
@@ -15,6 +15,9 @@ const woka: NonNullable<Translation["woka"]> = {
continue: "Auswählen",
customize: "Bearbeite dein WOKA",
},
menu: {
businessCard: "Visitenkarte",
},
};
export default woka;
+2
View File
@@ -11,6 +11,7 @@ import menu from "./menu";
import report from "./report";
import warning from "./warning";
import emoji from "./emoji";
import trigger from "./trigger";
const en_US: BaseTranslation = {
language: "English",
@@ -27,6 +28,7 @@ const en_US: BaseTranslation = {
report,
warning,
emoji,
trigger,
};
export default en_US;
+1
View File
@@ -70,6 +70,7 @@ const menu: BaseTranslation = {
description: "Share the link of the room!",
copy: "Copy",
share: "Share",
walk_automatically_to_position: "Walk automatically to my position",
},
globalMessage: {
text: "Text",
+9
View File
@@ -0,0 +1,9 @@
import type { BaseTranslation } from "../i18n-types";
const trigger: BaseTranslation = {
cowebsite: "Press SPACE or touch here to open web site",
jitsiRoom: "Press SPACE or touch here to enter Jitsi Meet room",
newTab: "Press SPACE or touch here to open web site in new tab",
};
export default trigger;
+3
View File
@@ -15,6 +15,9 @@ const woka: BaseTranslation = {
continue: "Continue",
customize: "Customize your WOKA",
},
menu: {
businessCard: "Business Card",
},
};
export default woka;
+3 -1
View File
@@ -12,9 +12,10 @@ import menu from "./menu";
import report from "./report";
import warning from "./warning";
import woka from "./woka";
import trigger from "./trigger";
const fr_FR: Translation = {
...en_US,
...(en_US as Translation),
language: "Français",
country: "France",
audio,
@@ -29,6 +30,7 @@ const fr_FR: Translation = {
report,
warning,
emoji,
trigger,
};
export default fr_FR;
+2 -1
View File
@@ -63,13 +63,14 @@ const menu: NonNullable<Translation["menu"]> = {
},
fullscreen: "Plein écran",
notifications: "Notifications",
cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de réunion Jitsi",
cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de conférence Jitsi",
ignoreFollowRequest: "Ignorer les demandes de suivi des autres utilisateurs",
},
invite: {
description: "Partager le lien de la salle!",
copy: "Copier",
share: "Partager",
walk_automatically_to_position: "Marcher automatiquement jusqu'à ma position",
},
globalMessage: {
text: "Texte",
+9
View File
@@ -0,0 +1,9 @@
import type { Translation } from "../i18n-types";
const trigger: NonNullable<Translation["trigger"]> = {
cowebsite: "Appuyez sur ESPACE ou ici pour ouvrir le site Web",
jitsiRoom: "Appuyez sur ESPACE ou ici pour entrer dans la salle conférence Jitsi",
newTab: "Appuyez sur ESPACE ou ici pour ouvrir le site Web dans un nouvel onglet",
};
export default trigger;
+3
View File
@@ -15,6 +15,9 @@ const woka: NonNullable<Translation["woka"]> = {
continue: "Continuer",
customize: "Personnalisez votre WOKA",
},
menu: {
businessCard: "Carte de visite",
},
};
export default woka;
@@ -1,4 +1,8 @@
//TextGlobalMessage
// TODO: load on demand with TextGlobalMessage component
@import "quill/dist/quill.snow.css";
section.section-input-send-text {
--height-toolbar: 20%;
height: 100%;
+1
View File
@@ -1,4 +1,5 @@
@import "~@fontsource/press-start-2p/index.css";
@import "~@16bits/nes.css/css/nes.min.css";
*{
font-family: PixelFont-7,monospace;
+8 -3
View File
@@ -2,6 +2,11 @@
# yarn lockfile v1
"@16bits/nes.css@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@16bits/nes.css/-/nes.css-2.3.2.tgz#e69db834119b33ae8d3cb044f106a07a17cadd6f"
integrity sha512-nEM5PIth+Bab5JSOa4uUR+PMNUsNTYxA55oVlG3gXI/4LoYtWS767Uv9Pu/KCbHXVvnIjt4ZXt13kZw3083qTw==
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
@@ -2795,9 +2800,9 @@ focus-trap@^5.1.0:
xtend "^4.0.1"
follow-redirects@^1.0.0, follow-redirects@^1.14.0:
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
for-in@^1.0.2:
version "1.0.2"