develop
Ludwig Behm 10 months ago
commit 9e30039551
  1. 106
      contrib/docker/.env.prod.template
  2. 126
      contrib/docker/docker-compose.prod.yaml
  3. 27
      docs/maps/api-nav.md
  4. 6
      docs/maps/entry-exit.md
  5. BIN
      docs/maps/images/icon_open_website.png
  6. 7
      docs/maps/opening-a-website.md
  7. 3
      front/.prettierrc.json
  8. 5
      front/package.json
  9. 1
      front/src/Api/Events/OpenCoWebsiteEvent.ts
  10. 2
      front/src/Api/iframe/nav.ts
  11. 68
      front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte
  12. 2
      front/src/Components/EmbedScreens/CoWebsitesContainer.svelte
  13. 14
      front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte
  14. 2
      front/src/Components/Login/LoginScene.svelte
  15. 104
      front/src/Components/Menu/GuestSubMenu.svelte
  16. 1
      front/src/Components/Menu/Menu.svelte
  17. 3
      front/src/Interfaces/UserInputHandlerInterface.ts
  18. 29
      front/src/Phaser/Entity/Character.ts
  19. 60
      front/src/Phaser/Game/ActivatablesManager.ts
  20. 9
      front/src/Phaser/Game/Game.ts
  21. 10
      front/src/Phaser/Game/GameMap.ts
  22. 1
      front/src/Phaser/Game/GameMapProperties.ts
  23. 280
      front/src/Phaser/Game/GameMapPropertiesListener.ts
  24. 246
      front/src/Phaser/Game/GameScene.ts
  25. 4
      front/src/Phaser/Game/OutlineableInterface.ts
  26. 67
      front/src/Phaser/Game/StartPositionCalculator.ts
  27. 2
      front/src/Phaser/Map/ITiledMap.ts
  28. 14
      front/src/Phaser/UserInput/GameSceneUserInputHandler.ts
  29. 9
      front/src/Phaser/UserInput/UserInputManager.ts
  30. 12
      front/src/Stores/CoWebsiteStore.ts
  31. 4
      front/src/Stores/EmbedScreensStore.ts
  32. 1
      front/src/Stores/LayoutManagerStore.ts
  33. 23
      front/src/Stores/OutlineColorStore.ts
  34. 6
      front/src/Stores/StartLayerNamesStore.ts
  35. 4
      front/src/Utils/PathfindingManager.ts
  36. 12
      front/src/Utils/StringUtils.ts
  37. 17
      front/src/WebRtc/CoWebsite/CoWesbite.ts
  38. 49
      front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts
  39. 133
      front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts
  40. 488
      front/src/WebRtc/CoWebsiteManager.ts
  41. 175
      front/src/WebRtc/JitsiFactory.ts
  42. 1
      front/src/WebRtc/LayoutManager.ts
  43. 2
      front/src/i18n/de-DE/index.ts
  44. 1
      front/src/i18n/de-DE/menu.ts
  45. 2
      front/src/i18n/en-US/index.ts
  46. 1
      front/src/i18n/en-US/menu.ts
  47. 9
      front/src/i18n/en-US/trigger.ts
  48. 4
      front/src/i18n/fr-FR/index.ts
  49. 3
      front/src/i18n/fr-FR/menu.ts
  50. 9
      front/src/i18n/fr-FR/trigger.ts
  51. 5
      maps/tests/CoWebsite/cowebsite_property_trigger.json
  52. 197
      maps/tests/DoorTest/map.json
  53. 26
      maps/tests/DoorTest/script.php
  54. 16
      maps/tests/index.html
  55. 69
      maps/tests/move_to.json
  56. 150
      maps/tests/parallax.json
  57. 7
      yarn.lock

@ -1,20 +1,118 @@
# Security
#
SECRET_KEY=
ADMIN_API_TOKEN=
#
# Networking
#
# The base domain
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
# 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_ISS=
SECRET_JITSI_KEY=
#
# Turn/Stun
#
# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections)
TURN_SERVER=
TURN_USER=
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)
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

@ -1,114 +1,128 @@
version: "3.3"
version: "3.5"
services:
reverse-proxy:
image: traefik:v2.3
image: traefik:v2.6
command:
- --log.level=WARN
#- --api.insecure=true
- --log.level=${LOG_LEVEL}
- --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.scheme=https
- --entryPoints.websecure.address=:443
- --entryPoints.websecure.address=:${HTTPS_PORT}
# HTTP challenge
- --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}
- --certificatesresolvers.myresolver.acme.storage=/acme.json
# used during the challenge
- --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:
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
#- "8080:8080"
depends_on:
- pusher
- front
- "${HTTP_PORT}:80"
- "${HTTPS_PORT}:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json
restart: unless-stopped
- ${DATA_DIR}/letsencrypt/acme.json:/acme.json
restart: ${RESTART_POLICY}
front:
build:
context: ../..
dockerfile: front/Dockerfile
#image: thecodingmachine/workadventure-front:master
#image: thecodingmachine/workadventure-front:${VERSION}
environment:
DEBUG_MODE: "$DEBUG_MODE"
JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
PUSHER_URL: //pusher.${DOMAIN}
ICON_URL: //icon.${DOMAIN}
TURN_SERVER: "${TURN_SERVER}"
TURN_USER: "${TURN_USER}"
TURN_PASSWORD: "${TURN_PASSWORD}"
START_ROOM_URL: "${START_ROOM_URL}"
- DEBUG_MODE
- JITSI_URL
- JITSI_PRIVATE_MODE
- PUSHER_URL=//${PUSHER_HOST}
- ICON_URL=//${ICON_HOST}
- TURN_SERVER
- TURN_USER
- TURN_PASSWORD
- TURN_STATIC_AUTH_SECRET
- STUN_SERVER
- START_ROOM_URL
- SKIP_RENDER_OPTIMIZATIONS
- MAX_PER_GROUP
- MAX_USERNAME_LENGTH
- DISABLE_ANONYMOUS
- DISABLE_NOTIFICATIONS
labels:
- "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.routers.front.rule=Host(`${FRONT_HOST}`)"
- "traefik.http.routers.front.entryPoints=web"
- "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.tls=true"
- "traefik.http.routers.front-ssl.service=front"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.tls.certresolver=myresolver"
restart: unless-stopped
restart: ${RESTART_POLICY}
pusher:
build:
context: ../..
dockerfile: pusher/Dockerfile
#image: thecodingmachine/workadventure-pusher:master
#image: thecodingmachine/workadventure-pusher:${VERSION}
command: yarn run runprod
environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
API_URL: back:50051
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
FRONT_URL: https://play.${DOMAIN}
- SECRET_JITSI_KEY
- SECRET_KEY
- API_URL
- FRONT_URL=https://${FRONT_HOST}
- JITSI_URL
- JITSI_ISS
- DISABLE_ANONYMOUS
labels:
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)"
- "traefik.http.routers.pusher.entryPoints=web,traefik"
- "traefik.http.routers.pusher.rule=Host(`${PUSHER_HOST}`)"
- "traefik.http.routers.pusher.entryPoints=web"
- "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.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver"
restart: unless-stopped
restart: ${RESTART_POLICY}
back:
build:
context: ../..
dockerfile: back/Dockerfile
#image: thecodingmachine/workadventure-back:master
#image: thecodingmachine/workadventure-back:${VERSION}
command: yarn run runprod
environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
ADMIN_API_URL: "$ADMIN_API_URL"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
- SECRET_JITSI_KEY
- SECRET_KEY
- ADMIN_API_TOKEN
- ADMIN_API_URL
- 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:
- "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)"
- "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back.entryPoints=web"
- "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.tls=true"
- "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped
restart: ${RESTART_POLICY}
icon:
image: matthiasluedtke/iconserver:v3.13.0
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.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.tls=true"
- "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"

@ -3,7 +3,7 @@
### Opening a web page in a new tab
```
```ts
WA.nav.openTab(url: string): void
```
@ -11,13 +11,13 @@ Opens the webpage at "url" in your browser, in a new tab.
Example:
```javascript
```ts
WA.nav.openTab('https://www.wikipedia.org/');
```
### Opening a web page in the current tab
```
```ts
WA.nav.goToPage(url: string): void
```
@ -25,14 +25,13 @@ Opens the webpage at "url" in your browser in place of WorkAdventure. WorkAdvent
Example:
```javascript
```ts
WA.nav.goToPage('https://www.wikipedia.org/');
```
### Going to a different map from the script
```
```ts
WA.nav.goToRoom(url: string): void
```
@ -43,7 +42,7 @@ global urls: "/_/global/domain/path/map.json[#start-layer-name]"
Example:
```javascript
```ts
WA.nav.goToRoom("/@/tcm/workadventure/floor0") // workadventure urls
WA.nav.goToRoom('../otherMap/map.json');
WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
@ -51,25 +50,25 @@ WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
### Opening/closing web page in Co-Websites
```
WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number, closable: boolean, lazy: boolean): Promise<CoWebsite>
```ts
WA.nav.openCoWebSite(url: string, allowApi?: boolean = false, allowPolicy?: string = "", percentWidth?: number, position?: number, closable?: boolean, lazy?: boolean): Promise<CoWebsite>
```
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow),widthPercent define the width of the main cowebsite beetween the min size and the max size (70% of the viewport), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy
it's to add the cowebsite but don't load it.
Example:
```javascript
```ts
const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/');
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1, true, true);
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 70, 1, true, true);
// ...
coWebsite.close();
```
### Get all Co-Websites
```
```ts
WA.nav.getCoWebSites(): Promise<CoWebsite[]>
```
@ -77,6 +76,6 @@ Get all opened co-websites with their ids and positions.
Example:
```javascript
```ts
const coWebsites = await WA.nav.getCowebSites();
```

@ -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
```
*...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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -52,6 +52,13 @@ If you set `openWebsiteTrigger: onaction`, when the user walks on the layer, an
If you set `openWebsiteTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to open the web site'.
If you set `openWebsiteTrigger: onicon`, when the user walks on the layer, an icon will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/icon_open_website.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The iFrame will only open if the user clicks on icon</figcaption>
</figure>
### Setting the iFrame "allow" attribute
By default, iFrames have limited rights in browsers. For instance, they cannot put their content in fullscreen, they cannot start your webcam, etc...

@ -1,5 +1,4 @@
{
"printWidth": 120,
"tabWidth": 4,
"plugins": ["prettier-plugin-svelte"]
"tabWidth": 4
}

@ -71,7 +71,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 +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\"",
"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": [

@ -5,6 +5,7 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface()
url: tg.isString,
allowApi: tg.isOptional(tg.isBoolean),
allowPolicy: tg.isOptional(tg.isString),
widthPercent: tg.isOptional(tg.isNumber),
position: tg.isOptional(tg.isNumber),
closable: tg.isOptional(tg.isBoolean),
lazy: tg.isOptional(tg.isBoolean),

@ -45,6 +45,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
url: string,
allowApi?: boolean,
allowPolicy?: string,
widthPercent?: number,
position?: number,
closable?: boolean,
lazy?: boolean
@ -55,6 +56,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
url,
allowApi,
allowPolicy,
widthPercent,
position,
closable,
lazy,

@ -2,9 +2,11 @@
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { coWebsitesNotAsleep, mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { iframeStates } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
export let index: number;
@ -13,16 +15,15 @@
let icon: HTMLImageElement;
let iconLoaded = false;
let state = coWebsite.state;
const coWebsiteUrl = coWebsite.iframe.src;
const urlObject = new URL(coWebsiteUrl);
let state = coWebsite.getStateSubscriber();
let isJitsi: boolean = coWebsite instanceof JitsiCoWebsite;
const mainState = coWebsiteManager.getMainStateSubscriber();
onMount(() => {
icon.src = coWebsite.jitsi
icon.src = isJitsi
? "/resources/logos/meet.svg"
: `${ICON_URL}/icon?url=${urlObject.hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = coWebsite.altMessage ?? urlObject.hostname;
: `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = coWebsite.getUrl().hostname;
icon.onload = () => {
iconLoaded = true;
};
@ -32,17 +33,24 @@
if (vertical) {
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
if ($mainCoWebsite.iframe.id === coWebsite.iframe.id) {
const coWebsites = $coWebsitesNotAsleep;
const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined;
if (newMain) {
coWebsiteManager.goToMain(newMain);
if ($mainCoWebsite.getId() === coWebsite.getId()) {
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,
});
}
}
}
@ -60,11 +68,14 @@
let isHighlight: boolean = false;
let isMain: boolean = false;
$: {
isMain = $mainCoWebsite !== undefined && $mainCoWebsite.iframe === coWebsite.iframe;
isMain =
$mainState === iframeStates.opened &&
$mainCoWebsite !== undefined &&
$mainCoWebsite.getId() === coWebsite.getId();
isHighlight =
$highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" &&
$highlightedEmbedScreen.embed.iframe === coWebsite.iframe;
$highlightedEmbedScreen.embed.getId() === coWebsite.getId();
}
</script>
@ -81,7 +92,7 @@
<img
class="cowebsite-icon noselect nes-pointer"
class:hide={!iconLoaded}
class:jitsi={coWebsite.jitsi}
class:jitsi={isJitsi}
bind:this={icon}
on:dragstart|preventDefault={noDrag}
alt=""
@ -208,7 +219,8 @@
}
&:not(.vertical) {
animation: bounce 0.35s ease 6 alternate;
transition: all 300ms;
transform: translateY(0px);
}
&.vertical {
@ -229,7 +241,7 @@
&.displayed {
&:not(.vertical) {
animation: activeThumbnail 300ms ease-in 0s forwards;
transform: translateY(-15px);
}
}
@ -258,16 +270,6 @@
}
}
@keyframes activeThumbnail {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-15px);
}
}
@keyframes bounce {
from {
transform: translateY(0);

@ -7,7 +7,7 @@
{#if $coWebsites.length > 0}
<div id="cowebsite-thumbnail-container" class:vertical>
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.iframe.id)}
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.getId())}
<CoWebsiteThumbnail {index} {coWebsite} {vertical} />
{/each}
</div>

@ -9,13 +9,11 @@
function closeCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
if ($highlightedEmbedScreen.embed.closable) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted closing");
});
if ($highlightedEmbedScreen.embed.isClosable()) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed);
} else {
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted unloading");
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch((err) => {
console.error("Cannot unload co-website", err);
});
}
}
@ -68,9 +66,9 @@
/>
{/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.iframe.id}
{#key $highlightedEmbedScreen.embed.getId()}
<div
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.iframe.id}
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
class="highlighted-cowebsite nes-container is-rounded"
>
<div class="actions">

@ -53,7 +53,7 @@
<section class="terms-and-conditions">
<a style="display: none;" href="traduction">Need for traduction</a>
<p>
{$LL.login.terms()}
{@html $LL.login.terms()}
</p>
</section>
{/if}

@ -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>

@ -69,6 +69,7 @@
} else {
const customMenu = customMenuIframe.get(menu.label);
if (customMenu !== undefined) {
activeSubMenu = menu;
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {

@ -9,4 +9,7 @@ export interface UserInputHandlerInterface {
handlePointerUpEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handlePointerDownEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handleSpaceKeyUpEvent: (event: Event) => Event;
addSpaceEventListener: (callback: Function) => void;
removeSpaceEventListner: (callback: Function) => void;
}

@ -159,6 +159,27 @@ export abstract class Character extends Container implements OutlineableInterfac
return { x: this.x, y: this.y };
}
/**
* Returns position based on where player is currently facing
* @param shift How far from player should the point of interest be.
*/
public getDirectionalActivationPosition(shift: number): { x: number; y: number } {
switch (this.lastDirection) {
case PlayerAnimationDirections.Down: {
return { x: this.x, y: this.y + shift };
}
case PlayerAnimationDirections.Left: {
return { x: this.x - shift, y: this.y };
}
case PlayerAnimationDirections.Right: {
return { x: this.x + shift, y: this.y };
}
case PlayerAnimationDirections.Up: {
return { x: this.x, y: this.y - shift };
}
}
}
public getObjectToOutline(): Phaser.GameObjects.GameObject {
return this.playerNameText;
}
@ -455,16 +476,16 @@ export abstract class Character extends Container implements OutlineableInterfac
this.outlineColorStore.removeApiColor();
}
public pointerOverOutline(): void {
this.outlineColorStore.pointerOver();
public pointerOverOutline(color: number): void {
this.outlineColorStore.pointerOver(color);
}
public pointerOutOutline(): void {
this.outlineColorStore.pointerOut();
}
public characterCloseByOutline(): void {
this.outlineColorStore.characterCloseBy();
public characterCloseByOutline(color: number): void {
this.outlineColorStore.characterCloseBy(color);
}
public characterFarAwayOutline(): void {

@ -11,6 +11,11 @@ export class ActivatablesManager {
private currentPlayer: Player;
private canSelectByDistance: boolean = true;
private readonly outlineColor = 0xffff00;
private readonly directionalActivationPositionShift = 50;
constructor(currentPlayer: Player) {
this.currentPlayer = currentPlayer;
}
@ -27,7 +32,7 @@ export class ActivatablesManager {
}
this.selectedActivatableObjectByPointer = object;
if (isOutlineable(this.selectedActivatableObjectByPointer)) {
this.selectedActivatableObjectByPointer?.pointerOverOutline();
this.selectedActivatableObjectByPointer?.pointerOverOutline(this.outlineColor);
}
}
@ -37,7 +42,7 @@ export class ActivatablesManager {
}
this.selectedActivatableObjectByPointer = undefined;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterCloseByOutline();
this.selectedActivatableObjectByDistance?.characterCloseByOutline(this.outlineColor);
}
}
@ -46,6 +51,9 @@ export class ActivatablesManager {
}
public deduceSelectedActivatableObjectByDistance(): void {
if (!this.canSelectByDistance) {
return;
}
const newNearestObject = this.findNearestActivatableObject();
if (this.selectedActivatableObjectByDistance === newNearestObject) {
return;
@ -60,10 +68,42 @@ export class ActivatablesManager {
}
this.selectedActivatableObjectByDistance = newNearestObject;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterCloseByOutline();
this.selectedActivatableObjectByDistance?.characterCloseByOutline(this.outlineColor);
}
}
public updateActivatableObjectsDistances(objects: ActivatableInterface[]): void {
const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition(
this.directionalActivationPositionShift
);
for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance);
}
}
public updateDistanceForSingleActivatableObject(object: ActivatableInterface): void {
this.activatableObjectsDistances.set(
object,
MathUtils.distanceBetween(
this.currentPlayer.getDirectionalActivationPosition(this.directionalActivationPositionShift),
object.getPosition()
)
);
}
public disableSelectingByDistance(): void {
this.canSelectByDistance = false;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
}
this.selectedActivatableObjectByDistance = undefined;
}
public enableSelectingByDistance(): void {
this.canSelectByDistance = true;
}
private findNearestActivatableObject(): ActivatableInterface | undefined {
let shortestDistance: number = Infinity;
let closestObject: ActivatableInterface | undefined = undefined;
@ -76,18 +116,8 @@ export class ActivatablesManager {
}
return closestObject;
}
public updateActivatableObjectsDistances(objects: ActivatableInterface[]): void {
const currentPlayerPos = this.currentPlayer.getPosition();
for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance);
}
}
public updateDistanceForSingleActivatableObject(object: ActivatableInterface): void {
this.activatableObjectsDistances.set(
object,
MathUtils.distanceBetween(this.currentPlayer.getPosition(), object.getPosition())
);
public isSelectingByDistanceEnabled(): boolean {
return this.canSelectByDistance;
}
}

@ -26,15 +26,6 @@ export class Game extends Phaser.Game {
}
}
});
/*window.addEventListener('resize', (event) => {
// Let's trigger the onResize method of any active scene that is a ResizableScene
for (const scene of this.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
scene.onResize(event);
}
}
});*/
}
public step(time: number, delta: number) {

@ -85,6 +85,7 @@ export class GameMap {
phaserMap
.createLayer(layer.name, terrains, (layer.x || 0) * 32, (layer.y || 0) * 32)
.setDepth(depth)
.setScrollFactor(layer.parallaxx ?? 1, layer.parallaxy ?? 1)
.setAlpha(layer.opacity)
.setVisible(layer.visible)
.setSize(layer.width, layer.height)
@ -120,7 +121,7 @@ export class GameMap {
return [];
}
public getCollisionsGrid(): number[][] {
public getCollisionGrid(): number[][] {
const grid: number[][] = [];
for (let y = 0; y < this.map.height; y += 1) {
const row: number[] = [];
@ -322,12 +323,19 @@ 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);
}
private isCollidingAt(x: number, y: number): boolean {
for (const layer of this.phaserLayers) {
if (!layer.visible) {
continue;
}
if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) {
return true;
}

@ -20,6 +20,7 @@ export enum GameMapProperties {
OPEN_WEBSITE = "openWebsite",
OPEN_WEBSITE_ALLOW_API = "openWebsiteAllowApi",
OPEN_WEBSITE_POLICY = "openWebsitePolicy",
OPEN_WEBSITE_WIDTH = "openWebsiteWidth",
OPEN_WEBSITE_POSITION = "openWebsitePosition",
OPEN_WEBSITE_TRIGGER = "openWebsiteTrigger",
OPEN_WEBSITE_TRIGGER_MESSAGE = "openWebsiteTriggerMessage",

@ -1,33 +1,36 @@
import type { GameScene } from "./GameScene";
import type { GameMap } from "./GameMap";
import { scriptUtils } from "../../Api/ScriptUtils";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { ON_ACTION_TRIGGER_BUTTON, ON_ICON_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
enum OpenCoWebsiteState {
ASLEEP,
OPENED,
MUST_BE_CLOSE,
}
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 {
coWebsite: CoWebsite;
state: OpenCoWebsiteState;
actionId: string;
coWebsite?: CoWebsite;
}
export class GameMapPropertiesListener {
private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>();
private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>();
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");
@ -38,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",
@ -53,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 = () => {
@ -64,6 +190,7 @@ export class GameMapPropertiesListener {
let openWebsiteProperty: string | undefined;
let allowApiProperty: boolean | undefined;
let websitePolicyProperty: string | undefined;
let websiteWidthProperty: number | undefined;
let websitePositionProperty: number | undefined;
let websiteTriggerProperty: string | undefined;
let websiteTriggerMessageProperty: string | undefined;
@ -79,6 +206,9 @@ export class GameMapPropertiesListener {
case GameMapProperties.OPEN_WEBSITE_POLICY:
websitePolicyProperty = property.value as string | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_WIDTH:
websiteWidthProperty = property.value as number | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_POSITION:
websitePositionProperty = property.value as number | undefined;
break;
@ -95,55 +225,75 @@ export class GameMapPropertiesListener {
return;
}
const actionUuid = "openWebsite-" + (Math.random() + 1).toString(36).substring(7);
const actionId = "openWebsite-" + (Math.random() + 1).toString(36).substring(7);
if (this.coWebsitesOpenByLayer.has(layer)) {
return;
}