Merge branch 'develop' of github.com:thecodingmachine/workadventure into audioPlayerImprovements
2
.github/workflows/build-and-deploy.yml
vendored
@ -199,4 +199,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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"
|
||||
|
13
.github/workflows/push-to-npm.yml
vendored
@ -2,6 +2,7 @@ name: Push @workadventure/iframe-api-typings to NPM
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@ -13,10 +14,6 @@ jobs:
|
||||
node-version: '14.x'
|
||||
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
|
||||
run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json'
|
||||
working-directory: "front/packages/iframe-api-typings"
|
||||
@ -47,15 +44,18 @@ jobs:
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Build"
|
||||
run: yarn run build
|
||||
run: yarn run build-typings
|
||||
env:
|
||||
API_URL: "localhost:8080"
|
||||
PUSHER_URL: "//localhost:8080"
|
||||
working-directory: "front"
|
||||
|
||||
# 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
|
||||
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
|
||||
run: yarn install
|
||||
working-directory: "front/packages/iframe-api-typings"
|
||||
@ -65,3 +65,4 @@ jobs:
|
||||
working-directory: "front/packages/iframe-api-typings"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
|
3
.gitignore
vendored
@ -7,4 +7,5 @@ docker-compose.override.yaml
|
||||
maps/yarn.lock
|
||||
maps/dist/computer.js
|
||||
maps/dist/computer.js.map
|
||||
/node_modules/
|
||||
node_modules
|
||||
_
|
28
CHANGELOG.md
@ -10,11 +10,35 @@
|
||||
- New scripting API features :
|
||||
- Use `WA.room.showLayer(): void` to show 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.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.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
|
||||
|
||||
|
@ -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:
|
||||
|
||||
```console
|
||||
$ yarn run install
|
||||
$ yarn install
|
||||
$ yarn run prepare
|
||||
```
|
||||
|
||||
|
@ -15,7 +15,7 @@ export class DebugController {
|
||||
const query = parse(req.getQuery());
|
||||
|
||||
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
|
||||
|
@ -5,8 +5,6 @@ import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||
import { PositionNotifier } from "./PositionNotifier";
|
||||
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 { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { ZoneSocket } from "src/RoomManager";
|
||||
@ -15,12 +13,6 @@ import { Admin } from "../Model/Admin";
|
||||
export type ConnectCallback = (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 {
|
||||
private readonly minDistance: number;
|
||||
private readonly groupRadius: number;
|
||||
@ -37,15 +29,12 @@ export class GameRoom {
|
||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||
|
||||
private readonly positionNotifier: PositionNotifier;
|
||||
public readonly roomId: string;
|
||||
public readonly roomSlug: string;
|
||||
public readonly worldSlug: string = "";
|
||||
public readonly organizationSlug: string = "";
|
||||
public readonly roomUrl: string;
|
||||
private versionNumber: number = 1;
|
||||
private nextUserId: number = 1;
|
||||
|
||||
constructor(
|
||||
roomId: string,
|
||||
roomUrl: string,
|
||||
connectCallback: ConnectCallback,
|
||||
disconnectCallback: DisconnectCallback,
|
||||
minDistance: number,
|
||||
@ -55,16 +44,7 @@ export class GameRoom {
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
) {
|
||||
this.roomId = roomId;
|
||||
|
||||
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.roomUrl = roomUrl;
|
||||
|
||||
this.users = new Map<number, User>();
|
||||
this.usersByUuid = new Map<string, User>();
|
||||
@ -183,7 +163,7 @@ export class GameRoom {
|
||||
} else {
|
||||
const closestUser: User = closestItem;
|
||||
const group: Group = new Group(
|
||||
this.roomId,
|
||||
this.roomUrl,
|
||||
[user, closestUser],
|
||||
this.connectCallback,
|
||||
this.disconnectCallback,
|
||||
|
@ -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 };
|
||||
};
|
@ -250,12 +250,12 @@ export class SocketManager {
|
||||
//user leave previous world
|
||||
room.leave(user);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomId);
|
||||
this.rooms.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
} finally {
|
||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
||||
console.log("A user left");
|
||||
}
|
||||
}
|
||||
@ -308,6 +308,7 @@ export class SocketManager {
|
||||
throw new Error("clientUser.userId is not an integer " + thing.id);
|
||||
}
|
||||
userJoinedZoneMessage.setUserid(thing.id);
|
||||
userJoinedZoneMessage.setUseruuid(thing.uuid);
|
||||
userJoinedZoneMessage.setName(thing.name);
|
||||
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||
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
|
||||
const webrtcStartMessage1 = new WebRtcStartMessage();
|
||||
webrtcStartMessage1.setUserid(otherUser.id);
|
||||
webrtcStartMessage1.setName(otherUser.name);
|
||||
webrtcStartMessage1.setInitiator(true);
|
||||
if (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();
|
||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||
|
||||
//if (!user.socket.disconnecting) {
|
||||
user.socket.write(serverToClientMessage1);
|
||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
||||
//}
|
||||
|
||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||
webrtcStartMessage2.setUserid(user.id);
|
||||
webrtcStartMessage2.setName(user.name);
|
||||
webrtcStartMessage2.setInitiator(false);
|
||||
if (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();
|
||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||
|
||||
//if (!otherUser.socket.disconnecting) {
|
||||
otherUser.socket.write(serverToClientMessage2);
|
||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@ -614,6 +607,7 @@ export class SocketManager {
|
||||
if (thing instanceof User) {
|
||||
const userJoinedMessage = new UserJoinedZoneMessage();
|
||||
userJoinedMessage.setUserid(thing.id);
|
||||
userJoinedMessage.setUseruuid(thing.uuid);
|
||||
userJoinedMessage.setName(thing.name);
|
||||
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||
@ -664,9 +658,9 @@ export class SocketManager {
|
||||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||
room.adminLeave(admin);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomId);
|
||||
this.rooms.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
})
|
@ -52,11 +52,11 @@ WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
|
||||
### 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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
@ -65,4 +65,3 @@ WA.nav.openCoWebSite('https://www.wikipedia.org/');
|
||||
// ...
|
||||
WA.nav.closeCoWebSite();
|
||||
```
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# API Player functions Reference
|
||||
|
||||
|
||||
### Listen to player movement
|
||||
```
|
||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||
|
@ -54,6 +54,7 @@ WA.room.showLayer(layerName : string): void
|
||||
WA.room.hideLayer(layerName : string) : void
|
||||
```
|
||||
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 :
|
||||
```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`.
|
||||
|
||||
Note :
|
||||
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
|
||||
|
||||
Example :
|
||||
```javascript
|
||||
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 :
|
||||
* **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.
|
||||
* **mapUrl (string) :** Url of the JSON map file
|
||||
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer
|
||||
Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`.
|
||||
|
||||
If `tile` is a string, it's not the id of the tile but the value of the property `name`.
|
||||
<div class="row">
|
||||
<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 :
|
||||
```javascript
|
||||
WA.room.getCurrentRoom((room) => {
|
||||
if (room.id === '42') {
|
||||
console.log(room.map);
|
||||
window.open(room.mapUrl, '_blank');
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 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);
|
||||
}
|
||||
})
|
||||
WA.room.setTiles([
|
||||
{x: 6, y: 4, tile: 'blue', layer: 'setTiles'},
|
||||
{x: 7, y: 4, tile: 109, layer: 'setTiles'},
|
||||
{x: 8, y: 4, tile: 109, layer: 'setTiles'},
|
||||
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
|
||||
]);
|
||||
```
|
||||
|
3
front/dist/index.tmpl.html
vendored
@ -37,8 +37,7 @@
|
||||
<div class="main-container" id="main-container">
|
||||
<!-- Create the editor container -->
|
||||
<div id="game" class="game">
|
||||
<div id="svelte-overlay">
|
||||
</div>
|
||||
<div id="svelte-overlay"></div>
|
||||
<div id="game-overlay" class="game-overlay">
|
||||
<div id="main-section" class="main-section">
|
||||
</div>
|
||||
|
3
front/dist/resources/html/gameMenu.html
vendored
@ -57,6 +57,9 @@
|
||||
<section>
|
||||
<button id="toggleFullscreen">Toggle fullscreen</button>
|
||||
</section>
|
||||
<section>
|
||||
<button id="enableNotification">Enable notifications</button>
|
||||
</section>
|
||||
<section>
|
||||
<button id="sparkButton">Create map</button>
|
||||
</section>
|
||||
|
BIN
front/dist/resources/logos/tcm_full.png
vendored
Before Width: | Height: | Size: 300 B |
BIN
front/dist/resources/logos/tcm_short.png
vendored
Before Width: | Height: | Size: 216 B |
53
front/dist/resources/service-worker.js
vendored
Normal 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
|
||||
});
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 880 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 933 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 978 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 985 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
front/dist/static/images/favicons/apple-icon.png
vendored
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
front/dist/static/images/favicons/favicon-16x16.png
vendored
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 713 B |
BIN
front/dist/static/images/favicons/favicon-32x32.png
vendored
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 848 B |
BIN
front/dist/static/images/favicons/favicon-96x96.png
vendored
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
front/dist/static/images/favicons/favicon.ico
vendored
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
front/dist/static/images/favicons/icon-512x512.png
vendored
Normal file
After Width: | Height: | Size: 13 KiB |
11
front/dist/static/images/favicons/manifest.json
vendored
@ -119,7 +119,13 @@
|
||||
"src": "/static/images/favicons/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"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": "/",
|
||||
@ -127,6 +133,7 @@
|
||||
"display_override": ["window-control-overlay", "minimal-ui"],
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"lang": "en",
|
||||
"theme_color": "#000000",
|
||||
"shortcuts": [
|
||||
{
|
||||
@ -134,7 +141,7 @@
|
||||
"short_name": "WA",
|
||||
"description": "WorkAdventure application",
|
||||
"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",
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
front/dist/static/images/favicons/ms-icon-70x70.png
vendored
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
front/dist/static/images/send.png
vendored
Normal file
After Width: | Height: | Size: 8.3 KiB |
@ -39,7 +39,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"axios": "^0.21.1",
|
||||
"cross-env": "^7.0.3",
|
||||
@ -51,7 +51,7 @@
|
||||
"queue-typescript": "^1.0.1",
|
||||
"quill": "1.3.6",
|
||||
"rxjs": "^6.6.3",
|
||||
"simple-peer": "^9.6.2",
|
||||
"simple-peer": "^9.11.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"standardized-audio-context": "^25.2.4"
|
||||
},
|
||||
@ -60,7 +60,8 @@
|
||||
"templater": "cross-env ./templater.sh",
|
||||
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
|
||||
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
|
||||
"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",
|
||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||
"precommit": "lint-staged",
|
||||
|
@ -1,11 +1,10 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isDataLayerEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
data: tg.isObject
|
||||
}).get();
|
||||
export const isDataLayerEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
data: tg.isObject,
|
||||
})
|
||||
.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
|
||||
|
@ -1,14 +1,15 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isGameStateEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isGameStateEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
roomId: tg.isString,
|
||||
mapUrl: tg.isString,
|
||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
}).get();
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when the gameState is received by the script
|
||||
*/
|
||||
|
@ -1,19 +1,17 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isHasPlayerMovedEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
direction: tg.isElementOf('right', 'left', 'up', 'down'),
|
||||
export const isHasPlayerMovedEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
direction: tg.isElementOf("right", "left", "up", "down"),
|
||||
moving: tg.isBoolean,
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber
|
||||
}).get();
|
||||
y: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* 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 HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void
|
||||
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;
|
||||
|
@ -1,73 +1,73 @@
|
||||
|
||||
import type { GameStateEvent } from './GameStateEvent';
|
||||
import type { ButtonClickedEvent } from './ButtonClickedEvent';
|
||||
import type { ChatEvent } from './ChatEvent';
|
||||
import type { ClosePopupEvent } from './ClosePopupEvent';
|
||||
import type { EnterLeaveEvent } from './EnterLeaveEvent';
|
||||
import type { GoToPageEvent } from './GoToPageEvent';
|
||||
import type { LoadPageEvent } from './LoadPageEvent';
|
||||
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
|
||||
import type { OpenPopupEvent } from './OpenPopupEvent';
|
||||
import type { OpenTabEvent } from './OpenTabEvent';
|
||||
import type { UserInputChatEvent } from './UserInputChatEvent';
|
||||
import type { GameStateEvent } from "./GameStateEvent";
|
||||
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||
import type { ChatEvent } from "./ChatEvent";
|
||||
import type { ClosePopupEvent } from "./ClosePopupEvent";
|
||||
import type { EnterLeaveEvent } from "./EnterLeaveEvent";
|
||||
import type { GoToPageEvent } from "./GoToPageEvent";
|
||||
import type { LoadPageEvent } from "./LoadPageEvent";
|
||||
import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
||||
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
||||
import type { DataLayerEvent } from "./DataLayerEvent";
|
||||
import type { LayerEvent } from './LayerEvent';
|
||||
import type { LayerEvent } from "./LayerEvent";
|
||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||
import type { PlaySoundEvent } from "./PlaySoundEvent";
|
||||
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 { SetTilesEvent } from "./SetTilesEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* List event types sent from an iFrame to WorkAdventure
|
||||
*/
|
||||
export type IframeEventMap = {
|
||||
//getState: GameStateEvent,
|
||||
// updateTile: UpdateTileEvent
|
||||
loadPage: LoadPageEvent
|
||||
chat: ChatEvent,
|
||||
openPopup: OpenPopupEvent
|
||||
closePopup: ClosePopupEvent
|
||||
openTab: OpenTabEvent
|
||||
goToPage: GoToPageEvent
|
||||
openCoWebSite: OpenCoWebSiteEvent
|
||||
closeCoWebSite: null
|
||||
disablePlayerControls: null
|
||||
restorePlayerControls: null
|
||||
displayBubble: null
|
||||
removeBubble: null
|
||||
onPlayerMove: undefined
|
||||
showLayer: LayerEvent
|
||||
hideLayer: LayerEvent
|
||||
setProperty: SetPropertyEvent
|
||||
getDataLayer: undefined
|
||||
loadSound: LoadSoundEvent
|
||||
playSound: PlaySoundEvent
|
||||
stopSound: null,
|
||||
getState: undefined,
|
||||
registerMenuCommand: MenuItemRegisterEvent
|
||||
}
|
||||
loadPage: LoadPageEvent;
|
||||
chat: ChatEvent;
|
||||
openPopup: OpenPopupEvent;
|
||||
closePopup: ClosePopupEvent;
|
||||
openTab: OpenTabEvent;
|
||||
goToPage: GoToPageEvent;
|
||||
openCoWebSite: OpenCoWebSiteEvent;
|
||||
closeCoWebSite: null;
|
||||
disablePlayerControls: null;
|
||||
restorePlayerControls: null;
|
||||
displayBubble: null;
|
||||
removeBubble: null;
|
||||
onPlayerMove: undefined;
|
||||
showLayer: LayerEvent;
|
||||
hideLayer: LayerEvent;
|
||||
setProperty: SetPropertyEvent;
|
||||
getDataLayer: undefined;
|
||||
loadSound: LoadSoundEvent;
|
||||
playSound: PlaySoundEvent;
|
||||
stopSound: null;
|
||||
getState: undefined;
|
||||
registerMenuCommand: MenuItemRegisterEvent;
|
||||
setTiles: SetTilesEvent;
|
||||
};
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
data: IframeEventMap[T];
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
userInputChat: UserInputChatEvent
|
||||
enterEvent: EnterLeaveEvent
|
||||
leaveEvent: EnterLeaveEvent
|
||||
buttonClickedEvent: ButtonClickedEvent
|
||||
gameState: GameStateEvent
|
||||
hasPlayerMoved: HasPlayerMovedEvent
|
||||
dataLayer: DataLayerEvent
|
||||
menuItemClicked: MenuItemClickedEvent
|
||||
userInputChat: UserInputChatEvent;
|
||||
enterEvent: EnterLeaveEvent;
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
dataLayer: DataLayerEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
@ -75,4 +75,49 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
@ -1,9 +1,10 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isLayerEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isLayerEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
name: tg.isString,
|
||||
}).get();
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to show/hide a layer.
|
||||
*/
|
||||
|
@ -1,11 +1,10 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isLoadPageEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isLoadPageEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
|
@ -1,11 +1,12 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isOpenCoWebsite =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isOpenCoWebsite = new tg.IsInterface()
|
||||
.withProperties({
|
||||
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.
|
||||
|
16
front/src/Api/Events/SetTilesEvent.ts
Normal 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>;
|
@ -1,11 +1,12 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isSetPropertyEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isSetPropertyEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
layerName: tg.isString,
|
||||
propertyName: tg.isString,
|
||||
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined)))
|
||||
}).get();
|
||||
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to change the value of the property of the layer
|
||||
*/
|
||||
|
@ -1,12 +1,11 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isMenuItemClickedEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
menuItem: tg.isString
|
||||
}).get();
|
||||
export const isMenuItemClickedEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
menuItem: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a menu item is clicked.
|
||||
*/
|
||||
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;
|
||||
|
||||
|
||||
|
@ -1,25 +1,26 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
export const isMenuItemRegisterEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
menutItem: tg.isString
|
||||
}).get();
|
||||
export const isMenuItemRegisterEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
menutItem: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a new menu item.
|
||||
*/
|
||||
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
|
||||
|
||||
export const isMenuItemRegisterIframeEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
type: tg.isSingletonString("registerMenuCommand"),
|
||||
data: isMenuItemRegisterEvent
|
||||
}).get();
|
||||
|
||||
data: isMenuItemRegisterEvent,
|
||||
})
|
||||
.get();
|
||||
|
||||
const _registerMenuCommandStream: Subject<string> = new Subject();
|
||||
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
|
||||
|
||||
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
|
||||
_registerMenuCommandStream.next(event.menutItem)
|
||||
_registerMenuCommandStream.next(event.menutItem);
|
||||
}
|
@ -10,33 +10,36 @@ import {scriptUtils} from "./ScriptUtils";
|
||||
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
|
||||
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
|
||||
import {
|
||||
IframeErrorAnswerEvent,
|
||||
IframeEvent,
|
||||
IframeEventMap,
|
||||
IframeEventMap, IframeQueryMap,
|
||||
IframeResponseEvent,
|
||||
IframeResponseEventMap,
|
||||
isIframeEventWrapper,
|
||||
TypedMessageEvent
|
||||
isIframeQueryWrapper,
|
||||
TypedMessageEvent,
|
||||
} from "./Events/IframeEvent";
|
||||
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
|
||||
//import { isLoadPageEvent } from './Events/LoadPageEvent';
|
||||
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
|
||||
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
|
||||
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
||||
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
||||
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 { GameStateEvent } from "./Events/GameStateEvent";
|
||||
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
||||
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||
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.
|
||||
* Also allows to send messages to those iframes.
|
||||
*/
|
||||
class IframeListener {
|
||||
|
||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||
public readonly chatStream = this._chatStream.asObservable();
|
||||
|
||||
@ -82,9 +85,6 @@ class IframeListener {
|
||||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||
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();
|
||||
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
||||
|
||||
@ -103,19 +103,28 @@ class IframeListener {
|
||||
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
|
||||
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 iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
private sendPlayerMove: boolean = false;
|
||||
|
||||
private answerers: {
|
||||
[key in keyof IframeQueryMap]?: AnswererCallback<key>
|
||||
} = {};
|
||||
|
||||
init() {
|
||||
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||
// Do we trust the sender of this message?
|
||||
// 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) {
|
||||
if (iframe.contentWindow === message.source) {
|
||||
foundSrc = iframe.src;
|
||||
@ -123,64 +132,104 @@ class IframeListener {
|
||||
}
|
||||
}
|
||||
|
||||
if (foundSrc === undefined) {
|
||||
const payload = message.data;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const payload = message.data;
|
||||
if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
|
||||
foundSrc = this.getBaseUrl(foundSrc, message.source);
|
||||
|
||||
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)) {
|
||||
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
|
||||
this._hideLayerStream.next(payload.data);
|
||||
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
|
||||
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
|
||||
this._setPropertyStream.next(payload.data);
|
||||
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
|
||||
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
|
||||
this._chatStream.next(payload.data);
|
||||
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
|
||||
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
|
||||
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
|
||||
this._closePopupStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
|
||||
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
|
||||
scriptUtils.openTab(payload.data.url);
|
||||
}
|
||||
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
|
||||
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
|
||||
scriptUtils.goToPage(payload.data.url);
|
||||
}
|
||||
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
|
||||
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
|
||||
this._loadPageStream.next(payload.data.url);
|
||||
}
|
||||
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
|
||||
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
|
||||
this._playSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
|
||||
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
|
||||
this._stopSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'loadSound' && isLoadSoundEvent(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') {
|
||||
} 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') {
|
||||
} else if (payload.type === "disablePlayerControls") {
|
||||
this._disablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'restorePlayerControls') {
|
||||
} else if (payload.type === "restorePlayerControls") {
|
||||
this._enablePlayerControlStream.next();
|
||||
} else if (payload.type === 'displayBubble') {
|
||||
} else if (payload.type === "displayBubble") {
|
||||
this._displayBubbleStream.next();
|
||||
} else if (payload.type === 'removeBubble') {
|
||||
} else if (payload.type === "removeBubble") {
|
||||
this._removeBubbleStream.next();
|
||||
} else if (payload.type == "getState") {
|
||||
this._gameStateStream.next();
|
||||
} else if (payload.type == "onPlayerMove") {
|
||||
this.sendPlayerMove = true
|
||||
this.sendPlayerMove = true;
|
||||
} else if (payload.type == "getDataLayer") {
|
||||
this._dataLayerChangeStream.next();
|
||||
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
||||
@ -188,26 +237,21 @@ class IframeListener {
|
||||
// @ts-ignore
|
||||
this.iframeCloseCallbacks.get(iframe).push(() => {
|
||||
this._unregisterMenuCommandStream.next(data);
|
||||
})
|
||||
handleMenuItemRegistrationEvent(payload.data)
|
||||
});
|
||||
handleMenuItemRegistrationEvent(payload.data);
|
||||
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
||||
this._setTilesStream.next(payload.data);
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
||||
this.postMessage({
|
||||
'type' : 'dataLayer',
|
||||
'data' : dataLayerEvent
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
sendGameStateEvent(gameStateEvent: GameStateEvent) {
|
||||
this.postMessage({
|
||||
'type': 'gameState',
|
||||
'data': gameStateEvent
|
||||
type: "dataLayer",
|
||||
data: dataLayerEvent,
|
||||
});
|
||||
}
|
||||
|
||||
@ -220,25 +264,25 @@ class IframeListener {
|
||||
}
|
||||
|
||||
unregisterIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => {
|
||||
this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
|
||||
callback();
|
||||
});
|
||||
this.iframes.delete(iframe);
|
||||
}
|
||||
|
||||
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 (
|
||||
const iframe = document.createElement('iframe');
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
|
||||
iframe.style.display = "none";
|
||||
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.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
iframe.sandbox.add("allow-scripts");
|
||||
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
@ -246,36 +290,50 @@ class IframeListener {
|
||||
this.registerIframe(iframe);
|
||||
} else {
|
||||
// production code
|
||||
const iframe = document.createElement('iframe');
|
||||
const iframe = document.createElement("iframe");
|
||||
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.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
iframe.sandbox.add("allow-scripts");
|
||||
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
||||
|
||||
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
||||
iframe.srcdoc = '<!doctype html>\n' +
|
||||
'\n' +
|
||||
iframe.srcdoc =
|
||||
"<!doctype html>\n" +
|
||||
"\n" +
|
||||
'<html lang="en">\n' +
|
||||
'<head>\n' +
|
||||
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
|
||||
'<script src="' + scriptUrl + '" ></script>\n' +
|
||||
'<title></title>\n' +
|
||||
'</head>\n' +
|
||||
'</html>\n';
|
||||
"<head>\n" +
|
||||
'<script src="' +
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
'/iframe_api.js" ></script>\n' +
|
||||
'<script src="' +
|
||||
scriptUrl +
|
||||
'" ></script>\n' +
|
||||
"<title></title>\n" +
|
||||
"</head>\n" +
|
||||
"</html>\n";
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
this.scripts.set(scriptUrl, 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 {
|
||||
return 'script' + btoa(scriptUrl);
|
||||
return "script" + btoa(scriptUrl);
|
||||
}
|
||||
|
||||
unregisterScript(scriptUrl: string): void {
|
||||
@ -292,47 +350,47 @@ class IframeListener {
|
||||
|
||||
sendUserInputChat(message: string) {
|
||||
this.postMessage({
|
||||
'type': 'userInputChat',
|
||||
'data': {
|
||||
'message': message,
|
||||
} as UserInputChatEvent
|
||||
type: "userInputChat",
|
||||
data: {
|
||||
message: message,
|
||||
} as UserInputChatEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendEnterEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'enterEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
type: "enterEvent",
|
||||
data: {
|
||||
name: name,
|
||||
} as EnterLeaveEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'leaveEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
type: "leaveEvent",
|
||||
data: {
|
||||
name: name,
|
||||
} as EnterLeaveEvent,
|
||||
});
|
||||
}
|
||||
|
||||
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
||||
if (this.sendPlayerMove) {
|
||||
this.postMessage({
|
||||
'type': 'hasPlayerMoved',
|
||||
'data': event
|
||||
type: "hasPlayerMoved",
|
||||
data: event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
||||
this.postMessage({
|
||||
'type': 'buttonClickedEvent',
|
||||
'data': {
|
||||
type: "buttonClickedEvent",
|
||||
data: {
|
||||
popupId,
|
||||
buttonId
|
||||
} as ButtonClickedEvent
|
||||
buttonId,
|
||||
} as ButtonClickedEvent,
|
||||
});
|
||||
}
|
||||
|
||||
@ -341,10 +399,25 @@ class IframeListener {
|
||||
*/
|
||||
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
||||
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();
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
|
||||
|
||||
class ScriptUtils {
|
||||
|
||||
public openTab(url: string) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
public goToPage(url: string) {
|
||||
window.location.href = url;
|
||||
|
||||
}
|
||||
|
||||
public openCoWebsite(url: string, base: string) {
|
||||
coWebsiteManager.loadCoWebsite(url, base);
|
||||
public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
|
||||
coWebsiteManager.loadCoWebsite(url, base, api, policy);
|
||||
}
|
||||
|
||||
public closeCoWebSite() {
|
||||
|
@ -1,9 +1,40 @@
|
||||
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>) {
|
||||
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
|
||||
|
||||
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent';
|
||||
import { iframeListener } from '../../IframeListener';
|
||||
import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
|
||||
import { iframeListener } from "../../IframeListener";
|
||||
|
||||
export function sendMenuClickedEvent(menuItem: string) {
|
||||
iframeListener.postMessage({
|
||||
'type': 'menuItemClicked',
|
||||
'data': {
|
||||
type: "menuItemClicked",
|
||||
data: {
|
||||
menuItem: menuItem,
|
||||
} as MenuItemClickedEvent
|
||||
} as MenuItemClickedEvent,
|
||||
});
|
||||
}
|
@ -1,30 +1,30 @@
|
||||
import type { ChatEvent } from '../Events/ChatEvent'
|
||||
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent'
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'
|
||||
import type { ChatEvent } from "../Events/ChatEvent";
|
||||
import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
const chatStream = new Subject<string>();
|
||||
|
||||
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
|
||||
|
||||
callbacks = [apiCallback({
|
||||
export class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
callback: (event: UserInputChatEvent) => {
|
||||
chatStream.next(event.message);
|
||||
},
|
||||
type: "userInputChat",
|
||||
typeChecker: isUserInputChatEvent
|
||||
})]
|
||||
|
||||
typeChecker: isUserInputChatEvent,
|
||||
}),
|
||||
];
|
||||
|
||||
sendChatMessage(message: string, author: string) {
|
||||
sendToWorkadventure({
|
||||
type: 'chat',
|
||||
type: "chat",
|
||||
data: {
|
||||
'message': message,
|
||||
'author': author
|
||||
}
|
||||
})
|
||||
message: message,
|
||||
author: author,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,4 +35,4 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureChatCommands()
|
||||
export default new WorkadventureChatCommands();
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
|
||||
class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
|
||||
callbacks = []
|
||||
export class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
|
||||
callbacks = [];
|
||||
|
||||
disablePlayerControls(): void {
|
||||
sendToWorkadventure({ 'type': 'disablePlayerControls', data: null });
|
||||
sendToWorkadventure({ type: "disablePlayerControls", data: null });
|
||||
}
|
||||
|
||||
restorePlayerControls(): void {
|
||||
sendToWorkadventure({ 'type': 'restorePlayerControls', data: null });
|
||||
sendToWorkadventure({ type: "restorePlayerControls", data: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureControlsCommands();
|
||||
|
@ -1,57 +1,56 @@
|
||||
import type { GoToPageEvent } from '../Events/GoToPageEvent';
|
||||
import type { OpenTabEvent } from '../Events/OpenTabEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import type { GoToPageEvent } from "../Events/GoToPageEvent";
|
||||
import type { OpenTabEvent } from "../Events/OpenTabEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent";
|
||||
import type { LoadPageEvent } from "../Events/LoadPageEvent";
|
||||
|
||||
|
||||
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
|
||||
callbacks = []
|
||||
|
||||
export class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
|
||||
callbacks = [];
|
||||
|
||||
openTab(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'openTab',
|
||||
"data": {
|
||||
url
|
||||
}
|
||||
type: "openTab",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
goToPage(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'goToPage',
|
||||
"data": {
|
||||
url
|
||||
}
|
||||
type: "goToPage",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
goToRoom(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'loadPage',
|
||||
"data": {
|
||||
url
|
||||
}
|
||||
type: "loadPage",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openCoWebSite(url: string): void {
|
||||
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'openCoWebSite',
|
||||
"data": {
|
||||
url
|
||||
}
|
||||
type: "openCoWebSite",
|
||||
data: {
|
||||
url,
|
||||
allowApi,
|
||||
allowPolicy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeCoWebSite(): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'closeCoWebSite',
|
||||
data: null
|
||||
type: "closeCoWebSite",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureNavigationCommands();
|
||||
|
@ -2,27 +2,39 @@ import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContributio
|
||||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||
import { Subject } from "rxjs";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { getGameState } from "./room";
|
||||
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||
|
||||
interface User {
|
||||
id: string | undefined;
|
||||
nickName: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||
|
||||
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: 'hasPlayerMoved',
|
||||
type: "hasPlayerMoved",
|
||||
typeChecker: isHasPlayerMovedEvent,
|
||||
callback: (payloadData) => {
|
||||
moveStream.next(payloadData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
];
|
||||
|
||||
onPlayerMove(callback: HasPlayerMovedEventCallback): void {
|
||||
moveStream.subscribe(callback);
|
||||
sendToWorkadventure({
|
||||
type: 'onPlayerMove',
|
||||
data: null
|
||||
})
|
||||
type: "onPlayerMove",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
getCurrentUser(): Promise<User> {
|
||||
return getGameState().then((gameState) => {
|
||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,87 +1,74 @@
|
||||
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 type {LayerEvent} from "../Events/LayerEvent";
|
||||
import type {SetPropertyEvent} from "../Events/setPropertyEvent";
|
||||
import type {GameStateEvent} from "../Events/GameStateEvent";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
import type { DataLayerEvent } from "../Events/DataLayerEvent";
|
||||
import {isGameStateEvent} from "../Events/GameStateEvent";
|
||||
import {isDataLayerEvent} from "../Events/DataLayerEvent";
|
||||
import type { GameStateEvent } from "../Events/GameStateEvent";
|
||||
|
||||
const enterStreams: 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 stateResolvers = new Subject<GameStateEvent>();
|
||||
|
||||
let immutableData: GameStateEvent;
|
||||
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
||||
|
||||
interface Room {
|
||||
id: string,
|
||||
mapUrl: string,
|
||||
map: ITiledMap,
|
||||
startLayer: string | null
|
||||
id: string;
|
||||
mapUrl: string;
|
||||
map: ITiledMap;
|
||||
startLayer: string | null;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string | undefined,
|
||||
nickName: string | null,
|
||||
tags: string[]
|
||||
interface TileDescriptor {
|
||||
x: number;
|
||||
y: number;
|
||||
tile: number | string | null;
|
||||
layer: string;
|
||||
}
|
||||
|
||||
|
||||
function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableData) {
|
||||
return Promise.resolve(immutableData);
|
||||
}
|
||||
else {
|
||||
return new Promise<GameStateEvent>((resolver, thrower) => {
|
||||
stateResolvers.subscribe(resolver);
|
||||
sendToWorkadventure({type: "getState", data: null});
|
||||
})
|
||||
export function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableDataPromise === undefined) {
|
||||
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
||||
}
|
||||
return immutableDataPromise;
|
||||
}
|
||||
|
||||
function getDataLayer(): Promise<DataLayerEvent> {
|
||||
return new Promise<DataLayerEvent>((resolver, thrower) => {
|
||||
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 = [
|
||||
apiCallback({
|
||||
callback: (payloadData: EnterLeaveEvent) => {
|
||||
enterStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
type: "enterEvent",
|
||||
typeChecker: isEnterLeaveEvent
|
||||
typeChecker: isEnterLeaveEvent,
|
||||
}),
|
||||
apiCallback({
|
||||
type: "leaveEvent",
|
||||
typeChecker: isEnterLeaveEvent,
|
||||
callback: (payloadData) => {
|
||||
leaveStreams.get(payloadData.name)?.next();
|
||||
}
|
||||
}),
|
||||
apiCallback({
|
||||
type: "gameState",
|
||||
typeChecker: isGameStateEvent,
|
||||
callback: (payloadData) => {
|
||||
stateResolvers.next(payloadData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "dataLayer",
|
||||
typeChecker: isDataLayerEvent,
|
||||
callback: (payloadData) => {
|
||||
dataLayerResolver.next(payloadData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
let subject = enterStreams.get(name);
|
||||
@ -90,7 +77,6 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
|
||||
enterStreams.set(name, subject);
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
|
||||
}
|
||||
onLeaveZone(name: string, callback: () => void): void {
|
||||
let subject = leaveStreams.get(name);
|
||||
@ -101,35 +87,39 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
|
||||
subject.subscribe(callback);
|
||||
}
|
||||
showLayer(layerName: string): void {
|
||||
sendToWorkadventure({type: 'showLayer', data: {'name': layerName}});
|
||||
sendToWorkadventure({ type: "showLayer", data: { name: layerName } });
|
||||
}
|
||||
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 {
|
||||
sendToWorkadventure({
|
||||
type: 'setProperty',
|
||||
type: "setProperty",
|
||||
data: {
|
||||
'layerName': layerName,
|
||||
'propertyName': propertyName,
|
||||
'propertyValue': propertyValue,
|
||||
}
|
||||
})
|
||||
layerName: layerName,
|
||||
propertyName: propertyName,
|
||||
propertyValue: propertyValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
getCurrentRoom(): Promise<Room> {
|
||||
return getGameState().then((gameState) => {
|
||||
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> {
|
||||
return getGameState().then((gameState) => {
|
||||
return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags};
|
||||
})
|
||||
setTiles(tiles: TileDescriptor[]) {
|
||||
sendToWorkadventure({
|
||||
type: "setTiles",
|
||||
data: tiles,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
||||
|
@ -1,17 +1,15 @@
|
||||
import type { LoadSoundEvent } from '../Events/LoadSoundEvent';
|
||||
import type { PlaySoundEvent } from '../Events/PlaySoundEvent';
|
||||
import type { StopSoundEvent } from '../Events/StopSoundEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
|
||||
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
|
||||
import type { StopSoundEvent } from "../Events/StopSoundEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { Sound } from "./Sound/Sound";
|
||||
|
||||
class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
|
||||
callbacks = []
|
||||
export class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
|
||||
callbacks = [];
|
||||
|
||||
loadSound(url: string): Sound {
|
||||
return new Sound(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureSoundCommands();
|
||||
|
@ -1,29 +1,31 @@
|
||||
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
|
||||
import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent';
|
||||
import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
|
||||
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
|
||||
import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
|
||||
import { Popup } from "./Ui/Popup";
|
||||
|
||||
let popupId = 0;
|
||||
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 {
|
||||
zone: string
|
||||
objectLayerName?: string,
|
||||
popupText: string,
|
||||
delay?: number
|
||||
popupOptions: Array<ButtonDescriptor>
|
||||
zone: string;
|
||||
objectLayerName?: string;
|
||||
popupText: string;
|
||||
delay?: number;
|
||||
popupOptions: Array<ButtonDescriptor>;
|
||||
}
|
||||
|
||||
|
||||
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||
|
||||
callbacks = [apiCallback({
|
||||
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "buttonClickedEvent",
|
||||
typeChecker: isButtonClickedEvent,
|
||||
callback: (payloadData) => {
|
||||
@ -35,19 +37,19 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
|
||||
if (callback) {
|
||||
callback(popup);
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "menuItemClicked",
|
||||
typeChecker: isMenuItemClickedEvent,
|
||||
callback: event => {
|
||||
callback: (event) => {
|
||||
const callback = menuCallbacks.get(event.menuItem);
|
||||
if (callback) {
|
||||
callback(event.menuItem)
|
||||
callback(event.menuItem);
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
popupId++;
|
||||
@ -66,40 +68,40 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
|
||||
}
|
||||
|
||||
sendToWorkadventure({
|
||||
'type': 'openPopup',
|
||||
'data': {
|
||||
type: "openPopup",
|
||||
data: {
|
||||
popupId,
|
||||
targetObject,
|
||||
message,
|
||||
buttons: buttons.map((button) => {
|
||||
return {
|
||||
label: button.label,
|
||||
className: button.className
|
||||
className: button.className,
|
||||
};
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
popups.set(popupId, popup)
|
||||
popups.set(popupId, popup);
|
||||
return popup;
|
||||
}
|
||||
|
||||
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
|
||||
menuCallbacks.set(commandDescriptor, callback);
|
||||
sendToWorkadventure({
|
||||
'type': 'registerMenuCommand',
|
||||
'data': {
|
||||
menutItem: commandDescriptor
|
||||
}
|
||||
type: "registerMenuCommand",
|
||||
data: {
|
||||
menutItem: commandDescriptor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
displayBubble(): void {
|
||||
sendToWorkadventure({ 'type': 'displayBubble', data: null });
|
||||
sendToWorkadventure({ type: "displayBubble", data: null });
|
||||
}
|
||||
|
||||
removeBubble(): void {
|
||||
sendToWorkadventure({ 'type': 'removeBubble', data: null });
|
||||
sendToWorkadventure({ type: "removeBubble", data: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,14 @@
|
||||
import {errorStore} from "../Stores/ErrorStore";
|
||||
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
||||
import LoginScene from "./Login/LoginScene.svelte";
|
||||
import Chat from "./Chat/Chat.svelte";
|
||||
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
||||
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
||||
import VisitCard from "./VisitCard/VisitCard.svelte";
|
||||
import {requestVisitCardsStore} from "../Stores/GameStore";
|
||||
|
||||
import type {Game} from "../Phaser/Game/Game";
|
||||
import {chatVisibilityStore} from "../Stores/ChatStore";
|
||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
@ -61,14 +63,6 @@
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
{#if $menuIconVisible}
|
||||
<div>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
{/if}
|
||||
-->
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<VideoOverlay></VideoOverlay>
|
||||
@ -94,4 +88,7 @@
|
||||
<ErrorDialog></ErrorDialog>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $chatVisibilityStore}
|
||||
<Chat></Chat>
|
||||
{/if}
|
||||
</div>
|
||||
|
101
front/src/Components/Chat/Chat.svelte
Normal 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}>×</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>
|
83
front/src/Components/Chat/ChatElement.svelte
Normal 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}
|
||||
>> {#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}
|
||||
<< {#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>
|
56
front/src/Components/Chat/ChatMessageForm.svelte
Normal 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>
|
51
front/src/Components/Chat/ChatPlayerName.svelte
Normal 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>
|
33
front/src/Components/Chat/ChatSubMenu.svelte
Normal 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>
|
@ -1,11 +1,11 @@
|
||||
<script lang="typescript">
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene";
|
||||
import {activeRowStore} from "../../Stores/CustomCharacterStore";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
const customCharacterScene = game.scene.getScene(CustomizeSceneName) as CustomizeScene;
|
||||
let activeRow = customCharacterScene.activeRow;
|
||||
|
||||
function selectLeft() {
|
||||
customCharacterScene.moveCursorHorizontally(-1);
|
||||
@ -17,12 +17,10 @@
|
||||
|
||||
function selectUp() {
|
||||
customCharacterScene.moveCursorVertically(-1);
|
||||
activeRow = customCharacterScene.activeRow;
|
||||
}
|
||||
|
||||
function selectDown() {
|
||||
customCharacterScene.moveCursorVertically(1);
|
||||
activeRow = customCharacterScene.activeRow;
|
||||
}
|
||||
|
||||
function previousScene() {
|
||||
@ -44,16 +42,16 @@
|
||||
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> > </button>
|
||||
</section>
|
||||
<section class="action">
|
||||
{#if activeRow === 0}
|
||||
{#if $activeRowStore === 0}
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
|
||||
{/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>
|
||||
{/if}
|
||||
{#if activeRow === 5}
|
||||
{#if $activeRowStore === 5}
|
||||
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
|
||||
{/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>
|
||||
{/if}
|
||||
</section>
|
||||
|
@ -37,9 +37,7 @@
|
||||
<img alt="Report this user" src={reportImg}>
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
{#if $streamStore }
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||
{#if $constraintStore && $constraintStore.audio !== false}
|
||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||
|
@ -1,3 +1,6 @@
|
||||
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;
|
||||
if (str.length === 0) {
|
||||
@ -7,21 +10,37 @@ export function getColorByString(str: string) : string|null {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
let color = '#';
|
||||
let color = "#";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 255;
|
||||
color += ('00' + value.toString(16)).substr(-2);
|
||||
color += ("00" + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
|
||||
node.srcObject = stream;
|
||||
return {
|
||||
update(newStream: MediaStream) {
|
||||
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;
|
||||
}
|
||||
|
@ -45,8 +45,9 @@
|
||||
|
||||
.visitCard {
|
||||
pointer-events: all;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
margin-top: 200px;
|
||||
max-width: 80vw;
|
||||
|
||||
|
@ -38,11 +38,9 @@ class ConnectionManager {
|
||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
||||
localUserStore.saveUser(this.localUser);
|
||||
|
||||
const organizationSlug = data.organizationSlug;
|
||||
const worldSlug = data.worldSlug;
|
||||
const roomSlug = data.roomSlug;
|
||||
const roomUrl = data.roomUrl;
|
||||
|
||||
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);
|
||||
return Promise.resolve(room);
|
||||
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
||||
@ -66,22 +64,21 @@ class ConnectionManager {
|
||||
throw "Error to store local user data";
|
||||
}
|
||||
|
||||
let roomId: string;
|
||||
let roomPath: string;
|
||||
if (connexionType === GameConnexionTypes.empty) {
|
||||
roomId = START_ROOM_URL;
|
||||
roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
|
||||
} 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
|
||||
const room = new Room(roomId);
|
||||
const mapDetail = await room.getMapDetail();
|
||||
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
|
||||
const room = await Room.createRoom(new URL(roomPath));
|
||||
if(room.textures != undefined && room.textures.length > 0) {
|
||||
//check if texture was changed
|
||||
if(localUser.textures.length === 0){
|
||||
localUser.textures = mapDetail.textures;
|
||||
localUser.textures = room.textures;
|
||||
}else{
|
||||
mapDetail.textures.forEach((newTexture) => {
|
||||
room.textures.forEach((newTexture) => {
|
||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
||||
return;
|
||||
@ -114,9 +111,9 @@ class ConnectionManager {
|
||||
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) => {
|
||||
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) => {
|
||||
console.log('An error occurred while connecting to socket server. Retrying');
|
||||
reject(error);
|
||||
@ -137,7 +134,7 @@ class ConnectionManager {
|
||||
this.reconnectingTimeout = setTimeout(() => {
|
||||
//todo: allow a way to break recursion?
|
||||
//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) );
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ export enum EventMessage{
|
||||
GROUP_CREATE_UPDATE = "group-create-update",
|
||||
GROUP_DELETE = "group-delete",
|
||||
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",
|
||||
CONNECTING_ERROR = "connecting_error",
|
||||
@ -47,6 +47,7 @@ export interface MessageUserPositionInterface {
|
||||
position: PointInterface;
|
||||
visitCardUrl: string | null;
|
||||
companion: string | null;
|
||||
userUuid: string;
|
||||
}
|
||||
|
||||
export interface MessageUserMovedInterface {
|
||||
@ -61,57 +62,58 @@ export interface MessageUserJoined {
|
||||
position: PointInterface;
|
||||
visitCardUrl: string | null;
|
||||
companion: string | null;
|
||||
userUuid: string;
|
||||
}
|
||||
|
||||
export interface PositionInterface {
|
||||
x: number,
|
||||
y: number
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface GroupCreatedUpdatedMessageInterface {
|
||||
position: PositionInterface,
|
||||
groupId: number,
|
||||
groupSize: number
|
||||
position: PositionInterface;
|
||||
groupId: number;
|
||||
groupSize: number;
|
||||
}
|
||||
|
||||
export interface WebRtcDisconnectMessageInterface {
|
||||
userId: number
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface WebRtcSignalReceivedMessageInterface {
|
||||
userId: number,
|
||||
signal: SignalData,
|
||||
webRtcUser: string | undefined,
|
||||
webRtcPassword: string | undefined
|
||||
userId: number;
|
||||
signal: SignalData;
|
||||
webRtcUser: string | undefined;
|
||||
webRtcPassword: string | undefined;
|
||||
}
|
||||
|
||||
export interface ViewportInterface {
|
||||
left: number,
|
||||
top: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface ItemEventMessageInterface {
|
||||
itemId: number,
|
||||
event: string,
|
||||
state: unknown,
|
||||
parameters: unknown
|
||||
itemId: number;
|
||||
event: string;
|
||||
state: unknown;
|
||||
parameters: unknown;
|
||||
}
|
||||
|
||||
export interface RoomJoinedMessageInterface {
|
||||
//users: MessageUserPositionInterface[],
|
||||
//groups: GroupCreatedUpdatedMessageInterface[],
|
||||
items: { [itemId: number] : unknown }
|
||||
items: { [itemId: number]: unknown };
|
||||
}
|
||||
|
||||
export interface PlayGlobalMessageInterface {
|
||||
id: string
|
||||
type: string
|
||||
message: string
|
||||
id: string;
|
||||
type: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface OnConnectInterface {
|
||||
connection: RoomConnection,
|
||||
room: RoomJoinedMessageInterface
|
||||
connection: RoomConnection;
|
||||
room: RoomJoinedMessageInterface;
|
||||
}
|
||||
|
@ -3,88 +3,103 @@ import {PUSHER_URL} from "../Enum/EnvironmentVariable";
|
||||
import type { CharacterTexture } from "./LocalUser";
|
||||
|
||||
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 {
|
||||
public readonly id: string;
|
||||
public readonly isPublic: boolean;
|
||||
private mapUrl: string|undefined;
|
||||
private textures: CharacterTexture[]|undefined;
|
||||
private _mapUrl: string | undefined;
|
||||
private _textures: CharacterTexture[] | undefined;
|
||||
private instance: string | undefined;
|
||||
private _search: URLSearchParams;
|
||||
private readonly _search: URLSearchParams;
|
||||
|
||||
constructor(id: string) {
|
||||
const url = new URL(id, 'https://example.com');
|
||||
private constructor(private roomUrl: URL) {
|
||||
this.id = roomUrl.pathname;
|
||||
|
||||
this.id = url.pathname;
|
||||
|
||||
if (this.id.startsWith('/')) {
|
||||
if (this.id.startsWith("/")) {
|
||||
this.id = this.id.substr(1);
|
||||
}
|
||||
if (this.id.startsWith('_/')) {
|
||||
if (this.id.startsWith("_/")) {
|
||||
this.isPublic = true;
|
||||
} else if (this.id.startsWith('@/')) {
|
||||
} else if (this.id.startsWith("@/")) {
|
||||
this.isPublic = false;
|
||||
} 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 = '';
|
||||
let hash = '';
|
||||
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.
|
||||
//We instead use 'workadventure' as a dummy base value.
|
||||
const baseUrlObject = new URL(baseUrl);
|
||||
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/'+currentInstance+'/'+baseUrlObject.hostname+baseUrlObject.pathname);
|
||||
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
|
||||
roomId = roomId.substring(1); //remove the leading slash
|
||||
hash = absoluteExitSceneUrl.hash;
|
||||
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]
|
||||
/**
|
||||
* Creates a "Room" object representing the room.
|
||||
* This method will follow room redirects if necessary, so the instance returned is a "real" room.
|
||||
*/
|
||||
public static async createRoom(roomUrl: URL): Promise<Room> {
|
||||
let redirectCount = 0;
|
||||
while (redirectCount < 32) {
|
||||
const room = new Room(roomUrl);
|
||||
const result = await room.getMapDetail();
|
||||
if (result instanceof MapDetail) {
|
||||
return room;
|
||||
}
|
||||
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> {
|
||||
return new Promise<MapDetail>((resolve, reject) => {
|
||||
if (this.mapUrl !== undefined && this.textures != undefined) {
|
||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
||||
return;
|
||||
public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
|
||||
const url = new URL(exitUrl, currentRoomUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
if (this.isPublic) {
|
||||
const match = /_\/[^/]+\/(.+)/.exec(this.id);
|
||||
if (!match) throw new Error('Could not extract url from "'+this.id+'"');
|
||||
this.mapUrl = window.location.protocol+'//'+match[1];
|
||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
||||
return;
|
||||
} else {
|
||||
// We have a private ID, we need to query the map URL from the server.
|
||||
const urlParts = this.parsePrivateUrl(this.id);
|
||||
/**
|
||||
* @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
|
||||
*/
|
||||
public static getRoomPathFromExitSceneUrl(
|
||||
exitSceneUrl: string,
|
||||
currentRoomUrl: string,
|
||||
currentMapUrl: string
|
||||
): URL {
|
||||
const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
|
||||
const baseUrl = new URL(currentRoomUrl);
|
||||
|
||||
Axios.get(`${PUSHER_URL}/map`, {
|
||||
params: urlParts
|
||||
}).then(({data}) => {
|
||||
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
|
||||
resolve(data);
|
||||
return;
|
||||
}).catch((reason) => {
|
||||
reject(reason);
|
||||
const currentRoom = new Room(baseUrl);
|
||||
let instance: string = "global";
|
||||
if (currentRoom.isPublic) {
|
||||
instance = currentRoom.instance as string;
|
||||
}
|
||||
|
||||
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,31 +120,33 @@ export class Room {
|
||||
} else {
|
||||
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(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;
|
||||
}
|
||||
}
|
||||
|
||||
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 match = regex.exec(url);
|
||||
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],
|
||||
worldSlug: match[2],
|
||||
}
|
||||
};
|
||||
if (match[3] !== undefined) {
|
||||
results.roomSlug = match[3];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public isDisconnected(): boolean
|
||||
{
|
||||
const alone = this._search.get('alone');
|
||||
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
|
||||
public isDisconnected(): boolean {
|
||||
const alone = this._search.get("alone");
|
||||
if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -138,4 +155,33 @@ export class Room {
|
||||
public get search(): URLSearchParams {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
RoomJoinedMessage,
|
||||
ServerToClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SilentMessage, StopGlobalMessage,
|
||||
SilentMessage,
|
||||
StopGlobalMessage,
|
||||
UserJoinedMessage,
|
||||
UserLeftMessage,
|
||||
UserMovedMessage,
|
||||
@ -31,17 +32,22 @@ import {
|
||||
EmotePromptMessage,
|
||||
SendUserMessage,
|
||||
BanUserMessage,
|
||||
} from "../Messages/generated/messages_pb"
|
||||
} from "../Messages/generated/messages_pb";
|
||||
|
||||
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
|
||||
import {
|
||||
EventMessage,
|
||||
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface,
|
||||
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface,
|
||||
GroupCreatedUpdatedMessageInterface,
|
||||
ItemEventMessageInterface,
|
||||
MessageUserJoined,
|
||||
OnConnectInterface,
|
||||
PlayGlobalMessageInterface,
|
||||
PositionInterface,
|
||||
RoomJoinedMessageInterface,
|
||||
ViewportInterface, WebRtcDisconnectMessageInterface,
|
||||
ViewportInterface,
|
||||
WebRtcDisconnectMessageInterface,
|
||||
WebRtcSignalReceivedMessageInterface,
|
||||
} from "./ConnexionModels";
|
||||
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||
@ -61,36 +67,45 @@ export class RoomConnection implements RoomConnection {
|
||||
private closed: boolean = false;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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();
|
||||
url = url.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
url = url.replace("http://", "ws://").replace("https://", "wss://");
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
url += 'room';
|
||||
url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : '');
|
||||
url += '&token=' + (token ? encodeURIComponent(token) : '');
|
||||
url += '&name=' + encodeURIComponent(name);
|
||||
url += "room";
|
||||
url += "?roomId=" + encodeURIComponent(roomUrl);
|
||||
url += "&token=" + (token ? encodeURIComponent(token) : "");
|
||||
url += "&name=" + encodeURIComponent(name);
|
||||
for (const layer of characterLayers) {
|
||||
url += '&characterLayers=' + encodeURIComponent(layer);
|
||||
url += "&characterLayers=" + encodeURIComponent(layer);
|
||||
}
|
||||
url += '&x=' + Math.floor(position.x);
|
||||
url += '&y=' + Math.floor(position.y);
|
||||
url += '&top=' + Math.floor(viewport.top);
|
||||
url += '&bottom=' + Math.floor(viewport.bottom);
|
||||
url += '&left=' + Math.floor(viewport.left);
|
||||
url += '&right=' + Math.floor(viewport.right);
|
||||
if (typeof companion === 'string') {
|
||||
url += '&companion=' + encodeURIComponent(companion);
|
||||
url += "&x=" + Math.floor(position.x);
|
||||
url += "&y=" + Math.floor(position.y);
|
||||
url += "&top=" + Math.floor(viewport.top);
|
||||
url += "&bottom=" + Math.floor(viewport.bottom);
|
||||
url += "&left=" + Math.floor(viewport.left);
|
||||
url += "&right=" + Math.floor(viewport.right);
|
||||
if (typeof companion === "string") {
|
||||
url += "&companion=" + encodeURIComponent(companion);
|
||||
}
|
||||
|
||||
if (RoomConnection.websocketFactory) {
|
||||
@ -99,7 +114,7 @@ export class RoomConnection implements RoomConnection {
|
||||
this.socket = new WebSocket(url);
|
||||
}
|
||||
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
this.socket.addEventListener("close", (event) => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
@ -150,7 +165,7 @@ export class RoomConnection implements RoomConnection {
|
||||
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
||||
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
||||
} else {
|
||||
throw new Error('Unexpected batch message type');
|
||||
throw new Error("Unexpected batch message type");
|
||||
}
|
||||
|
||||
if (event) {
|
||||
@ -171,8 +186,8 @@ export class RoomConnection implements RoomConnection {
|
||||
this.dispatch(EventMessage.CONNECT, {
|
||||
connection: this,
|
||||
room: {
|
||||
items
|
||||
} as RoomJoinedMessageInterface
|
||||
items,
|
||||
} as RoomJoinedMessageInterface,
|
||||
});
|
||||
} else if (message.hasWorldfullmessage()) {
|
||||
worldFullMessageStream.onMessage();
|
||||
@ -183,7 +198,10 @@ export class RoomConnection implements RoomConnection {
|
||||
} else if (message.hasWebrtcsignaltoclientmessage()) {
|
||||
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
|
||||
} 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()) {
|
||||
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
|
||||
} else if (message.hasWebrtcdisconnectmessage()) {
|
||||
@ -205,10 +223,9 @@ export class RoomConnection implements RoomConnection {
|
||||
} else if (message.hasRefreshroommessage()) {
|
||||
//todo: implement a way to notify the user the room was refreshed.
|
||||
} else {
|
||||
throw new Error('Unknown message received');
|
||||
}
|
||||
|
||||
throw new Error("Unknown message received");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private dispatch(event: string, payload: unknown): void {
|
||||
@ -243,16 +260,16 @@ export class RoomConnection implements RoomConnection {
|
||||
positionMessage.setY(Math.floor(y));
|
||||
let directionEnum: Direction;
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
case "up":
|
||||
directionEnum = Direction.UP;
|
||||
break;
|
||||
case 'down':
|
||||
case "down":
|
||||
directionEnum = Direction.DOWN;
|
||||
break;
|
||||
case 'left':
|
||||
case "left":
|
||||
directionEnum = Direction.LEFT;
|
||||
break;
|
||||
case 'right':
|
||||
case "right":
|
||||
directionEnum = Direction.RIGHT;
|
||||
break;
|
||||
default:
|
||||
@ -327,15 +344,17 @@ export class RoomConnection implements RoomConnection {
|
||||
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
|
||||
const position = message.getPosition();
|
||||
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
|
||||
.getCharacterlayersList()
|
||||
.map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
|
||||
return {
|
||||
name: characterLayer.getName(),
|
||||
img: characterLayer.getUrl()
|
||||
}
|
||||
})
|
||||
img: characterLayer.getUrl(),
|
||||
};
|
||||
});
|
||||
|
||||
const companion = message.getCompanion();
|
||||
|
||||
@ -345,8 +364,9 @@ export class RoomConnection implements RoomConnection {
|
||||
characterLayers,
|
||||
visitCardUrl: message.getVisitcardurl(),
|
||||
position: ProtobufClientUtils.toPointInterface(position),
|
||||
companion: companion ? companion.getName() : null
|
||||
}
|
||||
companion: companion ? companion.getName() : null,
|
||||
userUuid: message.getUseruuid(),
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
callback(this.toGroupCreatedUpdatedMessage(message));
|
||||
});
|
||||
@ -381,14 +403,14 @@ export class RoomConnection implements RoomConnection {
|
||||
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
|
||||
const position = message.getPosition();
|
||||
if (position === undefined) {
|
||||
throw new Error('Missing position in GROUP_CREATE_UPDATE');
|
||||
throw new Error("Missing position in GROUP_CREATE_UPDATE");
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: message.getGroupid(),
|
||||
position: position.toObject(),
|
||||
groupSize: message.getGroupsize()
|
||||
}
|
||||
groupSize: message.getGroupsize(),
|
||||
};
|
||||
}
|
||||
|
||||
public onGroupDeleted(callback: (groupId: number) => void): void {
|
||||
@ -404,7 +426,7 @@ export class RoomConnection implements RoomConnection {
|
||||
}
|
||||
|
||||
public onConnectError(callback: (error: Event) => void): void {
|
||||
this.socket.addEventListener('error', callback)
|
||||
this.socket.addEventListener("error", callback);
|
||||
}
|
||||
|
||||
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
|
||||
@ -445,7 +467,6 @@ export class RoomConnection implements RoomConnection {
|
||||
this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
|
||||
callback({
|
||||
userId: message.getUserid(),
|
||||
name: message.getName(),
|
||||
initiator: message.getInitiator(),
|
||||
webRtcUser: message.getWebrtcusername() ?? undefined,
|
||||
webRtcPassword: message.getWebrtcpassword() ?? undefined,
|
||||
@ -476,11 +497,11 @@ export class RoomConnection implements RoomConnection {
|
||||
}
|
||||
|
||||
public onServerDisconnected(callback: () => void): void {
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
this.socket.addEventListener("close", (event) => {
|
||||
if (this.closed === true || connectionManager.unloading) {
|
||||
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) {
|
||||
// Normal closure case
|
||||
return;
|
||||
@ -490,14 +511,14 @@ export class RoomConnection implements RoomConnection {
|
||||
}
|
||||
|
||||
public getUserId(): number {
|
||||
if (this.userId === null) throw 'UserId cannot be null!'
|
||||
if (this.userId === null) throw "UserId cannot be null!";
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
|
||||
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
|
||||
callback({
|
||||
userId: message.getUserid()
|
||||
userId: message.getUserid(),
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -521,21 +542,22 @@ export class RoomConnection implements RoomConnection {
|
||||
itemId: message.getItemid(),
|
||||
event: message.getEvent(),
|
||||
parameters: JSON.parse(message.getParametersjson()),
|
||||
state: JSON.parse(message.getStatejson())
|
||||
state: JSON.parse(message.getStatejson()),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
.then((res: { data: {} }) => {
|
||||
return res.data;
|
||||
}).catch((err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
|
||||
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
|
||||
callback({
|
||||
@ -570,9 +592,9 @@ export class RoomConnection implements RoomConnection {
|
||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
|
||||
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void {
|
||||
public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void {
|
||||
const reportPlayerMessage = new ReportPlayerMessage();
|
||||
reportPlayerMessage.setReporteduserid(reportedUserId);
|
||||
reportPlayerMessage.setReporteduseruuid(reportedUserUuid);
|
||||
reportPlayerMessage.setReportcomment(reportComment);
|
||||
|
||||
const clientToServerMessage = new ClientToServerMessage();
|
||||
@ -605,12 +627,12 @@ export class RoomConnection implements RoomConnection {
|
||||
}
|
||||
|
||||
public isAdmin(): boolean {
|
||||
return this.hasTag('admin');
|
||||
return this.hasTag("admin");
|
||||
}
|
||||
|
||||
public emitEmoteEvent(emoteName: string): void {
|
||||
const emoteMessage = new EmotePromptMessage();
|
||||
emoteMessage.setEmote(emoteName)
|
||||
emoteMessage.setEmote(emoteName);
|
||||
|
||||
const clientToServerMessage = new ClientToServerMessage();
|
||||
clientToServerMessage.setEmotepromptmessage(emoteMessage);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {discussionManager} from "../../WebRtc/DiscussionManager";
|
||||
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 {
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y, openChatIconName, 3);
|
||||
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
|
||||
this.setScrollFactor(0, 0);
|
||||
this.setOrigin(0, 1);
|
||||
this.setInteractive();
|
||||
this.setVisible(false);
|
||||
//this.setVisible(false);
|
||||
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||
|
||||
this.on("pointerup", () => discussionManager.showDiscussionPart());
|
||||
this.on("pointerup", () => chatVisibilityStore.set(true));
|
||||
}
|
||||
}
|
@ -44,7 +44,6 @@ export class TextUtils {
|
||||
options.align = object.text.halign;
|
||||
}
|
||||
|
||||
console.warn(options);
|
||||
const textElem = scene.add.text(object.x, object.y, object.text.text, options);
|
||||
textElem.setAngle(object.rotation);
|
||||
}
|
||||
|
@ -3,50 +3,58 @@ import type {CharacterTexture} from "../../Connexion/LocalUser";
|
||||
import { BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES } from "./PlayerTextures";
|
||||
|
||||
export interface FrameConfig {
|
||||
frameWidth: number,
|
||||
frameHeight: number,
|
||||
frameWidth: number;
|
||||
frameHeight: number;
|
||||
}
|
||||
|
||||
export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => {
|
||||
const returnArray: BodyResourceDescriptionInterface[][] = [];
|
||||
LAYERS.forEach(layer => {
|
||||
LAYERS.forEach((layer) => {
|
||||
const layerArray: BodyResourceDescriptionInterface[] = [];
|
||||
Object.values(layer).forEach((textureDescriptor) => {
|
||||
layerArray.push(textureDescriptor);
|
||||
load.spritesheet(textureDescriptor.name, textureDescriptor.img, { frameWidth: 32, frameHeight: 32 });
|
||||
})
|
||||
returnArray.push(layerArray)
|
||||
});
|
||||
returnArray.push(layerArray);
|
||||
});
|
||||
return returnArray;
|
||||
}
|
||||
};
|
||||
export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptionInterface[] => {
|
||||
const returnArray = Object.values(PLAYER_RESOURCES);
|
||||
returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => {
|
||||
load.spritesheet(playerResource.name, playerResource.img, { frameWidth: 32, frameHeight: 32 });
|
||||
});
|
||||
return returnArray;
|
||||
}
|
||||
};
|
||||
|
||||
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => {
|
||||
const name = 'customCharacterTexture'+texture.id;
|
||||
const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level}
|
||||
export const loadCustomTexture = (
|
||||
loaderPlugin: LoaderPlugin,
|
||||
texture: CharacterTexture
|
||||
): Promise<BodyResourceDescriptionInterface> => {
|
||||
const name = "customCharacterTexture" + texture.id;
|
||||
const playerResourceDescriptor: BodyResourceDescriptionInterface = { name, img: texture.url, level: texture.level };
|
||||
return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
|
||||
frameWidth: 32,
|
||||
frameHeight: 32
|
||||
frameHeight: 32,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
|
||||
export const lazyLoadPlayerCharacterTextures = (
|
||||
loadPlugin: LoaderPlugin,
|
||||
texturekeys: Array<string | BodyResourceDescriptionInterface>
|
||||
): Promise<string[]> => {
|
||||
const promisesList: Promise<unknown>[] = [];
|
||||
texturekeys.forEach((textureKey: string | BodyResourceDescriptionInterface) => {
|
||||
try {
|
||||
//TODO refactor
|
||||
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
|
||||
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, {
|
||||
promisesList.push(
|
||||
createLoadingPromise(loadPlugin, playerResourceDescriptor, {
|
||||
frameWidth: 32,
|
||||
frameHeight: 32
|
||||
}));
|
||||
frameHeight: 32,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -59,16 +67,24 @@ export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, textur
|
||||
} else {
|
||||
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 (typeof textureKey !== 'string' && textureKey.img) {
|
||||
//If the loading fail, we render the default model instead.
|
||||
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;
|
||||
}
|
||||
const textureName:string = typeof textureKey === 'string' ? textureKey : textureKey.name;
|
||||
const textureName: string = typeof textureKey === "string" ? textureKey : textureKey.name;
|
||||
const playerResource = PLAYER_RESOURCES[textureName];
|
||||
if (playerResource !== undefined) return playerResource;
|
||||
|
||||
@ -76,15 +92,32 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio
|
||||
const playerResource = LAYERS[i][textureName];
|
||||
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) => {
|
||||
return new Promise<BodyResourceDescriptionInterface>((res) => {
|
||||
export const createLoadingPromise = (
|
||||
loadPlugin: LoaderPlugin,
|
||||
playerResourceDescriptor: BodyResourceDescriptionInterface,
|
||||
frameConfig: FrameConfig
|
||||
) => {
|
||||
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
|
||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||
return res(playerResourceDescriptor);
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,11 +1,6 @@
|
||||
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
||||
import type {PlayerInterface} from "./PlayerInterface";
|
||||
|
||||
export interface AddPlayerInterface {
|
||||
userId: number;
|
||||
name: string;
|
||||
characterLayers: BodyResourceDescriptionInterface[];
|
||||
export interface AddPlayerInterface extends PlayerInterface {
|
||||
position: PointInterface;
|
||||
visitCardUrl: string|null;
|
||||
companion: string|null;
|
||||
}
|
||||
|
@ -59,7 +59,6 @@ export abstract class DirtyScene extends ResizableScene {
|
||||
this.physicsEnabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private trackAnimation(): void {
|
||||
@ -71,7 +70,7 @@ export abstract class DirtyScene extends ResizableScene {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -10,8 +10,6 @@ import {get} from "svelte/store";
|
||||
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
|
||||
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class should be responsible for any scene starting/stopping
|
||||
*/
|
||||
@ -30,7 +28,7 @@ export class GameManager {
|
||||
|
||||
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
||||
this.startRoom = await connectionManager.initGameConnexion();
|
||||
await this.loadMap(this.startRoom, scenePlugin);
|
||||
this.loadMap(this.startRoom, scenePlugin);
|
||||
|
||||
if (!this.playerName) {
|
||||
return LoginSceneName;
|
||||
@ -57,12 +55,11 @@ export class GameManager {
|
||||
|
||||
getCharacterLayers(): string[] {
|
||||
if (!this.characterLayers) {
|
||||
throw 'characterLayers are not set';
|
||||
throw "characterLayers are not set";
|
||||
}
|
||||
return this.characterLayers;
|
||||
}
|
||||
|
||||
|
||||
setCompanion(companion: string | null): void {
|
||||
this.companion = companion;
|
||||
}
|
||||
@ -71,23 +68,25 @@ export class GameManager {
|
||||
return this.companion;
|
||||
}
|
||||
|
||||
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
|
||||
const roomID = room.id;
|
||||
const mapDetail = await room.getMapDetail();
|
||||
public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
|
||||
const roomID = room.key;
|
||||
|
||||
const gameIndex = scenePlugin.getIndex(roomID);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
|
||||
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
|
||||
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
|
||||
console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
|
||||
scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
|
||||
scenePlugin.launch(MenuSceneName);
|
||||
|
||||
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
|
||||
if (
|
||||
!localUserStore.getHelpCameraSettingsShown() &&
|
||||
(!get(requestedMicrophoneState) || !get(requestedCameraState))
|
||||
) {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
localUserStore.setHelpCameraSettingsShown();
|
||||
}
|
||||
@ -104,7 +103,7 @@ export class GameManager {
|
||||
* 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 {
|
||||
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;
|
||||
gameScene.cleanupClosingScene();
|
||||
scene.scene.stop(this.currentGameSceneName);
|
||||
@ -123,13 +122,13 @@ export class GameManager {
|
||||
scene.scene.start(this.currentGameSceneName);
|
||||
scene.scene.wake(MenuSceneName);
|
||||
} else {
|
||||
scene.scene.run(fallbackSceneName)
|
||||
scene.scene.run(fallbackSceneName);
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentGameScene(scene: Phaser.Scene): GameScene {
|
||||
if (this.currentGameSceneName === null) throw 'No current scene id set!';
|
||||
return scene.scene.get(this.currentGameSceneName) as GameScene
|
||||
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||
return scene.scene.get(this.currentGameSceneName) as GameScene;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,11 @@ import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||
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.
|
||||
@ -13,39 +17,56 @@ export class GameMap {
|
||||
private key: number | undefined;
|
||||
private lastProperties = new Map<string, string | boolean | number>();
|
||||
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 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);
|
||||
let depth = -2;
|
||||
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));
|
||||
}
|
||||
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
|
||||
if (layer.type === "objectgroup" && layer.name === "floorLayer") {
|
||||
depth = DEPTH_OVERLAY_INDEX;
|
||||
}
|
||||
}
|
||||
for (const tileset of map.tilesets) {
|
||||
tileset?.tiles?.forEach(tile => {
|
||||
tileset?.tiles?.forEach((tile) => {
|
||||
if (tile.properties) {
|
||||
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties
|
||||
tile.properties.forEach(prop => {
|
||||
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties;
|
||||
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") {
|
||||
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)
|
||||
@ -89,7 +110,7 @@ export class GameMap {
|
||||
const properties = new Map<string, string | boolean | number>();
|
||||
|
||||
for (const layer of this.flatLayers) {
|
||||
if (layer.type !== 'tilelayer') {
|
||||
if (layer.type !== "tilelayer") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -99,7 +120,7 @@ export class GameMap {
|
||||
if (tiles[key] == 0) {
|
||||
continue;
|
||||
}
|
||||
tileIndex = tiles[key]
|
||||
tileIndex = tiles[key];
|
||||
}
|
||||
|
||||
// There is a tile in this layer, let's embed the properties
|
||||
@ -113,13 +134,13 @@ export class GameMap {
|
||||
}
|
||||
|
||||
if (tileIndex) {
|
||||
this.tileSetPropertyMap[tileIndex]?.forEach(property => {
|
||||
this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
|
||||
if (property.value) {
|
||||
properties.set(property.name, property.value)
|
||||
properties.set(property.name, property.value);
|
||||
} else if (properties.has(property.name)) {
|
||||
properties.delete(property.name)
|
||||
properties.delete(property.name);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,7 +151,19 @@ export class GameMap {
|
||||
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);
|
||||
if (callbacksArray !== undefined) {
|
||||
for (const callback of callbacksArray) {
|
||||
@ -159,10 +192,65 @@ export class GameMap {
|
||||
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
11
front/src/Phaser/Game/PlayerInterface.ts
Normal 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;
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
|
||||
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
|
||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||
|
||||
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 {
|
||||
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
|
||||
@ -24,14 +28,18 @@ export class PlayerMovement {
|
||||
return this.endPosition;
|
||||
}
|
||||
|
||||
const x = (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;
|
||||
const x =
|
||||
(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)
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
direction: this.endPosition.direction,
|
||||
moving: true
|
||||
}
|
||||
moving: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* This class is in charge of computing the position of all players.
|
||||
* 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";
|
||||
|
||||
export class PlayersPositionInterpolator {
|
||||
@ -24,7 +24,7 @@ export class PlayersPositionInterpolator {
|
||||
this.playerMovements.delete(userId);
|
||||
}
|
||||
//console.log("moving")
|
||||
positions.set(userId, playerMovement.getPosition(tick))
|
||||
positions.set(userId, playerMovement.getPosition(tick));
|
||||
});
|
||||
return positions;
|
||||
}
|
||||
|
127
front/src/Phaser/Game/StartPositionCalculator.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -9,10 +9,11 @@ import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
||||
import { AbstractCharacterScene } from "./AbstractCharacterScene";
|
||||
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
|
||||
import { SelectCharacterSceneName } from "./SelectCharacterScene";
|
||||
import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
|
||||
import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import { isMobile } from "../../Enum/EnvironmentVariable";
|
||||
import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export const CustomizeSceneName = "CustomizeScene";
|
||||
|
||||
@ -21,7 +22,6 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
|
||||
private selectedLayers: number[] = [0];
|
||||
private containersRow: CustomizedCharacter[][] = [];
|
||||
public activeRow:number = 0;
|
||||
private layers: BodyResourceDescriptionInterface[][] = [];
|
||||
|
||||
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
|
||||
@ -31,16 +31,19 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
key: CustomizeSceneName
|
||||
key: CustomizeSceneName,
|
||||
});
|
||||
}
|
||||
|
||||
preload() {
|
||||
|
||||
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
|
||||
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
||||
if(bodyResourceDescription.level == undefined || bodyResourceDescription.level < 0 || bodyResourceDescription.level > 5 ){
|
||||
throw 'Texture level is null';
|
||||
if (
|
||||
bodyResourceDescription.level == undefined ||
|
||||
bodyResourceDescription.level < 0 ||
|
||||
bodyResourceDescription.level > 5
|
||||
) {
|
||||
throw "Texture level is null";
|
||||
}
|
||||
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
|
||||
});
|
||||
@ -50,14 +53,13 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
this.layers = loadAllLayers(this.load);
|
||||
this.lazyloadingAttempt = false;
|
||||
|
||||
|
||||
//this function must stay at the end of preload function
|
||||
addLoader(this);
|
||||
}
|
||||
|
||||
create() {
|
||||
customCharacterSceneVisibleStore.set(true);
|
||||
this.events.addListener('wake', () => {
|
||||
this.events.addListener("wake", () => {
|
||||
waScaleManager.saveZoom();
|
||||
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
|
||||
customCharacterSceneVisibleStore.set(true);
|
||||
@ -66,8 +68,13 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
waScaleManager.saveZoom();
|
||||
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.setStrokeStyle(2, 0xFFFFFF);
|
||||
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.setStrokeStyle(2, 0xffffff);
|
||||
this.add.existing(this.Rectangle);
|
||||
|
||||
this.createCustomizeLayer(0, 0, 0);
|
||||
@ -78,24 +85,24 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
this.createCustomizeLayer(0, 0, 5);
|
||||
|
||||
this.moveLayers();
|
||||
this.input.keyboard.on('keyup-ENTER', () => {
|
||||
this.input.keyboard.on("keyup-ENTER", () => {
|
||||
this.nextSceneToCamera();
|
||||
});
|
||||
this.input.keyboard.on('keyup-BACKSPACE', () => {
|
||||
this.input.keyboard.on("keyup-BACKSPACE", () => {
|
||||
this.backToPreviousScene();
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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-LEFT', () => this.moveHorizontally = -1);
|
||||
this.input.keyboard.on('keyup-DOWN', () => this.moveVertically = 1);
|
||||
this.input.keyboard.on('keyup-UP', () => this.moveVertically = -1);
|
||||
this.input.keyboard.on("keyup-RIGHT", () => (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-UP", () => (this.moveVertically = -1));
|
||||
|
||||
const customCursorPosition = localUserStore.getCustomCursorPosition();
|
||||
if (customCursorPosition) {
|
||||
this.activeRow = customCursorPosition.activeRow;
|
||||
activeRowStore.set(customCursorPosition.activeRow);
|
||||
this.selectedLayers = customCursorPosition.selectedLayers;
|
||||
this.moveLayers();
|
||||
this.updateSelectedLayer();
|
||||
@ -113,11 +120,11 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
private doMoveCursorHorizontally(index: number): void {
|
||||
this.selectedLayers[this.activeRow] += index;
|
||||
if (this.selectedLayers[this.activeRow] < 0) {
|
||||
this.selectedLayers[this.activeRow] = 0
|
||||
} else if(this.selectedLayers[this.activeRow] > this.layers[this.activeRow].length - 1) {
|
||||
this.selectedLayers[this.activeRow] = this.layers[this.activeRow].length - 1
|
||||
this.selectedLayers[get(activeRowStore)] += index;
|
||||
if (this.selectedLayers[get(activeRowStore)] < 0) {
|
||||
this.selectedLayers[get(activeRowStore)] = 0;
|
||||
} else if (this.selectedLayers[get(activeRowStore)] > this.layers[get(activeRowStore)].length - 1) {
|
||||
this.selectedLayers[get(activeRowStore)] = this.layers[get(activeRowStore)].length - 1;
|
||||
}
|
||||
this.moveLayers();
|
||||
this.updateSelectedLayer();
|
||||
@ -125,19 +132,18 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
private doMoveCursorVertically(index: number): void {
|
||||
|
||||
this.activeRow += index;
|
||||
if (this.activeRow < 0) {
|
||||
this.activeRow = 0
|
||||
} else if (this.activeRow > this.layers.length - 1) {
|
||||
this.activeRow = this.layers.length - 1
|
||||
activeRowStore.set(get(activeRowStore) + index);
|
||||
if (get(activeRowStore) < 0) {
|
||||
activeRowStore.set(0);
|
||||
} else if (get(activeRowStore) > this.layers.length - 1) {
|
||||
activeRowStore.set(this.layers.length - 1);
|
||||
}
|
||||
this.moveLayers();
|
||||
this.saveInLocalStorage();
|
||||
}
|
||||
|
||||
private saveInLocalStorage() {
|
||||
localUserStore.setCustomCursorPosition(this.activeRow, this.selectedLayers);
|
||||
localUserStore.setCustomCursorPosition(get(activeRowStore), this.selectedLayers);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -207,12 +213,11 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
selectedX = 0;
|
||||
}
|
||||
this.containersRow[i][j].x = screenCenterX + (j - selectedX) * 40;
|
||||
this.containersRow[i][j].y = screenCenterY + (i - this.activeRow) * 40;
|
||||
const alpha1 = Math.abs(selectedX - j)*47*2/screenWidth;
|
||||
const alpha2 = Math.abs(this.activeRow - i)*49*2/screenHeight;
|
||||
this.containersRow[i][j].y = screenCenterY + (i - get(activeRowStore)) * 40;
|
||||
const alpha1 = (Math.abs(selectedX - j) * 47 * 2) / screenWidth;
|
||||
const alpha2 = (Math.abs(get(activeRowStore) - i) * 49 * 2) / screenHeight;
|
||||
this.containersRow[i][j].setAlpha((1 - alpha1) * (1 - alpha2));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,7 +242,6 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
update(time: number, delta: number): void {
|
||||
|
||||
if (this.lazyloadingAttempt) {
|
||||
this.moveLayers();
|
||||
this.lazyloadingAttempt = false;
|
||||
@ -253,9 +257,6 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public onResize(): void {
|
||||
this.moveLayers();
|
||||
|
||||
@ -279,7 +280,7 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
gameManager.setCharacterLayers(layers);
|
||||
this.scene.sleep(CustomizeSceneName);
|
||||
waScaleManager.restoreZoom();
|
||||
this.events.removeListener('wake');
|
||||
this.events.removeListener("wake");
|
||||
gameManager.tryResumingGame(this, EnableCameraSceneName);
|
||||
customCharacterSceneVisibleStore.set(false);
|
||||
}
|
||||
|
@ -13,23 +13,29 @@ export const EntrySceneName = "EntryScene";
|
||||
export class EntryScene extends Scene {
|
||||
constructor() {
|
||||
super({
|
||||
key: EntrySceneName
|
||||
key: EntrySceneName,
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
|
||||
gameManager.init(this.scene).then((nextSceneName) => {
|
||||
gameManager
|
||||
.init(this.scene)
|
||||
.then((nextSceneName) => {
|
||||
// Let's rescale before starting the game
|
||||
// We can do it at this stage.
|
||||
waScaleManager.applyNewSize();
|
||||
this.scene.start(nextSceneName);
|
||||
}).catch((err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status == 404) {
|
||||
ErrorScene.showError(new WAError(
|
||||
'Access link incorrect',
|
||||
'Could not find map. Please check your access link.',
|
||||
'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com'), this.scene);
|
||||
ErrorScene.showError(
|
||||
new WAError(
|
||||
"Access link incorrect",
|
||||
"Could not find map. Please check your access link.",
|
||||
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
|
||||
),
|
||||
this.scene
|
||||
);
|
||||
} else {
|
||||
ErrorScene.showError(err, this.scene);
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
preload() {
|
||||
|
||||
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
|
||||
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
||||
this.playerModels.push(bodyResourceDescription);
|
||||
@ -54,7 +53,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
|
||||
create() {
|
||||
selectCharacterSceneVisibleStore.set(true);
|
||||
this.events.addListener('wake', () => {
|
||||
this.events.addListener("wake", () => {
|
||||
waScaleManager.saveZoom();
|
||||
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
|
||||
selectCharacterSceneVisibleStore.set(true);
|
||||
@ -68,26 +67,26 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
|
||||
|
||||
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);
|
||||
|
||||
/*create user*/
|
||||
this.createCurrentPlayer();
|
||||
|
||||
this.input.keyboard.on('keyup-ENTER', () => {
|
||||
this.input.keyboard.on("keyup-ENTER", () => {
|
||||
return this.nextSceneToCameraScene();
|
||||
});
|
||||
|
||||
this.input.keyboard.on('keydown-RIGHT', () => {
|
||||
this.input.keyboard.on("keydown-RIGHT", () => {
|
||||
this.moveToRight();
|
||||
});
|
||||
this.input.keyboard.on('keydown-LEFT', () => {
|
||||
this.input.keyboard.on("keydown-LEFT", () => {
|
||||
this.moveToLeft();
|
||||
});
|
||||
this.input.keyboard.on('keydown-UP', () => {
|
||||
this.input.keyboard.on("keydown-UP", () => {
|
||||
this.moveToUp();
|
||||
});
|
||||
this.input.keyboard.on('keydown-DOWN', () => {
|
||||
this.input.keyboard.on("keydown-DOWN", () => {
|
||||
this.moveToDown();
|
||||
});
|
||||
}
|
||||
@ -105,7 +104,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
gameManager.tryResumingGame(this, EnableCameraSceneName);
|
||||
this.players = [];
|
||||
selectCharacterSceneVisibleStore.set(false);
|
||||
this.events.removeListener('wake');
|
||||
this.events.removeListener("wake");
|
||||
}
|
||||
|
||||
public nextSceneToCustomizeScene(): void {
|
||||
@ -134,7 +133,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
key: playerResource.name,
|
||||
frames: this.anims.generateFrameNumbers(playerResource.name, { start: 0, end: 11 }),
|
||||
frameRate: 8,
|
||||
repeat: -1
|
||||
repeat: -1,
|
||||
});
|
||||
player.setInteractive().on("pointerdown", () => {
|
||||
if (this.pointerClicked) {
|
||||
@ -153,6 +152,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
});
|
||||
this.players.push(player);
|
||||
}
|
||||
if (this.currentSelectUser >= this.players.length) {
|
||||
this.currentSelectUser = 0;
|
||||
}
|
||||
this.selectedPlayer = this.players[this.currentSelectUser];
|
||||
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
|
||||
}
|
||||
@ -174,7 +176,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
public moveToRight() {
|
||||
if(this.currentSelectUser === (this.players.length - 1)){
|
||||
if (this.currentSelectUser === this.players.length - 1) {
|
||||
return;
|
||||
}
|
||||
this.currentSelectUser += 1;
|
||||
@ -190,7 +192,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
protected moveToDown() {
|
||||
if((this.currentSelectUser + this.nbCharactersPerRow) > (this.players.length - 1)){
|
||||
if (this.currentSelectUser + this.nbCharactersPerRow > this.players.length - 1) {
|
||||
return;
|
||||
}
|
||||
this.currentSelectUser += this.nbCharactersPerRow;
|
||||
@ -202,8 +204,8 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
const deltaY = 32;
|
||||
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
|
||||
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column 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
|
||||
|
||||
const playerVisible = true;
|
||||
const playerScale = 1;
|
||||
@ -215,11 +217,10 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
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) {
|
||||
|
||||
const { playerX, playerY, playerScale, playerOpacity, playerVisible } = this.defineSetupPlayer(num);
|
||||
player.setBounce(0.2);
|
||||
player.setCollideWorldBounds(false);
|
||||
@ -234,10 +235,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
* Returns pixel position by on column and row number
|
||||
*/
|
||||
protected getCharacterPosition(): [number, number] {
|
||||
return [
|
||||
this.game.renderer.width / 2,
|
||||
this.game.renderer.height / 2.5
|
||||
];
|
||||
return [this.game.renderer.width / 2, this.game.renderer.height / 2.5];
|
||||
}
|
||||
|
||||
protected updateSelectedPlayer(): void {
|
||||
|
@ -48,7 +48,7 @@ export interface ITiledMapLayerProperty {
|
||||
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
|
||||
|
||||
export interface ITiledMapGroupLayer {
|
||||
id?: number,
|
||||
id?: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
properties?: ITiledMapLayerProperty[];
|
||||
@ -64,7 +64,7 @@ export interface ITiledMapGroupLayer {
|
||||
}
|
||||
|
||||
export interface ITiledMapTileLayer {
|
||||
id?: number,
|
||||
id?: number;
|
||||
data: number[] | string;
|
||||
height: number;
|
||||
name: string;
|
||||
@ -87,7 +87,7 @@ export interface ITiledMapTileLayer {
|
||||
}
|
||||
|
||||
export interface ITiledMapObjectLayer {
|
||||
id?: number,
|
||||
id?: number;
|
||||
height: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
@ -133,26 +133,26 @@ export interface ITiledMapObject {
|
||||
/**
|
||||
* Polygon points
|
||||
*/
|
||||
polygon: {x: number, y: number}[];
|
||||
polygon: { x: number; y: number }[];
|
||||
|
||||
/**
|
||||
* Polyline points
|
||||
*/
|
||||
polyline: {x: number, y: number}[];
|
||||
polyline: { x: number; y: number }[];
|
||||
|
||||
text?: ITiledText
|
||||
text?: ITiledText;
|
||||
}
|
||||
|
||||
export interface ITiledText {
|
||||
text: string,
|
||||
wrap?: boolean,
|
||||
fontfamily?: string,
|
||||
pixelsize?: number,
|
||||
color?: string,
|
||||
underline?: boolean,
|
||||
italic?: boolean,
|
||||
strikeout?: boolean,
|
||||
halign?: "center"|"right"|"justify"|"left"
|
||||
text: string;
|
||||
wrap?: boolean;
|
||||
fontfamily?: string;
|
||||
pixelsize?: number;
|
||||
color?: string;
|
||||
underline?: boolean;
|
||||
italic?: boolean;
|
||||
strikeout?: boolean;
|
||||
halign?: "center" | "right" | "justify" | "left";
|
||||
}
|
||||
|
||||
export interface ITiledTileSet {
|
||||
@ -179,10 +179,10 @@ export interface ITiledTileSet {
|
||||
}
|
||||
|
||||
export interface ITile {
|
||||
id: number,
|
||||
type?: string
|
||||
id: number;
|
||||
type?: string;
|
||||
|
||||
properties?: Array<ITiledMapLayerProperty>
|
||||
properties?: Array<ITiledMapLayerProperty>;
|
||||
}
|
||||
|
||||
export interface ITiledMapTerrain {
|
||||
|
@ -5,16 +5,16 @@ import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
|
||||
*/
|
||||
export function flattenGroupLayersMap(map: ITiledMap) {
|
||||
const flatLayers: ITiledMapLayer[] = [];
|
||||
flattenGroupLayers(map.layers, '', flatLayers);
|
||||
flattenGroupLayers(map.layers, "", flatLayers);
|
||||
return flatLayers;
|
||||
}
|
||||
|
||||
function flattenGroupLayers(layers: ITiledMapLayer[], prefix: string, flatLayers: ITiledMapLayer[]) {
|
||||
for (const layer of layers) {
|
||||
if (layer.type === 'group') {
|
||||
flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers);
|
||||
if (layer.type === "group") {
|
||||
flattenGroupLayers(layer.layers, prefix + layer.name + "/", flatLayers);
|
||||
} else {
|
||||
layer.name = prefix+layer.name
|
||||
layer.name = prefix + layer.name;
|
||||
flatLayers.push(layer);
|
||||
}
|
||||
}
|
||||
|