Merge pull request #1854 from thecodingmachine/develop

Deploy 2022-02-11
This commit is contained in:
David Négrier 2022-02-11 19:02:47 +01:00 committed by GitHub
commit c4d18716c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 650 additions and 366 deletions

View File

@ -1,20 +1,118 @@
# Security
#
SECRET_KEY=
ADMIN_API_TOKEN=
#
# Networking
#
# The base domain # The base domain
DOMAIN=workadventure.localhost DOMAIN=workadventure.localhost
DEBUG_MODE=false # Subdomains
# MUST match the DOMAIN variable above
FRONT_HOST=front.workadventure.localhost
PUSHER_HOST=pusher.workadventure.localhost
BACK_HOST=api.workadventure.localhost
MAPS_HOST=maps.workadventure.localhost
ICON_HOST=icon.workadventure.localhost
# SAAS admin panel
ADMIN_API_URL=
#
# Basic configuration
#
# The directory to store data in
DATA_DIR=./wa
# The URL used by default, in the form: "/_/global/map/url.json"
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json
# If you want to have a contact page in your menu,
# you MUST set CONTACT_URL to the URL of the page that you want
CONTACT_URL=
MAX_PER_GROUP=4
MAX_USERNAME_LENGTH=8
DISABLE_ANONYMOUS=false
# The version of the docker image to use
# MUST uncomment "image" keys in the docker-compose file for it to be effective
VERSION=master
TZ=Europe/Paris
#
# Jitsi
#
JITSI_URL=meet.jit.si JITSI_URL=meet.jit.si
# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret # If your Jitsi environment has authentication set up,
# you MUST set JITSI_PRIVATE_MODE to "true"
# and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret
JITSI_PRIVATE_MODE=false JITSI_PRIVATE_MODE=false
JITSI_ISS= JITSI_ISS=
SECRET_JITSI_KEY= SECRET_JITSI_KEY=
#
# Turn/Stun
#
# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections) # URL of the TURN server (needed to "punch a hole" through some networks for P2P connections)
TURN_SERVER= TURN_SERVER=
TURN_USER= TURN_USER=
TURN_PASSWORD= TURN_PASSWORD=
# If your Turn server is configured to use the Turn REST API, you MUST put the shared auth secret here.
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
# Keep empty if you are sharing hard coded / clear text credentials.
TURN_STATIC_AUTH_SECRET=
# URL of the STUN server
STUN_SERVER=
# The URL used by default, in the form: "/_/global/map/url.json" #
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json # Certificate config
#
# The email address used by Let's encrypt to send renewal warnings (compulsory) # The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL= ACME_EMAIL=
#
# Additional app configs
# Configuration for apps which are not workadventure itself
#
# openID
OPID_CLIENT_ID=
OPID_CLIENT_SECRET=
OPID_CLIENT_ISSUER=
OPID_CLIENT_REDIRECT_URL=
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
OPID_PROFILE_SCREEN_PROVIDER=
#
# Advanced configuration
# Generally does not need to be changed
#
# Networking
HTTP_PORT=80
HTTPS_PORT=443
# Workadventure settings
DISABLE_NOTIFICATIONS=false
SKIP_RENDER_OPTIMIZATIONS=false
STORE_VARIABLES_FOR_LOCAL_MAPS=true
# Debugging options
DEBUG_MODE=false
LOG_LEVEL=WARN
# Internal URLs
API_URL=back:50051
RESTART_POLICY=unless-stopped

View File

@ -1,114 +1,128 @@
version: "3.3" version: "3.5"
services: services:
reverse-proxy: reverse-proxy:
image: traefik:v2.3 image: traefik:v2.6
command: command:
- --log.level=WARN - --log.level=${LOG_LEVEL}
#- --api.insecure=true
- --providers.docker - --providers.docker
- --entryPoints.web.address=:80 # Entry points
- --entryPoints.web.address=:${HTTP_PORT}
- --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https - --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entryPoints.websecure.address=:443 - --entryPoints.websecure.address=:${HTTPS_PORT}
# HTTP challenge
- --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL} - --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}
- --certificatesresolvers.myresolver.acme.storage=/acme.json - --certificatesresolvers.myresolver.acme.storage=/acme.json
# used during the challenge
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
# Let's Encrypt's staging server
# uncomment during testing to avoid rate limiting
#- --certificatesresolvers.dnsresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
ports: ports:
- "80:80" - "${HTTP_PORT}:80"
- "443:443" - "${HTTPS_PORT}:443"
# The Web UI (enabled by --api.insecure=true)
#- "8080:8080"
depends_on:
- pusher
- front
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json - ${DATA_DIR}/letsencrypt/acme.json:/acme.json
restart: unless-stopped restart: ${RESTART_POLICY}
front: front:
build: build:
context: ../.. context: ../..
dockerfile: front/Dockerfile dockerfile: front/Dockerfile
#image: thecodingmachine/workadventure-front:master #image: thecodingmachine/workadventure-front:${VERSION}
environment: environment:
DEBUG_MODE: "$DEBUG_MODE" - DEBUG_MODE
JITSI_URL: $JITSI_URL - JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - JITSI_PRIVATE_MODE
PUSHER_URL: //pusher.${DOMAIN} - PUSHER_URL=//${PUSHER_HOST}
ICON_URL: //icon.${DOMAIN} - ICON_URL=//${ICON_HOST}
TURN_SERVER: "${TURN_SERVER}" - TURN_SERVER
TURN_USER: "${TURN_USER}" - TURN_USER
TURN_PASSWORD: "${TURN_PASSWORD}" - TURN_PASSWORD
START_ROOM_URL: "${START_ROOM_URL}" - TURN_STATIC_AUTH_SECRET
- STUN_SERVER
- START_ROOM_URL
- SKIP_RENDER_OPTIMIZATIONS
- MAX_PER_GROUP
- MAX_USERNAME_LENGTH
- DISABLE_ANONYMOUS
- DISABLE_NOTIFICATIONS
labels: labels:
- "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)" - "traefik.http.routers.front.rule=Host(`${FRONT_HOST}`)"
- "traefik.http.routers.front.entryPoints=web,traefik" - "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=80" - "traefik.http.services.front.loadbalancer.server.port=80"
- "traefik.http.routers.front-ssl.rule=Host(`play.${DOMAIN}`)" - "traefik.http.routers.front-ssl.rule=Host(`${FRONT_HOST}`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure" - "traefik.http.routers.front-ssl.entryPoints=websecure"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.service=front" - "traefik.http.routers.front-ssl.service=front"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.tls.certresolver=myresolver" - "traefik.http.routers.front-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: ${RESTART_POLICY}
pusher: pusher:
build: build:
context: ../.. context: ../..
dockerfile: pusher/Dockerfile dockerfile: pusher/Dockerfile
#image: thecodingmachine/workadventure-pusher:master #image: thecodingmachine/workadventure-pusher:${VERSION}
command: yarn run runprod command: yarn run runprod
environment: environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_JITSI_KEY
SECRET_KEY: yourSecretKey - SECRET_KEY
API_URL: back:50051 - API_URL
JITSI_URL: $JITSI_URL - FRONT_URL=https://${FRONT_HOST}
JITSI_ISS: $JITSI_ISS - JITSI_URL
FRONT_URL: https://play.${DOMAIN} - JITSI_ISS
- DISABLE_ANONYMOUS
labels: labels:
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)" - "traefik.http.routers.pusher.rule=Host(`${PUSHER_HOST}`)"
- "traefik.http.routers.pusher.entryPoints=web,traefik" - "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080" - "traefik.http.services.pusher.loadbalancer.server.port=8080"
- "traefik.http.routers.pusher-ssl.rule=Host(`pusher.${DOMAIN}`)" - "traefik.http.routers.pusher-ssl.rule=Host(${PUSHER_HOST}`)"
- "traefik.http.routers.pusher-ssl.entryPoints=websecure" - "traefik.http.routers.pusher-ssl.entryPoints=websecure"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher" - "traefik.http.routers.pusher-ssl.service=pusher"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver" - "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: ${RESTART_POLICY}
back: back:
build: build:
context: ../.. context: ../..
dockerfile: back/Dockerfile dockerfile: back/Dockerfile
#image: thecodingmachine/workadventure-back:master #image: thecodingmachine/workadventure-back:${VERSION}
command: yarn run runprod command: yarn run runprod
environment: environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_JITSI_KEY
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - SECRET_KEY
ADMIN_API_URL: "$ADMIN_API_URL" - ADMIN_API_TOKEN
JITSI_URL: $JITSI_URL - ADMIN_API_URL
JITSI_ISS: $JITSI_ISS - TURN_SERVER
- TURN_USER
- TURN_PASSWORD
- TURN_STATIC_AUTH_SECRET
- STUN_SERVER
- JITSI_URL
- JITSI_ISS
- MAX_PER_GROUP
- STORE_VARIABLES_FOR_LOCAL_MAPS
labels: labels:
- "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)" - "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back.entryPoints=web" - "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080" - "traefik.http.services.back.loadbalancer.server.port=8080"
- "traefik.http.routers.back-ssl.rule=Host(`api.${DOMAIN}`)" - "traefik.http.routers.back-ssl.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back-ssl.entryPoints=websecure" - "traefik.http.routers.back-ssl.entryPoints=websecure"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.service=back" - "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver" - "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: ${RESTART_POLICY}
icon: icon:
image: matthiasluedtke/iconserver:v3.13.0 image: matthiasluedtke/iconserver:v3.13.0
labels: labels:
- "traefik.http.routers.icon.rule=Host(`icon.${DOMAIN}`)" - "traefik.http.routers.icon.rule=Host(`${ICON_HOST}`)"
- "traefik.http.routers.icon.entryPoints=web,traefik" - "traefik.http.routers.icon.entryPoints=web,traefik"
- "traefik.http.services.icon.loadbalancer.server.port=8080" - "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=Host(`icon.${DOMAIN}`)" - "traefik.http.routers.icon-ssl.rule=Host(`${ICON_HOST}`)"
- "traefik.http.routers.icon-ssl.entryPoints=websecure" - "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon" - "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver" - "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"

View File

@ -82,7 +82,11 @@ We are able to direct a Woka to the desired place immediately after spawn. To ma
``` ```
.../my_map.json#moveTo=meeting-room&start .../my_map.json#moveTo=meeting-room&start
``` ```
*...or even like this!*
```
.../my_map.json#start&moveTo=200,100
```
For this to work, moveTo must be equal to the layer name of interest. This layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected. For this to work, moveTo must be equal to the x and y position, layer name, or object name of interest. Layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected.
![](images/moveTo-layer-example.png) ![](images/moveTo-layer-example.png)

View File

@ -71,7 +71,7 @@
"zod": "^3.11.6" "zod": "^3.11.6"
}, },
"scripts": { "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", "templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "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", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
@ -84,7 +84,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\"", "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": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check '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": { "lint-staged": {
"*.svelte": [ "*.svelte": [

View File

@ -2,9 +2,10 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable"; import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { coWebsitesNotAsleep, mainCoWebsite, jitsiCoWebsite } from "../../Stores/CoWebsiteStore"; import { mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore"; import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { iframeStates } from "../../WebRtc/CoWebsiteManager"; import { iframeStates } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
@ -15,10 +16,10 @@
let icon: HTMLImageElement; let icon: HTMLImageElement;
let iconLoaded = false; let iconLoaded = false;
let state = coWebsite.getStateSubscriber(); let state = coWebsite.getStateSubscriber();
let isJitsi: boolean = false; let isJitsi: boolean = coWebsite instanceof JitsiCoWebsite;
const mainState = coWebsiteManager.getMainStateSubscriber();
onMount(() => { onMount(() => {
isJitsi = Boolean($jitsiCoWebsite && $jitsiCoWebsite.getId() === coWebsite.getId());
icon.src = isJitsi icon.src = isJitsi
? "/resources/logos/meet.svg" ? "/resources/logos/meet.svg"
: `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`; : `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`;
@ -33,15 +34,17 @@
coWebsiteManager.goToMain(coWebsite); coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) { } else if ($mainCoWebsite) {
if ($mainCoWebsite.getId() === coWebsite.getId()) { if ($mainCoWebsite.getId() === coWebsite.getId()) {
const coWebsites = $coWebsitesNotAsleep; if (coWebsiteManager.getMainState() === iframeStates.closed) {
const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined;
if (newMain && newMain.getId() !== $mainCoWebsite.getId()) {
coWebsiteManager.goToMain(newMain);
} else if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.displayMain(); coWebsiteManager.displayMain();
} else if ($highlightedEmbedScreen?.type === "cowebsite") {
coWebsiteManager.goToMain($highlightedEmbedScreen.embed);
} else { } else {
coWebsiteManager.hideMain(); coWebsiteManager.hideMain();
} }
} else {
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.goToMain(coWebsite);
coWebsiteManager.displayMain();
} else { } else {
highlightedEmbedScreen.toggleHighlight({ highlightedEmbedScreen.toggleHighlight({
type: "cowebsite", type: "cowebsite",
@ -49,6 +52,7 @@
}); });
} }
} }
}
if ($state === "asleep") { if ($state === "asleep") {
await coWebsiteManager.loadCoWebsite(coWebsite); await coWebsiteManager.loadCoWebsite(coWebsite);
@ -64,7 +68,10 @@
let isHighlight: boolean = false; let isHighlight: boolean = false;
let isMain: boolean = false; let isMain: boolean = false;
$: { $: {
isMain = $mainCoWebsite !== undefined && $mainCoWebsite.getId() === coWebsite.getId(); isMain =
$mainState === iframeStates.opened &&
$mainCoWebsite !== undefined &&
$mainCoWebsite.getId() === coWebsite.getId();
isHighlight = isHighlight =
$highlightedEmbedScreen !== null && $highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" && $highlightedEmbedScreen.type === "cowebsite" &&
@ -212,7 +219,8 @@
} }
&:not(.vertical) { &:not(.vertical) {
animation: bounce 0.35s ease 6 alternate; transition: all 300ms;
transform: translateY(0px);
} }
&.vertical { &.vertical {
@ -233,7 +241,7 @@
&.displayed { &.displayed {
&:not(.vertical) { &: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 { @keyframes bounce {
from { from {
transform: translateY(0); transform: translateY(0);

View File

@ -1,5 +1,12 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte"; 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() { function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement; const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@ -8,8 +15,23 @@
document.execCommand("copy"); 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() { async function shareLink() {
const shareData = { url: location.toString() }; const shareData = { url: getLink() };
try { try {
await navigator.share(shareData); await navigator.share(shareData);
@ -22,16 +44,43 @@
<div class="guest-main"> <div class="guest-main">
<section class="container-overflow"> <section class="container-overflow">
{#if !canShare}
<section class="share-url not-mobile"> <section class="share-url not-mobile">
<h3>{$LL.menu.invite.description()}</h3> <h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" value={location.toString()} /> <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> <button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section> </section>
{:else}
<section class="is-mobile"> <section class="is-mobile">
<h3>{$LL.menu.invite.description()}</h3> <h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} /> <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> <button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section> </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> </section>
</div> </div>
@ -39,14 +88,27 @@
@import "../../../style/breakpoints.scss"; @import "../../../style/breakpoints.scss";
div.guest-main { div.guest-main {
width: 50%;
margin-left: auto;
margin-right: auto;
height: calc(100% - 56px); height: calc(100% - 56px);
text-align: center; input.link-url {
width: calc(100% - 200px);
}
.starting-points {
width: 80%;
}
section { section {
margin-bottom: 50px; margin-bottom: 50px;
} }
section.nes-select select:focus {
outline: none;
}
section.container-overflow { section.container-overflow {
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -55,25 +117,23 @@
} }
section.is-mobile { section.is-mobile {
display: none; display: block;
text-align: center;
margin-bottom: 20px;
} }
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
div.guest-main { div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow { section.container-overflow {
height: calc(100% - 120px); height: calc(100% - 120px);
} }
} }
} }
@include media-breakpoint-up(lg) {
div.guest-main {
width: 100%;
}
}
</style> </style>

View File

@ -323,6 +323,10 @@ export class GameMap {
throw new Error("No possible position found"); 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> { private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0); return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
} }

View File

@ -10,6 +10,13 @@ import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; 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 { interface OpenCoWebsite {
actionId: string; actionId: string;
@ -23,6 +30,7 @@ export class GameMapPropertiesListener {
constructor(private scene: GameScene, private gameMap: GameMap) {} constructor(private scene: GameScene, private gameMap: GameMap) {}
register() { register() {
// Website on new tab
this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
layoutManagerActionStore.removeAction("openTab"); layoutManagerActionStore.removeAction("openTab");
@ -33,7 +41,7 @@ export class GameMapPropertiesListener {
if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE); let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE);
if (message === undefined) { if (message === undefined) {
message = "Press SPACE or touch here to open web site in new tab"; message = get(LL).trigger.newTab();
} }
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: "openTab", 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. // Open a new co-website by the property.
this.gameMap.onEnterLayer((newLayers) => { this.gameMap.onEnterLayer((newLayers) => {
const handler = () => { const handler = () => {
@ -135,7 +266,7 @@ export class GameMapPropertiesListener {
websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON
) { ) {
if (!websiteTriggerMessageProperty) { if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site"; websiteTriggerMessageProperty = get(LL).trigger.cowebsite();
} }
this.coWebsitesActionTriggerByLayer.set(layer, actionId); this.coWebsitesActionTriggerByLayer.set(layer, actionId);

View File

@ -20,15 +20,8 @@ import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import { import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
DEBUG_MODE,
JITSI_PRIVATE_MODE,
JITSI_URL,
MAX_PER_GROUP,
POSITION_DELAY,
} from "../../Enum/EnvironmentVariable";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { Room } from "../../Connexion/Room"; import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { jitsiFactory } from "../../WebRtc/JitsiFactory";
@ -82,7 +75,7 @@ import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { userIsAdminStore } from "../../Stores/GameStore"; import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore"; import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; 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 EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
@ -98,6 +91,8 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore"; import { followUsersColorStore } from "../../Stores/FollowStore";
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte"; import { locale } from "../../i18n/i18n-svelte";
import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
@ -550,6 +545,8 @@ export class GameScene extends DirtyScene {
urlManager.getStartLayerNameFromUrl() urlManager.getStartLayerNameFromUrl()
); );
startLayerNamesStore.set(this.startPositionCalculator.getStartPositionNames());
//add entities //add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>(); this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@ -577,6 +574,8 @@ export class GameScene extends DirtyScene {
this.createCurrentPlayer(); this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
this.tryMovePlayerWithMoveToParameter();
this.cameraManager = new CameraManager( this.cameraManager = new CameraManager(
this, this,
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels }, { x: this.Map.widthInPixels, y: this.Map.heightInPixels },
@ -636,7 +635,6 @@ export class GameScene extends DirtyScene {
); );
new GameMapPropertiesListener(this, this.gameMap).register(); new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) { if (!this.room.isDisconnected()) {
this.scene.sleep(); this.scene.sleep();
@ -815,7 +813,7 @@ export class GameScene extends DirtyScene {
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false); const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0); 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) => { this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => {
@ -952,116 +950,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 { private listenToIframeEvents(): void {
this.iframeSubscriptionList = []; this.iframeSubscriptionList = [];
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
@ -1417,7 +1305,7 @@ ${escapedMessage}
//Create new colliders with the new GameMap //Create new colliders with the new GameMap
this.createCollisionWithPlayer(); this.createCollisionWithPlayer();
//Create new trigger with the new GameMap //Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange(); new GameMapPropertiesListener(this, this.gameMap).register();
resolve(newFirstgid); resolve(newFirstgid);
}); });
}); });
@ -1488,9 +1376,9 @@ ${escapedMessage}
}); });
iframeListener.registerAnswerer("movePlayerTo", async (message) => { iframeListener.registerAnswerer("movePlayerTo", async (message) => {
const index = this.getGameMap().getTileIndexAt(message.x, message.y); const startTileIndex = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const startTile = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y); const destinationTileIndex = this.getGameMap().getTileIndexAt(message.x, message.y);
const path = await this.getPathfindingManager().findPath(startTile, index, true, true); const path = await this.getPathfindingManager().findPath(startTileIndex, destinationTileIndex, true, true);
path.shift(); path.shift();
if (path.length === 0) { if (path.length === 0) {
throw new Error("no path available"); throw new Error("no path available");
@ -1534,11 +1422,11 @@ ${escapedMessage}
this.markDirty(); this.markDirty();
} }
private getMapDirUrl(): string { public getMapDirUrl(): string {
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); return this.MapUrlFile.substring(0, this.MapUrlFile.lastIndexOf("/"));
} }
private async onMapExit(roomUrl: URL) { public async onMapExit(roomUrl: URL) {
if (this.mapTransitioning) return; if (this.mapTransitioning) return;
this.mapTransitioning = true; this.mapTransitioning = true;
@ -1604,7 +1492,6 @@ ${escapedMessage}
iframeListener.unregisterScript(script); iframeListener.unregisterScript(script);
} }
this.stopJitsi();
audioManagerFileStore.unloadAudio(); audioManagerFileStore.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection?.closeConnection(); this.connection?.closeConnection();
@ -1655,6 +1542,36 @@ ${escapedMessage}
this.MapPlayersByKey.clear(); 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 { private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined; return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined;
} }
@ -1777,22 +1694,6 @@ ${escapedMessage}
this.connection?.emitEmoteEvent(emoteKey); this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(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) { } catch (err) {
if (err instanceof TextureError) { if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
@ -2145,7 +2046,7 @@ ${escapedMessage}
mediaManager.hideMyCamera(); 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 allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring( const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined, allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
@ -2157,9 +2058,9 @@ ${escapedMessage}
); );
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
coWebsite.setJitsiLoadPromise( coWebsite.setJitsiLoadPromise(() => {
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl) return jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl);
); });
coWebsiteManager.loadCoWebsite(coWebsite).catch((err) => { coWebsiteManager.loadCoWebsite(coWebsite).catch((err) => {
console.error(err); console.error(err);
@ -2168,13 +2069,6 @@ ${escapedMessage}
analyticsClient.enteredJitsi(roomName, this.room.id); 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?) //todo: put this into an 'orchestrator' scene (EntryScene?)
private bannedUser() { private bannedUser() {
this.cleanupClosingScene(); this.cleanupClosingScene();

View File

@ -16,32 +16,6 @@ export class StartPositionCalculator {
) { ) {
this.initStartXAndStartY(); 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 { private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, GameMapProperties.START_LAYER) == true; return this.getProperty(layer, GameMapProperties.START_LAYER) == true;
} }

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 type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../WebRtc/CoWebsite/JitsiCoWebsite";
function createCoWebsiteStore() { function createCoWebsiteStore() {
const { subscribe, set, update } = writable(Array<CoWebsite>()); const { subscribe, set, update } = writable(Array<CoWebsite>());
@ -50,7 +49,3 @@ export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) =>
export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) => export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => coWebsite.getState() !== "asleep") $coWebsites.find((coWebsite) => coWebsite.getState() !== "asleep")
); );
export const jitsiCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => coWebsite instanceof JitsiCoWebsite)
);

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[]>([]);

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] };
}
}

View File

@ -1,23 +1,12 @@
import CancelablePromise from "cancelable-promise"; import CancelablePromise from "cancelable-promise";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { coWebsiteManager } from "../CoWebsiteManager";
import { jitsiFactory } from "../JitsiFactory"; import { jitsiFactory } from "../JitsiFactory";
import { SimpleCoWebsite } from "./SimpleCoWebsite"; import { SimpleCoWebsite } from "./SimpleCoWebsite";
export class JitsiCoWebsite extends SimpleCoWebsite { export class JitsiCoWebsite extends SimpleCoWebsite {
private jitsiLoadPromise?: CancelablePromise<HTMLIFrameElement>; private jitsiLoadPromise?: () => CancelablePromise<HTMLIFrameElement>;
constructor(url: URL, allowApi?: boolean, allowPolicy?: string, widthPercent?: number, closable?: boolean) { setJitsiLoadPromise(promise: () => CancelablePromise<HTMLIFrameElement>): void {
const coWebsite = coWebsiteManager.searchJitsi();
if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
super(url, allowApi, allowPolicy, widthPercent, closable);
}
setJitsiLoadPromise(promise: CancelablePromise<HTMLIFrameElement>): void {
this.jitsiLoadPromise = promise; this.jitsiLoadPromise = promise;
} }
@ -26,13 +15,12 @@ export class JitsiCoWebsite extends SimpleCoWebsite {
this.state.set("loading"); this.state.set("loading");
gameManager.getCurrentGameScene().disableMediaBehaviors(); gameManager.getCurrentGameScene().disableMediaBehaviors();
jitsiFactory.restart();
if (!this.jitsiLoadPromise) { if (!this.jitsiLoadPromise) {
return reject("Undefined Jitsi start callback"); return reject("Undefined Jitsi start callback");
} }
const jitsiLoading = this.jitsiLoadPromise const jitsiLoading = this.jitsiLoadPromise()
.then((iframe) => { .then((iframe) => {
this.iframe = iframe; this.iframe = iframe;
this.iframe.classList.add("pixel"); this.iframe.classList.add("pixel");

View File

@ -1,8 +1,8 @@
import { HtmlUtils } from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { waScaleManager } from "../Phaser/Services/WaScaleManager"; import { waScaleManager } from "../Phaser/Services/WaScaleManager";
import { coWebsites, coWebsitesNotAsleep, jitsiCoWebsite, mainCoWebsite } from "../Stores/CoWebsiteStore"; import { coWebsites, coWebsitesNotAsleep, mainCoWebsite } from "../Stores/CoWebsiteStore";
import { get } from "svelte/store"; import { get, Readable, Writable, writable } from "svelte/store";
import { embedScreenLayout, highlightedEmbedScreen } from "../Stores/EmbedScreensStore"; import { embedScreenLayout, highlightedEmbedScreen } from "../Stores/EmbedScreensStore";
import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils"; import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils";
import { LayoutMode } from "./LayoutManager"; import { LayoutMode } from "./LayoutManager";
@ -34,7 +34,7 @@ interface TouchMoveCoordinates {
} }
class CoWebsiteManager { class CoWebsiteManager {
private openedMain: iframeStates = iframeStates.closed; private openedMain: Writable<iframeStates> = writable(iframeStates.closed);
private _onResize: Subject<void> = new Subject(); private _onResize: Subject<void> = new Subject();
public onResize = this._onResize.asObservable(); public onResize = this._onResize.asObservable();
@ -57,6 +57,10 @@ class CoWebsiteManager {
}); });
public getMainState() { public getMainState() {
return get(this.openedMain);
}
public getMainStateSubscriber(): Readable<iframeStates> {
return this.openedMain; return this.openedMain;
} }
@ -324,7 +328,7 @@ class CoWebsiteManager {
} }
this.cowebsiteDom.classList.add("closing"); this.cowebsiteDom.classList.add("closing");
this.cowebsiteDom.classList.remove("opened"); this.cowebsiteDom.classList.remove("opened");
this.openedMain = iframeStates.closed; this.openedMain.set(iframeStates.closed);
this.fire(); this.fire();
} }
@ -332,7 +336,7 @@ class CoWebsiteManager {
this.toggleFullScreenIcon(true); this.toggleFullScreenIcon(true);
this.cowebsiteDom.classList.add("closing"); this.cowebsiteDom.classList.add("closing");
this.cowebsiteDom.classList.remove("opened"); this.cowebsiteDom.classList.remove("opened");
this.openedMain = iframeStates.closed; this.openedMain.set(iframeStates.closed);
this.resetStyleMain(); this.resetStyleMain();
this.fire(); this.fire();
} }
@ -386,14 +390,14 @@ class CoWebsiteManager {
} }
this.cowebsiteDom.classList.add("opened"); this.cowebsiteDom.classList.add("opened");
this.openedMain = iframeStates.loading; this.openedMain.set(iframeStates.loading);
} }
private openMain(): void { private openMain(): void {
this.cowebsiteDom.addEventListener("transitionend", () => { this.cowebsiteDom.addEventListener("transitionend", () => {
this.resizeAllIframes(); this.resizeAllIframes();
}); });
this.openedMain = iframeStates.opened; this.openedMain.set(iframeStates.opened);
} }
public resetStyleMain() { public resetStyleMain() {
@ -556,19 +560,12 @@ class CoWebsiteManager {
mainCoWebsite.getId() !== coWebsite.getId() && mainCoWebsite.getId() !== coWebsite.getId() &&
mainCoWebsite.getState() !== "asleep" mainCoWebsite.getState() !== "asleep"
) { ) {
highlightedEmbedScreen.toggleHighlight({ highlightedEmbedScreen.removeHighlight();
type: "cowebsite",
embed: mainCoWebsite,
});
} }
this.resizeAllIframes(); this.resizeAllIframes();
} }
public searchJitsi(): CoWebsite | undefined {
return get(jitsiCoWebsite);
}
public addCoWebsiteToStore(coWebsite: CoWebsite, position: number | undefined) { public addCoWebsiteToStore(coWebsite: CoWebsite, position: number | undefined) {
const coWebsitePosition = position === undefined ? get(coWebsites).length : position; const coWebsitePosition = position === undefined ? get(coWebsites).length : position;
coWebsites.add(coWebsite, coWebsitePosition); coWebsites.add(coWebsite, coWebsitePosition);
@ -591,7 +588,7 @@ class CoWebsiteManager {
} }
// Check if the main is hide // Check if the main is hide
if (this.getMainCoWebsite() && this.openedMain === iframeStates.closed) { if (this.getMainCoWebsite() && this.getMainState() === iframeStates.closed) {
this.displayMain(); this.displayMain();
} }
@ -665,7 +662,7 @@ class CoWebsiteManager {
} }
public getGameSize(): { width: number; height: number } { public getGameSize(): { width: number; height: number } {
if (this.openedMain === iframeStates.closed) { if (this.getMainState() === iframeStates.closed) {
return { return {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,

View File

@ -2,7 +2,6 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { coWebsiteManager } from "./CoWebsiteManager"; import { coWebsiteManager } from "./CoWebsiteManager";
import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { CoWebsite } from "./CoWebsite/CoWesbite";
import CancelablePromise from "cancelable-promise"; import CancelablePromise from "cancelable-promise";
interface jitsiConfigInterface { interface jitsiConfigInterface {
@ -180,13 +179,14 @@ class JitsiFactory {
const iframe = coWebsiteManager const iframe = coWebsiteManager
.getCoWebsiteBuffer() .getCoWebsiteBuffer()
.querySelector<HTMLIFrameElement>('[id*="jitsi" i]'); .querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe && this.jitsiApi) { if (iframe && this.jitsiApi) {
this.jitsiApi.addListener("videoConferenceLeft", () => { this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(); this.closeOrUnload(iframe);
}); });
this.jitsiApi.addListener("readyToClose", () => { this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(); this.closeOrUnload(iframe);
}); });
return resolve(iframe); return resolve(iframe);
@ -209,8 +209,9 @@ class JitsiFactory {
}); });
} }
private closeOrUnload = function () { private closeOrUnload = function (iframe: HTMLIFrameElement) {
const coWebsite = coWebsiteManager.searchJitsi(); const coWebsite = coWebsiteManager.getCoWebsites().find((coWebsite) => coWebsite.getIframe() === iframe);
if (!coWebsite) { if (!coWebsite) {
return; 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() { public stop() {
if (!this.jitsiApi) { if (!this.jitsiApi) {
return; return;

View File

@ -14,7 +14,7 @@ import warning from "./warning";
import woka from "./woka"; import woka from "./woka";
const de_DE: Translation = { const de_DE: Translation = {
...en_US, ...(en_US as Translation),
language: "Deutsch", language: "Deutsch",
country: "Deutschland", country: "Deutschland",
audio, audio,

View File

@ -70,6 +70,7 @@ const menu: NonNullable<Translation["menu"]> = {
description: "Link zu diesem Raum teilen!", description: "Link zu diesem Raum teilen!",
copy: "Kopieren", copy: "Kopieren",
share: "Teilen", share: "Teilen",
walk_automatically_to_position: "Walk automatically to my position",
}, },
globalMessage: { globalMessage: {
text: "Text", text: "Text",

View File

@ -11,6 +11,7 @@ import menu from "./menu";
import report from "./report"; import report from "./report";
import warning from "./warning"; import warning from "./warning";
import emoji from "./emoji"; import emoji from "./emoji";
import trigger from "./trigger";
const en_US: BaseTranslation = { const en_US: BaseTranslation = {
language: "English", language: "English",
@ -27,6 +28,7 @@ const en_US: BaseTranslation = {
report, report,
warning, warning,
emoji, emoji,
trigger,
}; };
export default en_US; export default en_US;

View File

@ -70,6 +70,7 @@ const menu: BaseTranslation = {
description: "Share the link of the room!", description: "Share the link of the room!",
copy: "Copy", copy: "Copy",
share: "Share", share: "Share",
walk_automatically_to_position: "Walk automatically to my position",
}, },
globalMessage: { globalMessage: {
text: "Text", text: "Text",

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;

View File

@ -12,9 +12,10 @@ import menu from "./menu";
import report from "./report"; import report from "./report";
import warning from "./warning"; import warning from "./warning";
import woka from "./woka"; import woka from "./woka";
import trigger from "./trigger";
const fr_FR: Translation = { const fr_FR: Translation = {
...en_US, ...(en_US as Translation),
language: "Français", language: "Français",
country: "France", country: "France",
audio, audio,
@ -29,6 +30,7 @@ const fr_FR: Translation = {
report, report,
warning, warning,
emoji, emoji,
trigger,
}; };
export default fr_FR; export default fr_FR;

View File

@ -63,13 +63,14 @@ const menu: NonNullable<Translation["menu"]> = {
}, },
fullscreen: "Plein écran", fullscreen: "Plein écran",
notifications: "Notifications", 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", ignoreFollowRequest: "Ignorer les demandes de suivi des autres utilisateurs",
}, },
invite: { invite: {
description: "Partager le lien de la salle!", description: "Partager le lien de la salle!",
copy: "Copier", copy: "Copier",
share: "Partager", share: "Partager",
walk_automatically_to_position: "Marcher automatiquement jusqu'à ma position",
}, },
globalMessage: { globalMessage: {
text: "Texte", text: "Texte",

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;

View File

@ -3,7 +3,7 @@
"infinite":false, "infinite":false,
"layers":[ "layers":[
{ {
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10, "height":10,
"id":1, "id":1,
"name":"floor", "name":"floor",
@ -204,6 +204,71 @@
"width":92.7120717279925, "width":92.7120717279925,
"x":233.848901257569, "x":233.848901257569,
"y":135.845612785282 "y":135.845612785282
},
{
"height":0,
"id":9,
"name":"destination",
"point":true,
"rotation":0,
"type":"",
"visible":true,
"width":0,
"x":207.918025151374,
"y":243.31625523987
},
{
"height":45.0829063809967,
"id":10,
"name":"",
"rotation":0,
"text":
{
"halign":"center",
"text":"destination object",
"wrap":true
},
"type":"",
"visible":true,
"width":83,
"x":167.26,
"y":254.682580344667
},
{
"height":19.6921,
"id":11,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"...#start&moveTo=destination",
"wrap":true
},
"type":"",
"visible":true,
"width":202.260327899394,
"x":32.2652715416861,
"y":148.51445302748
},
{
"height":19.6921,
"id":12,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"...#start2&moveTo=200,100",
"wrap":true
},
"type":"",
"visible":true,
"width":202.26,
"x":32.2654354913834,
"y":169.008165183978
}], }],
"opacity":1, "opacity":1,
"type":"objectgroup", "type":"objectgroup",
@ -212,7 +277,7 @@
"y":0 "y":0
}], }],
"nextlayerid":11, "nextlayerid":11,
"nextobjectid":9, "nextobjectid":13,
"orientation":"orthogonal", "orientation":"orthogonal",
"renderorder":"right-down", "renderorder":"right-down",
"tiledversion":"1.7.2", "tiledversion":"1.7.2",