Merge branch 'develop' of github.com:thecodingmachine/workadventure into audioPlayerImprovements

This commit is contained in:
Lurkars 2021-07-21 19:26:29 +02:00
commit 285f1ebe89
175 changed files with 5414 additions and 3497 deletions

View File

@ -199,4 +199,4 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
msg: Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re msg: "Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re \nTests available at https://maps-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re/tests"

View File

@ -2,6 +2,7 @@ name: Push @workadventure/iframe-api-typings to NPM
on: on:
release: release:
types: [created] types: [created]
push:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -13,10 +14,6 @@ jobs:
node-version: '14.x' node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Edit tsconfig.json to add declarations
run: "sed -i 's/\"declaration\": false/\"declaration\": true/g' tsconfig.json"
working-directory: "front"
- name: Replace version number - name: Replace version number
run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json' run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json'
working-directory: "front/packages/iframe-api-typings" working-directory: "front/packages/iframe-api-typings"
@ -47,15 +44,18 @@ jobs:
working-directory: "front" working-directory: "front"
- name: "Build" - name: "Build"
run: yarn run build run: yarn run build-typings
env: env:
API_URL: "localhost:8080" PUSHER_URL: "//localhost:8080"
working-directory: "front" working-directory: "front"
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.
- name: Copy typings to package dir - name: Copy typings to package dir
run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts
- name: Copy typings to package dir (2)
run: cp -R front/dist/src/Api front/packages/iframe-api-typings/Api
- name: Install dependencies in package - name: Install dependencies in package
run: yarn install run: yarn install
working-directory: "front/packages/iframe-api-typings" working-directory: "front/packages/iframe-api-typings"
@ -65,3 +65,4 @@ jobs:
working-directory: "front/packages/iframe-api-typings" working-directory: "front/packages/iframe-api-typings"
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
if: ${{ github.event_name == 'release' }}

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ docker-compose.override.yaml
maps/yarn.lock maps/yarn.lock
maps/dist/computer.js maps/dist/computer.js
maps/dist/computer.js.map maps/dist/computer.js.map
/node_modules/ node_modules
_

View File

@ -10,11 +10,35 @@
- New scripting API features : - New scripting API features :
- Use `WA.room.showLayer(): void` to show a layer - Use `WA.room.showLayer(): void` to show a layer
- Use `WA.room.hideLayer(): void` to hide a layer - Use `WA.room.hideLayer(): void` to hide a layer
- Use `WA.room.setProperty() : void` to add or change existing property of a layer - Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player - Use `WA.player.onPlayerMove(): void` to track the movement of the current player
- Use `WA.room.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player - Use `WA.player.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu - Use `WA.ui.registerMenuCommand(): void` to add a custom menu
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
- The text chat was redesigned to be prettier and to use more features :
- The chat is now persistent bewteen discussions and always accesible
- The chat now tracks incoming and outcoming users in your conversation
- The chat allows your to see the visit card of users
- You can close the chat window with the escape key
- Added a 'Enable notifications' button in the menu.
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
- `/api/ban`: new endpoint to report users
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
## Version 1.4.3 - 1.4.4 - 1.4.5
## Bugfixes
- Fixing the generation of @workadventure/iframe-api-typings
## Version 1.4.2
## Updates
- A script in an iframe opened by another script can use the IFrame API.
## Version 1.4.1 ## Version 1.4.1

View File

@ -42,7 +42,7 @@ Before committing, be sure to install the "Prettier" precommit hook that will re
In order to enable the "Prettier" precommit hook, at the root of the project, run: In order to enable the "Prettier" precommit hook, at the root of the project, run:
```console ```console
$ yarn run install $ yarn install
$ yarn run prepare $ yarn run prepare
``` ```

View File

@ -15,7 +15,7 @@ export class DebugController {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.status(401).send("Invalid token sent!"); return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
} }
return res return res

View File

@ -5,8 +5,6 @@ import { PositionInterface } from "_Model/PositionInterface";
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
import { PositionNotifier } from "./PositionNotifier"; import { PositionNotifier } from "./PositionNotifier";
import { Movable } from "_Model/Movable"; import { Movable } from "_Model/Movable";
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
import { arrayIntersect } from "../Services/ArrayHelper";
import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { ZoneSocket } from "src/RoomManager"; import { ZoneSocket } from "src/RoomManager";
@ -15,12 +13,6 @@ import { Admin } from "../Model/Admin";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
export enum GameRoomPolicyTypes {
ANONYMOUS_POLICY = 1,
MEMBERS_ONLY_POLICY,
USE_TAGS_POLICY,
}
export class GameRoom { export class GameRoom {
private readonly minDistance: number; private readonly minDistance: number;
private readonly groupRadius: number; private readonly groupRadius: number;
@ -37,15 +29,12 @@ export class GameRoom {
private itemsState: Map<number, unknown> = new Map<number, unknown>(); private itemsState: Map<number, unknown> = new Map<number, unknown>();
private readonly positionNotifier: PositionNotifier; private readonly positionNotifier: PositionNotifier;
public readonly roomId: string; public readonly roomUrl: string;
public readonly roomSlug: string;
public readonly worldSlug: string = "";
public readonly organizationSlug: string = "";
private versionNumber: number = 1; private versionNumber: number = 1;
private nextUserId: number = 1; private nextUserId: number = 1;
constructor( constructor(
roomId: string, roomUrl: string,
connectCallback: ConnectCallback, connectCallback: ConnectCallback,
disconnectCallback: DisconnectCallback, disconnectCallback: DisconnectCallback,
minDistance: number, minDistance: number,
@ -55,16 +44,7 @@ export class GameRoom {
onLeaves: LeavesCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback onEmote: EmoteCallback
) { ) {
this.roomId = roomId; this.roomUrl = roomUrl;
if (isRoomAnonymous(roomId)) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else {
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
this.roomSlug = roomSlug;
this.organizationSlug = organizationSlug;
this.worldSlug = worldSlug;
}
this.users = new Map<number, User>(); this.users = new Map<number, User>();
this.usersByUuid = new Map<string, User>(); this.usersByUuid = new Map<string, User>();
@ -183,7 +163,7 @@ export class GameRoom {
} else { } else {
const closestUser: User = closestItem; const closestUser: User = closestItem;
const group: Group = new Group( const group: Group = new Group(
this.roomId, this.roomUrl,
[user, closestUser], [user, closestUser],
this.connectCallback, this.connectCallback,
this.disconnectCallback, this.disconnectCallback,

View File

@ -1,30 +0,0 @@
//helper functions to parse room IDs
export const isRoomAnonymous = (roomID: string): boolean => {
if (roomID.startsWith("_/")) {
return true;
} else if (roomID.startsWith("@/")) {
return false;
} else {
throw new Error("Incorrect room ID: " + roomID);
}
};
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
const idParts = roomId.split("/");
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
return idParts.slice(2).join("/");
};
export interface extractDataFromPrivateRoomIdResponse {
organizationSlug: string;
worldSlug: string;
roomSlug: string;
}
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
const idParts = roomId.split("/");
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
const organizationSlug = idParts[1];
const worldSlug = idParts[2];
const roomSlug = idParts[3];
return { organizationSlug, worldSlug, roomSlug };
};

View File

@ -250,12 +250,12 @@ export class SocketManager {
//user leave previous world //user leave previous world
room.leave(user); room.leave(user);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(room.roomId); this.rooms.delete(room.roomUrl);
gaugeManager.decNbRoomGauge(); gaugeManager.decNbRoomGauge();
debug('Room is empty. Deleting room "%s"', room.roomId); debug('Room is empty. Deleting room "%s"', room.roomUrl);
} }
} finally { } finally {
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
console.log("A user left"); console.log("A user left");
} }
} }
@ -308,6 +308,7 @@ export class SocketManager {
throw new Error("clientUser.userId is not an integer " + thing.id); throw new Error("clientUser.userId is not an integer " + thing.id);
} }
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid);
userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setName(thing.name);
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
@ -425,7 +426,6 @@ export class SocketManager {
// Let's send 2 messages: one to the user joining the group and one to the other user // Let's send 2 messages: one to the user joining the group and one to the other user
const webrtcStartMessage1 = new WebRtcStartMessage(); const webrtcStartMessage1 = new WebRtcStartMessage();
webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.name);
webrtcStartMessage1.setInitiator(true); webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
@ -436,14 +436,10 @@ export class SocketManager {
const serverToClientMessage1 = new ServerToClientMessage(); const serverToClientMessage1 = new ServerToClientMessage();
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
//if (!user.socket.disconnecting) {
user.socket.write(serverToClientMessage1); user.socket.write(serverToClientMessage1);
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
//}
const webrtcStartMessage2 = new WebRtcStartMessage(); const webrtcStartMessage2 = new WebRtcStartMessage();
webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.name);
webrtcStartMessage2.setInitiator(false); webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
@ -454,10 +450,7 @@ export class SocketManager {
const serverToClientMessage2 = new ServerToClientMessage(); const serverToClientMessage2 = new ServerToClientMessage();
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
//if (!otherUser.socket.disconnecting) {
otherUser.socket.write(serverToClientMessage2); otherUser.socket.write(serverToClientMessage2);
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
//}
} }
} }
@ -614,6 +607,7 @@ export class SocketManager {
if (thing instanceof User) { if (thing instanceof User) {
const userJoinedMessage = new UserJoinedZoneMessage(); const userJoinedMessage = new UserJoinedZoneMessage();
userJoinedMessage.setUserid(thing.id); userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setUseruuid(thing.uuid);
userJoinedMessage.setName(thing.name); userJoinedMessage.setName(thing.name);
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
@ -664,9 +658,9 @@ export class SocketManager {
public leaveAdminRoom(room: GameRoom, admin: Admin) { public leaveAdminRoom(room: GameRoom, admin: Admin) {
room.adminLeave(admin); room.adminLeave(admin);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(room.roomId); this.rooms.delete(room.roomUrl);
gaugeManager.decNbRoomGauge(); gaugeManager.decNbRoomGauge();
debug('Room is empty. Deleting room "%s"', room.roomId); debug('Room is empty. Deleting room "%s"', room.roomUrl);
} }
} }

View File

@ -1,19 +0,0 @@
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
describe("RoomIdentifier", () => {
it("should flag public id as anonymous", () => {
expect(isRoomAnonymous('_/global/test')).toBe(true);
});
it("should flag public id as not anonymous", () => {
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
});
it("should extract roomSlug from public ID", () => {
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
});
it("should extract correct from private ID", () => {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
expect(organizationSlug).toBe('afup');
expect(worldSlug).toBe('afup2020');
expect(roomSlug).toBe('1floor');
});
})

View File

@ -52,11 +52,11 @@ WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
### Opening/closing a web page in an iFrame ### Opening/closing a web page in an iFrame
``` ```
WA.nav.openCoWebSite(url: string): void WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void
WA.nav.closeCoWebSite(): void WA.nav.closeCoWebSite(): void
``` ```
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow).
Example: Example:
@ -65,4 +65,3 @@ WA.nav.openCoWebSite('https://www.wikipedia.org/');
// ... // ...
WA.nav.closeCoWebSite(); WA.nav.closeCoWebSite();
``` ```

View File

@ -1,6 +1,7 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Player functions Reference # API Player functions Reference
### Listen to player movement ### Listen to player movement
``` ```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;

View File

@ -54,6 +54,7 @@ WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void WA.room.hideLayer(layerName : string) : void
``` ```
These 2 methods can be used to show and hide a layer. These 2 methods can be used to show and hide a layer.
if `layerName` is the name of a group layer, show/hide all the layer in that group layer.
Example : Example :
```javascript ```javascript
@ -70,45 +71,43 @@ WA.room.setProperty(layerName : string, propertyName : string, propertyValue : s
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
Note :
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
Example : Example :
```javascript ```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
``` ```
### Getting information on the current room ### Changing tiles
``` ```
WA.room.getCurrentRoom(): Promise<Room> WA.room.setTiles(tiles: TileDescriptor[]): void
``` ```
Return a promise that resolves to a `Room` object with the following attributes : Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`.
* **id (string) :** ID of the current room
* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. If `tile` is a string, it's not the id of the tile but the value of the property `name`.
* **mapUrl (string) :** Url of the JSON map file <div class="row">
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer <div class="col">
<img src="https://workadventu.re/img/docs/nameIndexProperty.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
`TileDescriptor` has the following attributes :
* **x (number) :** The coordinate x of the tile that you want to replace.
* **y (number) :** The coordinate y of the tile that you want to replace.
* **tile (number | string) :** The id of the tile that will be placed in the map.
* **layer (string) :** The name of the layer where the tile will be placed.
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor.
Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
Example : Example :
```javascript ```javascript
WA.room.getCurrentRoom((room) => { WA.room.setTiles([
if (room.id === '42') { {x: 6, y: 4, tile: 'blue', layer: 'setTiles'},
console.log(room.map); {x: 7, y: 4, tile: 109, layer: 'setTiles'},
window.open(room.mapUrl, '_blank'); {x: 8, y: 4, tile: 109, layer: 'setTiles'},
} {x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
}) ]);
```
### Getting information on the current user
```
WA.player.getCurrentUser(): Promise<User>
```
Return a promise that resolves to a `User` object with the following attributes :
* **id (string) :** ID of the current user
* **nickName (string) :** name displayed above the current user
* **tags (string[]) :** list of all the tags of the current user
Example :
```javascript
WA.room.getCurrentUser().then((user) => {
if (user.nickName === 'ABC') {
console.log(user.tags);
}
})
``` ```

View File

@ -37,8 +37,7 @@
<div class="main-container" id="main-container"> <div class="main-container" id="main-container">
<!-- Create the editor container --> <!-- Create the editor container -->
<div id="game" class="game"> <div id="game" class="game">
<div id="svelte-overlay"> <div id="svelte-overlay"></div>
</div>
<div id="game-overlay" class="game-overlay"> <div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section"> <div id="main-section" class="main-section">
</div> </div>

View File

@ -57,6 +57,9 @@
<section> <section>
<button id="toggleFullscreen">Toggle fullscreen</button> <button id="toggleFullscreen">Toggle fullscreen</button>
</section> </section>
<section>
<button id="enableNotification">Enable notifications</button>
</section>
<section> <section>
<button id="sparkButton">Create map</button> <button id="sparkButton">Create map</button>
</section> </section>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

53
front/dist/resources/service-worker.js vendored Normal file
View File

@ -0,0 +1,53 @@
let CACHE_NAME = 'workavdenture-cache-v1';
let urlsToCache = [
'/'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
self.addEventListener('activate', function(event) {
//TODO activate service worker
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -119,7 +119,13 @@
"src": "/static/images/favicons/android-icon-192x192.png", "src": "/static/images/favicons/android-icon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image\/png", "type": "image\/png",
"density": "4.0" "density": "4.0",
"purpose": "any maskable"
},
{
"src": "/static/images/favicons/icon-512x512.png",
"sizes": "512x512",
"type": "image\/png"
} }
], ],
"start_url": "/", "start_url": "/",
@ -127,6 +133,7 @@
"display_override": ["window-control-overlay", "minimal-ui"], "display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"lang": "en",
"theme_color": "#000000", "theme_color": "#000000",
"shortcuts": [ "shortcuts": [
{ {
@ -134,7 +141,7 @@
"short_name": "WA", "short_name": "WA",
"description": "WorkAdventure application", "description": "WorkAdventure application",
"url": "/", "url": "/",
"icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192" }] "icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" }]
} }
], ],
"description": "WorkAdventure application", "description": "WorkAdventure application",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
front/dist/static/images/send.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -39,7 +39,7 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/press-start-2p": "^4.3.0", "@fontsource/press-start-2p": "^4.3.0",
"@types/simple-peer": "^9.6.0", "@types/simple-peer": "^9.11.1",
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.32",
"axios": "^0.21.1", "axios": "^0.21.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -51,7 +51,7 @@
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"simple-peer": "^9.6.2", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4" "standardized-audio-context": "^25.2.4"
}, },
@ -60,7 +60,8 @@
"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",
"test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
"test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
"precommit": "lint-staged", "precommit": "lint-staged",

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isDataLayerEvent = new tg.IsInterface()
.withProperties({
export const isDataLayerEvent = data: tg.isObject,
new tg.IsInterface().withProperties({ })
data: tg.isObject .get();
}).get();
/** /**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers

View File

@ -1,14 +1,15 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isGameStateEvent = export const isGameStateEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
roomId: tg.isString, roomId: tg.isString,
mapUrl: tg.isString, mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull), nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined), uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull), startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags : tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when the gameState is received by the script * A message sent from the game to the iFrame when the gameState is received by the script
*/ */

View File

@ -1,19 +1,17 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent = new tg.IsInterface()
.withProperties({
export const isHasPlayerMovedEvent = direction: tg.isElementOf("right", "left", "up", "down"),
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
moving: tg.isBoolean, moving: tg.isBoolean,
x: tg.isNumber, x: tg.isNumber,
y: tg.isNumber y: tg.isNumber,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame to notify a movement from the current player. * A message sent from the game to the iFrame to notify a movement from the current player.
*/ */
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>; export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void

View File

@ -1,73 +1,73 @@
import type { GameStateEvent } from "./GameStateEvent";
import type { GameStateEvent } from './GameStateEvent'; import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ButtonClickedEvent } from './ButtonClickedEvent'; import type { ChatEvent } from "./ChatEvent";
import type { ChatEvent } from './ChatEvent'; import type { ClosePopupEvent } from "./ClosePopupEvent";
import type { ClosePopupEvent } from './ClosePopupEvent'; import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { GoToPageEvent } from "./GoToPageEvent";
import type { GoToPageEvent } from './GoToPageEvent'; import type { LoadPageEvent } from "./LoadPageEvent";
import type { LoadPageEvent } from './LoadPageEvent'; import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from "./OpenTabEvent";
import type { OpenTabEvent } from './OpenTabEvent'; import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { DataLayerEvent } from "./DataLayerEvent"; import type { DataLayerEvent } from "./DataLayerEvent";
import type { LayerEvent } from './LayerEvent'; import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent"; import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T data: T;
} }
/**
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = { export type IframeEventMap = {
//getState: GameStateEvent, loadPage: LoadPageEvent;
// updateTile: UpdateTileEvent chat: ChatEvent;
loadPage: LoadPageEvent openPopup: OpenPopupEvent;
chat: ChatEvent, closePopup: ClosePopupEvent;
openPopup: OpenPopupEvent openTab: OpenTabEvent;
closePopup: ClosePopupEvent goToPage: GoToPageEvent;
openTab: OpenTabEvent openCoWebSite: OpenCoWebSiteEvent;
goToPage: GoToPageEvent closeCoWebSite: null;
openCoWebSite: OpenCoWebSiteEvent disablePlayerControls: null;
closeCoWebSite: null restorePlayerControls: null;
disablePlayerControls: null displayBubble: null;
restorePlayerControls: null removeBubble: null;
displayBubble: null onPlayerMove: undefined;
removeBubble: null showLayer: LayerEvent;
onPlayerMove: undefined hideLayer: LayerEvent;
showLayer: LayerEvent setProperty: SetPropertyEvent;
hideLayer: LayerEvent getDataLayer: undefined;
setProperty: SetPropertyEvent loadSound: LoadSoundEvent;
getDataLayer: undefined playSound: PlaySoundEvent;
loadSound: LoadSoundEvent stopSound: null;
playSound: PlaySoundEvent getState: undefined;
stopSound: null, registerMenuCommand: MenuItemRegisterEvent;
getState: undefined, setTiles: SetTilesEvent;
registerMenuCommand: MenuItemRegisterEvent };
}
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
data: IframeEventMap[T]; data: IframeEventMap[T];
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string'; export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export interface IframeResponseEventMap { export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent userInputChat: UserInputChatEvent;
enterEvent: EnterLeaveEvent enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent buttonClickedEvent: ButtonClickedEvent;
gameState: GameStateEvent hasPlayerMoved: HasPlayerMovedEvent;
hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent;
dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent;
menuItemClicked: MenuItemClickedEvent
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;
@ -75,4 +75,49 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string'; export const isIframeResponseEventWrapper = (event: {
type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
*/
export type IframeQueryMap = {
getState: {
query: undefined,
answer: GameStateEvent
},
}
export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T;
data: IframeQueryMap[T]['query'];
}
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
id: number;
query: IframeQuery<T>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === 'string';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
id: number;
type: T;
data: IframeQueryMap[T]['answer'];
}
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number';
export interface IframeErrorAnswerEvent {
id: number;
type: keyof IframeQueryMap;
error: string;
}
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string';

View File

@ -1,9 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isLayerEvent = export const isLayerEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
name: tg.isString, name: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to show/hide a layer. * A message sent from the iFrame to the game to show/hide a layer.
*/ */

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isLoadPageEvent = new tg.IsInterface()
.withProperties({
export const isLoadPageEvent =
new tg.IsInterface().withProperties({
url: tg.isString, url: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View File

@ -1,11 +1,12 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isOpenCoWebsite = new tg.IsInterface()
.withProperties({
export const isOpenCoWebsite =
new tg.IsInterface().withProperties({
url: tg.isString, url: tg.isString,
}).get(); allowApi: tg.isBoolean,
allowPolicy: tg.isString,
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View File

@ -0,0 +1,16 @@
import * as tg from "generic-type-guard";
export const isSetTilesEvent = tg.isArray(
new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull),
layer: tg.isString,
})
.get()
);
/**
* A message sent from the iFrame to the game to set one or many tiles.
*/
export type SetTilesEvent = tg.GuardedType<typeof isSetTilesEvent>;

View File

@ -1,11 +1,12 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isSetPropertyEvent = export const isSetPropertyEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
layerName: tg.isString, layerName: tg.isString,
propertyName: tg.isString, propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))) propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */

View File

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isMenuItemClickedEvent = export const isMenuItemClickedEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
menuItem: tg.isString menuItem: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when a menu item is clicked. * A message sent from the game to the iFrame when a menu item is clicked.
*/ */
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>; export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;

View File

@ -1,25 +1,26 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import { Subject } from 'rxjs'; import { Subject } from "rxjs";
export const isMenuItemRegisterEvent = export const isMenuItemRegisterEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
menutItem: tg.isString menutItem: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a new menu item. * A message sent from the iFrame to the game to add a new menu item.
*/ */
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>; export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent = export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
type: tg.isSingletonString("registerMenuCommand"), type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent data: isMenuItemRegisterEvent,
}).get(); })
.get();
const _registerMenuCommandStream: Subject<string> = new Subject(); const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem) _registerMenuCommandStream.next(event.menutItem);
} }

View File

@ -1,42 +1,45 @@
import {Subject} from "rxjs"; import { Subject } from "rxjs";
import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import {scriptUtils} from "./ScriptUtils"; import { scriptUtils } from "./ScriptUtils";
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import { import {
IframeErrorAnswerEvent,
IframeEvent, IframeEvent,
IframeEventMap, IframeEventMap, IframeQueryMap,
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
isIframeEventWrapper, isIframeEventWrapper,
TypedMessageEvent isIframeQueryWrapper,
TypedMessageEvent,
} from "./Events/IframeEvent"; } from "./Events/IframeEvent";
import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
//import { isLoadPageEvent } from './Events/LoadPageEvent'; import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent"; import type { DataLayerEvent } from "./Events/DataLayerEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent"; import type { GameStateEvent } from "./Events/GameStateEvent";
import type {GameStateEvent} from "./Events/GameStateEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
*/ */
class IframeListener { class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject(); private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable(); public readonly chatStream = this._chatStream.asObservable();
@ -82,9 +85,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject(); private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable(); public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject(); private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
@ -103,111 +103,155 @@ class IframeListener {
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject(); private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable(); public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>(); private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false; private sendPlayerMove: boolean = false;
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>
} = {};
init() { init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => { window.addEventListener(
// Do we trust the sender of this message? "message",
// Let's only accept messages from the iframe that are allowed. (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). // Do we trust the sender of this message?
let foundSrc: string | undefined; // Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
let iframe: HTMLIFrameElement; let iframe: HTMLIFrameElement | undefined;
for (iframe of this.iframes) { for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) { if (iframe.contentWindow === message.source) {
foundSrc = iframe.src; foundSrc = iframe.src;
break; break;
} }
}
if (foundSrc === undefined) {
return;
}
const payload = message.data;
if (isIframeEventWrapper(payload)) {
if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
}
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
} }
else if (payload.type === 'closeCoWebSite') { const payload = message.data;
scriptUtils.closeCoWebSite();
if (foundSrc === undefined || iframe === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn(
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
"If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
"parameter to WA.nav.openCoWebSite())"
);
}
return;
} }
else if (payload.type === 'disablePlayerControls') { foundSrc = this.getBaseUrl(foundSrc, message.source);
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next();
} else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next();
} else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next();
} else if (payload.type == "getState") {
this._gameStateStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
})
handleMenuItemRegistrationEvent(payload.data)
}
}
}, false);
if (isIframeQueryWrapper(payload)) {
const queryId = payload.id;
const query = payload.query;
const answerer = this.answerers[query.type];
if (answerer === undefined) {
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
console.error(errorMsg);
iframe.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: errorMsg
} as IframeErrorAnswerEvent, '*');
return;
}
Promise.resolve(answerer(query.data)).then((value) => {
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
data: value
}, '*');
}).catch(reason => {
console.error('An error occurred while responding to an iFrame query.', reason);
let reasonMsg: string;
if (reason instanceof Error) {
reasonMsg = reason.message;
} else {
reasonMsg = reason.toString();
}
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: reasonMsg
} as IframeErrorAnswerEvent, '*');
});
} else if (isIframeEventWrapper(payload)) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(
payload.data.url,
foundSrc,
payload.data.allowApi,
payload.data.allowPolicy
);
} else if (payload.type === "closeCoWebSite") {
scriptUtils.closeCoWebSite();
} else if (payload.type === "disablePlayerControls") {
this._disablePlayerControlStream.next();
} else if (payload.type === "restorePlayerControls") {
this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") {
this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
});
handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
}
}
},
false
);
} }
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({ this.postMessage({
'type' : 'dataLayer', type: "dataLayer",
'data' : dataLayerEvent data: dataLayerEvent,
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
}); });
} }
@ -220,25 +264,25 @@ class IframeListener {
} }
unregisterIframe(iframe: HTMLIFrameElement): void { unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => { this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
callback(); callback();
}); });
this.iframes.delete(iframe); this.iframes.delete(iframe);
} }
registerScript(scriptUrl: string): void { registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl) console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode ( // Using external iframe mode (
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = "none";
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add("allow-scripts");
iframe.sandbox.add('allow-top-navigation-by-user-activation'); iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe); document.body.prepend(iframe);
@ -246,36 +290,50 @@ class IframeListener {
this.registerIframe(iframe); this.registerIframe(iframe);
} else { } else {
// production code // production code
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = "none";
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add("allow-scripts");
iframe.sandbox.add('allow-top-navigation-by-user-activation'); iframe.sandbox.add("allow-top-navigation-by-user-activation");
//iframe.src = "data:text/html;charset=utf-8," + escape(html); //iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' + iframe.srcdoc =
'\n' + "<!doctype html>\n" +
"\n" +
'<html lang="en">\n' + '<html lang="en">\n' +
'<head>\n' + "<head>\n" +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' + '<script src="' +
'<script src="' + scriptUrl + '" ></script>\n' + window.location.protocol +
'<title></title>\n' + "//" +
'</head>\n' + window.location.host +
'</html>\n'; '/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
document.body.prepend(iframe); document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe); this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe); this.registerIframe(iframe);
} }
}
private getBaseUrl(src: string, source: MessageEventSource | null): string {
for (const script of this.scripts) {
if (script[1].contentWindow === source) {
return script[0];
}
}
return src;
} }
private static getIFrameId(scriptUrl: string): string { private static getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl); return "script" + btoa(scriptUrl);
} }
unregisterScript(scriptUrl: string): void { unregisterScript(scriptUrl: string): void {
@ -292,47 +350,47 @@ class IframeListener {
sendUserInputChat(message: string) { sendUserInputChat(message: string) {
this.postMessage({ this.postMessage({
'type': 'userInputChat', type: "userInputChat",
'data': { data: {
'message': message, message: message,
} as UserInputChatEvent } as UserInputChatEvent,
}); });
} }
sendEnterEvent(name: string) { sendEnterEvent(name: string) {
this.postMessage({ this.postMessage({
'type': 'enterEvent', type: "enterEvent",
'data': { data: {
"name": name name: name,
} as EnterLeaveEvent } as EnterLeaveEvent,
}); });
} }
sendLeaveEvent(name: string) { sendLeaveEvent(name: string) {
this.postMessage({ this.postMessage({
'type': 'leaveEvent', type: "leaveEvent",
'data': { data: {
"name": name name: name,
} as EnterLeaveEvent } as EnterLeaveEvent,
}); });
} }
hasPlayerMoved(event: HasPlayerMovedEvent) { hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) { if (this.sendPlayerMove) {
this.postMessage({ this.postMessage({
'type': 'hasPlayerMoved', type: "hasPlayerMoved",
'data': event data: event,
}); });
} }
} }
sendButtonClickedEvent(popupId: number, buttonId: number): void { sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({ this.postMessage({
'type': 'buttonClickedEvent', type: "buttonClickedEvent",
'data': { data: {
popupId, popupId,
buttonId buttonId,
} as ButtonClickedEvent } as ButtonClickedEvent,
}); });
} }
@ -341,10 +399,25 @@ class IframeListener {
*/ */
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) { public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*'); iframe.contentWindow?.postMessage(message, "*");
} }
} }
/**
* Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type).
*
* Important! There can be only one "answerer" so registering a new one will unregister the old one.
*
* @param key The "type" of the query we are answering
* @param callback
*/
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
} }
export const iframeListener = new IframeListener(); export const iframeListener = new IframeListener();

View File

@ -1,21 +1,19 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
class ScriptUtils { class ScriptUtils {
public openTab(url: string) {
public openTab(url : string){
window.open(url); window.open(url);
} }
public goToPage(url : string){ public goToPage(url: string) {
window.location.href = url; window.location.href = url;
} }
public openCoWebsite(url: string, base: string) { public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
coWebsiteManager.loadCoWebsite(url, base); coWebsiteManager.loadCoWebsite(url, base, api, policy);
} }
public closeCoWebSite(){ public closeCoWebSite() {
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
} }
} }

View File

@ -1,9 +1,40 @@
import type * as tg from "generic-type-guard"; import type * as tg from "generic-type-guard";
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; import type {
IframeEvent,
IframeEventMap, IframeQuery,
IframeQueryMap,
IframeResponseEventMap
} from '../Events/IframeEvent';
import type {IframeQueryWrapper} from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) { export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*") window.parent.postMessage(content, "*")
} }
let queryNumber = 0;
export const answerPromises = new Map<number, {
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void
}>();
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
window.parent.postMessage({
id: queryNumber,
query: content
} as IframeQueryWrapper<T>, "*");
answerPromises.set(queryNumber, {
resolve,
reject
});
queryNumber++;
});
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> { export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {

View File

@ -1,11 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent'; import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
import { iframeListener } from '../../IframeListener'; import { iframeListener } from "../../IframeListener";
export function sendMenuClickedEvent(menuItem: string) { export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({ iframeListener.postMessage({
'type': 'menuItemClicked', type: "menuItemClicked",
'data': { data: {
menuItem: menuItem, menuItem: menuItem,
} as MenuItemClickedEvent } as MenuItemClickedEvent,
}); });
} }

View File

@ -1,30 +1,30 @@
import type { ChatEvent } from '../Events/ChatEvent' import type { ChatEvent } from "../Events/ChatEvent";
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent' import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent";
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution' import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import {Subject} from "rxjs"; import { Subject } from "rxjs";
const chatStream = new Subject<string>(); const chatStream = new Subject<string>();
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> { export class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
callbacks = [
callbacks = [apiCallback({ apiCallback({
callback: (event: UserInputChatEvent) => { callback: (event: UserInputChatEvent) => {
chatStream.next(event.message); chatStream.next(event.message);
}, },
type: "userInputChat", type: "userInputChat",
typeChecker: isUserInputChatEvent typeChecker: isUserInputChatEvent,
})] }),
];
sendChatMessage(message: string, author: string) { sendChatMessage(message: string, author: string) {
sendToWorkadventure({ sendToWorkadventure({
type: 'chat', type: "chat",
data: { data: {
'message': message, message: message,
'author': author author: author,
} },
}) });
} }
/** /**
@ -35,4 +35,4 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
} }
} }
export default new WorkadventureChatCommands() export default new WorkadventureChatCommands();

View File

@ -1,16 +1,15 @@
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> { export class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
callbacks = [] callbacks = [];
disablePlayerControls(): void { disablePlayerControls(): void {
sendToWorkadventure({ 'type': 'disablePlayerControls', data: null }); sendToWorkadventure({ type: "disablePlayerControls", data: null });
} }
restorePlayerControls(): void { restorePlayerControls(): void {
sendToWorkadventure({ 'type': 'restorePlayerControls', data: null }); sendToWorkadventure({ type: "restorePlayerControls", data: null });
} }
} }
export default new WorkadventureControlsCommands(); export default new WorkadventureControlsCommands();

View File

@ -1,57 +1,56 @@
import type { GoToPageEvent } from '../Events/GoToPageEvent'; import type { GoToPageEvent } from "../Events/GoToPageEvent";
import type { OpenTabEvent } from '../Events/OpenTabEvent'; import type { OpenTabEvent } from "../Events/OpenTabEvent";
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent"; import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent";
import type {LoadPageEvent} from "../Events/LoadPageEvent"; import type { LoadPageEvent } from "../Events/LoadPageEvent";
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
callbacks = []
export class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
callbacks = [];
openTab(url: string): void { openTab(url: string): void {
sendToWorkadventure({ sendToWorkadventure({
"type": 'openTab', type: "openTab",
"data": { data: {
url url,
} },
}); });
} }
goToPage(url: string): void { goToPage(url: string): void {
sendToWorkadventure({ sendToWorkadventure({
"type": 'goToPage', type: "goToPage",
"data": { data: {
url url,
} },
}); });
} }
goToRoom(url: string): void { goToRoom(url: string): void {
sendToWorkadventure({ sendToWorkadventure({
"type": 'loadPage', type: "loadPage",
"data": { data: {
url url,
} },
}); });
} }
openCoWebSite(url: string): void { openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
sendToWorkadventure({ sendToWorkadventure({
"type": 'openCoWebSite', type: "openCoWebSite",
"data": { data: {
url url,
} allowApi,
allowPolicy,
},
}); });
} }
closeCoWebSite(): void { closeCoWebSite(): void {
sendToWorkadventure({ sendToWorkadventure({
"type": 'closeCoWebSite', type: "closeCoWebSite",
data: null data: null,
}); });
} }
} }
export default new WorkadventureNavigationCommands(); export default new WorkadventureNavigationCommands();

View File

@ -1,28 +1,40 @@
import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution"; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
import {Subject} from "rxjs"; import { Subject } from "rxjs";
import {apiCallback} from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent"; import { getGameState } from "./room";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
interface User {
id: string | undefined;
nickName: string | null;
tags: string[];
}
const moveStream = new Subject<HasPlayerMovedEvent>(); const moveStream = new Subject<HasPlayerMovedEvent>();
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> { export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: 'hasPlayerMoved', type: "hasPlayerMoved",
typeChecker: isHasPlayerMovedEvent, typeChecker: isHasPlayerMovedEvent,
callback: (payloadData) => { callback: (payloadData) => {
moveStream.next(payloadData); moveStream.next(payloadData);
} },
}), }),
] ];
onPlayerMove(callback: HasPlayerMovedEventCallback): void { onPlayerMove(callback: HasPlayerMovedEventCallback): void {
moveStream.subscribe(callback); moveStream.subscribe(callback);
sendToWorkadventure({ sendToWorkadventure({
type: 'onPlayerMove', type: "onPlayerMove",
data: null data: null,
}) });
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
} }
} }

View File

@ -1,87 +1,74 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution'; import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type {LayerEvent} from "../Events/LayerEvent";
import type {SetPropertyEvent} from "../Events/setPropertyEvent"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type {GameStateEvent} from "../Events/GameStateEvent"; import type { DataLayerEvent } from "../Events/DataLayerEvent";
import type {ITiledMap} from "../../Phaser/Map/ITiledMap"; import type { GameStateEvent } from "../Events/GameStateEvent";
import type {DataLayerEvent} from "../Events/DataLayerEvent";
import {isGameStateEvent} from "../Events/GameStateEvent";
import {isDataLayerEvent} from "../Events/DataLayerEvent";
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>(); const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>();
let immutableData: GameStateEvent; let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
interface Room { interface Room {
id: string, id: string;
mapUrl: string, mapUrl: string;
map: ITiledMap, map: ITiledMap;
startLayer: string | null startLayer: string | null;
} }
interface User { interface TileDescriptor {
id: string | undefined, x: number;
nickName: string | null, y: number;
tags: string[] tile: number | string | null;
layer: string;
} }
export function getGameState(): Promise<GameStateEvent> {
function getGameState(): Promise<GameStateEvent> { if (immutableDataPromise === undefined) {
if (immutableData) { immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
return Promise.resolve(immutableData);
}
else {
return new Promise<GameStateEvent>((resolver, thrower) => {
stateResolvers.subscribe(resolver);
sendToWorkadventure({type: "getState", data: null});
})
} }
return immutableDataPromise;
} }
function getDataLayer(): Promise<DataLayerEvent> { function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => { return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver); dataLayerResolver.subscribe(resolver);
sendToWorkadventure({type: "getDataLayer", data: null}) sendToWorkadventure({ type: "getDataLayer", data: null });
}) });
} }
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> { export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [ callbacks = [
apiCallback({ apiCallback({
callback: (payloadData: EnterLeaveEvent) => { callback: (payloadData: EnterLeaveEvent) => {
enterStreams.get(payloadData.name)?.next(); enterStreams.get(payloadData.name)?.next();
}, },
type: "enterEvent", type: "enterEvent",
typeChecker: isEnterLeaveEvent typeChecker: isEnterLeaveEvent,
}), }),
apiCallback({ apiCallback({
type: "leaveEvent", type: "leaveEvent",
typeChecker: isEnterLeaveEvent, typeChecker: isEnterLeaveEvent,
callback: (payloadData) => { callback: (payloadData) => {
leaveStreams.get(payloadData.name)?.next(); leaveStreams.get(payloadData.name)?.next();
} },
}),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
}
}), }),
apiCallback({ apiCallback({
type: "dataLayer", type: "dataLayer",
typeChecker: isDataLayerEvent, typeChecker: isDataLayerEvent,
callback: (payloadData) => { callback: (payloadData) => {
dataLayerResolver.next(payloadData); dataLayerResolver.next(payloadData);
} },
}), }),
] ];
onEnterZone(name: string, callback: () => void): void { onEnterZone(name: string, callback: () => void): void {
let subject = enterStreams.get(name); let subject = enterStreams.get(name);
@ -90,7 +77,6 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
enterStreams.set(name, subject); enterStreams.set(name, subject);
} }
subject.subscribe(callback); subject.subscribe(callback);
} }
onLeaveZone(name: string, callback: () => void): void { onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name); let subject = leaveStreams.get(name);
@ -101,35 +87,39 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
subject.subscribe(callback); subject.subscribe(callback);
} }
showLayer(layerName: string): void { showLayer(layerName: string): void {
sendToWorkadventure({type: 'showLayer', data: {'name': layerName}}); sendToWorkadventure({ type: "showLayer", data: { name: layerName } });
} }
hideLayer(layerName: string): void { hideLayer(layerName: string): void {
sendToWorkadventure({type: 'hideLayer', data: {'name': layerName}}); sendToWorkadventure({ type: "hideLayer", data: { name: layerName } });
} }
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
sendToWorkadventure({ sendToWorkadventure({
type: 'setProperty', type: "setProperty",
data: { data: {
'layerName': layerName, layerName: layerName,
'propertyName': propertyName, propertyName: propertyName,
'propertyValue': propertyValue, propertyValue: propertyValue,
} },
}) });
} }
getCurrentRoom(): Promise<Room> { getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => { return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => { return getDataLayer().then((mapJson) => {
return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName}; return {
}) id: gameState.roomId,
}) map: mapJson.data as ITiledMap,
mapUrl: gameState.mapUrl,
startLayer: gameState.startLayerName,
};
});
});
} }
getCurrentUser(): Promise<User> { setTiles(tiles: TileDescriptor[]) {
return getGameState().then((gameState) => { sendToWorkadventure({
return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; type: "setTiles",
}) data: tiles,
});
} }
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View File

@ -1,17 +1,15 @@
import type { LoadSoundEvent } from '../Events/LoadSoundEvent'; import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
import type { PlaySoundEvent } from '../Events/PlaySoundEvent'; import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from '../Events/StopSoundEvent'; import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import {Sound} from "./Sound/Sound"; import { Sound } from "./Sound/Sound";
class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> { export class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
callbacks = [] callbacks = [];
loadSound(url: string): Sound { loadSound(url: string): Sound {
return new Sound(url); return new Sound(url);
} }
} }
export default new WorkadventureSoundCommands(); export default new WorkadventureSoundCommands();

View File

@ -1,53 +1,55 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent'; import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup"; import { Popup } from "./Ui/Popup";
let popupId = 0; let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>(); const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>(); const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
number,
Map<number, ButtonClickedCallback>
>();
const menuCallbacks: Map<string, (command: string) => void> = new Map() const menuCallbacks: Map<string, (command: string) => void> = new Map();
interface ZonedPopupOptions { interface ZonedPopupOptions {
zone: string zone: string;
objectLayerName?: string, objectLayerName?: string;
popupText: string, popupText: string;
delay?: number delay?: number;
popupOptions: Array<ButtonDescriptor> popupOptions: Array<ButtonDescriptor>;
} }
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> { callbacks = [
apiCallback({
callbacks = [apiCallback({ type: "buttonClickedEvent",
type: "buttonClickedEvent", typeChecker: isButtonClickedEvent,
typeChecker: isButtonClickedEvent, callback: (payloadData) => {
callback: (payloadData) => { const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); const popup = popups.get(payloadData.popupId);
const popup = popups.get(payloadData.popupId); if (popup === undefined) {
if (popup === undefined) { throw new Error('Could not find popup with ID "' + payloadData.popupId + '"');
throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); }
} if (callback) {
if (callback) { callback(popup);
callback(popup); }
} },
} }),
}), apiCallback({
apiCallback({ type: "menuItemClicked",
type: "menuItemClicked", typeChecker: isMenuItemClickedEvent,
typeChecker: isMenuItemClickedEvent, callback: (event) => {
callback: event => { const callback = menuCallbacks.get(event.menuItem);
const callback = menuCallbacks.get(event.menuItem); if (callback) {
if (callback) { callback(event.menuItem);
callback(event.menuItem) }
} },
} }),
})]; ];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++; popupId++;
@ -66,40 +68,40 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
} }
sendToWorkadventure({ sendToWorkadventure({
'type': 'openPopup', type: "openPopup",
'data': { data: {
popupId, popupId,
targetObject, targetObject,
message, message,
buttons: buttons.map((button) => { buttons: buttons.map((button) => {
return { return {
label: button.label, label: button.label,
className: button.className className: button.className,
}; };
}) }),
} },
}); });
popups.set(popupId, popup) popups.set(popupId, popup);
return popup; return popup;
} }
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback); menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({ sendToWorkadventure({
'type': 'registerMenuCommand', type: "registerMenuCommand",
'data': { data: {
menutItem: commandDescriptor menutItem: commandDescriptor,
} },
}); });
} }
displayBubble(): void { displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null }); sendToWorkadventure({ type: "displayBubble", data: null });
} }
removeBubble(): void { removeBubble(): void {
sendToWorkadventure({ 'type': 'removeBubble', data: null }); sendToWorkadventure({ type: "removeBubble", data: null });
} }
} }

View File

@ -10,12 +10,14 @@
import {errorStore} from "../Stores/ErrorStore"; import {errorStore} from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte"; import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte"; import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore"; import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte"; import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte"; import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore"; import {requestVisitCardsStore} from "../Stores/GameStore";
import type {Game} from "../Phaser/Game/Game"; import type {Game} from "../Phaser/Game/Game";
import {chatVisibilityStore} from "../Stores/ChatStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"; import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte";
@ -61,14 +63,6 @@
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
</div> </div>
{/if} {/if}
<!--
{#if $menuIconVisible}
<div>
<MenuIcon />
</div>
{/if}
-->
{#if $gameOverlayVisibilityStore} {#if $gameOverlayVisibilityStore}
<div> <div>
<VideoOverlay></VideoOverlay> <VideoOverlay></VideoOverlay>
@ -94,4 +88,7 @@
<ErrorDialog></ErrorDialog> <ErrorDialog></ErrorDialog>
</div> </div>
{/if} {/if}
{#if $chatVisibilityStore}
<Chat></Chat>
{/if}
</div> </div>

View File

@ -0,0 +1,101 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte';
import { afterUpdate, beforeUpdate } from "svelte";
let listDom: HTMLElement;
let autoscroll: boolean;
beforeUpdate(() => {
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
});
afterUpdate(() => {
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
});
function closeChat() {
chatVisibilityStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeChat();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
<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>
{#each $chatMessagesStore as message, i}
<li><ChatElement message={message} line={i}></ChatElement></li>
{/each}
</ul>
</section>
<section class="messageForm">
<ChatMessageForm></ChatMessageForm>
</section>
</aside>
<style lang="scss">
p.close-icon {
position: absolute;
padding: 4px;
right: 12px;
font-size: 30px;
line-height: 25px;
cursor: pointer;
}
p.system-text {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
background: gray;
display: inline-block;
}
aside.chatWindow {
z-index:100;
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
height: 100vh;
width:30vw;
min-width: 350px;
background: rgb(5, 31, 51, 0.9);
color: whitesmoke;
display: flex;
flex-direction: column;
padding: 10px;
border-bottom-right-radius: 16px;
border-top-right-radius: 16px;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
ul {
list-style-type: none;
padding-left: 0;
}
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
}
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts">
import {ChatMessageTypes} from "../../Stores/ChatStore";
import type {ChatMessage} from "../../Stores/ChatStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import ChatPlayerName from './ChatPlayerName.svelte';
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
export let message: ChatMessage;
export let line: number;
$: author = message.author as PlayerInterface;
$: targets = message.targets || [];
$: texts = message.text || [];
function urlifyText(text: string): string {
return HtmlUtils.urlify(text)
}
function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, {
hour: '2-digit',
minute:'2-digit'
});
}
function isLastIteration(index: number) {
return targets.length -1 === index;
}
</script>
<div class="chatElement">
<div class="messagePart">
{#if message.type === ChatMessageTypes.userIncoming}
&gt;&gt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.userOutcoming}
&lt;&lt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.me}
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="my-text">{@html urlifyText(text)}</p></div>
{/each}
{:else}
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="other-text">{@html urlifyText(text)}</p></div>
{/each}
{/if}
</div>
</div>
<style lang="scss">
h4, p {
font-family: Lato;
}
div.chatElement {
display: flex;
margin-bottom: 20px;
.messagePart {
flex-grow:1;
max-width: 100%;
span.date {
font-size: 80%;
color: gray;
}
div > p {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
&.other-text {
background: gray;
}
&.my-text {
background: #6489ff;
}
}
}
}
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
let newMessageText = '';
function onFocus() {
chatInputFocusStore.set(true);
}
function onBlur() {
chatInputFocusStore.set(false);
}
function saveMessage() {
if (!newMessageText) return;
chatMessagesStore.addPersonnalMessage(newMessageText);
newMessageText = '';
}
</script>
<form on:submit|preventDefault={saveMessage}>
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
<button type="submit">
<img src="/static/images/send.png" alt="Send" width="20">
</button>
</form>
<style lang="scss">
form {
display: flex;
padding-left: 4px;
padding-right: 4px;
input {
flex: auto;
background-color: #254560;
color: white;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
border: none;
font-size: 22px;
font-family: Lato;
padding-left: 6px;
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
outline: none;
}
button {
background-color: #254560;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
border: none;
border-left: solid white 1px;
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,51 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {chatSubMenuVisbilityStore} from "../../Stores/ChatStore";
import {onDestroy, onMount} from "svelte";
import type {Unsubscriber} from "svelte/store";
import ChatSubMenu from "./ChatSubMenu.svelte";
export let player: PlayerInterface;
export let line: number;
let isSubMenuOpen: boolean;
let chatSubMenuVisivilytUnsubcribe: Unsubscriber;
function openSubMenu() {
chatSubMenuVisbilityStore.openSubMenu(player.name, line);
}
onMount(() => {
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisbilityStore.subscribe((newValue) => {
isSubMenuOpen = (newValue === player.name + line);
})
})
onDestroy(() => {
chatSubMenuVisivilytUnsubcribe();
})
</script>
<span class="subMenu">
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
{player.name}
</span>
{#if isSubMenuOpen}
<ChatSubMenu player={player}/>
{/if}
</span>
<style lang="scss">
span.subMenu {
display: inline-block;
}
span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {requestVisitCardsStore} from "../../Stores/GameStore";
export let player: PlayerInterface;
function openVisitCard() {
if (player.visitCardUrl) {
requestVisitCardsStore.set(player.visitCardUrl);
}
}
</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>
</ul>
<style lang="scss">
ul.selectMenu {
background-color: whitesmoke;
position: absolute;
padding: 5px;
border-radius: 4px;
list-style-type: none;
li {
text-align: center;
}
}
</style>

View File

@ -1,11 +1,11 @@
<script lang="typescript"> <script lang="typescript">
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene"; import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene";
import {activeRowStore} from "../../Stores/CustomCharacterStore";
export let game: Game; export let game: Game;
const customCharacterScene = game.scene.getScene(CustomizeSceneName) as CustomizeScene; const customCharacterScene = game.scene.getScene(CustomizeSceneName) as CustomizeScene;
let activeRow = customCharacterScene.activeRow;
function selectLeft() { function selectLeft() {
customCharacterScene.moveCursorHorizontally(-1); customCharacterScene.moveCursorHorizontally(-1);
@ -17,12 +17,10 @@
function selectUp() { function selectUp() {
customCharacterScene.moveCursorVertically(-1); customCharacterScene.moveCursorVertically(-1);
activeRow = customCharacterScene.activeRow;
} }
function selectDown() { function selectDown() {
customCharacterScene.moveCursorVertically(1); customCharacterScene.moveCursorVertically(1);
activeRow = customCharacterScene.activeRow;
} }
function previousScene() { function previousScene() {
@ -44,16 +42,16 @@
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button> <button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button>
</section> </section>
<section class="action"> <section class="action">
{#if activeRow === 0} {#if $activeRowStore === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button> <button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
{/if} {/if}
{#if activeRow !== 0} {#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> <button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button>
{/if} {/if}
{#if activeRow === 5} {#if $activeRowStore === 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button> <button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
{/if} {/if}
{#if activeRow !== 5} {#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> <button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ selectDown }>Next <img src="resources/objects/arrow_down.png" alt=""/></button>
{/if} {/if}
</section> </section>

View File

@ -37,9 +37,7 @@
<img alt="Report this user" src={reportImg}> <img alt="Report this user" src={reportImg}>
<span>Report/Block</span> <span>Report/Block</span>
</button> </button>
{#if $streamStore }
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video> <video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
{/if}
<img src={blockSignImg} class="block-logo" alt="Block" /> <img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false} {#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget> <SoundMeterWidget stream={$streamStore}></SoundMeterWidget>

View File

@ -1,4 +1,7 @@
export function getColorByString(str: string) : string|null { import type { UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../../Enum/EnvironmentVariable";
export function getColorByString(str: string): string | null {
let hash = 0; let hash = 0;
if (str.length === 0) { if (str.length === 0) {
return null; return null;
@ -7,21 +10,37 @@ export function getColorByString(str: string) : string|null {
hash = str.charCodeAt(i) + ((hash << 5) - hash); hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; hash = hash & hash;
} }
let color = '#'; let color = "#";
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255; const value = (hash >> (i * 8)) & 255;
color += ('00' + value.toString(16)).substr(-2); color += ("00" + value.toString(16)).substr(-2);
} }
return color; return color;
} }
export function srcObject(node: HTMLVideoElement, stream: MediaStream) { export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
node.srcObject = stream; node.srcObject = stream;
return { return {
update(newStream: MediaStream) { update(newStream: MediaStream) {
if (node.srcObject != newStream) { if (node.srcObject != newStream) {
node.srcObject = newStream node.srcObject = newStream;
} }
} },
} };
}
export function getIceServersConfig(user: UserSimplePeerInterface): RTCIceServer[] {
const config: RTCIceServer[] = [
{
urls: STUN_SERVER.split(","),
},
];
if (TURN_SERVER !== "") {
config.push({
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
});
}
return config;
} }

View File

@ -45,8 +45,9 @@
.visitCard { .visitCard {
pointer-events: all; pointer-events: all;
margin-left: auto; position: absolute;
margin-right: auto; left: 50%;
transform: translate(-50%, 0);
margin-top: 200px; margin-top: 200px;
max-width: 80vw; max-width: 80vw;

View File

@ -38,11 +38,9 @@ class ConnectionManager {
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures); this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
const organizationSlug = data.organizationSlug; const roomUrl = data.roomUrl;
const worldSlug = data.worldSlug;
const roomSlug = data.roomSlug;
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash); const room = await Room.createRoom(new URL(window.location.protocol + '//' + window.location.host + roomUrl + window.location.search + window.location.hash));
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room); return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) { } else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
@ -66,22 +64,21 @@ class ConnectionManager {
throw "Error to store local user data"; throw "Error to store local user data";
} }
let roomId: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomId = START_ROOM_URL; roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
} else { } else {
roomId = window.location.pathname + window.location.search + window.location.hash; roomPath = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + window.location.hash;
} }
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
const room = new Room(roomId); const room = await Room.createRoom(new URL(roomPath));
const mapDetail = await room.getMapDetail(); if(room.textures != undefined && room.textures.length > 0) {
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
//check if texture was changed //check if texture was changed
if(localUser.textures.length === 0){ if(localUser.textures.length === 0){
localUser.textures = mapDetail.textures; localUser.textures = room.textures;
}else{ }else{
mapDetail.textures.forEach((newTexture) => { room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id); const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){ if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
return; return;
@ -114,9 +111,9 @@ class ConnectionManager {
this.localUser = new LocalUser('', 'test', []); this.localUser = new LocalUser('', 'test', []);
} }
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> { public connectToRoomSocket(roomUrl: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => { return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion); const connection = new RoomConnection(this.localUser.jwtToken, roomUrl, name, characterLayers, position, viewport, companion);
connection.onConnectError((error: object) => { connection.onConnectError((error: object) => {
console.log('An error occurred while connecting to socket server. Retrying'); console.log('An error occurred while connecting to socket server. Retrying');
reject(error); reject(error);
@ -137,7 +134,7 @@ class ConnectionManager {
this.reconnectingTimeout = setTimeout(() => { this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion? //todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection)); this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
}, 4000 + Math.floor(Math.random() * 2000) ); }, 4000 + Math.floor(Math.random() * 2000) );
}); });
}); });

View File

@ -1,8 +1,8 @@
import type {SignalData} from "simple-peer"; import type { SignalData } from "simple-peer";
import type {RoomConnection} from "./RoomConnection"; import type { RoomConnection } from "./RoomConnection";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
export enum EventMessage{ export enum EventMessage {
CONNECT = "connect", CONNECT = "connect",
WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
@ -17,7 +17,7 @@ export enum EventMessage{
GROUP_CREATE_UPDATE = "group-create-update", GROUP_CREATE_UPDATE = "group-create-update",
GROUP_DELETE = "group-delete", GROUP_DELETE = "group-delete",
SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
ITEM_EVENT = 'item-event', ITEM_EVENT = "item-event",
CONNECT_ERROR = "connect_error", CONNECT_ERROR = "connect_error",
CONNECTING_ERROR = "connecting_error", CONNECTING_ERROR = "connecting_error",
@ -36,7 +36,7 @@ export enum EventMessage{
export interface PointInterface { export interface PointInterface {
x: number; x: number;
y: number; y: number;
direction : string; direction: string;
moving: boolean; moving: boolean;
} }
@ -45,8 +45,9 @@ export interface MessageUserPositionInterface {
name: string; name: string;
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
visitCardUrl: string|null; visitCardUrl: string | null;
companion: string|null; companion: string | null;
userUuid: string;
} }
export interface MessageUserMovedInterface { export interface MessageUserMovedInterface {
@ -60,58 +61,59 @@ export interface MessageUserJoined {
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string|null; companion: string | null;
userUuid: string;
} }
export interface PositionInterface { export interface PositionInterface {
x: number, x: number;
y: number y: number;
} }
export interface GroupCreatedUpdatedMessageInterface { export interface GroupCreatedUpdatedMessageInterface {
position: PositionInterface, position: PositionInterface;
groupId: number, groupId: number;
groupSize: number groupSize: number;
} }
export interface WebRtcDisconnectMessageInterface { export interface WebRtcDisconnectMessageInterface {
userId: number userId: number;
} }
export interface WebRtcSignalReceivedMessageInterface { export interface WebRtcSignalReceivedMessageInterface {
userId: number, userId: number;
signal: SignalData, signal: SignalData;
webRtcUser: string | undefined, webRtcUser: string | undefined;
webRtcPassword: string | undefined webRtcPassword: string | undefined;
} }
export interface ViewportInterface { export interface ViewportInterface {
left: number, left: number;
top: number, top: number;
right: number, right: number;
bottom: number, bottom: number;
} }
export interface ItemEventMessageInterface { export interface ItemEventMessageInterface {
itemId: number, itemId: number;
event: string, event: string;
state: unknown, state: unknown;
parameters: unknown parameters: unknown;
} }
export interface RoomJoinedMessageInterface { export interface RoomJoinedMessageInterface {
//users: MessageUserPositionInterface[], //users: MessageUserPositionInterface[],
//groups: GroupCreatedUpdatedMessageInterface[], //groups: GroupCreatedUpdatedMessageInterface[],
items: { [itemId: number] : unknown } items: { [itemId: number]: unknown };
} }
export interface PlayGlobalMessageInterface { export interface PlayGlobalMessageInterface {
id: string id: string;
type: string type: string;
message: string message: string;
} }
export interface OnConnectInterface { export interface OnConnectInterface {
connection: RoomConnection, connection: RoomConnection;
room: RoomJoinedMessageInterface room: RoomJoinedMessageInterface;
} }

View File

@ -1,90 +1,105 @@
import Axios from "axios"; import Axios from "axios";
import {PUSHER_URL} from "../Enum/EnvironmentVariable"; import { PUSHER_URL } from "../Enum/EnvironmentVariable";
import type {CharacterTexture} from "./LocalUser"; import type { CharacterTexture } from "./LocalUser";
export class MapDetail{ export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) { constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
} }
export interface RoomRedirect {
redirectUrl: string;
} }
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; public readonly isPublic: boolean;
private mapUrl: string|undefined; private _mapUrl: string | undefined;
private textures: CharacterTexture[]|undefined; private _textures: CharacterTexture[] | undefined;
private instance: string|undefined; private instance: string | undefined;
private _search: URLSearchParams; private readonly _search: URLSearchParams;
constructor(id: string) { private constructor(private roomUrl: URL) {
const url = new URL(id, 'https://example.com'); this.id = roomUrl.pathname;
this.id = url.pathname; if (this.id.startsWith("/")) {
if (this.id.startsWith('/')) {
this.id = this.id.substr(1); this.id = this.id.substr(1);
} }
if (this.id.startsWith('_/')) { if (this.id.startsWith("_/")) {
this.isPublic = true; this.isPublic = true;
} else if (this.id.startsWith('@/')) { } else if (this.id.startsWith("@/")) {
this.isPublic = false; this.isPublic = false;
} else { } else {
throw new Error('Invalid room ID'); throw new Error("Invalid room ID");
} }
this._search = new URLSearchParams(url.search); this._search = new URLSearchParams(roomUrl.search);
} }
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} { /**
let roomId = ''; * Creates a "Room" object representing the room.
let hash = ''; * This method will follow room redirects if necessary, so the instance returned is a "real" room.
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link */
//Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects. public static async createRoom(roomUrl: URL): Promise<Room> {
//We instead use 'workadventure' as a dummy base value. let redirectCount = 0;
const baseUrlObject = new URL(baseUrl); while (redirectCount < 32) {
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/'+currentInstance+'/'+baseUrlObject.hostname+baseUrlObject.pathname); const room = new Room(roomUrl);
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId const result = await room.getMapDetail();
roomId = roomId.substring(1); //remove the leading slash if (result instanceof MapDetail) {
hash = absoluteExitSceneUrl.hash; return room;
hash = hash.substring(1); //remove the leading diese
} else { //absolute room Id
const parts = identifier.split('#');
roomId = parts[0];
roomId = roomId.substring(1); //remove the leading slash
if (parts.length > 1) {
hash = parts[1]
} }
redirectCount++;
roomUrl = new URL(result.redirectUrl);
} }
return {roomId, hash} throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
} }
public async getMapDetail(): Promise<MapDetail> { public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
return new Promise<MapDetail>((resolve, reject) => { const url = new URL(exitUrl, currentRoomUrl);
if (this.mapUrl !== undefined && this.textures != undefined) { return url;
resolve(new MapDetail(this.mapUrl, this.textures)); }
return;
}
if (this.isPublic) { /**
const match = /_\/[^/]+\/(.+)/.exec(this.id); * @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
if (!match) throw new Error('Could not extract url from "'+this.id+'"'); */
this.mapUrl = window.location.protocol+'//'+match[1]; public static getRoomPathFromExitSceneUrl(
resolve(new MapDetail(this.mapUrl, this.textures)); exitSceneUrl: string,
return; currentRoomUrl: string,
} else { currentMapUrl: string
// We have a private ID, we need to query the map URL from the server. ): URL {
const urlParts = this.parsePrivateUrl(this.id); const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
const baseUrl = new URL(currentRoomUrl);
Axios.get(`${PUSHER_URL}/map`, { const currentRoom = new Room(baseUrl);
params: urlParts let instance: string = "global";
}).then(({data}) => { if (currentRoom.isPublic) {
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl); instance = currentRoom.instance as string;
resolve(data); }
return;
}).catch((reason) => { baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
reject(reason); if (absoluteExitSceneUrl.hash) {
}); baseUrl.hash = absoluteExitSceneUrl.hash;
} }
return baseUrl;
}
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
const result = await Axios.get(`${PUSHER_URL}/map`, {
params: {
playUri: this.roomUrl.toString(),
},
}); });
const data = result.data;
if (data.redirectUrl) {
return {
redirectUrl: data.redirectUrl as string,
};
}
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
return new MapDetail(data.mapUrl, data.textures);
} }
/** /**
@ -99,37 +114,39 @@ export class Room {
if (this.isPublic) { if (this.isPublic) {
const match = /_\/([^/]+)\/.+/.exec(this.id); const match = /_\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "'+this.id+'"'); if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1]; this.instance = match[1];
return this.instance; return this.instance;
} else { } else {
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id); const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "'+this.id+'"'); if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1]+'/'+match[2]; this.instance = match[1] + "/" + match[2];
return this.instance; return this.instance;
} }
} }
private parsePrivateUrl(url: string): { organizationSlug: string, worldSlug: string, roomSlug?: string } { /**
* @deprecated
*/
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm; const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
const match = regex.exec(url); const match = regex.exec(url);
if (!match) { if (!match) {
throw new Error('Invalid URL '+url); throw new Error("Invalid URL " + url);
} }
const results: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { const results: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
organizationSlug: match[1], organizationSlug: match[1],
worldSlug: match[2], worldSlug: match[2],
} };
if (match[3] !== undefined) { if (match[3] !== undefined) {
results.roomSlug = match[3]; results.roomSlug = match[3];
} }
return results; return results;
} }
public isDisconnected(): boolean public isDisconnected(): boolean {
{ const alone = this._search.get("alone");
const alone = this._search.get('alone'); if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
return true; return true;
} }
return false; return false;
@ -138,4 +155,33 @@ export class Room {
public get search(): URLSearchParams { public get search(): URLSearchParams {
return this._search; return this._search;
} }
/**
* 2 rooms are equal if they share the same path (but not necessarily the same hash)
* @param room
*/
public isEqual(room: Room): boolean {
return room.key === this.key;
}
/**
* A key representing this room
*/
public get key(): string {
const newUrl = new URL(this.roomUrl.toString());
newUrl.search = "";
newUrl.hash = "";
return newUrl.toString();
}
get textures(): CharacterTexture[] | undefined {
return this._textures;
}
get mapUrl(): string {
if (!this._mapUrl) {
throw new Error("Map URL not fetched yet");
}
return this._mapUrl;
}
} }

View File

@ -11,7 +11,8 @@ import {
RoomJoinedMessage, RoomJoinedMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SilentMessage, StopGlobalMessage, SilentMessage,
StopGlobalMessage,
UserJoinedMessage, UserJoinedMessage,
UserLeftMessage, UserLeftMessage,
UserMovedMessage, UserMovedMessage,
@ -31,17 +32,22 @@ import {
EmotePromptMessage, EmotePromptMessage,
SendUserMessage, SendUserMessage,
BanUserMessage, BanUserMessage,
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
import { import {
EventMessage, EventMessage,
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, GroupCreatedUpdatedMessageInterface,
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface, ItemEventMessageInterface,
MessageUserJoined,
OnConnectInterface,
PlayGlobalMessageInterface,
PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
ViewportInterface, WebRtcDisconnectMessageInterface, ViewportInterface,
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels"; } from "./ConnexionModels";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
@ -61,36 +67,45 @@ export class RoomConnection implements RoomConnection {
private closed: boolean = false; private closed: boolean = false;
private tags: string[] = []; private tags: string[] = [];
public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
} }
/** /**
* *
* @param token A JWT token containing the UUID of the user * @param token A JWT token containing the UUID of the user
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" * @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
*/ */
public constructor(token: string | null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string | null) { public constructor(
token: string | null,
roomUrl: string,
name: string,
characterLayers: string[],
position: PositionInterface,
viewport: ViewportInterface,
companion: string | null
) {
let url = new URL(PUSHER_URL, window.location.toString()).toString(); let url = new URL(PUSHER_URL, window.location.toString()).toString();
url = url.replace('http://', 'ws://').replace('https://', 'wss://'); url = url.replace("http://", "ws://").replace("https://", "wss://");
if (!url.endsWith('/')) { if (!url.endsWith("/")) {
url += '/'; url += "/";
} }
url += 'room'; url += "room";
url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : ''); url += "?roomId=" + encodeURIComponent(roomUrl);
url += '&token=' + (token ? encodeURIComponent(token) : ''); url += "&token=" + (token ? encodeURIComponent(token) : "");
url += '&name=' + encodeURIComponent(name); url += "&name=" + encodeURIComponent(name);
for (const layer of characterLayers) { for (const layer of characterLayers) {
url += '&characterLayers=' + encodeURIComponent(layer); url += "&characterLayers=" + encodeURIComponent(layer);
} }
url += '&x=' + Math.floor(position.x); url += "&x=" + Math.floor(position.x);
url += '&y=' + Math.floor(position.y); url += "&y=" + Math.floor(position.y);
url += '&top=' + Math.floor(viewport.top); url += "&top=" + Math.floor(viewport.top);
url += '&bottom=' + Math.floor(viewport.bottom); url += "&bottom=" + Math.floor(viewport.bottom);
url += '&left=' + Math.floor(viewport.left); url += "&left=" + Math.floor(viewport.left);
url += '&right=' + Math.floor(viewport.right); url += "&right=" + Math.floor(viewport.right);
if (typeof companion === 'string') { if (typeof companion === "string") {
url += '&companion=' + encodeURIComponent(companion); url += "&companion=" + encodeURIComponent(companion);
} }
if (RoomConnection.websocketFactory) { if (RoomConnection.websocketFactory) {
@ -99,7 +114,7 @@ export class RoomConnection implements RoomConnection {
this.socket = new WebSocket(url); this.socket = new WebSocket(url);
} }
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = "arraybuffer";
let interval: ReturnType<typeof setInterval> | undefined = undefined; let interval: ReturnType<typeof setInterval> | undefined = undefined;
@ -109,7 +124,7 @@ export class RoomConnection implements RoomConnection {
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
}; };
this.socket.addEventListener('close', (event) => { this.socket.addEventListener("close", (event) => {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
} }
@ -126,7 +141,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) { if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) { for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
let event: string|null = null; let event: string | null = null;
let payload; let payload;
if (subMessage.hasUsermovedmessage()) { if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED; event = EventMessage.USER_MOVED;
@ -150,7 +165,7 @@ export class RoomConnection implements RoomConnection {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else { } else {
throw new Error('Unexpected batch message type'); throw new Error("Unexpected batch message type");
} }
if (event) { if (event) {
@ -171,8 +186,8 @@ export class RoomConnection implements RoomConnection {
this.dispatch(EventMessage.CONNECT, { this.dispatch(EventMessage.CONNECT, {
connection: this, connection: this,
room: { room: {
items items,
} as RoomJoinedMessageInterface } as RoomJoinedMessageInterface,
}); });
} else if (message.hasWorldfullmessage()) { } else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
@ -183,7 +198,10 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWebrtcsignaltoclientmessage()) { } else if (message.hasWebrtcsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage()); this.dispatch(
EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
message.getWebrtcscreensharingsignaltoclientmessage()
);
} else if (message.hasWebrtcstartmessage()) { } else if (message.hasWebrtcstartmessage()) {
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage()); this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
} else if (message.hasWebrtcdisconnectmessage()) { } else if (message.hasWebrtcdisconnectmessage()) {
@ -205,10 +223,9 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else { } else {
throw new Error('Unknown message received'); throw new Error("Unknown message received");
} }
};
}
} }
private dispatch(event: string, payload: unknown): void { private dispatch(event: string, payload: unknown): void {
@ -243,16 +260,16 @@ export class RoomConnection implements RoomConnection {
positionMessage.setY(Math.floor(y)); positionMessage.setY(Math.floor(y));
let directionEnum: Direction; let directionEnum: Direction;
switch (direction) { switch (direction) {
case 'up': case "up":
directionEnum = Direction.UP; directionEnum = Direction.UP;
break; break;
case 'down': case "down":
directionEnum = Direction.DOWN; directionEnum = Direction.DOWN;
break; break;
case 'left': case "left":
directionEnum = Direction.LEFT; directionEnum = Direction.LEFT;
break; break;
case 'right': case "right":
directionEnum = Direction.RIGHT; directionEnum = Direction.RIGHT;
break; break;
default: default:
@ -327,15 +344,17 @@ export class RoomConnection implements RoomConnection {
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined { private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Invalid JOIN_ROOM message'); throw new Error("Invalid JOIN_ROOM message");
} }
const characterLayers = message.getCharacterlayersList().map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => { const characterLayers = message
return { .getCharacterlayersList()
name: characterLayer.getName(), .map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
img: characterLayer.getUrl() return {
} name: characterLayer.getName(),
}) img: characterLayer.getUrl(),
};
});
const companion = message.getCompanion(); const companion = message.getCompanion();
@ -345,8 +364,9 @@ export class RoomConnection implements RoomConnection {
characterLayers, characterLayers,
visitCardUrl: message.getVisitcardurl(), visitCardUrl: message.getVisitcardurl(),
position: ProtobufClientUtils.toPointInterface(position), position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null companion: companion ? companion.getName() : null,
} userUuid: message.getUseruuid(),
};
} }
public onUserMoved(callback: (message: UserMovedMessage) => void): void { public onUserMoved(callback: (message: UserMovedMessage) => void): void {
@ -372,7 +392,9 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { public onGroupUpdatedOrCreated(
callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
): void {
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => { this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
callback(this.toGroupCreatedUpdatedMessage(message)); callback(this.toGroupCreatedUpdatedMessage(message));
}); });
@ -381,14 +403,14 @@ export class RoomConnection implements RoomConnection {
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface { private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Missing position in GROUP_CREATE_UPDATE'); throw new Error("Missing position in GROUP_CREATE_UPDATE");
} }
return { return {
groupId: message.getGroupid(), groupId: message.getGroupid(),
position: position.toObject(), position: position.toObject(),
groupSize: message.getGroupsize() groupSize: message.getGroupsize(),
} };
} }
public onGroupDeleted(callback: (groupId: number) => void): void { public onGroupDeleted(callback: (groupId: number) => void): void {
@ -404,7 +426,7 @@ export class RoomConnection implements RoomConnection {
} }
public onConnectError(callback: (error: Event) => void): void { public onConnectError(callback: (error: Event) => void): void {
this.socket.addEventListener('error', callback) this.socket.addEventListener("error", callback);
} }
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void { public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
@ -445,7 +467,6 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => { this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
callback({ callback({
userId: message.getUserid(), userId: message.getUserid(),
name: message.getName(),
initiator: message.getInitiator(), initiator: message.getInitiator(),
webRtcUser: message.getWebrtcusername() ?? undefined, webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined, webRtcPassword: message.getWebrtcpassword() ?? undefined,
@ -476,11 +497,11 @@ export class RoomConnection implements RoomConnection {
} }
public onServerDisconnected(callback: () => void): void { public onServerDisconnected(callback: () => void): void {
this.socket.addEventListener('close', (event) => { this.socket.addEventListener("close", (event) => {
if (this.closed === true || connectionManager.unloading) { if (this.closed === true || connectionManager.unloading) {
return; return;
} }
console.log('Socket closed with code ' + event.code + ". Reason: " + event.reason); console.log("Socket closed with code " + event.code + ". Reason: " + event.reason);
if (event.code === 1000) { if (event.code === 1000) {
// Normal closure case // Normal closure case
return; return;
@ -490,14 +511,14 @@ export class RoomConnection implements RoomConnection {
} }
public getUserId(): number { public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!' if (this.userId === null) throw "UserId cannot be null!";
return this.userId; return this.userId;
} }
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => { this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
callback({ callback({
userId: message.getUserid() userId: message.getUserid(),
}); });
}); });
} }
@ -521,21 +542,22 @@ export class RoomConnection implements RoomConnection {
itemId: message.getItemid(), itemId: message.getItemid(),
event: message.getEvent(), event: message.getEvent(),
parameters: JSON.parse(message.getParametersjson()), parameters: JSON.parse(message.getParametersjson()),
state: JSON.parse(message.getStatejson()) state: JSON.parse(message.getStatejson()),
}); });
}); });
} }
public uploadAudio(file: FormData) { public uploadAudio(file: FormData) {
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: { data: {} }) => { return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
return res.data; .then((res: { data: {} }) => {
}).catch((err) => { return res.data;
console.error(err); })
throw err; .catch((err) => {
}); console.error(err);
throw err;
});
} }
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) { public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => { return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({ callback({
@ -570,9 +592,9 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void { public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void {
const reportPlayerMessage = new ReportPlayerMessage(); const reportPlayerMessage = new ReportPlayerMessage();
reportPlayerMessage.setReporteduserid(reportedUserId); reportPlayerMessage.setReporteduseruuid(reportedUserUuid);
reportPlayerMessage.setReportcomment(reportComment); reportPlayerMessage.setReportcomment(reportComment);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
@ -605,12 +627,12 @@ export class RoomConnection implements RoomConnection {
} }
public isAdmin(): boolean { public isAdmin(): boolean {
return this.hasTag('admin'); return this.hasTag("admin");
} }
public emitEmoteEvent(emoteName: string): void { public emitEmoteEvent(emoteName: string): void {
const emoteMessage = new EmotePromptMessage(); const emoteMessage = new EmotePromptMessage();
emoteMessage.setEmote(emoteName) emoteMessage.setEmote(emoteName);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setEmotepromptmessage(emoteMessage); clientToServerMessage.setEmotepromptmessage(emoteMessage);
@ -618,7 +640,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public getAllTags() : string[] { public getAllTags(): string[] {
return this.tags; return this.tags;
} }
} }

View File

@ -1,7 +1,7 @@
import {discussionManager} from "../../WebRtc/DiscussionManager"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import { chatVisibilityStore } from "../../Stores/ChatStore";
export const openChatIconName = 'openChatIcon'; export const openChatIconName = "openChatIcon";
export class OpenChatIcon extends Phaser.GameObjects.Image { export class OpenChatIcon extends Phaser.GameObjects.Image {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, openChatIconName, 3); super(scene, x, y, openChatIconName, 3);
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
this.setScrollFactor(0, 0); this.setScrollFactor(0, 0);
this.setOrigin(0, 1); this.setOrigin(0, 1);
this.setInteractive(); this.setInteractive();
this.setVisible(false); //this.setVisible(false);
this.setDepth(DEPTH_INGAME_TEXT_INDEX); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
this.on("pointerup", () => discussionManager.showDiscussionPart()); this.on("pointerup", () => chatVisibilityStore.set(true));
} }
} }

View File

@ -44,7 +44,6 @@ export class TextUtils {
options.align = object.text.halign; options.align = object.text.halign;
} }
console.warn(options);
const textElem = scene.add.text(object.x, object.y, object.text.text, options); const textElem = scene.add.text(object.x, object.y, object.text.text, options);
textElem.setAngle(object.rotation); textElem.setAngle(object.rotation);
} }

View File

@ -1,90 +1,123 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type {CharacterTexture} from "../../Connexion/LocalUser"; import type { CharacterTexture } from "../../Connexion/LocalUser";
import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures"; import { BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES } from "./PlayerTextures";
export interface FrameConfig { export interface FrameConfig {
frameWidth: number, frameWidth: number;
frameHeight: number, frameHeight: number;
} }
export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => { export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => {
const returnArray:BodyResourceDescriptionInterface[][] = []; const returnArray: BodyResourceDescriptionInterface[][] = [];
LAYERS.forEach(layer => { LAYERS.forEach((layer) => {
const layerArray:BodyResourceDescriptionInterface[] = []; const layerArray: BodyResourceDescriptionInterface[] = [];
Object.values(layer).forEach((textureDescriptor) => { Object.values(layer).forEach((textureDescriptor) => {
layerArray.push(textureDescriptor); layerArray.push(textureDescriptor);
load.spritesheet(textureDescriptor.name,textureDescriptor.img,{frameWidth: 32, frameHeight: 32}); load.spritesheet(textureDescriptor.name, textureDescriptor.img, { frameWidth: 32, frameHeight: 32 });
}) });
returnArray.push(layerArray) returnArray.push(layerArray);
}); });
return returnArray; return returnArray;
} };
export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptionInterface[] => { export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptionInterface[] => {
const returnArray = Object.values(PLAYER_RESOURCES); const returnArray = Object.values(PLAYER_RESOURCES);
returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => { returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => {
load.spritesheet(playerResource.name, playerResource.img, {frameWidth: 32, frameHeight: 32}); load.spritesheet(playerResource.name, playerResource.img, { frameWidth: 32, frameHeight: 32 });
}); });
return returnArray; return returnArray;
} };
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => { export const loadCustomTexture = (
const name = 'customCharacterTexture'+texture.id; loaderPlugin: LoaderPlugin,
const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level} texture: CharacterTexture
): Promise<BodyResourceDescriptionInterface> => {
const name = "customCharacterTexture" + texture.id;
const playerResourceDescriptor: BodyResourceDescriptionInterface = { name, img: texture.url, level: texture.level };
return createLoadingPromise(loaderPlugin, playerResourceDescriptor, { return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
frameWidth: 32, frameWidth: 32,
frameHeight: 32 frameHeight: 32,
}); });
} };
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => { export const lazyLoadPlayerCharacterTextures = (
const promisesList:Promise<unknown>[] = []; loadPlugin: LoaderPlugin,
texturekeys.forEach((textureKey: string|BodyResourceDescriptionInterface) => { texturekeys: Array<string | BodyResourceDescriptionInterface>
): Promise<string[]> => {
const promisesList: Promise<unknown>[] = [];
texturekeys.forEach((textureKey: string | BodyResourceDescriptionInterface) => {
try { try {
//TODO refactor //TODO refactor
const playerResourceDescriptor = getRessourceDescriptor(textureKey); const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, { promisesList.push(
frameWidth: 32, createLoadingPromise(loadPlugin, playerResourceDescriptor, {
frameHeight: 32 frameWidth: 32,
})); frameHeight: 32,
})
);
} }
}catch (err){ } catch (err) {
console.error(err); console.error(err);
} }
}); });
let returnPromise:Promise<Array<string|BodyResourceDescriptionInterface>>; let returnPromise: Promise<Array<string | BodyResourceDescriptionInterface>>;
if (promisesList.length > 0) { if (promisesList.length > 0) {
loadPlugin.start(); loadPlugin.start();
returnPromise = Promise.all(promisesList).then(() => texturekeys); returnPromise = Promise.all(promisesList).then(() => texturekeys);
} else { } else {
returnPromise = Promise.resolve(texturekeys); returnPromise = Promise.resolve(texturekeys);
} }
return returnPromise.then((keys) => keys.map((key) => {
return typeof key !== 'string' ? key.name : key;
}))
}
export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptionInterface): BodyResourceDescriptionInterface => { //If the loading fail, we render the default model instead.
if (typeof textureKey !== 'string' && textureKey.img) { return returnPromise
.then((keys) =>
keys.map((key) => {
return typeof key !== "string" ? key.name : key;
})
)
.catch(() => lazyLoadPlayerCharacterTextures(loadPlugin, ["color_22", "eyes_23"]));
};
export const getRessourceDescriptor = (
textureKey: string | BodyResourceDescriptionInterface
): BodyResourceDescriptionInterface => {
if (typeof textureKey !== "string" && textureKey.img) {
return textureKey; return textureKey;
} }
const textureName:string = typeof textureKey === 'string' ? textureKey : textureKey.name; const textureName: string = typeof textureKey === "string" ? textureKey : textureKey.name;
const playerResource = PLAYER_RESOURCES[textureName]; const playerResource = PLAYER_RESOURCES[textureName];
if (playerResource !== undefined) return playerResource; if (playerResource !== undefined) return playerResource;
for (let i=0; i<LAYERS.length;i++) { for (let i = 0; i < LAYERS.length; i++) {
const playerResource = LAYERS[i][textureName]; const playerResource = LAYERS[i][textureName];
if (playerResource !== undefined) return playerResource; if (playerResource !== undefined) return playerResource;
} }
throw 'Could not find a data for texture '+textureName; throw "Could not find a data for texture " + textureName;
} };
export const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface, frameConfig: FrameConfig) => { export const createLoadingPromise = (
return new Promise<BodyResourceDescriptionInterface>((res) => { loadPlugin: LoaderPlugin,
playerResourceDescriptor: BodyResourceDescriptionInterface,
frameConfig: FrameConfig
) => {
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor); return res(playerResourceDescriptor);
} }
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig); loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor)); const errorCallback = (file: { src: string }) => {
if (file.src !== playerResourceDescriptor.img) return;
console.error("failed loading player ressource: ", playerResourceDescriptor);
rej(playerResourceDescriptor);
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
loadPlugin.off("loaderror", errorCallback);
};
const successCallback = () => {
loadPlugin.off("loaderror", errorCallback);
res(playerResourceDescriptor);
};
loadPlugin.once("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
loadPlugin.on("loaderror", errorCallback);
}); });
} };

View File

@ -1,11 +1,6 @@
import type {PointInterface} from "../../Connexion/ConnexionModels"; import type {PointInterface} from "../../Connexion/ConnexionModels";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type {PlayerInterface} from "./PlayerInterface";
export interface AddPlayerInterface { export interface AddPlayerInterface extends PlayerInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
visitCardUrl: string|null;
companion: string|null;
} }

View File

@ -1,17 +1,17 @@
import {ResizableScene} from "../Login/ResizableScene"; import { ResizableScene } from "../Login/ResizableScene";
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import Events = Phaser.Scenes.Events; import Events = Phaser.Scenes.Events;
import AnimationEvents = Phaser.Animations.Events; import AnimationEvents = Phaser.Animations.Events;
import StructEvents = Phaser.Structs.Events; import StructEvents = Phaser.Structs.Events;
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
/** /**
* A scene that can track its dirty/pristine state. * A scene that can track its dirty/pristine state.
*/ */
export abstract class DirtyScene extends ResizableScene { export abstract class DirtyScene extends ResizableScene {
private isAlreadyTracking: boolean = false; private isAlreadyTracking: boolean = false;
protected dirty:boolean = true; protected dirty: boolean = true;
private objectListChanged:boolean = true; private objectListChanged: boolean = true;
private physicsEnabled: boolean = false; private physicsEnabled: boolean = false;
/** /**
@ -59,7 +59,6 @@ export abstract class DirtyScene extends ResizableScene {
this.physicsEnabled = false; this.physicsEnabled = false;
} }
}); });
} }
private trackAnimation(): void { private trackAnimation(): void {
@ -71,7 +70,7 @@ export abstract class DirtyScene extends ResizableScene {
} }
public markDirty(): void { public markDirty(): void {
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true); this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => (this.dirty = true));
} }
public onResize(): void { public onResize(): void {

View File

@ -1,26 +1,24 @@
import {GameScene} from "./GameScene"; import { GameScene } from "./GameScene";
import {connectionManager} from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import type {Room} from "../../Connexion/Room"; import type { Room } from "../../Connexion/Room";
import {MenuScene, MenuSceneName} from "../Menu/MenuScene"; import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import {LoginSceneName} from "../Login/LoginScene"; import { LoginSceneName } from "../Login/LoginScene";
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import { EnableCameraSceneName } from "../Login/EnableCameraScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {get} from "svelte/store"; import { get } from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore"; import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
/** /**
* This class should be responsible for any scene starting/stopping * This class should be responsible for any scene starting/stopping
*/ */
export class GameManager { export class GameManager {
private playerName: string|null; private playerName: string | null;
private characterLayers: string[]|null; private characterLayers: string[] | null;
private companion: string|null; private companion: string | null;
private startRoom!:Room; private startRoom!: Room;
currentGameSceneName: string|null = null; currentGameSceneName: string | null = null;
constructor() { constructor() {
this.playerName = localUserStore.getName(); this.playerName = localUserStore.getName();
@ -30,7 +28,7 @@ export class GameManager {
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
this.startRoom = await connectionManager.initGameConnexion(); this.startRoom = await connectionManager.initGameConnexion();
await this.loadMap(this.startRoom, scenePlugin); this.loadMap(this.startRoom, scenePlugin);
if (!this.playerName) { if (!this.playerName) {
return LoginSceneName; return LoginSceneName;
@ -51,43 +49,44 @@ export class GameManager {
localUserStore.setCharacterLayers(layers); localUserStore.setCharacterLayers(layers);
} }
getPlayerName(): string|null { getPlayerName(): string | null {
return this.playerName; return this.playerName;
} }
getCharacterLayers(): string[] { getCharacterLayers(): string[] {
if (!this.characterLayers) { if (!this.characterLayers) {
throw 'characterLayers are not set'; throw "characterLayers are not set";
} }
return this.characterLayers; return this.characterLayers;
} }
setCompanion(companion: string | null): void {
setCompanion(companion: string|null): void {
this.companion = companion; this.companion = companion;
} }
getCompanion(): string|null { getCompanion(): string | null {
return this.companion; return this.companion;
} }
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> { public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
const roomID = room.id; const roomID = room.key;
const mapDetail = await room.getMapDetail();
const gameIndex = scenePlugin.getIndex(roomID); const gameIndex = scenePlugin.getIndex(roomID);
if(gameIndex === -1){ if (gameIndex === -1) {
const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl); const game: Phaser.Scene = new GameScene(room, room.mapUrl);
scenePlugin.add(roomID, game, false); scenePlugin.add(roomID, game, false);
} }
} }
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void { public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id)) console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
scenePlugin.start(this.currentGameSceneName || this.startRoom.id); scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
scenePlugin.launch(MenuSceneName); scenePlugin.launch(MenuSceneName);
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){ if (
!localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState))
) {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
localUserStore.setHelpCameraSettingsShown(); localUserStore.setHelpCameraSettingsShown();
} }
@ -104,7 +103,7 @@ export class GameManager {
* This will close the socket connections and stop the gameScene, but won't remove it. * This will close the socket connections and stop the gameScene, but won't remove it.
*/ */
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void { leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void {
if (this.currentGameSceneName === null) throw 'No current scene id set!'; if (this.currentGameSceneName === null) throw "No current scene id set!";
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene; const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
gameScene.cleanupClosingScene(); gameScene.cleanupClosingScene();
scene.scene.stop(this.currentGameSceneName); scene.scene.stop(this.currentGameSceneName);
@ -123,13 +122,13 @@ export class GameManager {
scene.scene.start(this.currentGameSceneName); scene.scene.start(this.currentGameSceneName);
scene.scene.wake(MenuSceneName); scene.scene.wake(MenuSceneName);
} else { } else {
scene.scene.run(fallbackSceneName) scene.scene.run(fallbackSceneName);
} }
} }
public getCurrentGameScene(scene: Phaser.Scene): GameScene { public getCurrentGameScene(scene: Phaser.Scene): GameScene {
if (this.currentGameSceneName === null) throw 'No current scene id set!'; if (this.currentGameSceneName === null) throw "No current scene id set!";
return scene.scene.get(this.currentGameSceneName) as GameScene return scene.scene.get(this.currentGameSceneName) as GameScene;
} }
} }

View File

@ -1,9 +1,13 @@
import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap"; import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void; export type PropertyChangeCallback = (
newValue: string | number | boolean | undefined,
oldValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) => void;
/** /**
* A wrapper around a ITiledMap interface to provide additional capabilities. * A wrapper around a ITiledMap interface to provide additional capabilities.
@ -13,39 +17,56 @@ export class GameMap {
private key: number | undefined; private key: number | undefined;
private lastProperties = new Map<string, string | boolean | number>(); private lastProperties = new Map<string, string | boolean | number>();
private callbacks = new Map<string, Array<PropertyChangeCallback>>(); private callbacks = new Map<string, Array<PropertyChangeCallback>>();
private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {} private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
public readonly flatLayers: ITiledMapLayer[]; public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = []; public readonly phaserLayers: TilemapLayer[] = [];
public exitUrls: Array<string> = [] public exitUrls: Array<string> = [];
public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array<Phaser.Tilemaps.Tileset>) { public hasStartTile = false;
public constructor(
private map: ITiledMap,
phaserMap: Phaser.Tilemaps.Tilemap,
terrains: Array<Phaser.Tilemaps.Tileset>
) {
this.flatLayers = flattenGroupLayersMap(map); this.flatLayers = flattenGroupLayersMap(map);
let depth = -2; let depth = -2;
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if(layer.type === 'tilelayer'){ if (layer.type === "tilelayer") {
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
} }
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { if (layer.type === "objectgroup" && layer.name === "floorLayer") {
depth = DEPTH_OVERLAY_INDEX; depth = DEPTH_OVERLAY_INDEX;
} }
} }
for (const tileset of map.tilesets) { for (const tileset of map.tilesets) {
tileset?.tiles?.forEach(tile => { tileset?.tiles?.forEach((tile) => {
if (tile.properties) { if (tile.properties) {
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties;
tile.properties.forEach(prop => { tile.properties.forEach((prop) => {
if (prop.name == "name" && typeof prop.value == "string") {
this.tileNameMap.set(prop.value, tileset.firstgid + tile.id);
}
if (prop.name == "exitUrl" && typeof prop.value == "string") { if (prop.name == "exitUrl" && typeof prop.value == "string") {
this.exitUrls.push(prop.value); this.exitUrls.push(prop.value);
} else if (prop.name == "start") {
this.hasStartTile = true;
} }
}) });
} }
}) });
} }
} }
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
return [];
}
/** /**
* Sets the position of the current player (in pixels) * Sets the position of the current player (in pixels)
@ -89,7 +110,7 @@ export class GameMap {
const properties = new Map<string, string | boolean | number>(); const properties = new Map<string, string | boolean | number>();
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if (layer.type !== 'tilelayer') { if (layer.type !== "tilelayer") {
continue; continue;
} }
@ -99,7 +120,7 @@ export class GameMap {
if (tiles[key] == 0) { if (tiles[key] == 0) {
continue; continue;
} }
tileIndex = tiles[key] tileIndex = tiles[key];
} }
// There is a tile in this layer, let's embed the properties // There is a tile in this layer, let's embed the properties
@ -113,24 +134,36 @@ export class GameMap {
} }
if (tileIndex) { if (tileIndex) {
this.tileSetPropertyMap[tileIndex]?.forEach(property => { this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
if (property.value) { if (property.value) {
properties.set(property.name, property.value) properties.set(property.name, property.value);
} else if (properties.has(property.name)) { } else if (properties.has(property.name)) {
properties.delete(property.name) properties.delete(property.name);
} }
}) });
} }
} }
return properties; return properties;
} }
public getMap(): ITiledMap{ public getMap(): ITiledMap {
return this.map; return this.map;
} }
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) { private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
return [];
}
private trigger(
propName: string,
oldValue: string | number | boolean | undefined,
newValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) {
const callbacksArray = this.callbacks.get(propName); const callbacksArray = this.callbacks.get(propName);
if (callbacksArray !== undefined) { if (callbacksArray !== undefined) {
for (const callback of callbacksArray) { for (const callback of callbacksArray) {
@ -159,10 +192,65 @@ export class GameMap {
return this.phaserLayers.find((layer) => layer.layer.name === layerName); return this.phaserLayers.find((layer) => layer.layer.name === layerName);
} }
public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { public findPhaserLayers(groupName: string): TilemapLayer[] {
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
}
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) { for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain); phaserLayer.tileset.push(terrain);
} }
} }
private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void {
const fLayer = this.findLayer(layer);
if (fLayer == undefined) {
console.error("The layer '" + layer + "' that you want to change doesn't exist.");
return;
}
if (fLayer.type !== "tilelayer") {
console.error(
"The layer '" +
layer +
"' that you want to change is not a tilelayer. Tile can only be put in tilelayer."
);
return;
}
if (typeof fLayer.data === "string") {
console.error("Data of the layer '" + layer + "' that you want to change is only readable.");
return;
}
fLayer.data[x + y * fLayer.width] = index;
}
public putTile(tile: string | number | null, x: number, y: number, layer: string): void {
const phaserLayer = this.findPhaserLayer(layer);
if (phaserLayer) {
if (tile === null) {
phaserLayer.putTileAt(-1, x, y);
return;
}
const tileIndex = this.getIndexForTileType(tile);
if (tileIndex !== undefined) {
this.putTileInFlatLayer(tileIndex, x, y, layer);
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
for (const property of this.getTileProperty(tileIndex)) {
if (property.name === "collides" && property.value) {
phaserTile.setCollision(true);
}
}
} else {
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
}
} else {
console.error("The layer '" + layer + "' does not exist (or is not a tilelaye).");
}
}
private getIndexForTileType(tile: string | number): number | undefined {
if (typeof tile == "number") {
return tile;
}
return this.tileNameMap.get(tile);
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
export interface PlayerInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
visitCardUrl: string | null;
companion: string | null;
userUuid: string;
color?: string;
}

View File

@ -1,10 +1,14 @@
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
import type { PositionInterface } from "../../Connexion/ConnexionModels"; import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
export class PlayerMovement { export class PlayerMovement {
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) { public constructor(
} private startPosition: PositionInterface,
private startTick: number,
private endPosition: HasPlayerMovedEvent,
private endTick: number
) {}
public isOutdated(tick: number): boolean { public isOutdated(tick: number): boolean {
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME) //console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
@ -24,14 +28,18 @@ export class PlayerMovement {
return this.endPosition; return this.endPosition;
} }
const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; const x =
const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.x;
const y =
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.y;
//console.log('Computed position ', x, y) //console.log('Computed position ', x, y)
return { return {
x, x,
y, y,
direction: this.endPosition.direction, direction: this.endPosition.direction,
moving: true moving: true,
} };
} }
} }

View File

@ -2,7 +2,7 @@
* This class is in charge of computing the position of all players. * This class is in charge of computing the position of all players.
* Player movement is delayed by 200ms so position depends on ticks. * Player movement is delayed by 200ms so position depends on ticks.
*/ */
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import type { PlayerMovement } from "./PlayerMovement"; import type { PlayerMovement } from "./PlayerMovement";
export class PlayersPositionInterpolator { export class PlayersPositionInterpolator {
@ -24,7 +24,7 @@ export class PlayersPositionInterpolator {
this.playerMovements.delete(userId); this.playerMovements.delete(userId);
} }
//console.log("moving") //console.log("moving")
positions.set(userId, playerMovement.getPosition(tick)) positions.set(userId, playerMovement.getPosition(tick));
}); });
return positions; return positions;
} }

View File

@ -0,0 +1,127 @@
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { GameMap } from "./GameMap";
const defaultStartLayerName = "start";
export class StartPositionCalculator {
public startPosition!: PositionInterface;
constructor(
private readonly gameMap: GameMap,
private readonly mapFile: ITiledMap,
private readonly initPosition: PositionInterface | null,
public readonly startLayerName: string | null
) {
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,
};
}
}
/**
*
* @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
*/
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
if (!selectedOrDefaultLayer) {
selectedOrDefaultLayer = defaultStartLayerName;
}
for (const layer of this.gameMap.flatLayers) {
if (
(selectedOrDefaultLayer === layer.name || layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
layer.type === "tilelayer" &&
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))
) {
const startPosition = this.startUser(layer, selectedLayer);
this.startPosition = {
x: startPosition.x + this.mapFile.tilewidth / 2,
y: startPosition.y + this.mapFile.tileheight / 2,
};
}
}
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
/**
*
* @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
*/
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
const tiles = selectedOrDefaultLayer.data;
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 possibleStartPositions: PositionInterface[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
const y = Math.floor(key / selectedOrDefaultLayer.width);
const x = key % selectedOrDefaultLayer.width;
if (selectedLayer && this.gameMap.hasStartTile) {
const properties = this.gameMap.getPropertiesForIndex(objectKey);
if (
!properties.length ||
!properties.some((property) => property.name == "start" && property.value == selectedLayer)
) {
return;
}
}
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
});
// Get a value at random amongst allowed values
if (possibleStartPositions.length === 0) {
console.warn('The start layer "' + selectedOrDefaultLayer.name + '" for this map is empty.');
return {
x: 0,
y: 0,
};
}
// Choose one of the available start positions at random amongst the list of available start positions.
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;
}
return obj.value;
}
}

View File

@ -1,18 +1,19 @@
import {EnableCameraSceneName} from "./EnableCameraScene"; import { EnableCameraSceneName } from "./EnableCameraScene";
import Rectangle = Phaser.GameObjects.Rectangle; import Rectangle = Phaser.GameObjects.Rectangle;
import {loadAllLayers} from "../Entity/PlayerTexturesLoadingManager"; import { loadAllLayers } from "../Entity/PlayerTexturesLoadingManager";
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {addLoader} from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene"; import { AbstractCharacterScene } from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser"; import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import { SelectCharacterSceneName } from "./SelectCharacterScene"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore"; import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable"; import { isMobile } from "../../Enum/EnvironmentVariable";
import {CustomizedCharacter} from "../Entity/CustomizedCharacter"; import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
import { get } from "svelte/store";
export const CustomizeSceneName = "CustomizeScene"; export const CustomizeSceneName = "CustomizeScene";
@ -21,7 +22,6 @@ export class CustomizeScene extends AbstractCharacterScene {
private selectedLayers: number[] = [0]; private selectedLayers: number[] = [0];
private containersRow: CustomizedCharacter[][] = []; private containersRow: CustomizedCharacter[][] = [];
public activeRow:number = 0;
private layers: BodyResourceDescriptionInterface[][] = []; private layers: BodyResourceDescriptionInterface[][] = [];
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
@ -31,16 +31,19 @@ export class CustomizeScene extends AbstractCharacterScene {
constructor() { constructor() {
super({ super({
key: CustomizeSceneName key: CustomizeSceneName,
}); });
} }
preload() { preload() {
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => { this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => { bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if(bodyResourceDescription.level == undefined || bodyResourceDescription.level < 0 || bodyResourceDescription.level > 5 ){ if (
throw 'Texture level is null'; bodyResourceDescription.level == undefined ||
bodyResourceDescription.level < 0 ||
bodyResourceDescription.level > 5
) {
throw "Texture level is null";
} }
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription); this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
}); });
@ -50,14 +53,13 @@ export class CustomizeScene extends AbstractCharacterScene {
this.layers = loadAllLayers(this.load); this.layers = loadAllLayers(this.load);
this.lazyloadingAttempt = false; this.lazyloadingAttempt = false;
//this function must stay at the end of preload function //this function must stay at the end of preload function
addLoader(this); addLoader(this);
} }
create() { create() {
customCharacterSceneVisibleStore.set(true); customCharacterSceneVisibleStore.set(true);
this.events.addListener('wake', () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1; waScaleManager.zoomModifier = isMobile() ? 3 : 1;
customCharacterSceneVisibleStore.set(true); customCharacterSceneVisibleStore.set(true);
@ -66,8 +68,13 @@ export class CustomizeScene extends AbstractCharacterScene {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1; waScaleManager.zoomModifier = isMobile() ? 3 : 1;
this.Rectangle = this.add.rectangle(this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.y + this.cameras.main.height / 3, 32, 33) this.Rectangle = this.add.rectangle(
this.Rectangle.setStrokeStyle(2, 0xFFFFFF); this.cameras.main.worldView.x + this.cameras.main.width / 2,
this.cameras.main.worldView.y + this.cameras.main.height / 3,
32,
33
);
this.Rectangle.setStrokeStyle(2, 0xffffff);
this.add.existing(this.Rectangle); this.add.existing(this.Rectangle);
this.createCustomizeLayer(0, 0, 0); this.createCustomizeLayer(0, 0, 0);
@ -78,24 +85,24 @@ export class CustomizeScene extends AbstractCharacterScene {
this.createCustomizeLayer(0, 0, 5); this.createCustomizeLayer(0, 0, 5);
this.moveLayers(); this.moveLayers();
this.input.keyboard.on('keyup-ENTER', () => { this.input.keyboard.on("keyup-ENTER", () => {
this.nextSceneToCamera(); this.nextSceneToCamera();
}); });
this.input.keyboard.on('keyup-BACKSPACE', () => { this.input.keyboard.on("keyup-BACKSPACE", () => {
this.backToPreviousScene(); this.backToPreviousScene();
}); });
// Note: the key bindings are not directly put on the moveCursorVertically or moveCursorHorizontally methods // Note: the key bindings are not directly put on the moveCursorVertically or moveCursorHorizontally methods
// because if 2 such events are fired close to one another, it makes the whole application crawl to a halt (for a reason I cannot // because if 2 such events are fired close to one another, it makes the whole application crawl to a halt (for a reason I cannot
// explain, the list of sprites managed by the update list become immense // explain, the list of sprites managed by the update list become immense
this.input.keyboard.on('keyup-RIGHT', () => this.moveHorizontally = 1); this.input.keyboard.on("keyup-RIGHT", () => (this.moveHorizontally = 1));
this.input.keyboard.on('keyup-LEFT', () => this.moveHorizontally = -1); this.input.keyboard.on("keyup-LEFT", () => (this.moveHorizontally = -1));
this.input.keyboard.on('keyup-DOWN', () => this.moveVertically = 1); this.input.keyboard.on("keyup-DOWN", () => (this.moveVertically = 1));
this.input.keyboard.on('keyup-UP', () => this.moveVertically = -1); this.input.keyboard.on("keyup-UP", () => (this.moveVertically = -1));
const customCursorPosition = localUserStore.getCustomCursorPosition(); const customCursorPosition = localUserStore.getCustomCursorPosition();
if (customCursorPosition) { if (customCursorPosition) {
this.activeRow = customCursorPosition.activeRow; activeRowStore.set(customCursorPosition.activeRow);
this.selectedLayers = customCursorPosition.selectedLayers; this.selectedLayers = customCursorPosition.selectedLayers;
this.moveLayers(); this.moveLayers();
this.updateSelectedLayer(); this.updateSelectedLayer();
@ -113,31 +120,30 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
private doMoveCursorHorizontally(index: number): void { private doMoveCursorHorizontally(index: number): void {
this.selectedLayers[this.activeRow] += index; this.selectedLayers[get(activeRowStore)] += index;
if (this.selectedLayers[this.activeRow] < 0) { if (this.selectedLayers[get(activeRowStore)] < 0) {
this.selectedLayers[this.activeRow] = 0 this.selectedLayers[get(activeRowStore)] = 0;
} else if(this.selectedLayers[this.activeRow] > this.layers[this.activeRow].length - 1) { } else if (this.selectedLayers[get(activeRowStore)] > this.layers[get(activeRowStore)].length - 1) {
this.selectedLayers[this.activeRow] = this.layers[this.activeRow].length - 1 this.selectedLayers[get(activeRowStore)] = this.layers[get(activeRowStore)].length - 1;
} }
this.moveLayers(); this.moveLayers();
this.updateSelectedLayer(); this.updateSelectedLayer();
this.saveInLocalStorage(); this.saveInLocalStorage();
} }
private doMoveCursorVertically(index:number): void { private doMoveCursorVertically(index: number): void {
activeRowStore.set(get(activeRowStore) + index);
this.activeRow += index; if (get(activeRowStore) < 0) {
if (this.activeRow < 0) { activeRowStore.set(0);
this.activeRow = 0 } else if (get(activeRowStore) > this.layers.length - 1) {
} else if (this.activeRow > this.layers.length - 1) { activeRowStore.set(this.layers.length - 1);
this.activeRow = this.layers.length - 1
} }
this.moveLayers(); this.moveLayers();
this.saveInLocalStorage(); this.saveInLocalStorage();
} }
private saveInLocalStorage() { private saveInLocalStorage() {
localUserStore.setCustomCursorPosition(this.activeRow, this.selectedLayers); localUserStore.setCustomCursorPosition(get(activeRowStore), this.selectedLayers);
} }
/** /**
@ -173,7 +179,7 @@ export class CustomizeScene extends AbstractCharacterScene {
* @param selectedItem, The number of the item select (0 for black body...) * @param selectedItem, The number of the item select (0 for black body...)
*/ */
private generateCharacter(x: number, y: number, layerNumber: number, selectedItem: number) { private generateCharacter(x: number, y: number, layerNumber: number, selectedItem: number) {
return new CustomizedCharacter(this, x, y, this.getContainerChildren(layerNumber,selectedItem)); return new CustomizedCharacter(this, x, y, this.getContainerChildren(layerNumber, selectedItem));
} }
private getContainerChildren(layerNumber: number, selectedItem: number): Array<string> { private getContainerChildren(layerNumber: number, selectedItem: number): Array<string> {
@ -188,7 +194,7 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
children.push(this.layers[j][layer].name); children.push(this.layers[j][layer].name);
} }
} }
return children; return children;
} }
@ -202,17 +208,16 @@ export class CustomizeScene extends AbstractCharacterScene {
const screenHeight = this.game.renderer.height; const screenHeight = this.game.renderer.height;
for (let i = 0; i < this.containersRow.length; i++) { for (let i = 0; i < this.containersRow.length; i++) {
for (let j = 0; j < this.containersRow[i].length; j++) { for (let j = 0; j < this.containersRow[i].length; j++) {
let selectedX = this.selectedLayers[i]; let selectedX = this.selectedLayers[i];
if (selectedX === undefined) { if (selectedX === undefined) {
selectedX = 0; selectedX = 0;
} }
this.containersRow[i][j].x = screenCenterX + (j - selectedX) * 40; this.containersRow[i][j].x = screenCenterX + (j - selectedX) * 40;
this.containersRow[i][j].y = screenCenterY + (i - this.activeRow) * 40; this.containersRow[i][j].y = screenCenterY + (i - get(activeRowStore)) * 40;
const alpha1 = Math.abs(selectedX - j)*47*2/screenWidth; const alpha1 = (Math.abs(selectedX - j) * 47 * 2) / screenWidth;
const alpha2 = Math.abs(this.activeRow - i)*49*2/screenHeight; const alpha2 = (Math.abs(get(activeRowStore) - i) * 49 * 2) / screenHeight;
this.containersRow[i][j].setAlpha((1 -alpha1)*(1 - alpha2)); this.containersRow[i][j].setAlpha((1 - alpha1) * (1 - alpha2));
} }
} }
} }
@ -228,8 +233,8 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
private updateSelectedLayer() { private updateSelectedLayer() {
for(let i = 0; i < this.containersRow.length; i++){ for (let i = 0; i < this.containersRow.length; i++) {
for(let j = 0; j < this.containersRow[i].length; j++){ for (let j = 0; j < this.containersRow[i].length; j++) {
const children = this.getContainerChildren(i, j); const children = this.getContainerChildren(i, j);
this.containersRow[i][j].updateSprites(children); this.containersRow[i][j].updateSprites(children);
} }
@ -237,8 +242,7 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
update(time: number, delta: number): void { update(time: number, delta: number): void {
if (this.lazyloadingAttempt) {
if(this.lazyloadingAttempt){
this.moveLayers(); this.moveLayers();
this.lazyloadingAttempt = false; this.lazyloadingAttempt = false;
} }
@ -253,38 +257,35 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
} }
public onResize(): void {
public onResize(): void {
this.moveLayers(); this.moveLayers();
this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2;
this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 3; this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 3;
}
public nextSceneToCamera(){
const layers: string[] = [];
let i = 0;
for (const layerItem of this.selectedLayers) {
if (layerItem !== undefined) {
layers.push(this.layers[i][layerItem].name);
}
i++;
}
if (!areCharacterLayersValid(layers)) {
return;
}
gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom();
this.events.removeListener('wake');
gameManager.tryResumingGame(this, EnableCameraSceneName);
customCharacterSceneVisibleStore.set(false);
} }
public backToPreviousScene(){ public nextSceneToCamera() {
const layers: string[] = [];
let i = 0;
for (const layerItem of this.selectedLayers) {
if (layerItem !== undefined) {
layers.push(this.layers[i][layerItem].name);
}
i++;
}
if (!areCharacterLayersValid(layers)) {
return;
}
gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom();
this.events.removeListener("wake");
gameManager.tryResumingGame(this, EnableCameraSceneName);
customCharacterSceneVisibleStore.set(false);
}
public backToPreviousScene() {
this.scene.sleep(CustomizeSceneName); this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom(); waScaleManager.restoreZoom();
this.scene.run(SelectCharacterSceneName); this.scene.run(SelectCharacterSceneName);

View File

@ -1,8 +1,8 @@
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {Scene} from "phaser"; import { Scene } from "phaser";
import {ErrorScene} from "../Reconnecting/ErrorScene"; import { ErrorScene } from "../Reconnecting/ErrorScene";
import {WAError} from "../Reconnecting/WAError"; import { WAError } from "../Reconnecting/WAError";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
@ -13,26 +13,32 @@ export const EntrySceneName = "EntryScene";
export class EntryScene extends Scene { export class EntryScene extends Scene {
constructor() { constructor() {
super({ super({
key: EntrySceneName key: EntrySceneName,
}); });
} }
create() { create() {
gameManager
gameManager.init(this.scene).then((nextSceneName) => { .init(this.scene)
// Let's rescale before starting the game .then((nextSceneName) => {
// We can do it at this stage. // Let's rescale before starting the game
waScaleManager.applyNewSize(); // We can do it at this stage.
this.scene.start(nextSceneName); waScaleManager.applyNewSize();
}).catch((err) => { this.scene.start(nextSceneName);
if (err.response && err.response.status == 404) { })
ErrorScene.showError(new WAError( .catch((err) => {
'Access link incorrect', if (err.response && err.response.status == 404) {
'Could not find map. Please check your access link.', ErrorScene.showError(
'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com'), this.scene); new WAError(
} else { "Access link incorrect",
ErrorScene.showError(err, this.scene); "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"
}); ),
this.scene
);
} else {
ErrorScene.showError(err, this.scene);
}
});
} }
} }

View File

@ -1,25 +1,25 @@
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import Rectangle = Phaser.GameObjects.Rectangle; import Rectangle = Phaser.GameObjects.Rectangle;
import {EnableCameraSceneName} from "./EnableCameraScene"; import { EnableCameraSceneName } from "./EnableCameraScene";
import {CustomizeSceneName} from "./CustomizeScene"; import { CustomizeSceneName } from "./CustomizeScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager"; import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
import {addLoader} from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene"; import { AbstractCharacterScene } from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser"; import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable"; import { isMobile } from "../../Enum/EnvironmentVariable";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
export class SelectCharacterScene extends AbstractCharacterScene { export class SelectCharacterScene extends AbstractCharacterScene {
protected readonly nbCharactersPerRow = 6; protected readonly nbCharactersPerRow = 6;
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite|null; // null if we are selecting the "customize" option protected selectedPlayer!: Phaser.Physics.Arcade.Sprite | null; // null if we are selecting the "customize" option
protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>(); protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
protected playerModels!: BodyResourceDescriptionInterface[]; protected playerModels!: BodyResourceDescriptionInterface[];
@ -38,7 +38,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
preload() { preload() {
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => { this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => { bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription); this.playerModels.push(bodyResourceDescription);
@ -54,7 +53,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
create() { create() {
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
this.events.addListener('wake', () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMobile() ? 2 : 1;
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
@ -68,26 +67,26 @@ export class SelectCharacterScene extends AbstractCharacterScene {
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMobile() ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);
this.selectedRectangle.setDepth(2); this.selectedRectangle.setDepth(2);
/*create user*/ /*create user*/
this.createCurrentPlayer(); this.createCurrentPlayer();
this.input.keyboard.on('keyup-ENTER', () => { this.input.keyboard.on("keyup-ENTER", () => {
return this.nextSceneToCameraScene(); return this.nextSceneToCameraScene();
}); });
this.input.keyboard.on('keydown-RIGHT', () => { this.input.keyboard.on("keydown-RIGHT", () => {
this.moveToRight(); this.moveToRight();
}); });
this.input.keyboard.on('keydown-LEFT', () => { this.input.keyboard.on("keydown-LEFT", () => {
this.moveToLeft(); this.moveToLeft();
}); });
this.input.keyboard.on('keydown-UP', () => { this.input.keyboard.on("keydown-UP", () => {
this.moveToUp(); this.moveToUp();
}); });
this.input.keyboard.on('keydown-DOWN', () => { this.input.keyboard.on("keydown-DOWN", () => {
this.moveToDown(); this.moveToDown();
}); });
} }
@ -96,7 +95,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) { if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
return; return;
} }
if(!this.selectedPlayer){ if (!this.selectedPlayer) {
return; return;
} }
this.scene.stop(SelectCharacterSceneName); this.scene.stop(SelectCharacterSceneName);
@ -105,7 +104,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(this, EnableCameraSceneName);
this.players = []; this.players = [];
selectCharacterSceneVisibleStore.set(false); selectCharacterSceneVisibleStore.set(false);
this.events.removeListener('wake'); this.events.removeListener("wake");
} }
public nextSceneToCustomizeScene(): void { public nextSceneToCustomizeScene(): void {
@ -119,11 +118,11 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
createCurrentPlayer(): void { createCurrentPlayer(): void {
for (let i = 0; i <this.playerModels.length; i++) { for (let i = 0; i < this.playerModels.length; i++) {
const playerResource = this.playerModels[i]; const playerResource = this.playerModels[i];
//check already exist texture //check already exist texture
if(this.players.find((c) => c.texture.key === playerResource.name)){ if (this.players.find((c) => c.texture.key === playerResource.name)) {
continue; continue;
} }
@ -132,9 +131,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.setUpPlayer(player, i); this.setUpPlayer(player, i);
this.anims.create({ this.anims.create({
key: playerResource.name, key: playerResource.name,
frames: this.anims.generateFrameNumbers(playerResource.name, {start: 0, end: 11}), frames: this.anims.generateFrameNumbers(playerResource.name, { start: 0, end: 11 }),
frameRate: 8, frameRate: 8,
repeat: -1 repeat: -1,
}); });
player.setInteractive().on("pointerdown", () => { player.setInteractive().on("pointerdown", () => {
if (this.pointerClicked) { if (this.pointerClicked) {
@ -153,77 +152,79 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}); });
this.players.push(player); this.players.push(player);
} }
if (this.currentSelectUser >= this.players.length) {
this.currentSelectUser = 0;
}
this.selectedPlayer = this.players[this.currentSelectUser]; this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name); this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
} }
protected moveUser(){ protected moveUser() {
for(let i = 0; i < this.players.length; i++){ for (let i = 0; i < this.players.length; i++) {
const player = this.players[i]; const player = this.players[i];
this.setUpPlayer(player, i); this.setUpPlayer(player, i);
} }
this.updateSelectedPlayer(); this.updateSelectedPlayer();
} }
public moveToLeft(){ public moveToLeft() {
if(this.currentSelectUser === 0){ if (this.currentSelectUser === 0) {
return; return;
} }
this.currentSelectUser -= 1; this.currentSelectUser -= 1;
this.moveUser(); this.moveUser();
} }
public moveToRight(){ public moveToRight() {
if(this.currentSelectUser === (this.players.length - 1)){ if (this.currentSelectUser === this.players.length - 1) {
return; return;
} }
this.currentSelectUser += 1; this.currentSelectUser += 1;
this.moveUser(); this.moveUser();
} }
protected moveToUp(){ protected moveToUp() {
if(this.currentSelectUser < this.nbCharactersPerRow){ if (this.currentSelectUser < this.nbCharactersPerRow) {
return; return;
} }
this.currentSelectUser -= this.nbCharactersPerRow; this.currentSelectUser -= this.nbCharactersPerRow;
this.moveUser(); this.moveUser();
} }
protected moveToDown(){ protected moveToDown() {
if((this.currentSelectUser + this.nbCharactersPerRow) > (this.players.length - 1)){ if (this.currentSelectUser + this.nbCharactersPerRow > this.players.length - 1) {
return; return;
} }
this.currentSelectUser += this.nbCharactersPerRow; this.currentSelectUser += this.nbCharactersPerRow;
this.moveUser(); this.moveUser();
} }
protected defineSetupPlayer(num: number){ protected defineSetupPlayer(num: number) {
const deltaX = 32; const deltaX = 32;
const deltaY = 32; const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users playerX = playerX - deltaX * 2.5 + deltaX * (num % this.nbCharactersPerRow); // calcul position on line users
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users playerY = playerY - deltaY * 2 + deltaY * Math.floor(num / this.nbCharactersPerRow); // calcul position on column users
const playerVisible = true; const playerVisible = true;
const playerScale = 1; const playerScale = 1;
const playerOpacity = 1; const playerOpacity = 1;
// if selected // if selected
if( num === this.currentSelectUser ){ if (num === this.currentSelectUser) {
this.selectedRectangle.setX(playerX); this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY); this.selectedRectangle.setY(playerY);
} }
return {playerX, playerY, playerScale, playerOpacity, playerVisible} return { playerX, playerY, playerScale, playerOpacity, playerVisible };
} }
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){ protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number) {
const { playerX, playerY, playerScale, playerOpacity, playerVisible } = this.defineSetupPlayer(num);
const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
player.setBounce(0.2); player.setBounce(0.2);
player.setCollideWorldBounds(false); player.setCollideWorldBounds(false);
player.setVisible( playerVisible ); player.setVisible(playerVisible);
player.setScale(playerScale, playerScale); player.setScale(playerScale, playerScale);
player.setAlpha(playerOpacity); player.setAlpha(playerOpacity);
player.setX(playerX); player.setX(playerX);
@ -234,10 +235,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
* Returns pixel position by on column and row number * Returns pixel position by on column and row number
*/ */
protected getCharacterPosition(): [number, number] { protected getCharacterPosition(): [number, number] {
return [ return [this.game.renderer.width / 2, this.game.renderer.height / 2.5];
this.game.renderer.width / 2,
this.game.renderer.height / 2.5
];
} }
protected updateSelectedPlayer(): void { protected updateSelectedPlayer(): void {
@ -256,7 +254,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.pointerClicked = false; this.pointerClicked = false;
} }
if(this.lazyloadingAttempt){ if (this.lazyloadingAttempt) {
//re-render players list //re-render players list
this.createCurrentPlayer(); this.createCurrentPlayer();
this.moveUser(); this.moveUser();

View File

@ -36,7 +36,7 @@ export interface ITiledMap {
export interface ITiledMapLayerProperty { export interface ITiledMapLayerProperty {
name: string; name: string;
type: string; type: string;
value: string|boolean|number|undefined; value: string | boolean | number | undefined;
} }
/*export interface ITiledMapLayerBooleanProperty { /*export interface ITiledMapLayerBooleanProperty {
@ -48,7 +48,7 @@ export interface ITiledMapLayerProperty {
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer; export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
export interface ITiledMapGroupLayer { export interface ITiledMapGroupLayer {
id?: number, id?: number;
name: string; name: string;
opacity: number; opacity: number;
properties?: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
@ -64,8 +64,8 @@ export interface ITiledMapGroupLayer {
} }
export interface ITiledMapTileLayer { export interface ITiledMapTileLayer {
id?: number, id?: number;
data: number[]|string; data: number[] | string;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
@ -87,7 +87,7 @@ export interface ITiledMapTileLayer {
} }
export interface ITiledMapObjectLayer { export interface ITiledMapObjectLayer {
id?: number, id?: number;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
@ -117,7 +117,7 @@ export interface ITiledMapObject {
gid: number; gid: number;
height: number; height: number;
name: string; name: string;
properties: {[key: string]: string}; properties: { [key: string]: string };
rotation: number; rotation: number;
type: string; type: string;
visible: boolean; visible: boolean;
@ -133,26 +133,26 @@ export interface ITiledMapObject {
/** /**
* Polygon points * Polygon points
*/ */
polygon: {x: number, y: number}[]; polygon: { x: number; y: number }[];
/** /**
* Polyline points * Polyline points
*/ */
polyline: {x: number, y: number}[]; polyline: { x: number; y: number }[];
text?: ITiledText text?: ITiledText;
} }
export interface ITiledText { export interface ITiledText {
text: string, text: string;
wrap?: boolean, wrap?: boolean;
fontfamily?: string, fontfamily?: string;
pixelsize?: number, pixelsize?: number;
color?: string, color?: string;
underline?: boolean, underline?: boolean;
italic?: boolean, italic?: boolean;
strikeout?: boolean, strikeout?: boolean;
halign?: "center"|"right"|"justify"|"left" halign?: "center" | "right" | "justify" | "left";
} }
export interface ITiledTileSet { export interface ITiledTileSet {
@ -163,7 +163,7 @@ export interface ITiledTileSet {
imagewidth: number; imagewidth: number;
margin: number; margin: number;
name: string; name: string;
properties: {[key: string]: string}; properties: { [key: string]: string };
spacing: number; spacing: number;
tilecount: number; tilecount: number;
tileheight: number; tileheight: number;
@ -179,10 +179,10 @@ export interface ITiledTileSet {
} }
export interface ITile { export interface ITile {
id: number, id: number;
type?: string type?: string;
properties?: Array<ITiledMapLayerProperty> properties?: Array<ITiledMapLayerProperty>;
} }
export interface ITiledMapTerrain { export interface ITiledMapTerrain {

View File

@ -1,20 +1,20 @@
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap"; import type { ITiledMap, ITiledMapLayer } from "./ITiledMap";
/** /**
* Flatten the grouped layers * Flatten the grouped layers
*/ */
export function flattenGroupLayersMap(map: ITiledMap) { export function flattenGroupLayersMap(map: ITiledMap) {
const flatLayers: ITiledMapLayer[] = []; const flatLayers: ITiledMapLayer[] = [];
flattenGroupLayers(map.layers, '', flatLayers); flattenGroupLayers(map.layers, "", flatLayers);
return flatLayers; return flatLayers;
} }
function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) { function flattenGroupLayers(layers: ITiledMapLayer[], prefix: string, flatLayers: ITiledMapLayer[]) {
for (const layer of layers) { for (const layer of layers) {
if (layer.type === 'group') { if (layer.type === "group") {
flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); flattenGroupLayers(layer.layers, prefix + layer.name + "/", flatLayers);
} else { } else {
layer.name = prefix+layer.name layer.name = prefix + layer.name;
flatLayers.push(layer); flatLayers.push(layer);
} }
} }

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