Merge pull request #1843 from thecodingmachine/move-to-improvements

Move to improvements
This commit is contained in:
David Négrier 2022-02-11 18:28:50 +01:00 committed by GitHub
commit 55c2c2e555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 262 additions and 72 deletions

View File

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

View File

@ -71,7 +71,7 @@
"zod": "^3.11.6" "zod": "^3.11.6"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve svelte-check-watch typesafe-i18n", "start": "run-p templater serve svelte-check-watch typesafe-i18n-watch",
"templater": "cross-env ./templater.sh", "templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
@ -84,7 +84,8 @@
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"", "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'", "pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'", "pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'",
"typesafe-i18n": "typesafe-i18n --no-watch" "typesafe-i18n": "typesafe-i18n --no-watch",
"typesafe-i18n-watch": "typesafe-i18n"
}, },
"lint-staged": { "lint-staged": {
"*.svelte": [ "*.svelte": [

View File

@ -1,5 +1,12 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
let entryPoint: string = $startLayerNamesStore[0];
let walkAutomatically: boolean = false;
const currentPlayer = gameManager.getCurrentGameScene().CurrentPlayer;
const playerPos = { x: Math.floor(currentPlayer.x), y: Math.floor(currentPlayer.y) };
function copyLink() { function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement; const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@ -8,8 +15,23 @@
document.execCommand("copy"); document.execCommand("copy");
} }
function getLink() {
return `${location.origin}${location.pathname}#${entryPoint}${
walkAutomatically ? `&moveTo=${playerPos.x},${playerPos.y}` : ""
}`;
}
function updateInputFieldValue() {
const input = document.getElementById("input-share-link");
if (input) {
(input as HTMLInputElement).value = getLink();
}
}
let canShare = navigator.share !== undefined;
async function shareLink() { async function shareLink() {
const shareData = { url: location.toString() }; const shareData = { url: getLink() };
try { try {
await navigator.share(shareData); await navigator.share(shareData);
@ -22,16 +44,43 @@
<div class="guest-main"> <div class="guest-main">
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> {#if !canShare}
<h3>{$LL.menu.invite.description()}</h3> <section class="share-url not-mobile">
<input type="text" readonly id="input-share-link" value={location.toString()} /> <h3>{$LL.menu.invite.description()}</h3>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button> <input type="text" readonly id="input-share-link" class="link-url" value={location.toString()} />
</section> <button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
<section class="is-mobile"> </section>
<h3>{$LL.menu.invite.description()}</h3> {:else}
<input type="hidden" readonly id="input-share-link" value={location.toString()} /> <section class="is-mobile">
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button> <h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section>
{/if}
<h3>Select an entry point</h3>
<section class="nes-select is-dark starting-points">
<select
bind:value={entryPoint}
on:blur={() => {
updateInputFieldValue();
}}
>
{#each $startLayerNamesStore as entryPointName}
<option value={entryPointName}>{entryPointName}</option>
{/each}
</select>
</section> </section>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={walkAutomatically}
on:change={() => {
updateInputFieldValue();
}}
/>
<span>{$LL.menu.invite.walk_automatically_to_position()}</span>
</label>
</section> </section>
</div> </div>
@ -39,14 +88,27 @@
@import "../../../style/breakpoints.scss"; @import "../../../style/breakpoints.scss";
div.guest-main { div.guest-main {
width: 50%;
margin-left: auto;
margin-right: auto;
height: calc(100% - 56px); height: calc(100% - 56px);
text-align: center; input.link-url {
width: calc(100% - 200px);
}
.starting-points {
width: 80%;
}
section { section {
margin-bottom: 50px; margin-bottom: 50px;
} }
section.nes-select select:focus {
outline: none;
}
section.container-overflow { section.container-overflow {
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -55,25 +117,23 @@
} }
section.is-mobile { section.is-mobile {
display: none; display: block;
text-align: center;
margin-bottom: 20px;
} }
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
div.guest-main { div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow { section.container-overflow {
height: calc(100% - 120px); height: calc(100% - 120px);
} }
} }
} }
@include media-breakpoint-up(lg) {
div.guest-main {
width: 100%;
}
}
</style> </style>

View File

@ -323,6 +323,10 @@ export class GameMap {
throw new Error("No possible position found"); throw new Error("No possible position found");
} }
public getObjectWithName(name: string): ITiledMapObject | undefined {
return this.tiledObjects.find((object) => object.name === name);
}
private getLayersByKey(key: number): Array<ITiledMapLayer> { private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0); return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
} }

View File

@ -91,6 +91,8 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore"; import { followUsersColorStore } from "../../Stores/FollowStore";
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte"; import { locale } from "../../i18n/i18n-svelte";
import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
@ -543,6 +545,8 @@ export class GameScene extends DirtyScene {
urlManager.getStartLayerNameFromUrl() urlManager.getStartLayerNameFromUrl()
); );
startLayerNamesStore.set(this.startPositionCalculator.getStartPositionNames());
//add entities //add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>(); this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@ -570,6 +574,8 @@ export class GameScene extends DirtyScene {
this.createCurrentPlayer(); this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
this.tryMovePlayerWithMoveToParameter();
this.cameraManager = new CameraManager( this.cameraManager = new CameraManager(
this, this,
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels }, { x: this.Map.widthInPixels, y: this.Map.heightInPixels },
@ -1370,9 +1376,9 @@ ${escapedMessage}
}); });
iframeListener.registerAnswerer("movePlayerTo", async (message) => { iframeListener.registerAnswerer("movePlayerTo", async (message) => {
const index = this.getGameMap().getTileIndexAt(message.x, message.y); const startTileIndex = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const startTile = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y); const destinationTileIndex = this.getGameMap().getTileIndexAt(message.x, message.y);
const path = await this.getPathfindingManager().findPath(startTile, index, true, true); const path = await this.getPathfindingManager().findPath(startTileIndex, destinationTileIndex, true, true);
path.shift(); path.shift();
if (path.length === 0) { if (path.length === 0) {
throw new Error("no path available"); throw new Error("no path available");
@ -1536,6 +1542,36 @@ ${escapedMessage}
this.MapPlayersByKey.clear(); this.MapPlayersByKey.clear();
} }
private tryMovePlayerWithMoveToParameter(): void {
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
let endPos;
const posFromParam = StringUtils.parsePointFromParam(moveToParam);
if (posFromParam) {
endPos = this.gameMap.getTileIndexAt(posFromParam.x, posFromParam.y);
} else {
const destinationObject = this.gameMap.getObjectWithName(moveToParam);
if (destinationObject) {
endPos = this.gameMap.getTileIndexAt(destinationObject.x, destinationObject.y);
} else {
endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
}
}
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
}
private getExitUrl(layer: ITiledMapLayer): string | undefined { private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined; return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined;
} }
@ -1658,22 +1694,6 @@ ${escapedMessage}
this.connection?.emitEmoteEvent(emoteKey); this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey); analyticsClient.launchEmote(emoteKey);
}); });
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) { } catch (err) {
if (err instanceof TextureError) { if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());

View File

@ -16,32 +16,6 @@ export class StartPositionCalculator {
) { ) {
this.initStartXAndStartY(); this.initStartXAndStartY();
} }
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
/** /**
* *
@ -76,6 +50,47 @@ export class StartPositionCalculator {
} }
} }
public getStartPositionNames(): string[] {
const names: string[] = [];
for (const layer of this.gameMap.flatLayers) {
if (layer.name === "start") {
names.push(layer.name);
continue;
}
if (this.isStartLayer(layer)) {
names.push(layer.name);
}
}
return names;
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
private isStartLayer(layer: ITiledMapLayer): boolean { private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, GameMapProperties.START_LAYER) == true; return this.getProperty(layer, GameMapProperties.START_LAYER) == true;
} }

View File

@ -0,0 +1,6 @@
import { Readable, writable } from "svelte/store";
/**
* A store that contains the map starting layers names
*/
export const startLayerNamesStore = writable<string[]>([]);

View File

@ -0,0 +1,12 @@
export class StringUtils {
public static parsePointFromParam(param: string, separator: string = ","): { x: number; y: number } | undefined {
const values = param.split(separator).map((val) => parseInt(val));
if (values.length !== 2) {
return;
}
if (isNaN(values[0]) || isNaN(values[1])) {
return;
}
return { x: values[0], y: values[1] };
}
}

View File

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

View File

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

View File

@ -70,6 +70,7 @@ const menu: NonNullable<Translation["menu"]> = {
description: "Partager le lien de la salle!", description: "Partager le lien de la salle!",
copy: "Copier", copy: "Copier",
share: "Partager", share: "Partager",
walk_automatically_to_position: "Marcher automatiquement jusqu'à ma position",
}, },
globalMessage: { globalMessage: {
text: "Texte", text: "Texte",

View File

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