Merge branch 'develop' into pre-compile-front

This commit is contained in:
Lukas Hass 2022-01-25 18:02:04 +01:00
commit 4108076373
No known key found for this signature in database
GPG Key ID: 7C8CEF72C4039178
102 changed files with 2254 additions and 347 deletions

View File

@ -10,7 +10,6 @@ on:
pull_request:
jobs:
continuous-integration-front:
name: "Continuous Integration Front"
@ -46,6 +45,10 @@ jobs:
run: ./templater.sh
working-directory: "front"
- name: "Generate i18n files"
run: yarn run typesafe-i18n
working-directory: "front"
- name: "Build"
run: yarn run build
env:

View File

@ -43,6 +43,10 @@ jobs:
run: ./templater.sh
working-directory: "front"
- name: "Generate i18n files"
run: yarn run typesafe-i18n
working-directory: "front"
- name: "Build"
run: yarn run build-typings
env:

View File

@ -107,3 +107,7 @@ $ LIVE_RELOAD=0 docker-compose up -d
# Wait 2-3 minutes for the environment to start, then:
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
```
### A bad wording or a missing language
If you notice a translation error or missing language you can help us by following the [how to translate](docs/dev/how-to-translate.md) documentation.

View File

@ -1504,9 +1504,9 @@ natural-compare@^1.4.0:
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-fetch@^2.6.5:
version "2.6.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"

View File

@ -0,0 +1,76 @@
# How to translate WorkAdventure
We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation.
## Add a new language
It is very easy to add a new language!
First, in the `front/src/i18n` folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)).
In the previously created folder, add a file named index.ts with the following content containing your language information (french from France in this example):
```ts
import type { Translation } from "../i18n-types";
const fr_FR: Translation = {
...en_US,
language: "Français",
country: "France",
};
export default fr_FR;
```
## Add a new key
### Add a simple key
The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object.
Please use kamelcase to name your keys!
Example:
```ts
{
messages: {
coffeMachine: {
start: "Coffe machine has been started!";
}
}
}
```
In the code you can translate using `$LL`:
```ts
import LL from "../../i18n/i18n-svelte";
console.log($LL.messages.coffeMachine.start());
```
### Add a key with parameters
You can also use parameters to make the translation dynamic.
Use the tag { [parameter name] } to apply your parameters in the translations
Example:
```ts
{
messages: {
coffeMachine: {
playerStart: "{ playerName } started the coffee machine!";
}
}
}
```
In the code you can use it like this:
```ts
$LL.messages.coffeMachine.playerStart.start({
playerName: "John",
});
```

View File

@ -36,6 +36,23 @@ WA.onInit().then(() => {
})
```
### Get the player language
```
WA.player.language: string;
```
The current language of player is available from the `WA.player.language` property.
{.alert.alert-info}
You need to wait for the end of the initialization before accessing `WA.player.language`
```typescript
WA.onInit().then(() => {
console.log('Player language: ', WA.player.language);
})
```
### Get the tags of the player
```
@ -173,6 +190,37 @@ Example:
WA.player.state.toto //will retrieve the variable
```
### Move player to position
```typescript
WA.player.moveTo(x: number, y: number, speed?: number): Promise<{ x: number, y: number, cancelled: boolean }>;
```
Player will try to find shortest path to the destination point and proceed to move there.
```typescript
// Let's move player to x: 250 y: 250 with speed of 10
WA.player.moveTo(250, 250, 10);
```
You can also chain movement like this:
```typescript
// Player will move to the next point after reaching first one
await WA.player.moveTo(250, 250, 10);
await WA.player.moveTo(500, 0, 10);
```
Or like this:
```typescript
// Player will move to the next point after reaching first one or stop if the movement was cancelled
WA.player.moveTo(250, 250, 10).then((result) => {
if (!result.cancelled) {
WA.player.moveTo(500, 0, 10);
}
});
```
It is possible to get the information about current player's position on stop and if the movement was interrupted
```typescript
// Result will store x and y of Player at the moment of movement's end and information if the movement was interrupted
const result = await WA.player.moveTo(250, 250, 10);
// result: { x: number, y: number, cancelled: boolean }
```
### Set the outline color of the player
```
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;

View File

@ -65,3 +65,24 @@ How to use entry point :
* To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`".
* You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
## Defining destination point with moveTo parameter
We are able to direct a Woka to the desired place immediately after spawn. To make users spawn on an entry point and then, walk automatically to a meeting room, simply add `moveTo` as an additional parameter of URL:
*Use default entry point*
```
.../my_map.json#&moveTo=exit
```
*Define entry point and moveTo parameter like this...*
```
.../my_map.json#start&moveTo=meeting-room
```
*...or like this*
```
.../my_map.json#moveTo=meeting-room&start
```
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.
![](images/moveTo-layer-example.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -8,7 +8,7 @@ module.exports = {
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
"plugin:@typescript-eslint/recommended-requiring-type-checking",
],
"globals": {
"Atomics": "readonly",
@ -23,7 +23,7 @@ module.exports = {
},
"plugins": [
"@typescript-eslint",
"svelte3"
"svelte3",
],
"overrides": [
{
@ -33,6 +33,7 @@ module.exports = {
],
"rules": {
"no-unused-vars": "off",
"eol-last": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error",
"no-throw-literal": "error",
// TODO: remove those ignored rules and write a stronger code!

View File

@ -1,2 +1,5 @@
src/Messages/generated
src/Messages/JsonMessages
src/i18n/i18n-svelte.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts

View File

@ -0,0 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json",
"baseLocale": "en-US",
"adapter": "svelte"
}

View File

@ -0,0 +1 @@
*.json

View File

@ -64,10 +64,12 @@
"socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4",
"ts-proto": "^1.96.0",
"uuidv4": "^6.2.10"
"typesafe-i18n": "^2.59.0",
"uuidv4": "^6.2.10",
"zod": "^3.11.6"
},
"scripts": {
"start": "run-p templater serve svelte-check-watch",
"start": "run-p templater serve svelte-check-watch typesafe-i18n",
"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",
@ -79,7 +81,8 @@
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
"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}'"
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'",
"typesafe-i18n": "typesafe-i18n --no-watch"
},
"lint-staged": {
"*.svelte": [

View File

@ -5,6 +5,7 @@ export const isGameStateEvent = new tg.IsInterface()
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isString,
language: tg.isUnion(tg.isString, tg.isUndefined),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),

View File

@ -34,6 +34,8 @@ import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import type { CameraSetEvent } from "./CameraSetEvent";
import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
import { isColorEvent } from "./ColorEvent";
import { isMovePlayerToEventConfig } from "./MovePlayerToEvent";
import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -173,6 +175,10 @@ export const iframeQueryMapTypeGuards = {
query: tg.isUndefined,
answer: isPlayerPosition,
},
movePlayerTo: {
query: isMovePlayerToEventConfig,
answer: isMovePlayerToEventAnswer,
},
};
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isMovePlayerToEventConfig = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
speed: tg.isOptional(tg.isNumber),
})
.get();
export type MovePlayerToEvent = tg.GuardedType<typeof isMovePlayerToEventConfig>;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isMovePlayerToEventAnswer = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
cancelled: tg.isBoolean,
})
.get();
export type MovePlayerToEventAnswer = tg.GuardedType<typeof isMovePlayerToEventAnswer>;

View File

@ -13,6 +13,12 @@ export const setPlayerName = (name: string) => {
playerName = name;
};
let playerLanguage: string | undefined;
export const setPlayerLanguage = (language: string | undefined) => {
playerLanguage = language;
};
let tags: string[] | undefined;
export const setTags = (_tags: string[]) => {
@ -61,6 +67,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
return playerName;
}
get language(): string {
if (playerLanguage === undefined) {
throw new Error(
"Player language not initialized yet. You should call WA.player.language within a WA.onInit callback."
);
}
return playerLanguage;
}
get tags(): string[] {
if (tags === undefined) {
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
@ -84,6 +99,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
});
}
public async moveTo(x: number, y: number, speed?: number): Promise<{ x: number; y: number; cancelled: boolean }> {
return await queryWorkadventure({
type: "movePlayerTo",
data: { x, y, speed },
});
}
get userRoomToken(): string | undefined {
if (userRoomToken === undefined) {
throw new Error(

View File

@ -5,6 +5,7 @@
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import { onDestroy, onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement;
@ -144,7 +145,7 @@
</div>
<div class="audio-manager-reduce-conversation">
<label>
reduce in conversations
{$LL.audio.manager.reduce()}
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
</label>
<section class="audio-manager-file">

View File

@ -5,6 +5,7 @@
import ChatElement from "./ChatElement.svelte";
import { afterUpdate, beforeUpdate, onMount } from "svelte";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import LL from "../../i18n/i18n-svelte";
let listDom: HTMLElement;
let chatWindowElement: HTMLElement;
@ -45,7 +46,7 @@
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
<li><p class="system-text">Here is your chat history:</p></li>
<li><p class="system-text">{$LL.chat.intro()}</p></li>
{#each $chatMessagesStore as message, i}
<li><ChatElement {message} line={i} /></li>
{/each}

View File

@ -1,4 +1,5 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore";
export const handleForm = {
@ -27,7 +28,7 @@
<input
type="text"
bind:value={newMessageText}
placeholder="Enter your message..."
placeholder={$LL.chat.enter()}
on:focus={onFocus}
on:blur={onBlur}
bind:this={inputElement}

View File

@ -1,4 +1,5 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import { requestVisitCardsStore } from "../../Stores/GameStore";
@ -12,8 +13,12 @@
</script>
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li>
<li><button class="text-btn" disabled>Add friend</button></li>
<li>
<button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}
>{$LL.chat.menu.visitCard()}</button
>
</li>
<li><button class="text-btn" disabled>{$LL.chat.menu.addFriend}</button></li>
</ul>
<style lang="scss">

View File

@ -2,6 +2,7 @@
import type { Game } from "../../Phaser/Game/Game";
import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
import { activeRowStore } from "../../Stores/CustomCharacterStore";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
@ -34,7 +35,7 @@
<form class="customCharacterScene">
<section class="text-center">
<h2>Customize your WOKA</h2>
<h2>{$LL.woka.customWoka.title()}</h2>
</section>
<section class="action action-move">
<button
@ -53,26 +54,29 @@
<section class="action">
{#if $activeRowStore === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene}
>Return</button
>{$LL.woka.customWoka.navigation.return()}</button
>
{/if}
{#if $activeRowStore !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp}
>Back <img src="resources/objects/arrow_up_black.png" alt="" /></button
>{$LL.woka.customWoka.navigation.back()}
<img src="resources/objects/arrow_up_black.png" alt="" /></button
>
{/if}
{#if $activeRowStore === 5}
<button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={finish}>Finish</button
on:click|preventDefault={finish}>{$LL.woka.customWoka.navigation.finish()}</button
>
{/if}
{#if $activeRowStore !== 5}
<button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectDown}>Next <img src="resources/objects/arrow_down.png" alt="" /></button
on:click|preventDefault={selectDown}
>{$LL.woka.customWoka.navigation.next()}
<img src="resources/objects/arrow_down.png" alt="" /></button
>
{/if}
</section>

View File

@ -13,6 +13,7 @@
import cinemaCloseImg from "../images/cinema-close.svg";
import cinemaImg from "../images/cinema.svg";
import microphoneImg from "../images/microphone.svg";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
let selectedCamera: string | undefined = undefined;
@ -76,7 +77,7 @@
<form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="text-center">
<h2>Turn on your camera and microphone</h2>
<h2>{$LL.camera.enable.title()}</h2>
</section>
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline />
@ -121,7 +122,7 @@
{/if}
</section>
<section class="action">
<button type="submit" class="nes-btn is-primary letsgo">Let's go!</button>
<button type="submit" class="nes-btn is-primary letsgo">{$LL.camera.enable.start()}</button>
</section>
</form>

View File

@ -4,13 +4,14 @@ vim: ft=typescript
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
import LL from "../../i18n/i18n-svelte";
const gameScene = gameManager.getCurrentGameScene();
function name(userId: number): string | undefined {
return gameScene.MapPlayersByKey.get(userId)?.PlayerValue;
function name(userId: number): string {
const user = gameScene.MapPlayersByKey.get(userId);
return user ? user.PlayerValue : "";
}
function sendFollowRequest() {
@ -42,11 +43,15 @@ vim: ft=typescript
{#if $followStateStore === "requesting" && $followRoleStore === "follower"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Do you want to follow {name($followUsersStore[0])}?</h2>
<h2>{$LL.follow.interactMenu.title.follow({ leader: name($followUsersStore[0]) })}</h2>
</section>
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}>Yes</button>
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button>
<button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}
>{$LL.follow.interactMenu.yes()}</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}
>{$LL.follow.interactMenu.no()}</button
>
</section>
</div>
{/if}
@ -54,20 +59,24 @@ vim: ft=typescript
{#if $followStateStore === "ending"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Interaction</h2>
<h2>{$LL.follow.interactMenu.title.interact()}</h2>
</section>
{#if $followRoleStore === "follower"}
<section class="interact-menu-question">
<p>Do you want to stop following {name($followUsersStore[0])}?</p>
<p>{$LL.follow.interactMenu.stop.follower({ leader: name($followUsersStore[0]) })}</p>
</section>
{:else if $followRoleStore === "leader"}
<section class="interact-menu-question">
<p>Do you want to stop leading the way?</p>
<p>{$LL.follow.interactMenu.stop.leader()}</p>
</section>
{/if}
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={reset}>Yes</button>
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}>No</button>
<button type="button" class="nes-btn is-success" on:click|preventDefault={reset}
>{$LL.follow.interactMenu.yes()}</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}
>{$LL.follow.interactMenu.no()}</button
>
</section>
</div>
{/if}
@ -76,18 +85,24 @@ vim: ft=typescript
<div class="interact-status nes-container is-rounded">
<section class="interact-status">
{#if $followRoleStore === "follower"}
<p>Following {name($followUsersStore[0])}</p>
<p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 0}
<p>Waiting for followers' confirmation</p>
<p>{$LL.follow.interactStatus.waitingFollowers()}</p>
{:else if $followUsersStore.length === 1}
<p>{name($followUsersStore[0])} is following you</p>
<p>{$LL.follow.interactStatus.followed.one({ follower: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 2}
<p>{name($followUsersStore[0])} and {name($followUsersStore[1])} are following you</p>
<p>
{$LL.follow.interactStatus.followed.two({
firstFollower: name($followUsersStore[0]),
secondFollower: name($followUsersStore[1]),
})}
</p>
{:else}
<p>
{$followUsersStore.slice(0, -1).map(name).join(", ")} and {name(
$followUsersStore[$followUsersStore.length - 1]
)} are following you
{$LL.follow.interactStatus.followed.many({
followers: $followUsersStore.slice(0, -1).map(name).join(", "),
lastFollower: name($followUsersStore[$followUsersStore.length - 1]),
})}
</p>
{/if}
</section>

View File

@ -4,6 +4,7 @@
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
import LL from "../../i18n/i18n-svelte";
let isAndroid = isAndroidFct();
let isFirefox = getNavigatorType() === NavigatorType.firefox;
@ -24,14 +25,13 @@
transition:fly={{ y: -900, duration: 500 }}
>
<section>
<h2>Camera / Microphone access needed</h2>
<p class="err">Permission denied</p>
<p>You must allow camera and microphone access in your browser.</p>
<h2>{$LL.camera.help.title()}</h2>
<p class="err">{$LL.camera.help.permissionDenied()}</p>
<p>{$LL.camera.help.content()}</p>
<p>
{#if isFirefox}
<p class="err">
Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the
authorization.
{$LL.camera.help.firefoxContent()}
</p>
<img src={firefoxImg} alt="" />
{:else if isChrome && !isAndroid}
@ -40,9 +40,11 @@
</p>
</section>
<section>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}
>{$LL.camera.help.refresh()}</button
>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}
>Continue without webcam</button
>{$LL.camera.help.continue()}</button
>
</section>
</form>

View File

@ -4,6 +4,7 @@
import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png";
import { gameManager } from "../../Phaser/Game/GameManager";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
@ -27,7 +28,7 @@
<img src={logoImg} alt="WorkAdventure logo" />
</section>
<section class="text-center">
<h2>Enter your name</h2>
<h2>{$LL.login.input.name.placeholder()}</h2>
</section>
<!-- svelte-ignore a11y-autofocus -->
<input
@ -44,22 +45,20 @@
/>
<section class="error-section">
{#if name.trim() === "" && startValidating}
<p class="err">The name is empty</p>
<p class="err">{$LL.login.input.name.empty()}</p>
{/if}
</section>
{#if DISPLAY_TERMS_OF_USE}
<section class="terms-and-conditions">
<a style="display: none;" href="traduction">Need for traduction</a>
<p>
By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank"
>terms of use</a
>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and
<a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.
{$LL.login.terms()}
</p>
</section>
{/if}
<section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">{$LL.login.continue()}</button>
</section>
</form>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import { onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
let gameScene = gameManager.getCurrentGameScene();
@ -11,7 +12,7 @@
let mapName: string = "";
let mapLink: string = "";
let mapDescription: string = "";
let mapCopyright: string = "The map creator did not declare a copyright for the map.";
let mapCopyright: string = $LL.menu.about.copyrights.map.empty();
let tilesetCopyright: string[] = [];
let audioCopyright: string[] = [];
@ -62,41 +63,37 @@
</script>
<div class="about-room-main">
<h2>Information on the map</h2>
<h2>{$LL.menu.about.mapInfo()}</h2>
<section class="container-overflow">
<h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p>
{#if mapLink}
<p class="string-HTML">&gt; <a href={mapLink} target="_blank">link to this map</a> &lt;</p>
<p class="string-HTML">
&gt; <a href={mapLink} target="_blank">{$LL.menu.about.mapLink()}</a> &lt;
</p>
{/if}
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
Copyrights of the map
{$LL.menu.about.copyrights.map.title()}
</h3>
<p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}>
Copyrights of the tilesets
{$LL.menu.about.copyrights.tileset.title()}
</h3>
<section hidden={!expandedTilesetCopyright}>
{#each tilesetCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>
The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets
have no license.
</p>
<p>{$LL.menu.about.copyrights.tileset.empty()}</p>
{/each}
</section>
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
Copyrights of audio files
{$LL.menu.about.copyrights.audio.title()}
</h3>
<section hidden={!expandedAudioCopyright}>
{#each audioCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>
The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
have no license.
</p>
<p>{$LL.menu.about.copyrights.audio.empty()}</p>
{/each}
</section>
</section>

View File

@ -4,6 +4,7 @@
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import uploadFile from "../images/music-file.svg";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import LL from "../../i18n/i18n-svelte";
interface EventTargetFiles extends EventTarget {
files: Array<File>;
@ -76,7 +77,7 @@
<img
class="nes-pointer"
src={uploadFile}
alt="Upload a file"
alt={$LL.menu.globalAudio.uploadInfo()}
on:click|preventDefault={() => {
fileInput.click();
}}
@ -85,7 +86,7 @@
<p>{fileName} : {fileSize}</p>
{/if}
{#if errorFile}
<p class="err">No file selected. You need to upload a file before sending it.</p>
<p class="err">{$LL.menu.globalAudio.error()}</p>
{/if}
<input
type="file"

View File

@ -1,4 +1,7 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { contactPageStore } from "../../Stores/MenuStore";
function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
@ -8,25 +11,24 @@
const sparkHost = "https://workadventu.re/map-building/";
window.open(sparkHost, "_blank");
}
import { contactPageStore } from "../../Stores/MenuStore";
</script>
<div class="create-map-main">
<section class="container-overflow">
<section>
<h3>Getting started</h3>
<p>
WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all
starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
<h3>{$LL.menu.contact.gettingStarted.title()}</h3>
<p>{$LL.menu.contact.gettingStarted.description()}</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}
>{$LL.menu.contact.gettingStarted.title()}</button
>
</section>
<section>
<h3>Create your map</h3>
<p>You can also create your own custom map by following the step of the documentation.</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
<h3>{$LL.menu.contact.createMap.title()}</h3>
<p>{$LL.menu.contact.createMap.description()}</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}
>{$LL.menu.contact.createMap.title()}</button
>
</section>
<iframe

View File

@ -1,6 +1,7 @@
<script lang="ts">
import TextGlobalMessage from "./TextGlobalMessage.svelte";
import AudioGlobalMessage from "./AudioGlobalMessage.svelte";
import LL from "../../i18n/i18n-svelte";
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
@ -35,14 +36,14 @@
<button
type="button"
class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateInputText}>Text</button
on:click|preventDefault={activateInputText}>{$LL.menu.globalMessage.text()}</button
>
</section>
<section>
<button
type="button"
class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateUploadAudio}>Audio</button
on:click|preventDefault={activateUploadAudio}>{$LL.menu.globalMessage.audio()}</button
>
</section>
</div>
@ -57,10 +58,10 @@
<div class="global-message-footer">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld} />
<span>Broadcast to all rooms of the world</span>
<span>{$LL.menu.globalMessage.warning()}</span>
</label>
<section>
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button>
<button class="nes-btn is-primary" on:click|preventDefault={send}>{$LL.menu.globalMessage.send()}</button>
</section>
</div>
</div>

View File

@ -1,4 +1,6 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
input.focus();
@ -21,14 +23,14 @@
<div class="guest-main">
<section class="container-overflow">
<section class="share-url not-mobile">
<h3>Share the link of the room!</h3>
<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}>Copy</button>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section>
<section class="is-mobile">
<h3>Share the link of the room!</h3>
<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}>Share</button>
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section>
</section>
</div>

View File

@ -14,26 +14,27 @@
SubMenusInterface,
subMenusStore,
} from "../../Stores/MenuStore";
import type { MenuItem } from "../../Stores/MenuStore";
import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import LL from "../../i18n/i18n-svelte";
let activeSubMenu: string = SubMenusInterface.profile;
let activeSubMenu: MenuItem = $subMenusStore[0];
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
let props: { url: string; allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if (!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.profile);
if (!$subMenusStore.includes(activeSubMenu)) {
switchMenu($subMenusStore[0]);
}
});
checkSubMenuToShow();
switchMenu(SubMenusInterface.profile);
switchMenu($subMenusStore[0]);
});
onDestroy(() => {
@ -42,10 +43,10 @@
}
});
function switchMenu(menu: string) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
function switchMenu(menu: MenuItem) {
if (menu.type === "translated") {
activeSubMenu = menu;
switch (menu) {
switch (menu.key) {
case SubMenusInterface.settings:
activeComponent = SettingsSubMenu;
break;
@ -64,36 +65,46 @@
case SubMenusInterface.contact:
activeComponent = ContactSubMenu;
break;
default: {
const customMenu = customMenuIframe.get(menu);
}
} else {
const customMenu = customMenuIframe.get(menu.label);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu);
sendMenuClickedEvent(menu.label);
menuVisiblilityStore.set(false);
}
break;
}
}
} else throw new Error("There is no menu called " + menu);
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
closeMenu();
}
}
function translateMenuName(menu: MenuItem) {
if (menu.type === "scripting") {
return menu.label;
}
// Bypass the proxy of typesafe for getting the menu name : https://github.com/ivanhofer/typesafe-i18n/issues/156
const getMenuName = $LL.menu.sub[menu.key];
return getMenuName();
}
</script>
<svelte:window on:keydown={onKeyDown} />
<div class="menu-container-main">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>Menu</h2>
<h2>{$LL.menu.title()}</h2>
<nav>
{#each $subMenusStore as submenu}
<button
@ -101,14 +112,14 @@
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => switchMenu(submenu)}
>
{submenu}
{translateMenuName(submenu)}
</button>
{/each}
</nav>
</div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{activeSubMenu}</h2>
<h2>{translateMenuName(activeSubMenu)}</h2>
<svelte:component this={activeComponent} {...props} />
</div>
</div>

View File

@ -9,6 +9,7 @@
import { get } from "svelte/store";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
import LL from "../../i18n/i18n-svelte";
function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore));
@ -29,11 +30,21 @@
<main class="menuIcon">
{#if $limitMapStore}
<img src={logoInvite} alt="open menu" class="nes-pointer" on:click|preventDefault={showInvite} />
<img src={logoRegister} alt="open menu" class="nes-pointer" on:click|preventDefault={register} />
<img
src={logoInvite}
alt={$LL.menu.icon.open.invite()}
class="nes-pointer"
on:click|preventDefault={showInvite}
/>
<img
src={logoRegister}
alt={$LL.menu.icon.open.register()}
class="nes-pointer"
on:click|preventDefault={register}
/>
{:else}
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
<img src={logoWA} alt={$LL.menu.icon.open.menu()} class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt={$LL.menu.icon.open.chat()} class="nes-pointer" on:click|preventDefault={showChat} />
{/if}
</main>

View File

@ -17,6 +17,7 @@
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
import Woka from "../Woka/Woka.svelte";
import Companion from "../Companion/Companion.svelte";
import LL from "../../i18n/i18n-svelte";
function disableMenuStores() {
menuVisiblilityStore.set(false);
@ -62,20 +63,20 @@
<div class="submenu">
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
<img src={btnProfileSubMenuIdentity} alt="Edit your name" />
<span class="btn-hover">Edit your name</span>
<img src={btnProfileSubMenuIdentity} alt={$LL.menu.profile.edit.name()} />
<span class="btn-hover">{$LL.menu.profile.edit.name()}</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
<span class="btn-hover">Edit your WOKA</span>
<span class="btn-hover">{$LL.menu.profile.edit.woka()}</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
<span class="btn-hover">Edit your companion</span>
<span class="btn-hover">{$LL.menu.profile.edit.companion()}</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
<img src={btnProfileSubMenuCamera} alt="Edit your camera" />
<span class="btn-hover">Edit your camera</span>
<img src={btnProfileSubMenuCamera} alt={$LL.menu.profile.edit.camera()} />
<span class="btn-hover">{$LL.menu.profile.edit.camera()}</span>
</button>
</section>
</div>
@ -88,11 +89,13 @@
{/if}
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}>Log out</button>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}
>{$LL.menu.profile.logout()}</button
>
</section>
{:else}
<section>
<a type="button" class="nes-btn" href="/login">Sign in</a>
<a type="button" class="nes-btn" href="/login">{$LL.menu.profile.login()}</a>
</section>
{/if}
</div>

View File

@ -4,6 +4,9 @@
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { menuVisiblilityStore } from "../../Stores/MenuStore";
import LL, { locale } from "../../i18n/i18n-svelte";
import type { Locales } from "../../i18n/i18n-types";
import { displayableLocales, setCurrentLocale } from "../../i18n/locales";
let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted";
@ -11,14 +14,17 @@
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue();
let valueLocale: string = $locale;
let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
let previewValueLocale = valueLocale;
function saveSetting() {
if (valueGame !== previewValueGame) {
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame);
window.location.reload();
let change = false;
if (valueLocale !== previewValueLocale) {
previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales);
}
if (valueVideo !== previewValueVideo) {
@ -26,6 +32,16 @@
videoConstraintStore.setFrameRate(valueVideo);
}
if (valueGame !== previewValueGame) {
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame);
change = true;
}
if (change) {
window.location.reload();
}
closeMenu();
}
@ -73,34 +89,74 @@
<div class="settings-main" on:submit|preventDefault={saveSetting}>
<section>
<h3>Game quality</h3>
<h3>{$LL.menu.settings.gameQuality.title()}</h3>
<div class="nes-select is-dark">
<select bind:value={valueGame}>
<option value={120}>{isMobile() ? "High (120 fps)" : "High video quality (120 fps)"}</option>
<option value={60}
>{isMobile() ? "Medium (60 fps)" : "Medium video quality (60 fps, recommended)"}</option
<option value={120}
>{isMobile()
? $LL.menu.settings.gameQuality.short.high()
: $LL.menu.settings.gameQuality.long.high()}</option
>
<option value={60}
>{isMobile()
? $LL.menu.settings.gameQuality.short.medium()
: $LL.menu.settings.gameQuality.long.medium()}</option
>
<option value={40}
>{isMobile()
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
>
<option value={20}
>{isMobile()
? $LL.menu.settings.gameQuality.short.small()
: $LL.menu.settings.gameQuality.long.small()}</option
>
<option value={40}>{isMobile() ? "Minimum (40 fps)" : "Minimum video quality (40 fps)"}</option>
<option value={20}>{isMobile() ? "Small (20 fps)" : "Small video quality (20 fps)"}</option>
</select>
</div>
</section>
<section>
<h3>Video quality</h3>
<h3>{$LL.menu.settings.videoQuality.title()}</h3>
<div class="nes-select is-dark">
<select bind:value={valueVideo}>
<option value={30}>{isMobile() ? "High (30 fps)" : "High video quality (30 fps)"}</option>
<option value={20}
>{isMobile() ? "Medium (20 fps)" : "Medium video quality (20 fps, recommended)"}</option
<option value={30}
>{isMobile()
? $LL.menu.settings.videoQuality.short.high()
: $LL.menu.settings.videoQuality.long.high()}</option
>
<option value={10}>{isMobile() ? "Minimum (10 fps)" : "Minimum video quality (10 fps)"}</option>
<option value={5}>{isMobile() ? "Small (5 fps)" : "Small video quality (5 fps)"}</option>
<option value={20}
>{isMobile()
? $LL.menu.settings.videoQuality.short.medium()
: $LL.menu.settings.videoQuality.long.medium()}</option
>
<option value={10}
>{isMobile()
? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option
>
<option value={5}
>{isMobile()
? $LL.menu.settings.videoQuality.short.small()
: $LL.menu.settings.videoQuality.long.small()}</option
>
</select>
</div>
</section>
<section>
<h3>{$LL.menu.settings.language.title()}</h3>
<div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLocale}>
{#each displayableLocales as locale (locale.id)}
<option value={locale.id}>{`${locale.language} (${locale.country})`}</option>
{/each}
</select>
</div>
</section>
<section class="settings-section-save">
<p>(Saving these settings will restart the game)</p>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}>Save</button>
<p>{$LL.menu.settings.save.warning()}</p>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}
>{$LL.menu.settings.save.button()}</button
>
</section>
<section class="settings-section-noSaveOption">
<label>
@ -110,7 +166,7 @@
bind:checked={fullscreen}
on:change={changeFullscreen}
/>
<span>Fullscreen</span>
<span>{$LL.menu.settings.fullscreen()}</span>
</label>
<label>
<input
@ -119,7 +175,7 @@
bind:checked={notification}
on:change={changeNotification}
/>
<span>Notifications</span>
<span>{$LL.menu.settings.notifications()}</span>
</label>
<label>
<input
@ -128,7 +184,7 @@
bind:checked={forceCowebsiteTrigger}
on:change={changeForceCowebsiteTrigger}
/>
<span>Always ask before opening websites and Jitsi Meet rooms</span>
<span>{$LL.menu.settings.cowebsiteTrigger()}</span>
</label>
<label>
<input
@ -137,7 +193,7 @@
bind:checked={ignoreFollowRequests}
on:change={changeIgnoreFollowRequests}
/>
<span>Ignore requests to follow other users</span>
<span>{$LL.menu.settings.ignoreFollowRequest()}</span>
</label>
</section>
</div>
@ -174,6 +230,10 @@
margin: 0 0 15px;
}
}
.languages-switcher option {
text-transform: capitalize;
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {

View File

@ -5,6 +5,7 @@
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { Quill } from "quill";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import LL from "../../i18n/i18n-svelte";
//toolbar
const toolbarOptions = [
@ -58,7 +59,7 @@
const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(QUILL_EDITOR, {
placeholder: "Enter your message here...",
placeholder: $LL.menu.globalMessage.enter(),
theme: "snow",
modules: {
toolbar: toolbarOptions,

View File

@ -4,6 +4,7 @@
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy } from "svelte";
import { srcObject } from "./Video/utils";
import LL from "../i18n/i18n-svelte";
let stream: MediaStream | null;
@ -32,5 +33,5 @@
<SoundMeterWidget {stream} />
{/if}
</div>
<div class="is-silent" class:hide={isSilent}>Silent zone</div>
<div class="is-silent" class:hide={isSilent}>{$LL.camera.my.silentZone()}</div>
</div>

View File

@ -2,6 +2,7 @@
import { blackListManager } from "../../WebRtc/BlackListManager";
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined;
export let userName: string;
@ -29,10 +30,10 @@
</script>
<div class="block-container">
<h3>Block</h3>
<p>Block any communication from and to {userName}. This can be reverted.</p>
<h3>{$LL.report.block.title()}</h3>
<p>{$LL.report.block.content({ userName })}</p>
<button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}>
{userIsBlocked ? "Unblock this user" : "Block this user"}
{userIsBlocked ? $LL.report.block.unblock() : $LL.report.block.block()}
</button>
</div>

View File

@ -7,6 +7,7 @@
import { playersStore } from "../../Stores/PlayersStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { get } from "svelte/store";
import LL from "../../i18n/i18n-svelte";
let blockActive = true;
let reportActive = !blockActive;
@ -59,7 +60,7 @@
<div class="report-menu-main nes-container is-rounded">
<section class="report-menu-title">
<h2>Moderate {userName}</h2>
<h2>{$LL.report.moderate.title({ userName })}</h2>
<section class="justify-center">
<button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
</section>
@ -69,14 +70,14 @@
<button
type="button"
class="nes-btn {blockActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateBlock}>Block</button
on:click|preventDefault={activateBlock}>{$LL.report.moderate.block()}</button
>
</section>
<section class="justify-center">
<button
type="button"
class="nes-btn {reportActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateReport}>Report</button
on:click|preventDefault={activateReport}>{$LL.report.moderate.report()}</button
>
</section>
</section>
@ -86,7 +87,7 @@
{:else if reportActive}
<ReportSubMenu {userUUID} />
{:else}
<p>ERROR : There is no action selected.</p>
<p>{$LL.report.moderate.noSelect()}</p>
{/if}
</section>
</div>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager";
import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined;
let reportMessage: string;
@ -22,18 +23,18 @@
</script>
<div class="report-container-main">
<h3>Report</h3>
<p>Send a report message to the administrators of this room. They may later ban this user.</p>
<h3>{$LL.report.title()}</h3>
<p>{$LL.report.content()}</p>
<form>
<section>
<label>
<span>Your message: </span>
<span>{$LL.report.message.title()}</span>
<textarea type="text" class="nes-textarea" bind:value={reportMessage} />
</label>
<p hidden={hiddenError}>Report message cannot to be empty.</p>
<p hidden={hiddenError}>{$LL.report.message.empty()}</p>
</section>
<section>
<button type="submit" class="nes-btn is-error" on:click={submitReport}>Report this user</button>
<button type="submit" class="nes-btn is-error" on:click={submitReport}>{$LL.report.submit()}</button>
</section>
</form>
</div>

View File

@ -1,4 +1,5 @@
<script lang="typescript">
import LL from "../../i18n/i18n-svelte";
import type { Game } from "../../Phaser/Game/Game";
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
@ -25,7 +26,7 @@
<form class="selectCompanionScene">
<section class="text-center">
<h2>Select your companion</h2>
<h2>{$LL.companion.select.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt;
</button>
@ -35,12 +36,12 @@
</section>
<section class="action">
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}
>No companion</button
>{$LL.companion.select.any()}</button
>
<button
type="submit"
class="selectCompanionSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectCompanion}>Continue</button
on:click|preventDefault={selectCompanion}>{$LL.companion.select.continue()}</button
>
</section>
</form>

View File

@ -3,6 +3,7 @@
import { onMount } from "svelte";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import LL from "../../i18n/i18n-svelte";
export let message: Message;
@ -37,7 +38,8 @@
out:fade={{ duration: 200 }}
>
<h2 class="title-ban-message">
<img src="resources/logos/report.svg" alt="***" /> Important message
<img src="resources/logos/report.svg" alt="***" />
{$LL.warning.importantMessage()}
<img src="resources/logos/report.svg" alt="***" />
</h2>
<div class="content-ban-message">

View File

@ -3,6 +3,7 @@
import megaphoneImg from "./images/megaphone.svg";
import { soundPlayingStore } from "../../Stores/SoundPlayingStore";
import { afterUpdate } from "svelte";
import LL from "../../i18n/i18n-svelte";
export let url: string;
let audio: HTMLAudioElement;
@ -18,7 +19,7 @@
<div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}>
<img src={megaphoneImg} alt="Audio playing" />
<p>Audio message</p>
<p>{$LL.audio.message()}</p>
<audio bind:this={audio} src={url} on:ended={soundEnded}>
<track kind="captions" />
</audio>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
import LL from "../../i18n/i18n-svelte";
function close(): boolean {
errorStore.clearClosableMessages();
@ -8,7 +9,7 @@
</script>
<div class="error-div nes-container is-dark is-rounded" open>
<p class="nes-text is-error title">Error</p>
<p class="nes-text is-error title">{$LL.error.error()}</p>
<div class="body">
{#each $errorStore as error}
<p>{error.message}</p>

View File

@ -2,6 +2,7 @@
import { fly } from "svelte/transition";
import { requestVisitCardsStore } from "../../Stores/GameStore";
import { onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
export let visitCardUrl: string;
let w = "500px";
@ -40,7 +41,7 @@
/>
{#if !hidden}
<div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
<button class="nes-btn is-popUpElement" on:click={closeCard}>{$LL.menu.visitCard.close()}</button>
</div>
{/if}
</section>

View File

@ -2,6 +2,7 @@
import { fly } from "svelte/transition";
import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import LL from "../../i18n/i18n-svelte";
const upgradeLink = ADMIN_URL + "/pricing";
const registerLink = ADMIN_URL + "/second-step-register";
@ -9,19 +10,17 @@
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
{#if $userIsAdminStore}
<h2>Warning!</h2>
<h2>{$LL.warning.title()}</h2>
<p>
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
>here</a
>
{$LL.warning.content({ upgradeLink })}
</p>
{:else if $limitMapStore}
<p>
This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>!
</p>
{:else}
<h2>Warning!</h2>
<p>This world is close to its limit!</p>
<h2>{$LL.warning.title()}</h2>
<p>{$LL.warning.limit()}</p>
{/if}
</main>
@ -29,7 +28,8 @@
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
background-color: #f9e81e;
color: #14304c;
text-align: center;
position: absolute;
left: 50%;
@ -38,8 +38,13 @@
min-width: 300px;
opacity: 0.9;
z-index: 2;
h2 {
padding: 5px;
}
a {
color: #ff475a;
}
}
</style>

View File

@ -1,6 +1,7 @@
<script lang="typescript">
import type { Game } from "../../Phaser/Game/Game";
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
@ -25,7 +26,7 @@
<form class="selectCharacterScene">
<section class="text-center">
<h2>Select your WOKA</h2>
<h2>{$LL.woka.selectWoka.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt;
</button>
@ -37,12 +38,12 @@
<button
type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>Continue</button
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
>
<button
type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>Customize your WOKA</button
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
>
</section>
</form>

View File

@ -1,6 +1,8 @@
import axios from "axios";
import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore";
import LL from "../i18n/i18n-svelte";
import { get } from "svelte/store";
/**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
@ -26,7 +28,7 @@ axiosWithRetry.defaults.raxConfig = {
console.log(err);
console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage("Unable to connect to WorkAdventure. Are you connected to internet?", {
errorStore.addErrorMessage(get(LL).error.connectionRetry.unableConnect(), {
closable: false,
id: "axios_retry",
});

View File

@ -40,6 +40,7 @@ export const POSTHOG_API_KEY: string = (getEnv("POSTHOG_API_KEY") as string) ||
export const POSTHOG_URL = getEnv("POSTHOG_URL") || undefined;
export const DISABLE_ANONYMOUS: boolean = getEnv("DISABLE_ANONYMOUS") === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = getEnv("OPID_LOGIN_SCREEN_PROVIDER");
const FALLBACK_LOCALE = getEnv("FALLBACK_LOCALE") || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
@ -59,4 +60,5 @@ export {
TURN_PASSWORD,
JITSI_URL,
JITSI_PRIVATE_MODE,
FALLBACK_LOCALE,
};

View File

@ -88,6 +88,11 @@ export abstract class Character extends Container {
fontSize: "8px",
strokeThickness: 2,
stroke: "gray",
metrics: {
ascent: 20,
descent: 10,
fontSize: 35,
},
});
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName);

View File

@ -1,14 +1,14 @@
import { GameScene } from "./GameScene";
import { get } from "svelte/store";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { localUserStore } from "../../Connexion/LocalUserStore";
import type { Room } from "../../Connexion/Room";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import { menuIconVisiblilityStore } from "../../Stores/MenuStore";
import { EnableCameraSceneName } from "../Login/EnableCameraScene";
import { LoginSceneName } from "../Login/LoginScene";
import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { EnableCameraSceneName } from "../Login/EnableCameraScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store";
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import { menuIconVisiblilityStore } from "../../Stores/MenuStore";
import { GameScene } from "./GameScene";
/**
* This class should be responsible for any scene starting/stopping

View File

@ -1,4 +1,10 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapObject, ITiledMapProperty } from "../Map/ITiledMap";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapProperty,
ITiledMapTileLayer,
} from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
@ -291,6 +297,31 @@ export class GameMap {
}
}
public getRandomPositionFromLayer(layerName: string): { x: number; y: number } {
const layer = this.findLayer(layerName) as ITiledMapTileLayer;
if (!layer) {
throw new Error(`No layer "${layerName}" was found`);
}
const tiles = layer.data;
if (!tiles) {
throw new Error(`No tiles in "${layerName}" were found`);
}
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possiblePositions: { x: number; y: number }[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
possiblePositions.push({ x: key % layer.width, y: Math.floor(key / layer.width) });
});
if (possiblePositions.length > 0) {
return MathUtils.randomFromArray(possiblePositions);
}
throw new Error("No possible position found");
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
}

View File

@ -91,6 +91,7 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore";
import Camera = Phaser.Cameras.Scene2D.Camera;
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -559,6 +560,12 @@ export class GameScene extends DirtyScene {
.catch((e) => console.error(e));
}
this.pathfindingManager = new PathfindingManager(
this,
this.gameMap.getCollisionsGrid(),
this.gameMap.getTileDimensions()
);
//notify game manager can to create currentUser in map
this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
@ -569,7 +576,6 @@ export class GameScene extends DirtyScene {
waScaleManager
);
this.pathfindingManager = new PathfindingManager(this, this.gameMap.getCollisionsGrid());
biggestAvailableAreaStore.recompute();
this.cameraManager.startFollowPlayer(this.CurrentPlayer);
@ -1321,6 +1327,7 @@ ${escapedMessage}
startLayerName: this.startPositionCalculator.startLayerName,
uuid: localUserStore.getLocalUser()?.uuid,
nickname: this.playerName,
language: get(locale),
roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables,
@ -1456,6 +1463,17 @@ ${escapedMessage}
y: this.CurrentPlayer.y,
};
});
iframeListener.registerAnswerer("movePlayerTo", async (message) => {
const index = this.getGameMap().getTileIndexAt(message.x, message.y);
const startTile = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const path = await this.getPathfindingManager().findPath(startTile, index, true, true);
path.shift();
if (path.length === 0) {
throw new Error("no path available");
}
return this.CurrentPlayer.setPathToFollow(path, message.speed);
});
}
private setPropertyLayer(
@ -1709,6 +1727,22 @@ ${escapedMessage}
this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey);
});
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) {
if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());

View File

@ -4,6 +4,11 @@ import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store";
import { localeDetector } from "../../i18n/locales";
const $LL = get(LL);
export const EntrySceneName = "EntryScene";
@ -27,6 +32,8 @@ export class EntryScene extends Scene {
}
create() {
localeDetector()
.then(() => {
gameManager
.init(this.scene)
.then((nextSceneName) => {
@ -39,20 +46,20 @@ export class EntryScene extends Scene {
if (err.response && err.response.status == 404) {
ErrorScene.showError(
new WAError(
"Access link incorrect",
"Could not find map. Please check your access link.",
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
$LL.error.accessLink.title(),
$LL.error.accessLink.subTitle(),
$LL.error.accessLink.details()
),
this.scene
);
} else if (err.response && err.response.status == 403) {
ErrorScene.showError(
new WAError(
"Connection rejected",
"You cannot join the World. Try again later" +
(err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "") +
".",
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
$LL.error.connectionRejected.title(),
$LL.error.connectionRejected.subTitle({
error: err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "",
}),
$LL.error.connectionRejected.details()
),
this.scene
);
@ -60,5 +67,9 @@ export class EntryScene extends Scene {
ErrorScene.showError(err, this.scene);
}
});
})
.catch(() => {
throw new Error("Cannot load locale!");
});
}
}

View File

@ -12,6 +12,8 @@ export const requestEmoteEventName = "requestEmote";
export class Player extends Character {
private pathToFollow?: { x: number; y: number }[];
private followingPathPromiseResolve?: (result: { x: number; y: number; cancelled: boolean }) => void;
private pathWalkingSpeed?: number;
constructor(
Scene: GameScene,
@ -43,7 +45,7 @@ export class Player extends Character {
}
if (this.pathToFollow && activeUserInputEvents.anyExcept(UserInputEvent.SpeedUp)) {
this.pathToFollow = undefined;
this.finishFollowingPath(true);
}
let x = 0;
@ -68,9 +70,22 @@ export class Player extends Character {
this.scene.connection?.emitFollowConfirmation();
}
public setPathToFollow(path: { x: number; y: number }[]): void {
public async setPathToFollow(
path: { x: number; y: number }[],
speed?: number
): Promise<{ x: number; y: number; cancelled: boolean }> {
const isPreviousPathInProgress = this.pathToFollow !== undefined && this.pathToFollow.length > 0;
// take collider offset into consideraton
this.pathToFollow = this.adjustPathToFollowToColliderBounds(path);
this.pathWalkingSpeed = speed;
return new Promise((resolve) => {
this.followingPathPromiseResolve?.call(this, { x: this.x, y: this.y, cancelled: isPreviousPathInProgress });
this.followingPathPromiseResolve = resolve;
});
}
private deduceSpeed(speedUp: boolean, followMode: boolean): number {
return this.pathWalkingSpeed ? this.pathWalkingSpeed : speedUp && !followMode ? 25 : 9;
}
private adjustPathToFollowToColliderBounds(path: { x: number; y: number }[]): { x: number; y: number }[] {
@ -95,8 +110,8 @@ export class Player extends Character {
// Compute movement deltas
const followMode = get(followStateStore) !== "off";
const speedup = activeEvents.get(UserInputEvent.SpeedUp) && !followMode ? 25 : 9;
const moveAmount = speedup * 20;
const speed = this.deduceSpeed(activeEvents.get(UserInputEvent.SpeedUp), followMode);
const moveAmount = speed * 20;
x = x * moveAmount;
y = y * moveAmount;
@ -148,8 +163,8 @@ export class Player extends Character {
}
private computeFollowPathMovement(): number[] {
if (this.pathToFollow?.length === 0) {
this.pathToFollow = undefined;
if (this.pathToFollow !== undefined && this.pathToFollow.length === 0) {
this.finishFollowingPath();
}
if (!this.pathToFollow) {
return [0, 0];
@ -166,6 +181,12 @@ export class Player extends Character {
return this.getMovementDirection(xDistance, yDistance, distance);
}
private finishFollowingPath(cancelled: boolean = false): void {
this.pathToFollow = undefined;
this.pathWalkingSpeed = undefined;
this.followingPathPromiseResolve?.call(this, { x: this.x, y: this.y, cancelled });
}
private getMovementDirection(xDistance: number, yDistance: number, distance: number): [number, number] {
return [xDistance / Math.sqrt(distance), yDistance / Math.sqrt(distance)];
}

View File

@ -1,6 +1,8 @@
import { TextField } from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store";
export const ReconnectingSceneName = "ReconnectingScene";
export enum ReconnectingTextures {
@ -38,7 +40,7 @@ export class ReconnectingScene extends Phaser.Scene {
this,
this.game.renderer.width / 2,
this.game.renderer.height / 2,
"Connection lost. Reconnecting..."
get(LL).warning.connectionLost()
);
const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");

View File

@ -31,18 +31,11 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
.getTileIndexAt(this.gameScene.CurrentPlayer.x, this.gameScene.CurrentPlayer.y);
this.gameScene
.getPathfindingManager()
.findPath(startTile, index, true)
.findPath(startTile, index, true, true)
.then((path) => {
const tileDimensions = this.gameScene.getGameMap().getTileDimensions();
const pixelPath = path.map((step) => {
return {
x: step.x * tileDimensions.width + tileDimensions.width * 0.5,
y: step.y * tileDimensions.height + tileDimensions.height * 0.5,
};
});
// Remove first step as it is for the tile we are currently standing on
pixelPath.shift();
this.gameScene.CurrentPlayer.setPathToFollow(pixelPath);
path.shift();
this.gameScene.CurrentPlayer.setPathToFollow(path).catch((reason) => {});
})
.catch((reason) => {
console.warn(reason);

View File

@ -54,7 +54,7 @@ export class UserInputManager {
private scene: Phaser.Scene;
private isInputDisabled: boolean;
private joystick!: MobileJoystick;
private joystick?: MobileJoystick;
private joystickEvents = new ActiveEventList();
private joystickForceThreshold = 60;
private joystickForceAccuX = 0;
@ -81,9 +81,9 @@ export class UserInputManager {
initVirtualJoystick() {
this.joystick = new MobileJoystick(this.scene);
this.joystick.on("update", () => {
this.joystickForceAccuX = this.joystick.forceX ? this.joystickForceAccuX : 0;
this.joystickForceAccuY = this.joystick.forceY ? this.joystickForceAccuY : 0;
const cursorKeys = this.joystick.createCursorKeys();
this.joystickForceAccuX = this.joystick?.forceX ? this.joystickForceAccuX : 0;
this.joystickForceAccuY = this.joystick?.forceY ? this.joystickForceAccuY : 0;
const cursorKeys = this.joystick?.createCursorKeys();
for (const name in cursorKeys) {
const key = cursorKeys[name as Direction];
switch (name) {
@ -192,7 +192,7 @@ export class UserInputManager {
return eventsMap;
}
this.joystickEvents.forEach((value, key) => {
if (value) {
if (value && this.joystick) {
switch (key) {
case UserInputEvent.MoveUp:
case UserInputEvent.MoveDown:
@ -253,7 +253,7 @@ export class UserInputManager {
this.scene.input.on(
Phaser.Input.Events.POINTER_UP,
(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => {
this.joystick.hide();
this.joystick?.hide();
this.userInputHandler.handlePointerUpEvent(pointer, gameObjects);
}
);
@ -267,9 +267,9 @@ export class UserInputManager {
this.userInputHandler.handlePointerDownEvent(pointer, gameObjects);
// Let's only display the joystick if there is one finger on the screen
if ((pointer.event as TouchEvent).touches.length === 1) {
this.joystick.showAt(pointer.x, pointer.y);
this.joystick?.showAt(pointer.x, pointer.y);
} else {
this.joystick.hide();
this.joystick?.hide();
}
}
);

View File

@ -3,6 +3,7 @@ import Timeout = NodeJS.Timeout;
import { userIsAdminStore } from "./GameStore";
import { CONTACT_URL } from "../Enum/EnvironmentVariable";
import { analyticsClient } from "../Administration/AnalyticsClient";
import type { Translation } from "../i18n/i18n-types";
export const menuIconVisiblilityStore = writable(false);
export const menuVisiblilityStore = writable(false);
@ -32,37 +33,95 @@ function createWarningContainerStore() {
export const warningContainerStore = createWarningContainerStore();
export enum SubMenusInterface {
settings = "Settings",
profile = "Profile",
invite = "Invite",
aboutRoom = "Credit",
globalMessages = "Global Messages",
contact = "Contact",
settings = "settings",
profile = "profile",
invite = "invite",
aboutRoom = "credit",
globalMessages = "globalMessages",
contact = "contact",
}
type MenuKeys = keyof Translation["menu"]["sub"];
interface TranslatedMenu {
type: "translated";
key: MenuKeys;
}
/**
* A menu item from the scripting API
*/
interface ScriptingMenu {
type: "scripting";
label: string;
}
export type MenuItem = TranslatedMenu | ScriptingMenu;
function createSubMenusStore() {
const { subscribe, update } = writable<string[]>([
SubMenusInterface.profile,
SubMenusInterface.globalMessages,
SubMenusInterface.contact,
SubMenusInterface.settings,
SubMenusInterface.invite,
SubMenusInterface.aboutRoom,
const { subscribe, update } = writable<MenuItem[]>([
{
type: "translated",
key: SubMenusInterface.profile,
},
{
type: "translated",
key: SubMenusInterface.globalMessages,
},
{
type: "translated",
key: SubMenusInterface.contact,
},
{
type: "translated",
key: SubMenusInterface.settings,
},
{
type: "translated",
key: SubMenusInterface.invite,
},
{
type: "translated",
key: SubMenusInterface.aboutRoom,
},
]);
return {
subscribe,
addMenu(menuCommand: string) {
update((menuList: string[]) => {
if (!menuList.find((menu) => menu === menuCommand)) {
menuList.push(menuCommand);
addTranslatedMenu(menuCommand: MenuKeys) {
update((menuList) => {
if (!menuList.find((menu) => menu.type === "translated" && menu.key === menuCommand)) {
menuList.push({
type: "translated",
key: menuCommand,
});
}
return menuList;
});
},
removeMenu(menuCommand: string) {
update((menuList: string[]) => {
const index = menuList.findIndex((menu) => menu === menuCommand);
removeTranslatedMenu(menuCommand: MenuKeys) {
update((menuList) => {
const index = menuList.findIndex((menu) => menu.type === "translated" && menu.key === menuCommand);
if (index !== -1) {
menuList.splice(index, 1);
}
return menuList;
});
},
addScriptingMenu(menuCommand: string) {
update((menuList) => {
if (!menuList.find((menu) => menu.type === "scripting" && menu.label === menuCommand)) {
menuList.push({
type: "scripting",
label: menuCommand,
});
}
return menuList;
});
},
removeScriptingMenu(menuCommand: string) {
update((menuList) => {
const index = menuList.findIndex((menu) => menu.type === "scripting" && menu.label === menuCommand);
if (index !== -1) {
menuList.splice(index, 1);
}
@ -77,15 +136,15 @@ export const subMenusStore = createSubMenusStore();
export const contactPageStore = writable<string | undefined>(CONTACT_URL);
export function checkSubMenuToShow() {
subMenusStore.removeMenu(SubMenusInterface.globalMessages);
subMenusStore.removeMenu(SubMenusInterface.contact);
subMenusStore.removeTranslatedMenu(SubMenusInterface.globalMessages);
subMenusStore.removeTranslatedMenu(SubMenusInterface.contact);
if (get(userIsAdminStore)) {
subMenusStore.addMenu(SubMenusInterface.globalMessages);
subMenusStore.addTranslatedMenu(SubMenusInterface.globalMessages);
}
if (get(contactPageStore) !== undefined) {
subMenusStore.addMenu(SubMenusInterface.contact);
subMenusStore.addTranslatedMenu(SubMenusInterface.contact);
}
}
@ -97,12 +156,12 @@ export function handleMenuRegistrationEvent(
source: string | undefined = undefined,
options: { allowApi: boolean }
) {
if (get(subMenusStore).includes(menuName)) {
if (get(subMenusStore).find((item) => item.type === "scripting" && item.label === menuName)) {
console.warn("The menu " + menuName + " already exist.");
return;
}
subMenusStore.addMenu(menuName);
subMenusStore.addScriptingMenu(menuName);
if (iframeUrl !== undefined) {
const url = new URL(iframeUrl, source);
@ -111,12 +170,6 @@ export function handleMenuRegistrationEvent(
}
export function handleMenuUnregisterEvent(menuName: string) {
const subMenuGeneral: string[] = Object.values(SubMenusInterface);
if (subMenuGeneral.includes(menuName)) {
console.warn("The menu " + menuName + " is a mandatory menu. It can't be remove");
return;
}
subMenusStore.removeMenu(menuName);
subMenusStore.removeScriptingMenu(menuName);
customMenuIframe.delete(menuName);
}

View File

@ -45,8 +45,28 @@ class UrlManager {
}
public getStartLayerNameFromUrl(): string | null {
const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null;
const parameters = this.getHashParameters();
for (const key in parameters) {
if (parameters[key] === undefined) {
return key;
}
}
return null;
}
public getHashParameter(name: string): string | undefined {
return this.getHashParameters()[name];
}
private getHashParameters(): Record<string, string> {
return window.location.hash
.substring(1)
.split("&")
.reduce((res: Record<string, string>, item: string) => {
const parts = item.split("=");
res[parts[0]] = parts[1];
return res;
}, {});
}
pushStartLayerNameToUrl(startLayerName: string): void {

View File

@ -31,4 +31,8 @@ export class MathUtils {
const distance = Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
return squared ? Math.sqrt(distance) : distance;
}
public static randomFromArray<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
}

View File

@ -6,20 +6,23 @@ export class PathfindingManager {
private easyStar;
private grid: number[][];
private tileDimensions: { width: number; height: number };
constructor(scene: Phaser.Scene, collisionsGrid: number[][]) {
constructor(scene: Phaser.Scene, collisionsGrid: number[][], tileDimensions: { width: number; height: number }) {
this.scene = scene;
this.easyStar = new EasyStar.js();
this.easyStar.enableDiagonals();
this.grid = collisionsGrid;
this.tileDimensions = tileDimensions;
this.setEasyStarGrid(collisionsGrid);
}
public async findPath(
start: { x: number; y: number },
end: { x: number; y: number },
measuredInPixels: boolean = true,
tryFindingNearestAvailable: boolean = false
): Promise<{ x: number; y: number }[]> {
let endPoints: { x: number; y: number }[] = [end];
@ -48,12 +51,21 @@ export class PathfindingManager {
// rejected Promise will return undefined for path
path = await this.getPath(start, endPoint).catch();
if (path && path.length > 0) {
return path;
return measuredInPixels ? this.mapTileUnitsToPixels(path) : path;
}
}
return [];
}
private mapTileUnitsToPixels(path: { x: number; y: number }[]): { x: number; y: number }[] {
return path.map((step) => {
return {
x: step.x * this.tileDimensions.width + this.tileDimensions.width * 0.5,
y: step.y * this.tileDimensions.height + this.tileDimensions.height * 0.5,
};
});
}
private getNeighbouringTiles(tile: { x: number; y: number }): { x: number; y: number }[] {
const xOffsets = [-1, 0, 1, 1, 1, 0, -1, -1];
const yOffsets = [-1, -1, -1, 0, 1, 1, 1, 0];

View File

@ -9,10 +9,12 @@ export type StopScreenSharingCallback = (media: MediaStream) => void;
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import { get } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore";
import { layoutManagerActionStore } from "../Stores/LayoutManagerStore";
import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError";
import { localUserStore } from "../Connexion/LocalUserStore";
import LL from "../i18n/i18n-svelte";
import { get } from "svelte/store";
import { localeDetector } from "../i18n/locales";
export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
@ -23,13 +25,18 @@ export class MediaManager {
private userInputManager?: UserInputManager;
constructor() {
localeDetector()
.catch(() => {
throw new Error("Cannot load locale on media manager");
})
.finally(() => {
localStreamStore.subscribe((result) => {
if (result.type === "error") {
if (result.error.name !== MediaStreamConstraintsError.NAME) {
layoutManagerActionStore.addAction({
uuid: "cameraAccessDenied",
type: "warning",
message: "Camera access denied. Click here and check your browser permissions.",
message: get(LL).warning.accessDenied.camera(),
callback: () => {
helpCameraSettingsVisibleStore.set(true);
},
@ -50,7 +57,7 @@ export class MediaManager {
layoutManagerActionStore.addAction({
uuid: "screenSharingAccessDenied",
type: "warning",
message: "Screen sharing denied. Click here and check your browser permissions.",
message: get(LL).warning.accessDenied.screenSharing(),
callback: () => {
helpCameraSettingsVisibleStore.set(true);
},
@ -63,6 +70,7 @@ export class MediaManager {
return;
}
});
});
}
public showGameOverlay(): void {

3
front/src/i18n/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
i18n-svelte.ts
i18n-types.ts
i18n-util.ts

View File

@ -0,0 +1,10 @@
import type { BaseTranslation } from "../i18n-types";
const audio: BaseTranslation = {
manager: {
reduce: "reduce in conversations",
},
message: "Audio message",
};
export default audio;

View File

@ -0,0 +1,22 @@
import type { BaseTranslation } from "../i18n-types";
const camera: BaseTranslation = {
enable: {
title: "Turn on your camera and microphone",
start: "Let's go!",
},
help: {
title: "Camera / Microphone access needed",
permissionDenied: "Permission denied",
content: "You must allow camera and microphone access in your browser.",
firefoxContent:
'Please click the "Remember this decision" checkbox, if you don\'t want Firefox to keep asking you the authorization.',
refresh: "Refresh",
continue: "Continue without webcam",
},
my: {
silentZone: "Silent zone",
},
};
export default camera;

View File

@ -0,0 +1,12 @@
import type { BaseTranslation } from "../i18n-types";
const chat: BaseTranslation = {
intro: "Here is your chat history:",
enter: "Enter your message...",
menu: {
visitCard: "Visit card",
addFriend: "Add friend",
},
};
export default chat;

View File

@ -0,0 +1,11 @@
import type { BaseTranslation } from "../i18n-types";
const companion: BaseTranslation = {
select: {
title: "Select your companion",
any: "No companion",
continue: "Continue",
},
};
export default companion;

View File

@ -0,0 +1,20 @@
import type { BaseTranslation } from "../i18n-types";
const error: BaseTranslation = {
accessLink: {
title: "Access link incorrect",
subTitle: "Could not find map. Please check your access link.",
details: "If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
},
connectionRejected: {
title: "Connection rejected",
subTitle: "You cannot join the World. Try again later {error}.",
details: "If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
},
connectionRetry: {
unableConnect: "Unable to connect to WorkAdventure. Are you connected to internet?",
},
error: "Error",
};
export default error;

View File

@ -0,0 +1,27 @@
import type { BaseTranslation } from "../i18n-types";
const follow: BaseTranslation = {
interactStatus: {
following: "Following {leader}",
waitingFollowers: "Waiting for followers confirmation",
followed: {
one: "{follower} is following you",
two: "{firstFollower} and {secondFollower} are following you",
many: "{followers} and {lastFollower} are following you",
},
},
interactMenu: {
title: {
interact: "Interaction",
follow: "Do you want to follow {leader}?",
},
stop: {
leader: "Do you want to stop leading the way?",
follower: "Do you want to stop following {leader}?",
},
yes: "Yes",
no: "No",
},
};
export default follow;

View File

@ -0,0 +1,30 @@
import type { BaseTranslation } from "../i18n-types";
import audio from "./audio";
import camera from "./camera";
import chat from "./chat";
import companion from "./companion";
import woka from "./woka";
import error from "./error";
import follow from "./follow";
import login from "./login";
import menu from "./menu";
import report from "./report";
import warning from "./warning";
const en_US: BaseTranslation = {
language: "English",
country: "United States",
audio,
camera,
chat,
companion,
woka,
error,
follow,
login,
menu,
report,
warning,
};
export default en_US;

View File

@ -0,0 +1,14 @@
import type { BaseTranslation } from "../i18n-types";
const login: BaseTranslation = {
input: {
name: {
placeholder: "Enter your name",
empty: "The name is empty",
},
},
terms: 'By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.',
continue: "Continue",
};
export default login;

View File

@ -0,0 +1,124 @@
import type { BaseTranslation } from "../i18n-types";
const menu: BaseTranslation = {
title: "Menu",
icon: {
open: {
menu: "Open menu",
invite: "Show invite",
register: "Register",
chat: "Open chat",
},
},
visitCard: {
close: "Close",
},
profile: {
edit: {
name: "Edit your name",
woka: "Edit your WOKA",
companion: "Edit your companion",
camera: "Edit your camera",
},
login: "Sign in",
logout: "Log out",
},
settings: {
gameQuality: {
title: "Game quality",
short: {
high: "High (120 fps)",
medium: "Medium (60 fps)",
minimum: "Minimum (40 fps)",
small: "Small (20 fps)",
},
long: {
high: "High video quality (120 fps)",
medium: "Medium video quality (60 fps, recommended)",
minimum: "Minimum video quality (40 fps)",
small: "Small video quality (20 fps)",
},
},
videoQuality: {
title: "Video quality",
short: {
high: "High (30 fps)",
medium: "Medium (20 fps)",
minimum: "Minimum (10 fps)",
small: "Small (5 fps)",
},
long: {
high: "High video quality (30 fps)",
medium: "Medium video quality (20 fps, recommended)",
minimum: "Minimum video quality (10 fps)",
small: "Small video quality (5 fps)",
},
},
language: {
title: "Language",
},
save: {
warning: "(Saving these settings will restart the game)",
button: "Save",
},
fullscreen: "Fullscreen",
notifications: "Notifications",
cowebsiteTrigger: "Always ask before opening websites and Jitsi Meet rooms",
ignoreFollowRequest: "Ignore requests to follow other users",
},
invite: {
description: "Share the link of the room!",
copy: "Copy",
share: "Share",
},
globalMessage: {
text: "Text",
audio: "Audio",
warning: "Broadcast to all rooms of the world",
enter: "Enter your message here...",
send: "Send",
},
globalAudio: {
uploadInfo: "Upload a file",
error: "No file selected. You need to upload a file before sending it.",
},
contact: {
gettingStarted: {
title: "Getting started",
description:
"WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.",
},
createMap: {
title: "Create your map",
description: "You can also create your own custom map by following the step of the documentation.",
},
},
about: {
mapInfo: "Information on the map",
mapLink: "link to this map",
copyrights: {
map: {
title: "Copyrights of the map",
empty: "The map creator did not declare a copyright for the map.",
},
tileset: {
title: "Copyrights of the tilesets",
empty: "The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets have no license.",
},
audio: {
title: "Copyrights of audio files",
empty: "The map creator did not declare a copyright for audio files. This doesn't mean that those audio files have no license.",
},
},
},
sub: {
profile: "Profile",
settings: "Settings",
invite: "Invite",
credit: "Credit",
globalMessages: "Global Messages",
contact: "Contact",
},
};
export default menu;

View File

@ -0,0 +1,25 @@
import type { BaseTranslation } from "../i18n-types";
const report: BaseTranslation = {
block: {
title: "Block",
content: "Block any communication from and to {userName}. This can be reverted.",
unblock: "Unblock this user",
block: "Block this user",
},
title: "Report",
content: "Send a report message to the administrators of this room. They may later ban this user.",
message: {
title: "Your message: ",
empty: "Report message cannot to be empty.",
},
submit: "Report this user",
moderate: {
title: "Moderate {userName}",
block: "Block",
report: "Report",
noSelect: "ERROR : There is no action selected.",
},
};
export default report;

View File

@ -0,0 +1,16 @@
import type { BaseTranslation } from "../i18n-types";
const warning: BaseTranslation = {
title: "Warning!",
content:
'This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank">here</a>',
limit: "This world is close to its limit!",
accessDenied: {
camera: "Camera access denied. Click here and check your browser permissions.",
screenSharing: "Screen sharing denied. Click here and check your browser permissions.",
},
importantMessage: "Important message",
connectionLost: "Connection lost. Reconnecting...",
};
export default warning;

View File

@ -0,0 +1,20 @@
import type { BaseTranslation } from "../i18n-types";
const woka: BaseTranslation = {
customWoka: {
title: "Customize your WOKA",
navigation: {
return: "Return",
back: "Back",
finish: "Finish",
next: "Next",
},
},
selectWoka: {
title: "Select your WOKA",
continue: "Continue",
customize: "Customize your WOKA",
},
};
export default woka;

View File

@ -0,0 +1,11 @@
import type { AsyncFormattersInitializer } from "typesafe-i18n";
import type { Locales, Formatters } from "./i18n-types";
// eslint-disable-next-line @typescript-eslint/require-await
export const initFormatters: AsyncFormattersInitializer<Locales, Formatters> = async (locale: Locales) => {
const formatters: Formatters = {
// add your formatter functions here
};
return formatters;
};

View File

@ -0,0 +1,10 @@
import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "réduit dans les conversations",
},
message: "Message audio",
};
export default audio;

View File

@ -0,0 +1,22 @@
import type { Translation } from "../i18n-types";
const camera: NonNullable<Translation["camera"]> = {
enable: {
title: "Allumez votre caméra et votre microphone",
start: "C'est partie!",
},
help: {
title: "Accès à la caméra / au microphone nécessaire",
permissionDenied: "Permission refusée",
content: "Vous devez autoriser l'accès à la caméra et au microphone dans votre navigateur.",
firefoxContent:
'Veuillez cocher la case "Se souvenir de cette décision" si vous ne voulez pas que Firefox vous demande sans cesse l\'autorisation.',
refresh: "Rafraîchir",
continue: "Continuer sans webcam",
},
my: {
silentZone: "Zone silencieuse",
},
};
export default camera;

View File

@ -0,0 +1,12 @@
import type { Translation } from "../i18n-types";
const chat: NonNullable<Translation["chat"]> = {
intro: "Voici l'historique de votre chat:",
enter: "Entrez votre message...",
menu: {
visitCard: "Carte de visite",
addFriend: "Ajouter un ami",
},
};
export default chat;

View File

@ -0,0 +1,11 @@
import type { Translation } from "../i18n-types";
const companion: NonNullable<Translation["companion"]> = {
select: {
title: "Sélectionnez votre compagnon",
any: "Pas de compagnon",
continue: "Continuer",
},
};
export default companion;

View File

@ -0,0 +1,22 @@
import type { Translation } from "../i18n-types";
const error: NonNullable<Translation["error"]> = {
accessLink: {
title: "Lien d'accès incorrect",
subTitle: "Impossible de trouver la carte. Veuillez vérifier votre lien d'accès.",
details:
"Si vous souhaitez obtenir de plus amples informations, vous pouvez contacter l'administrateur ou nous contacter à l'adresse suivante: hello@workadventu.re",
},
connectionRejected: {
title: "Connexion rejetée",
subTitle: "Vous ne pouvez pas rejoindre le monde. Réessayer plus tard {error}.",
details:
"Si vous souhaitez obtenir de plus amples informations, vous pouvez contacter l'administrateur ou nous contacter à l'adresse suivante: hello@workadventu.re",
},
connectionRetry: {
unableConnect: "Impossible de se connecter à WorkAdventure. Etes vous connecté à Internet?",
},
error: "Erreur",
};
export default error;

View File

@ -0,0 +1,27 @@
import type { Translation } from "../i18n-types";
const follow: NonNullable<Translation["follow"]> = {
interactStatus: {
following: "Vous suivez {leader}",
waitingFollowers: "En attente de la confirmation des suiveurs",
followed: {
one: "{follower} vous suit",
two: "{firstFollower} et {secondFollower} vous suivent",
many: "{followers} et {lastFollower} vous suivent",
},
},
interactMenu: {
title: {
interact: "Interaction",
follow: "Voulez-vous suivre {leader}?",
},
stop: {
leader: "Voulez-vous qu'on arrête de vous suivre?",
follower: "Voulez-vous arrêter de suivre {leader}?",
},
yes: "Oui",
no: "Non",
},
};
export default follow;

View File

@ -0,0 +1,32 @@
import en_US from "../en-US";
import type { Translation } from "../i18n-types";
import audio from "./audio";
import camera from "./camera";
import chat from "./chat";
import companion from "./companion";
import error from "./error";
import follow from "./follow";
import login from "./login";
import menu from "./menu";
import report from "./report";
import warning from "./warning";
import woka from "./woka";
const fr_FR: Translation = {
...en_US,
language: "Français",
country: "France",
audio,
camera,
chat,
companion,
woka,
error,
follow,
login,
menu,
report,
warning,
};
export default fr_FR;

View File

@ -0,0 +1,14 @@
import type { Translation } from "../i18n-types";
const login: NonNullable<Translation["login"]> = {
input: {
name: {
placeholder: "Entrez votre nom",
empty: "Le nom est vide",
},
},
terms: 'En continuant, vous acceptez nos <a href="https://workadventu.re/terms-of-use" target="_blank">conditions d\'utilisation</a>, notre <a href="https://workadventu.re/privacy-policy" target="_blank">politique de confidentialité</a> et notre <a href="https://workadventu.re/cookie-policy" target="_blank">politique relative aux cookies</a>.',
continue: "Continuer",
};
export default login;

View File

@ -0,0 +1,124 @@
import type { Translation } from "../i18n-types";
const menu: NonNullable<Translation["menu"]> = {
title: "Menu",
icon: {
open: {
menu: "Ouvrir le menu",
invite: "Afficher l'invitation",
register: "Enregistrez vous",
chat: "Ouvrir le chat",
},
},
visitCard: {
close: "Fermer",
},
profile: {
edit: {
name: "Modifier votre nom",
woka: "Modifier votre WOKA",
companion: "Modifier votre compagnon",
camera: "Modifier votre caméra",
},
login: "S'identifier",
logout: "Déconnexion",
},
settings: {
gameQuality: {
title: "Qualité du jeu",
short: {
high: "Haute (120 fps)",
medium: "Moyenne (60 fps)",
minimum: "Minimale (40 fps)",
small: "Reduite (20 fps)",
},
long: {
high: "Haute (120 fps)",
medium: "Moyenne (60 fps, recommandée)",
minimum: "Minimale (40 fps)",
small: "Reduite (20 fps)",
},
},
videoQuality: {
title: "Qualité de la vidéo",
short: {
high: "Haute (30 fps)",
medium: "Moyenne (20 fps)",
minimum: "Minimale (10 fps)",
small: "Reduite (5 fps)",
},
long: {
high: "Haute (30 fps)",
medium: "Moyenne (20 fps, recommandée)",
minimum: "Minimale (10 fps)",
small: "Reduite (5 fps)",
},
},
language: {
title: "Langage",
},
save: {
warning: "(La sauvegarde de ces paramètres redémarre le jeu)",
button: "Sauvegarder",
},
fullscreen: "Plein écran",
notifications: "Notifications",
cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de réunion Jitsi",
ignoreFollowRequest: "Ignorer les demandes de suivi des autres utilisateurs",
},
invite: {
description: "Partager le lien de la salle!",
copy: "Copier",
share: "Partager",
},
globalMessage: {
text: "Texte",
audio: "Audio",
warning: "Diffusion dans toutes les salles du monde",
enter: "Entrez votre message ici...",
send: "Envoyer",
},
globalAudio: {
uploadInfo: "Télécharger un fichier",
error: "Aucun fichier sélectionné. Vous devez télécharger un fichier avant de l'envoyer.",
},
contact: {
gettingStarted: {
title: "Pour commencer",
description:
"WorkAdventure vous permet de créer un espace en ligne pour communiquer spontanément avec d'autres personnes. Et tout commence par la création de votre propre espace. Choisissez parmi une large sélection de cartes préfabriquées par notre équipe.",
},
createMap: {
title: "Créer votre carte",
description: "Vous pouvez également créer votre propre carte personnalisée en suivant la documentation.",
},
},
about: {
mapInfo: "Informations sur la carte",
mapLink: "lien vers cette carte",
copyrights: {
map: {
title: "Droits d'auteur de la carte",
empty: "Le créateur de la carte n'a pas déclaré de droit d'auteur pour la carte.",
},
tileset: {
title: "Droits d'auteur des tilesets",
empty: "Le créateur de la carte n'a pas déclaré de droit d'auteur pour les tilesets. Cela ne signifie pas que les tilesets n'ont pas de licence.",
},
audio: {
title: "Droits d'auteur des fichiers audio",
empty: "aré de droit d'auteur pour les fichiers audio. Cela ne signifie pas que les fichiers audio n'ont pas de licence.",
},
},
},
sub: {
profile: "Profile",
settings: "Paramètres",
invite: "Inviter",
credit: "Crédits",
globalMessages: "Messages globaux",
contact: "Contact",
},
};
export default menu;

View File

@ -0,0 +1,25 @@
import type { Translation } from "../i18n-types";
const report: NonNullable<Translation["report"]> = {
block: {
title: "Bloquer",
content: "Bloquer toute communication en provenance et à destination de {userName}. Cela peut être annulé.",
unblock: "Débloquer cet utilisateur",
block: "Bloquer cet utilisateur",
},
title: "Signaler",
content: "Signaler aux administrateurs de cette salle. Ils pourront par la suite bannir cet utilisateur.",
message: {
title: "Votre message: ",
empty: "Le message du signalement ne peut pas être vide.",
},
submit: "Signaler cet utilisateur",
moderate: {
title: "Modérer {userName}",
block: "Bloquer",
report: "Signaler",
noSelect: "ERREUR : Il n'y a pas d'action sélectionnée.",
},
};
export default report;

View File

@ -0,0 +1,16 @@
import type { Translation } from "../i18n-types";
const warning: NonNullable<Translation["warning"]> = {
title: "Attention!",
content:
'Ce monde est proche de sa limite ! Vous pouvez améliorer sa capacité <a href={upgradeLink} target="_blank">içi</a>',
limit: "Ce monde est proche de ses limites!",
accessDenied: {
camera: "Accès à la caméra refusé. Cliquez ici et vérifiez les autorisations de votre navigateur.",
screenSharing: "Partage d'écran refusé. Cliquez ici et vérifiez les autorisations de votre navigateur.",
},
importantMessage: "Message important",
connectionLost: "Connexion perdue. Reconnexion...",
};
export default warning;

View File

@ -0,0 +1,20 @@
import type { Translation } from "../i18n-types";
const woka: NonNullable<Translation["woka"]> = {
customWoka: {
title: "Personnalisez votre WOKA",
navigation: {
return: "Retour",
back: "Précédent",
finish: "Terminer",
next: "Suivant",
},
},
selectWoka: {
title: "Sélectionnez votre WOKA",
continue: "Continuer",
customize: "Personnalisez votre WOKA",
},
};
export default woka;

52
front/src/i18n/locales.ts Normal file
View File

@ -0,0 +1,52 @@
import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors";
import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable";
import { initI18n, setLocale } from "./i18n-svelte";
import type { Locales, Translation } from "./i18n-types";
import { baseLocale, getTranslationForLocale, locales } from "./i18n-util";
const fallbackLocale = FALLBACK_LOCALE || baseLocale;
const localStorageProperty = "language";
export const localeDetector = async () => {
const exist = localStorage.getItem(localStorageProperty);
let detectedLocale: Locales = fallbackLocale as Locales;
if (exist) {
const localStorageDetector = initLocalStorageDetector(localStorageProperty);
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales;
} else {
detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector) as Locales;
}
await initI18n(detectedLocale);
};
export const setCurrentLocale = (locale: Locales) => {
localStorage.setItem(localStorageProperty, locale);
setLocale(locale).catch(() => {
console.log("Cannot reload the locale!");
});
};
export type DisplayableLocale = { id: Locales; language: string; country: string };
function getDisplayableLocales() {
const localesObject: DisplayableLocale[] = [];
locales.forEach((locale) => {
getTranslationForLocale(locale)
.then((translations) => {
localesObject.push({
id: locale,
language: translations.language,
country: translations.country,
});
})
.catch((error) => {
console.log(error);
});
});
return localesObject;
}
export const displayableLocales = getDisplayableLocales();

View File

@ -15,7 +15,7 @@ import ui from "./Api/iframe/ui";
import sound from "./Api/iframe/sound";
import room, { setMapURL, setRoomId } from "./Api/iframe/room";
import { createState } from "./Api/iframe/state";
import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
import player, { setPlayerName, setPlayerLanguage, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
@ -30,6 +30,7 @@ const initPromise = queryWorkadventure({
data: undefined,
}).then((gameState) => {
setPlayerName(gameState.nickname);
setPlayerLanguage(gameState.language);
setRoomId(gameState.roomId);
setMapURL(gameState.mapUrl);
setTags(gameState.tags);

View File

@ -1,13 +1,13 @@
import type { Configuration } from "webpack";
import type WebpackDevServer from "webpack-dev-server";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import fs from 'fs/promises';
import path from "path";
import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import sveltePreprocess from "svelte-preprocess";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import path from "path";
import sveltePreprocess from "svelte-preprocess";
import type { Configuration } from "webpack";
import webpack from "webpack";
import type WebpackDevServer from "webpack-dev-server";
const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -171,6 +171,11 @@ module.exports = {
filename: "fonts/[name][ext]",
},
},
{
test: /\.json$/,
exclude: /node_modules/,
type: "asset",
},
],
},
resolve: {

View File

@ -4032,9 +4032,9 @@ nan@^2.12.1:
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
nanoid@^3.1.23:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
nanomatch@^1.2.9:
version "1.2.13"
@ -6044,6 +6044,11 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
typesafe-i18n@^2.59.0:
version "2.59.0"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4"
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg==
typescript@*:
version "4.3.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
@ -6534,3 +6539,8 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.11.6:
version "3.11.6"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"
integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==

View File

@ -0,0 +1,207 @@
{ "compressionlevel":-1,
"height":30,
"infinite":false,
"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, 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, 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, 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, 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, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 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, 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],
"height":30,
"id":1,
"name":"floor",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"script.php"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 29, 28, 29, 28, 29, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 0, 0, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 0, 0, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 6, 7, 6, 7, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 6, 7, 6, 7, 0, 17, 18, 17, 18, 17, 18, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 17, 18, 17, 18, 0, 28, 29, 28, 29, 28, 29, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 29, 28, 29, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":30,
"id":6,
"name":"furnitures",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":30,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":159.371714951095,
"id":4,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"MS Shell Dlg 2",
"pixelsize":21,
"text":"Set position of where you want the player character to move to. On movement's end or interruption, player's current position will be logged into the console",
"wrap":true
},
"type":"",
"visible":true,
"width":315.438,
"x":64.7309301350722,
"y":95.2559217512811
},
{
"height":30.8202477876106,
"id":8,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"MS Shell Dlg 2",
"pixelsize":21,
"text":"x: 16 y: 16",
"wrap":true
},
"type":"",
"visible":true,
"width":111.991330228225,
"x":39.0206367023754,
"y":2.47529762459247
},
{
"height":30.8202,
"id":9,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"MS Shell Dlg 2",
"pixelsize":21,
"text":"x: 464 y: 432",
"wrap":true
},
"type":"",
"visible":true,
"width":149.997520726595,
"x":483.920662086633,
"y":417.193532976246
},
{
"height":113.333333333333,
"id":10,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"MS Shell Dlg 2",
"pixelsize":21,
"text":"Top: 256\/512 Center: 496\/655 Width: 480 Height: 286",
"wrap":true
},
"type":"",
"visible":true,
"width":172,
"x":256,
"y":512
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":7,
"nextobjectid":11,
"orientation":"orthogonal",
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"script.php"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"renderorder":"right-down",
"tiledversion":"1.7.2",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tiles":[
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":27,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":28,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":30
}

View File

@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<script src="<?php echo $_SERVER["FRONT_URL"] ?>/iframe_api.js"></script>
<script>
window.addEventListener('load', () => {
//@ts-ignore
WA.onInit().then(() => {
console.log('After WA init');
const movePlayerButton = document.getElementById('movePlayerButton');
const randomChainedMovementButton = document.getElementById('randomChainedMovementButton');
const xField = document.getElementById('x');
const yField = document.getElementById('y');
const speedField = document.getElementById('speed');
randomChainedMovementButton.addEventListener('click', async () => {
WA.player.moveTo(500, 500, 10)
const result = await WA.player.moveTo(100, 100, 10).then((result) => {
if (result.completed)
if (result.cancelled) {
return;
}
WA.player.moveTo(500, 100, 20);
});
});
movePlayerButton.addEventListener('click', async () => {
const position = await WA.player.moveTo(
parseInt(xField.value),
parseInt(yField.value),
parseInt(speedField.value),
);
console.log(position);
});
});
})
</script>
</head>
<body>
X: <input type="text" id="x" value="496" /><br/>
Y: <input type="text" id="y" value="655" /><br/>
Speed: <input type="text" id="speed" value="20" /><br/>
<button id="movePlayerButton">Move Player</button>
<button id="randomChainedMovementButton">Do random chained movement</button>
</body>
</html>

View File

@ -56,6 +56,14 @@
<a href="#" class="testLink" data-testmap="start-tile.json#S2" target="_blank">Test start tile (S2)</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-exits"> Success <input type="radio" name="test-exits"> Failure <input type="radio" name="test-exits" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="move_to.json" target="_blank">Test moveTo parameter</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-exits"> Success <input type="radio" name="test-exits"> Failure <input type="radio" name="test-exits" checked> Pending
@ -227,6 +235,14 @@
<a href="#" class="testLink" data-testmap="CameraApi/camera_api_test.json" target="_blank">Test camera API</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-set-tiles"> Success <input type="radio" name="test-set-tiles"> Failure <input type="radio" name="test-set-tiles" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="MovePlayer/move_player_api_test.json" target="_blank">Test Player Movement API</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-variables"> Success <input type="radio" name="test-variables"> Failure <input type="radio" name="test-variables" checked> Pending

275
maps/tests/move_to.json Normal file
View File

@ -0,0 +1,275 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"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],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17],
"height":10,
"id":7,
"name":"walls",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":8,
"name":"meeting-room",
"opacity":1,
"properties":[
{
"name":"startLayer",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":10,
"name":"start2",
"opacity":1,
"properties":[
{
"name":"startLayer",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":33.4788210765457,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"Add \"moveTo\" parameter to the URL to make character move at game start.",
"wrap":true
},
"type":"",
"visible":true,
"width":249.954975648686,
"x":35.2740564642832,
"y":34.4372323693377
},
{
"height":19.6921,
"id":3,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"...#start2&moveTo=meeting-room",
"wrap":true
},
"type":"",
"visible":true,
"width":223.499265952492,
"x":32,
"y":114
},
{
"height":19.6921,
"id":4,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"start",
"wrap":true
},
"type":"",
"visible":true,
"width":26.7596292501164,
"x":3.30880298090358,
"y":135.124359571495
},
{
"height":19.6921,
"id":5,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"...#start2&moveTo=start",
"wrap":true
},
"type":"",
"visible":true,
"width":158.292,
"x":32,
"y":132
},
{
"height":19.6921,
"id":6,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"...#start&moveTo=meeting-room",
"wrap":true
},
"type":"",
"visible":true,
"width":217.164845831393,
"x":32,
"y":93.740349627387
},
{
"height":19.6921,
"id":7,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"start2",
"wrap":true
},
"type":"",
"visible":true,
"width":33.0940201210992,
"x":74.556855798789,
"y":245.393819585468
},
{
"height":19.6921,
"id":8,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"meeting-room",
"wrap":true
},
"type":"",
"visible":true,
"width":92.7120717279925,
"x":233.848901257569,
"y":135.845612785282
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":11,
"nextobjectid":9,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.7.2",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tiles":[
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":10
}

View File

@ -1622,9 +1622,11 @@ nice-try@^1.0.4:
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
nopt@^5.0.0:
version "5.0.0"
@ -2265,6 +2267,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
@ -2376,6 +2383,19 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

View File

@ -3,7 +3,7 @@ import {assertLogMessage} from "./utils/log";
const fs = require('fs');
const Docker = require('dockerode');
import { Selector } from 'testcafe';
import {login} from "./utils/roles";
import {login, resetLanguage} from "./utils/roles";
import {findContainer, rebootBack, rebootPusher, resetRedis, startContainer, stopContainer} from "./utils/containers";
fixture `Reconnection`
@ -16,6 +16,8 @@ test("Test that connection can succeed even if WorkAdventure starts while pusher
const errorMessage = Selector('.error-div');
await resetLanguage('en-US');
await t
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json')
.expect(errorMessage.innerText).contains('Unable to connect to WorkAdventure')

Some files were not shown because too many files have changed in this diff Show More