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

This commit is contained in:
David Négrier 2022-01-05 10:19:23 +01:00
commit c85679b42c
117 changed files with 3650 additions and 3102 deletions

View file

@ -39,7 +39,7 @@ jobs:
working-directory: "messages" working-directory: "messages"
- name: "Build proto messages" - name: "Build proto messages"
run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
working-directory: "messages" working-directory: "messages"
- name: "Create index.html" - name: "Create index.html"

View file

@ -11,10 +11,43 @@ on:
jobs: jobs:
start-runner:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
name: Start self-hosted EC2 runner
runs-on: ubuntu-latest
outputs:
label: ${{ steps.start-ec2-runner.outputs.label }}
ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Start EC2 runner
id: start-ec2-runner
uses: machulav/ec2-github-runner@v2
with:
mode: start
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
ec2-image-id: ami-094dbcc53250a2480
ec2-instance-type: t3.xlarge
subnet-id: subnet-0ac40025f559df1bc
security-group-id: sg-0e36e96e3b8ed2d64
#iam-role-name: my-role-name # optional, requires additional permissions
#aws-resource-tags: > # optional, requires additional permissions
# [
# {"Key": "Name", "Value": "ec2-github-runner"},
# {"Key": "GitHubRepository", "Value": "${{ github.repository }}"}
# ]
end-to-end-tests: end-to-end-tests:
name: "End-to-end testcafe tests" name: "End-to-end testcafe tests"
runs-on: "ubuntu-latest" needs: start-runner # required to start the main job when the runner is ready
runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner
steps: steps:
- name: "Checkout" - name: "Checkout"
@ -67,3 +100,27 @@ jobs:
- name: Display logs - name: Display logs
if: ${{ failure() }} if: ${{ failure() }}
run: docker-compose logs run: docker-compose logs
stop-runner:
name: Stop self-hosted EC2 runner
needs:
- start-runner # required to get output from the start-runner job
- end-to-end-tests # required to wait when the main job is done
runs-on: ubuntu-latest
if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs
steps:
- name: Configure AWS credentials
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Stop EC2 runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
uses: machulav/ec2-github-runner@v2
with:
mode: stop
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
label: ${{ needs.start-runner.outputs.label }}
ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }}

View file

@ -36,7 +36,7 @@ jobs:
working-directory: "messages" working-directory: "messages"
- name: "Build proto messages" - name: "Build proto messages"
run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
working-directory: "messages" working-directory: "messages"
- name: "Create index.html" - name: "Create index.html"

View file

@ -1,11 +1,11 @@
# protobuf build # protobuf build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
WORKDIR /usr/src WORKDIR /usr/src
COPY messages . COPY messages .
RUN yarn install && yarn proto RUN yarn install && yarn proto
# typescript build # typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2 FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
WORKDIR /usr/src WORKDIR /usr/src
COPY back/yarn.lock back/package.json ./ COPY back/yarn.lock back/package.json ./
RUN yarn install RUN yarn install
@ -15,7 +15,7 @@ ENV NODE_ENV=production
RUN yarn run tsc RUN yarn run tsc
# final production image # final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d
WORKDIR /usr/src WORKDIR /usr/src
COPY back/yarn.lock back/package.json ./ COPY back/yarn.lock back/package.json ./
COPY --from=builder2 /usr/src/dist /usr/src/dist COPY --from=builder2 /usr/src/dist /usr/src/dist

View file

@ -68,14 +68,14 @@
"@types/mkdirp": "^1.0.1", "@types/mkdirp": "^1.0.1",
"@types/redis": "^2.8.31", "@types/redis": "^2.8.31",
"@types/uuidv4": "^5.0.0", "@types/uuidv4": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^2.26.0", "@typescript-eslint/parser": "^5.8.0",
"eslint": "^6.8.0", "eslint": "^8.5.0",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"ts-node-dev": "^1.0.0-pre.44", "ts-node-dev": "^1.1.8",
"typescript": "^3.8.3" "typescript": "^4.5.4"
}, },
"lint-staged": { "lint-staged": {
"*.ts": [ "*.ts": [

View file

@ -10,6 +10,6 @@ App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port
const server = new grpc.Server(); const server = new grpc.Server();
server.addService<IRoomManagerServer>(RoomManagerService, roomManager); server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
server.bind('0.0.0.0:'+GRPC_PORT, grpc.ServerCredentials.createInsecure()); server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure());
server.start(); server.start();
console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT); console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT);

View file

@ -36,9 +36,11 @@ export class DebugController {
return "BatchedMessages"; return "BatchedMessages";
} }
if (value instanceof Map) { if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any const obj: { [key: string | number]: unknown } = {};
for (const [mapKey, mapValue] of value.entries()) { for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue; if (typeof mapKey === "number" || typeof mapKey === "string") {
obj[mapKey] = mapValue;
}
} }
return obj; return obj;
} else if (value instanceof Set) { } else if (value instanceof Set) {

View file

@ -1,12 +1,10 @@
import { App } from "../Server/sifrr.server"; import { App } from "../Server/sifrr.server";
import { HttpRequest, HttpResponse } from "uWebSockets.js"; import { HttpRequest, HttpResponse } from "uWebSockets.js";
const register = require("prom-client").register; import { register, collectDefaultMetrics } from "prom-client";
const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
export class PrometheusController { export class PrometheusController {
constructor(private App: App) { constructor(private App: App) {
collectDefaultMetrics({ collectDefaultMetrics({
timeout: 10000,
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets. gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
}); });

View file

@ -21,6 +21,7 @@ import {
SubToPusherRoomMessage, SubToPusherRoomMessage,
VariableMessage, VariableMessage,
VariableWithTagMessage, VariableWithTagMessage,
ServerToClientMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager"; import { RoomSocket, ZoneSocket } from "src/RoomManager";
@ -34,6 +35,7 @@ import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { LocalUrlError } from "../Services/LocalUrlError"; import { LocalUrlError } from "../Services/LocalUrlError";
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers"; import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
import { VariableError } from "../Services/VariableError"; import { VariableError } from "../Services/VariableError";
import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
@ -109,10 +111,6 @@ export class GameRoom {
return gameRoom; return gameRoom;
} }
public getGroups(): Group[] {
return Array.from(this.groups.values());
}
public getUsers(): Map<number, User> { public getUsers(): Map<number, User> {
return this.users; return this.users;
} }
@ -175,6 +173,14 @@ export class GameRoom {
if (userObj !== undefined && typeof userObj.group !== "undefined") { if (userObj !== undefined && typeof userObj.group !== "undefined") {
this.leaveGroup(userObj); this.leaveGroup(userObj);
} }
if (user.hasFollowers()) {
user.stopLeading();
}
if (user.following) {
user.following.delFollower(user);
}
this.users.delete(user.id); this.users.delete(user.id);
this.usersByUuid.delete(user.uuid); this.usersByUuid.delete(user.uuid);
@ -213,8 +219,8 @@ export class GameRoom {
if (user.silent) { if (user.silent) {
return; return;
} }
const group = user.group;
if (user.group === undefined) { if (group === undefined) {
// If the user is not part of a group: // If the user is not part of a group:
// should he join a group? // should he join a group?
@ -245,13 +251,40 @@ export class GameRoom {
} else { } else {
// If the user is part of a group: // If the user is part of a group:
// should he leave the group? // should he leave the group?
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); let noOneOutOfBounds = true;
if (distance > this.groupRadius) { group.getUsers().forEach((foreignUser: User) => {
this.leaveGroup(user); if (foreignUser.group === undefined) {
return;
}
const usrPos = foreignUser.getPosition();
const grpPos = foreignUser.group.getPosition();
const distance = GameRoom.computeDistanceBetweenPositions(usrPos, grpPos);
if (distance > this.groupRadius) {
if (foreignUser.hasFollowers() || foreignUser.following) {
// If one user is out of the group bounds BUT following, the group still exists... but should be hidden.
// We put it in 'outOfBounds' mode
group.setOutOfBounds(true);
noOneOutOfBounds = false;
} else {
this.leaveGroup(foreignUser);
}
}
});
if (noOneOutOfBounds && !user.group?.isEmpty()) {
group.setOutOfBounds(false);
} }
} }
} }
public sendToOthersInGroupIncludingUser(user: User, message: ServerToClientMessage): void {
user.group?.getUsers().forEach((currentUser: User) => {
if (currentUser.id !== user.id) {
currentUser.socket.write(message);
}
});
}
setSilent(user: User, silent: boolean) { setSilent(user: User, silent: boolean) {
if (user.silent === silent) { if (user.silent === silent) {
return; return;
@ -279,12 +312,9 @@ export class GameRoom {
} }
group.leave(user); group.leave(user);
if (group.isEmpty()) { if (group.isEmpty()) {
this.positionNotifier.leave(group);
group.destroy(); group.destroy();
if (!this.groups.has(group)) { if (!this.groups.has(group)) {
throw new Error( throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`);
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
);
} }
this.groups.delete(group); this.groups.delete(group);
//todo: is the group garbage collected? //todo: is the group garbage collected?
@ -485,9 +515,9 @@ export class GameRoom {
} }
const result = await adminApi.fetchMapDetails(roomUrl); const result = await adminApi.fetchMapDetails(roomUrl);
if (!isMapDetailsData(result)) { if (isRoomRedirect(result)) {
console.error("Unexpected room details received from server", result); console.error("Unexpected room redirect received while querying map details", result);
throw new Error("Unexpected room details received from server"); throw new Error("Unexpected room redirect received while querying map details");
} }
return result; return result;
} }

View file

@ -16,6 +16,10 @@ export class Group implements Movable {
private wasDestroyed: boolean = false; private wasDestroyed: boolean = false;
private roomId: string; private roomId: string;
private currentZone: Zone | null = null; private currentZone: Zone | null = null;
/**
* When outOfBounds = true, a user if out of the bounds of the group BUT still considered inside it (because we are in following mode)
*/
private outOfBounds = false;
constructor( constructor(
roomId: string, roomId: string,
@ -78,6 +82,10 @@ export class Group implements Movable {
this.x = x; this.x = x;
this.y = y; this.y = y;
if (this.outOfBounds) {
return;
}
if (oldX === undefined) { if (oldX === undefined) {
this.currentZone = this.positionNotifier.enter(this); this.currentZone = this.positionNotifier.enter(this);
} else { } else {
@ -116,7 +124,7 @@ export class Group implements Movable {
leave(user: User): void { leave(user: User): void {
const success = this.users.delete(user); const success = this.users.delete(user);
if (success === false) { if (success === false) {
throw new Error("Could not find user " + user.id + " in the group " + this.id); throw new Error(`Could not find user ${user.id} in the group ${this.id}`);
} }
user.group = undefined; user.group = undefined;
@ -133,6 +141,10 @@ export class Group implements Movable {
* Usually used when there is only one user left. * Usually used when there is only one user left.
*/ */
destroy(): void { destroy(): void {
if (!this.outOfBounds) {
this.positionNotifier.leave(this);
}
for (const user of this.users) { for (const user of this.users) {
this.leave(user); this.leave(user);
} }
@ -142,4 +154,26 @@ export class Group implements Movable {
get getSize() { get getSize() {
return this.users.size; return this.users.size;
} }
/**
* A group can have at most one person leading the way in it.
*/
get leader(): User | undefined {
for (const user of this.users) {
if (user.hasFollowers()) {
return user;
}
}
return undefined;
}
setOutOfBounds(outOfBounds: boolean): void {
if (this.outOfBounds === true && outOfBounds === false) {
this.positionNotifier.enter(this);
this.outOfBounds = false;
} else if (this.outOfBounds === false && outOfBounds === true) {
this.positionNotifier.leave(this);
this.outOfBounds = true;
}
}
} }

View file

@ -7,6 +7,8 @@ import { ServerDuplexStream } from "grpc";
import { import {
BatchMessage, BatchMessage,
CompanionMessage, CompanionMessage,
FollowAbortMessage,
FollowConfirmationMessage,
PusherToBackMessage, PusherToBackMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
@ -19,6 +21,8 @@ export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientM
export class User implements Movable { export class User implements Movable {
public listenedZones: Set<Zone>; public listenedZones: Set<Zone>;
public group?: Group; public group?: Group;
private _following: User | undefined;
private followedBy: Set<User> = new Set<User>();
public constructor( public constructor(
public id: number, public id: number,
@ -50,6 +54,45 @@ export class User implements Movable {
this.positionNotifier.updatePosition(this, position, oldPosition); this.positionNotifier.updatePosition(this, position, oldPosition);
} }
public addFollower(follower: User): void {
this.followedBy.add(follower);
follower._following = this;
const message = new FollowConfirmationMessage();
message.setFollower(follower.id);
message.setLeader(this.id);
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowconfirmationmessage(message);
this.socket.write(clientMessage);
}
public delFollower(follower: User): void {
this.followedBy.delete(follower);
follower._following = undefined;
const message = new FollowAbortMessage();
message.setFollower(follower.id);
message.setLeader(this.id);
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowabortmessage(message);
this.socket.write(clientMessage);
follower.socket.write(clientMessage);
}
public hasFollowers(): boolean {
return this.followedBy.size !== 0;
}
get following(): User | undefined {
return this._following;
}
public stopLeading(): void {
for (const follower of this.followedBy) {
this.delFollower(follower);
}
}
private batchedMessages: BatchMessage = new BatchMessage(); private batchedMessages: BatchMessage = new BatchMessage();
private batchTimeout: NodeJS.Timeout | null = null; private batchTimeout: NodeJS.Timeout | null = null;

View file

@ -39,21 +39,13 @@ export class Zone {
const result = this.things.delete(thing); const result = this.things.delete(thing);
if (!result) { if (!result) {
if (thing instanceof User) { if (thing instanceof User) {
throw new Error("Could not find user in zone " + thing.id); throw new Error(`Could not find user in zone ${thing.id}`);
} }
if (thing instanceof Group) { if (thing instanceof Group) {
throw new Error( throw new Error(
"Could not find group " + `Could not find group ${thing.getId()} in zone (${this.x},${this.y}). Position of group: (${
thing.getId() + thing.getPosition().x
" in zone (" + },${thing.getPosition().y})`
this.x +
"," +
this.y +
"). Position of group: (" +
thing.getPosition().x +
"," +
thing.getPosition().y +
")"
); );
} }
} }

View file

@ -9,6 +9,9 @@ import {
BatchToPusherMessage, BatchToPusherMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
EmotePromptMessage, EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
EmptyMessage, EmptyMessage,
ItemEventMessage, ItemEventMessage,
JoinRoomMessage, JoinRoomMessage,
@ -103,11 +106,6 @@ const roomManager: IRoomManagerServer = {
user, user,
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
); );
} else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(
room,
message.getPlayglobalmessage() as PlayGlobalMessage
);
} else if (message.hasQueryjitsijwtmessage()) { } else if (message.hasQueryjitsijwtmessage()) {
socketManager.handleQueryJitsiJwtMessage( socketManager.handleQueryJitsiJwtMessage(
user, user,
@ -119,6 +117,24 @@ const roomManager: IRoomManagerServer = {
user, user,
message.getEmotepromptmessage() as EmotePromptMessage message.getEmotepromptmessage() as EmotePromptMessage
); );
} else if (message.hasFollowrequestmessage()) {
socketManager.handleFollowRequestMessage(
room,
user,
message.getFollowrequestmessage() as FollowRequestMessage
);
} else if (message.hasFollowconfirmationmessage()) {
socketManager.handleFollowConfirmationMessage(
room,
user,
message.getFollowconfirmationmessage() as FollowConfirmationMessage
);
} else if (message.hasFollowabortmessage()) {
socketManager.handleFollowAbortMessage(
room,
user,
message.getFollowabortmessage() as FollowAbortMessage
);
} else if (message.hasSendusermessage()) { } else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage); socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
@ -166,7 +182,7 @@ const roomManager: IRoomManagerServer = {
socketManager socketManager
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()) .addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
.catch((e) => { .catch((e) => {
emitErrorOnZoneSocket(call, e.toString()); emitErrorOnZoneSocket(call, e);
}); });
call.on("cancelled", () => { call.on("cancelled", () => {
@ -196,7 +212,7 @@ const roomManager: IRoomManagerServer = {
const roomMessage = call.request; const roomMessage = call.request;
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => { socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
emitErrorOnRoomSocket(call, e.toString()); emitErrorOnRoomSocket(call, e);
}); });
call.on("cancelled", () => { call.on("cancelled", () => {

View file

@ -1,3 +1,5 @@
/* eslint-disable */
import { Readable } from "stream"; import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";

View file

@ -1,3 +1,5 @@
/* eslint-disable */
import { createWriteStream } from "fs"; import { createWriteStream } from "fs";
import { join, dirname } from "path"; import { join, dirname } from "path";
import Busboy from "busboy"; import Busboy from "busboy";

View file

@ -1,3 +1,5 @@
/* eslint-disable */
import { ReadStream } from "fs"; import { ReadStream } from "fs";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -1,7 +1,7 @@
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import { MapDetailsData } from "./AdminApi/MapDetailsData"; import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
import { RoomRedirect } from "./AdminApi/RoomRedirect"; import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
class AdminApi { class AdminApi {
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> { async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
@ -17,6 +17,12 @@ class AdminApi {
headers: { Authorization: `${ADMIN_API_TOKEN}` }, headers: { Authorization: `${ADMIN_API_TOKEN}` },
params, params,
}); });
if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) {
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
throw new Error("Unexpected answer from the /api/map admin endpoint.");
}
return res.data; return res.data;
} }
} }

View file

@ -1,4 +1,4 @@
const EventEmitter = require("events"); import { EventEmitter } from "events";
const clientJoinEvent = "clientJoin"; const clientJoinEvent = "clientJoin";
const clientLeaveEvent = "clientLeave"; const clientLeaveEvent = "clientLeave";

View file

@ -32,7 +32,7 @@ class MapFetcher {
//throw new Error("Invalid map format for map " + mapUrl); //throw new Error("Invalid map format for map " + mapUrl);
console.error("Invalid map format for map " + mapUrl); console.error("Invalid map format for map " + mapUrl);
} }
/* eslint-disable-next-line @typescript-eslint/no-unsafe-return */
return res.data; return res.data;
} }

View file

@ -10,7 +10,19 @@ import {
import { UserSocket } from "_Model/User"; import { UserSocket } from "_Model/User";
import { RoomSocket, ZoneSocket } from "../RoomManager"; import { RoomSocket, ZoneSocket } from "../RoomManager";
export function emitError(Client: UserSocket, message: string): void { function getMessageFromError(error: unknown): string {
if (error instanceof Error) {
return error.message;
} else if (typeof error === "string") {
return error;
} else {
return "Unknown error";
}
}
export function emitError(Client: UserSocket, error: unknown): void {
const message = getMessageFromError(error);
const errorMessage = new ErrorMessage(); const errorMessage = new ErrorMessage();
errorMessage.setMessage(message); errorMessage.setMessage(message);
@ -23,8 +35,9 @@ export function emitError(Client: UserSocket, message: string): void {
console.warn(message); console.warn(message);
} }
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void { export function emitErrorOnRoomSocket(Client: RoomSocket, error: unknown): void {
console.error(message); console.error(error);
const message = getMessageFromError(error);
const errorMessage = new ErrorMessage(); const errorMessage = new ErrorMessage();
errorMessage.setMessage(message); errorMessage.setMessage(message);
@ -41,8 +54,9 @@ export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void
console.warn(message); console.warn(message);
} }
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void { export function emitErrorOnZoneSocket(Client: ZoneSocket, error: unknown): void {
console.error(message); console.error(error);
const message = getMessageFromError(error);
const errorMessage = new ErrorMessage(); const errorMessage = new ErrorMessage();
errorMessage.setMessage(message); errorMessage.setMessage(message);

View file

@ -30,6 +30,9 @@ import {
BanUserMessage, BanUserMessage,
RefreshRoomMessage, RefreshRoomMessage,
EmotePromptMessage, EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
VariableMessage, VariableMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
SubToPusherRoomMessage, SubToPusherRoomMessage,
@ -197,7 +200,7 @@ export class SocketManager {
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer" // TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password); webrtcSignalToClient.setWebrtcpassword(password);
} }
@ -227,7 +230,7 @@ export class SocketManager {
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer" // TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password); webrtcSignalToClient.setWebrtcpassword(password);
} }
@ -310,7 +313,7 @@ export class SocketManager {
if (thing instanceof User) { if (thing instanceof User) {
const userJoinedZoneMessage = new UserJoinedZoneMessage(); const userJoinedZoneMessage = new UserJoinedZoneMessage();
if (!Number.isInteger(thing.id)) { if (!Number.isInteger(thing.id)) {
throw new Error("clientUser.userId is not an integer " + thing.id); throw new Error(`clientUser.userId is not an integer ${thing.id}`);
} }
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid); userJoinedZoneMessage.setUseruuid(thing.uuid);
@ -446,7 +449,10 @@ export class SocketManager {
webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setInitiator(true); webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials(
otherUser.id.toString(),
TURN_STATIC_AUTH_SECRET
);
webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcusername(username);
webrtcStartMessage1.setWebrtcpassword(password); webrtcStartMessage1.setWebrtcpassword(password);
} }
@ -460,7 +466,7 @@ export class SocketManager {
webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setInitiator(false); webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcusername(username);
webrtcStartMessage2.setWebrtcpassword(password); webrtcStartMessage2.setWebrtcpassword(password);
} }
@ -484,7 +490,7 @@ export class SocketManager {
hmac.setEncoding("base64"); hmac.setEncoding("base64");
hmac.write(username); hmac.write(username);
hmac.end(); hmac.end();
const password = hmac.read(); const password = hmac.read() as string;
return { return {
username: username, username: username,
password: password, password: password,
@ -525,15 +531,6 @@ export class SocketManager {
} }
} }
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
for (const [id, user] of room.getUsers().entries()) {
user.socket.write(serverToClientMessage);
}
}
public getWorlds(): Map<string, PromiseLike<GameRoom>> { public getWorlds(): Map<string, PromiseLike<GameRoom>> {
return this.roomsPromises; return this.roomsPromises;
} }
@ -839,6 +836,39 @@ export class SocketManager {
emoteEventMessage.setActoruserid(user.id); emoteEventMessage.setActoruserid(user.id);
room.emitEmoteEvent(user, emoteEventMessage); room.emitEmoteEvent(user, emoteEventMessage);
} }
handleFollowRequestMessage(room: GameRoom, user: User, message: FollowRequestMessage) {
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowrequestmessage(message);
room.sendToOthersInGroupIncludingUser(user, clientMessage);
}
handleFollowConfirmationMessage(room: GameRoom, user: User, message: FollowConfirmationMessage) {
const leader = room.getUserById(message.getLeader());
if (!leader) {
const message = `Could not follow user "{message.getLeader()}" in room "{room.roomUrl}".`;
console.info(message, "Maybe the user just left.");
return;
}
// By security, we look at the group leader. If the group leader is NOT the leader in the message,
// everybody should stop following the group leader (to avoid having 2 group leaders)
if (user?.group?.leader && user?.group?.leader !== leader) {
user?.group?.leader?.stopLeading();
}
leader.addFollower(user);
}
handleFollowAbortMessage(room: GameRoom, user: User, message: FollowAbortMessage) {
if (user.id === message.getLeader()) {
user?.group?.leader?.stopLeading();
} else {
// Forward message
const leader = room.getUserById(message.getLeader());
leader?.delFollower(user);
}
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

View file

@ -101,11 +101,11 @@ export class VariablesManager {
} }
// We store a copy of the object (to make it immutable) // We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object)); objects.set(object.name as string, this.iTiledObjectToVariable(object));
} }
} }
} else if (layer.type === "group") { } else if (layer.type === "group") {
for (const innerLayer of layer.layers) { for (const innerLayer of layer.layers as ITiledMapLayer[]) {
this.recursiveFindVariablesInLayer(innerLayer, objects); this.recursiveFindVariablesInLayer(innerLayer, objects);
} }
} }
@ -116,7 +116,7 @@ export class VariablesManager {
if (object.properties) { if (object.properties) {
for (const property of object.properties) { for (const property of object.properties) {
const value = property.value; const value = property.value as unknown;
switch (property.name) { switch (property.name) {
case "default": case "default":
variable.defaultValue = JSON.stringify(value); variable.defaultValue = JSON.stringify(value);

File diff suppressed because it is too large Load diff

24
docs/maps/api-camera.md Normal file
View file

@ -0,0 +1,24 @@
{.section-title.accent.text-primary}
# API Camera functions Reference
### Listen to camera updates
```
WA.camera.onCameraUpdate(): Subscription
```
Listens to updates of the camera viewport. It will trigger for every update of the camera's properties (position or scale for instance). An event will be sent.
The event has the following attributes :
* **x (number):** coordinate X of the camera's world view (the area looked at by the camera).
* **y (number):** coordinate Y of the camera's world view.
* **width (number):** the width of the camera's world view.
* **height (number):** the height of the camera's world view.
**callback:** the function that will be called when the camera is updated.
Example :
```javascript
const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView));
//later...
subscription.unsubscribe();

View file

@ -86,6 +86,27 @@ WA.onInit().then(() => {
}) })
``` ```
### Get the position of the player
```
WA.player.getPosition(): Promise<Position>
```
The player's current position is available using the `WA.player.getPosition()` function.
`Position` has the following attributes :
* **x (number) :** The coordinate x of the current player's position.
* **y (number) :** The coordinate y of the current player's position.
{.alert.alert-info}
You need to wait for the end of the initialization before calling `WA.player.getPosition()`
```typescript
WA.onInit().then(async () => {
console.log('Position: ', await WA.player.getPosition());
})
```
### Listen to player movement ### Listen to player movement
``` ```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
@ -107,6 +128,30 @@ Example :
WA.player.onPlayerMove(console.log); WA.player.onPlayerMove(console.log);
``` ```
## Player specific variables
Similarly to maps (see [API state related functions](api-state.md)), it is possible to store data **related to a specific player** in a "state". Such data will be stored using the local storage from the user's browser. Any value that is serializable in JSON can be stored.
{.alert.alert-info}
In the future, player-related variables will be stored on the WorkAdventure server if the current player is logged.
Any value that is serializable in JSON can be stored.
### Setting a property
A player property can be set simply by assigning a value.
Example:
```javascript
WA.player.state.toto = "value" //will set the "toto" key to "value"
```
### Reading a variable
A player variable can be read by calling its key from the player's state.
Example:
```javascript
WA.player.state.toto //will retrieve the variable
```
### Set the outline color of the player ### Set the outline color of the player
``` ```
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>; WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;

View file

@ -10,5 +10,6 @@
- [UI functions](api-ui.md) - [UI functions](api-ui.md)
- [Sound functions](api-sound.md) - [Sound functions](api-sound.md)
- [Controls functions](api-controls.md) - [Controls functions](api-controls.md)
- [Camera functions](api-camera.md)
- [List of deprecated functions](api-deprecated.md) - [List of deprecated functions](api-deprecated.md)

View file

@ -1,8 +1,11 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Room functions Reference # API Room functions Reference
### Working with group layers ### Working with group layers
If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together.
If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names
together.
Example : Example :
<div class="row"> <div class="row">
@ -12,6 +15,7 @@ Example :
</div> </div>
The name of the layers of this map are : The name of the layers of this map are :
* `entries/start` * `entries/start`
* `bottom/ground/under` * `bottom/ground/under`
* `bottom/build/carpet` * `bottom/build/carpet`
@ -26,29 +30,32 @@ WA.room.onLeaveLayer(name: string): Subscription
Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer. Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer.
* **name**: the name of the layer who as defined in Tiled. * **name**: the name of the layer who as defined in Tiled.
Example: Example:
```javascript ```javascript
WA.room.onEnterLayer('myLayer').subscribe(() => { WA.room.onEnterLayer('myLayer').subscribe(() => {
WA.chat.sendChatMessage("Hello!", 'Mr Robot'); WA.chat.sendChatMessage("Hello!", 'Mr Robot');
}); });
WA.room.onLeaveLayer('myLayer').subscribe(() => { WA.room.onLeaveLayer('myLayer').subscribe(() => {
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
}); });
``` ```
### Show / Hide a layer ### Show / Hide a layer
``` ```
WA.room.showLayer(layerName : string): void WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void WA.room.hideLayer(layerName : string) : void
``` ```
These 2 methods can be used to show and hide a layer.
if `layerName` is the name of a group layer, show/hide all the layer in that group layer. These 2 methods can be used to show and hide a layer. if `layerName` is the name of a group layer, show/hide all the
layer in that group layer.
Example : Example :
```javascript ```javascript
WA.room.showLayer('bottom'); WA.room.showLayer('bottom');
//... //...
@ -61,12 +68,14 @@ WA.room.hideLayer('bottom');
WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void;
``` ```
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist,
create the property `propertyName` and set the value of the property at `propertyValue`.
Note : Note :
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`. To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
Example : Example :
```javascript ```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
``` ```
@ -79,13 +88,12 @@ WA.room.id: string;
The ID of the current room is available from the `WA.room.id` property. The ID of the current room is available from the `WA.room.id` property.
{.alert.alert-info} {.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.id`
You need to wait for the end of the initialization before accessing `WA.room.id`
```typescript ```typescript
WA.onInit().then(() => { WA.onInit().then(() => {
console.log('Room id: ', WA.room.id); console.log('Room id: ', WA.room.id);
// Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
}) })
``` ```
@ -97,19 +105,17 @@ WA.room.mapURL: string;
The URL of the map is available from the `WA.room.mapURL` property. The URL of the map is available from the `WA.room.mapURL` property.
{.alert.alert-info} {.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.mapURL`
You need to wait for the end of the initialization before accessing `WA.room.mapURL`
```typescript ```typescript
WA.onInit().then(() => { WA.onInit().then(() => {
console.log('Map URL: ', WA.room.mapURL); console.log('Map URL: ', WA.room.mapURL);
// Will output something like: 'https://mymap.org/map.json" // Will output something like: 'https://mymap.org/map.json"
}) })
``` ```
### Getting map data ### Getting map data
``` ```
WA.room.getTiledMap(): Promise<ITiledMap> WA.room.getTiledMap(): Promise<ITiledMap>
``` ```
@ -121,12 +127,16 @@ const map = await WA.room.getTiledMap();
console.log("Map generated with Tiled version ", map.tiledversion); console.log("Map generated with Tiled version ", map.tiledversion);
``` ```
Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/). Check
the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/)
.
### Changing tiles ### Changing tiles
``` ```
WA.room.setTiles(tiles: TileDescriptor[]): void WA.room.setTiles(tiles: TileDescriptor[]): void
``` ```
Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. 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`. If `tile` is a string, it's not the id of the tile but the value of the property `name`.
@ -137,43 +147,48 @@ If `tile` is a string, it's not the id of the tile but the value of the property
</div> </div>
`TileDescriptor` has the following attributes : `TileDescriptor` has the following attributes :
* **x (number) :** The coordinate x of the tile that you want to replace. * **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. * **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. * **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. * **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. **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`. Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
Example : Example :
```javascript ```javascript
WA.room.setTiles([ WA.room.setTiles([
{x: 6, y: 4, tile: 'blue', layer: 'setTiles'}, { x: 6, y: 4, tile: 'blue', layer: 'setTiles' },
{x: 7, y: 4, tile: 109, layer: 'setTiles'}, { x: 7, y: 4, tile: 109, layer: 'setTiles' },
{x: 8, y: 4, tile: 109, layer: 'setTiles'}, { x: 8, y: 4, tile: 109, layer: 'setTiles' },
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'} { x: 9, y: 4, tile: 'blue', layer: 'setTiles' }
]); ]);
``` ```
### Loading a tileset ### Loading a tileset
``` ```
WA.room.loadTileset(url: string): Promise<number> WA.room.loadTileset(url: string): Promise<number>
``` ```
Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset. Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
You can create a tileset file in Tile Editor. You can create a tileset file in Tile Editor.
```javascript ```javascript
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]); WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: 'bottom' }]);
}) })
``` ```
## Embedding websites in a map ## Embedding websites in a map
You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)). You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using
the ["website" objects](website-in-map.md)).
### Getting an instance of a website already embedded in the map ### Getting an instance of a website already embedded in the map
@ -181,8 +196,8 @@ You can use the scripting API to embed websites in a map, or to edit websites th
WA.room.website.get(objectName: string): Promise<EmbeddedWebsite> WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
``` ```
You can get an instance of an embedded website by using the `WA.room.website.get()` method. You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of
It returns a promise of an `EmbeddedWebsite` instance. an `EmbeddedWebsite` instance.
```javascript ```javascript
// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) // Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
@ -191,7 +206,6 @@ website.url = 'https://example.com';
website.visible = true; website.visible = true;
``` ```
### Adding a new website in a map ### Adding a new website in a map
``` ```
@ -201,34 +215,38 @@ interface CreateEmbeddedWebsiteEvent {
name: string; // A unique name for this iframe name: string; // A unique name for this iframe
url: string; // The URL the iframe points to. url: string; // The URL the iframe points to.
position: { position: {
x: number, // In pixels, relative to the map coordinates x: number, // In "game" pixels, relative to the map or player coordinates, depending on origin
y: number, // In pixels, relative to the map coordinates y: number, // In "game" pixels, relative to the map or player coordinates, depending on origin
width: number, // In pixels, sensitive to zoom level width: number, // In "game" pixels
height: number, // In pixels, sensitive to zoom level height: number, // In "game" pixels
}, },
visible?: boolean, // Whether to display the iframe or not visible?: boolean, // Whether to display the iframe or not
allowApi?: boolean, // Whether the scripting API should be available to the iframe allowApi?: boolean, // Whether the scripting API should be available to the iframe
allow?: string, // The list of feature policies allowed allow?: string, // The list of feature policies allowed
origin: "player" | "map" // The origin used to place the x and y coordinates of the iframe's top-left corner, defaults to "map"
scale: number, // A ratio used to resize the iframe
} }
``` ```
You can create an instance of an embedded website by using the `WA.room.website.create()` method. You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns
It returns an `EmbeddedWebsite` instance. an `EmbeddedWebsite` instance.
```javascript ```javascript
// Create a new website object // Create a new website object
const website = WA.room.website.create({ const website = WA.room.website.create({
name: "my_website", name: "my_website",
url: "https://example.com", url: "https://example.com",
position: { position: {
x: 64, x: 64,
y: 128, y: 128,
width: 320, width: 320,
height: 240, height: 240,
}, },
visible: true, visible: true,
allowApi: true, allowApi: true,
allow: "fullscreen", allow: "fullscreen",
origin: "map",
scale: 1,
}); });
``` ```
@ -240,30 +258,28 @@ WA.room.website.delete(name: string): Promise<void>
Use `WA.room.website.delete` to completely remove an embedded website from your map. Use `WA.room.website.delete` to completely remove an embedded website from your map.
### The EmbeddedWebsite class ### The EmbeddedWebsite class
Instances of the `EmbeddedWebsite` class represent the website displayed on the map. Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
```typescript ```typescript
class EmbeddedWebsite { class EmbeddedWebsite {
readonly name: string; readonly name: string;
url: string; url: string;
visible: boolean; visible: boolean;
allow: string; allow: string;
allowApi: boolean; allowApi: boolean;
x: number; // In pixels, relative to the map coordinates x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
y: number; // In pixels, relative to the map coordinates y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
width: number; // In pixels, sensitive to zoom level width: number; // In "game" pixels
height: number; // In pixels, sensitive to zoom level height: number; // In "game" pixels
origin: "player" | "map";
scale: number;
} }
``` ```
When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map. When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map.
{.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them
{.alert.alert-warning} to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users.
The websites you add/edit/delete via the scripting API are only shown locally. If you want them
to be displayed for every player, you can use [variables](api-start.md) to share a common state
between all users.

View file

@ -35,7 +35,6 @@ module.exports = {
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
// TODO: remove those ignored rules and write a stronger code! // TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off", "@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-assignment": "off",

View file

@ -1,13 +1,14 @@
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
WORKDIR /usr/src WORKDIR /usr/src
COPY messages . COPY messages .
RUN yarn install && yarn proto RUN yarn install && yarn ts-proto
# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL # we are rebuilding on each deploy to cope with the PUSHER_URL environment URL
FROM thecodingmachine/nodejs:14-apache FROM thecodingmachine/nodejs:14-apache
COPY --chown=docker:docker front . COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /usr/src/generated /var/www/html/src/Messages/generated COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated
RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts
COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages
# Removing the iframe.html file from the final image as this adds a XSS attack. # Removing the iframe.html file from the final image as this adds a XSS attack.

View file

@ -62,6 +62,7 @@
"simple-peer": "^9.11.0", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4", "standardized-audio-context": "^25.2.4",
"ts-proto": "^1.96.0",
"uuidv4": "^6.2.10" "uuidv4": "^6.2.10"
}, },
"scripts": { "scripts": {

View file

@ -18,64 +18,84 @@ class AnalyticsClient {
} }
identifyUser(uuid: string, email: string | null) { identifyUser(uuid: string, email: string | null) {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.identify(uuid, { uuid, email, wa: true }); ?.then((posthog) => {
}); posthog.identify(uuid, { uuid, email, wa: true });
})
.catch((e) => console.error(e));
} }
loggedWithSso() { loggedWithSso() {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-logged-sso"); ?.then((posthog) => {
}); posthog.capture("wa-logged-sso");
})
.catch((e) => console.error(e));
} }
loggedWithToken() { loggedWithToken() {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-logged-token"); ?.then((posthog) => {
}); posthog.capture("wa-logged-token");
})
.catch((e) => console.error(e));
} }
enteredRoom(roomId: string, roomGroup: string | null) { enteredRoom(roomId: string, roomGroup: string | null) {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("$pageView", { roomId, roomGroup }); ?.then((posthog) => {
posthog.capture("enteredRoom"); posthog.capture("$pageView", { roomId, roomGroup });
}); posthog.capture("enteredRoom");
})
.catch((e) => console.error(e));
} }
openedMenu() { openedMenu() {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-opened-menu"); ?.then((posthog) => {
}); posthog.capture("wa-opened-menu");
})
.catch((e) => console.error(e));
} }
launchEmote(emote: string) { launchEmote(emote: string) {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-emote-launch", { emote }); ?.then((posthog) => {
}); posthog.capture("wa-emote-launch", { emote });
})
.catch((e) => console.error(e));
} }
enteredJitsi(roomName: string, roomId: string) { enteredJitsi(roomName: string, roomId: string) {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-entered-jitsi", { roomName, roomId }); ?.then((posthog) => {
}); posthog.capture("wa-entered-jitsi", { roomName, roomId });
})
.catch((e) => console.error(e));
} }
validationName() { validationName() {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-name-validation"); ?.then((posthog) => {
}); posthog.capture("wa-name-validation");
})
.catch((e) => console.error(e));
} }
validationWoka(scene: string) { validationWoka(scene: string) {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-woka-validation", { scene }); ?.then((posthog) => {
}); posthog.capture("wa-woka-validation", { scene });
})
.catch((e) => console.error(e));
} }
validationVideo() { validationVideo() {
this.posthogPromise?.then((posthog) => { this.posthogPromise
posthog.capture("wa-video-validation"); ?.then((posthog) => {
}); posthog.capture("wa-video-validation");
})
.catch((e) => console.error(e));
} }
} }
export const analyticsClient = new AnalyticsClient(); export const analyticsClient = new AnalyticsClient();

View file

@ -1,27 +1,22 @@
import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService"; import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService";
import { textMessageContentStore, textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore"; import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { soundPlayingStore } from "../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import { UPLOADER_URL } from "../Enum/EnvironmentVariable"; import { UPLOADER_URL } from "../Enum/EnvironmentVariable";
import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore"; import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
class UserMessageManager { class UserMessageManager {
receiveBannedMessageListener!: Function; receiveBannedMessageListener!: Function;
constructor() { constructor() {
adminMessagesService.messageStream.subscribe((event) => { adminMessagesService.messageStream.subscribe((event) => {
textMessageVisibleStore.set(false);
banMessageVisibleStore.set(false);
if (event.type === AdminMessageEventTypes.admin) { if (event.type === AdminMessageEventTypes.admin) {
textMessageContentStore.set(event.text); textMessageStore.addMessage(event.text);
textMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.audio) { } else if (event.type === AdminMessageEventTypes.audio) {
soundPlayingStore.playSound(UPLOADER_URL + event.text); soundPlayingStore.playSound(UPLOADER_URL + event.text);
} else if (event.type === AdminMessageEventTypes.ban) { } else if (event.type === AdminMessageEventTypes.ban) {
banMessageContentStore.set(event.text); banMessageStore.addMessage(event.text);
banMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.banned) { } else if (event.type === AdminMessageEventTypes.banned) {
banMessageContentStore.set(event.text); banMessageStore.addMessage(event.text);
banMessageVisibleStore.set(true);
this.receiveBannedMessageListener(); this.receiveBannedMessageListener();
} }
}); });

View file

@ -22,6 +22,8 @@ export const isEmbeddedWebsiteEvent = new tg.IsInterface()
y: tg.isNumber, y: tg.isNumber,
width: tg.isNumber, width: tg.isNumber,
height: tg.isNumber, height: tg.isNumber,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
}) })
.get(); .get();
@ -35,6 +37,8 @@ export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
visible: tg.isBoolean, visible: tg.isBoolean,
allowApi: tg.isBoolean, allowApi: tg.isBoolean,
allow: tg.isString, allow: tg.isString,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
}) })
.get(); .get();

View file

@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface()
tags: tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
variables: tg.isObject, variables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
playerVariables: tg.isObject,
}) })
.get(); .get();
/** /**

View file

@ -30,6 +30,8 @@ import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEv
import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent"; import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import { isColorEvent } from "./ColorEvent"; import { isColorEvent } from "./ColorEvent";
import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -50,6 +52,7 @@ export type IframeEventMap = {
displayBubble: null; displayBubble: null;
removeBubble: null; removeBubble: null;
onPlayerMove: undefined; onPlayerMove: undefined;
onCameraUpdate: undefined;
showLayer: LayerEvent; showLayer: LayerEvent;
hideLayer: LayerEvent; hideLayer: LayerEvent;
setProperty: SetPropertyEvent; setProperty: SetPropertyEvent;
@ -82,6 +85,7 @@ export interface IframeResponseEventMap {
leaveZoneEvent: ChangeZoneEvent; leaveZoneEvent: ChangeZoneEvent;
buttonClickedEvent: ButtonClickedEvent; buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent; hasPlayerMoved: HasPlayerMovedEvent;
wasCameraUpdated: WasCameraUpdatedEvent;
menuItemClicked: MenuItemClickedEvent; menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent; setVariable: SetVariableEvent;
messageTriggered: MessageReferenceEvent; messageTriggered: MessageReferenceEvent;
@ -161,6 +165,10 @@ export const iframeQueryMapTypeGuards = {
query: tg.isUndefined, query: tg.isUndefined,
answer: tg.isUndefined, answer: tg.isUndefined,
}, },
getPlayerPosition: {
query: tg.isUndefined,
answer: isPlayerPosition,
},
}; };
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;

View file

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

View file

@ -4,6 +4,7 @@ export const isSetVariableEvent = new tg.IsInterface()
.withProperties({ .withProperties({
key: tg.isString, key: tg.isString,
value: tg.isUnknown, value: tg.isUnknown,
target: tg.isSingletonStringUnion("global", "player"),
}) })
.get(); .get();
/** /**

View file

@ -0,0 +1,19 @@
import * as tg from "generic-type-guard";
export const isWasCameraUpdatedEvent = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
zoom: tg.isNumber,
})
.get();
/**
* A message sent from the game to the iFrame to notify a movement from the camera.
*/
export type WasCameraUpdatedEvent = tg.GuardedType<typeof isWasCameraUpdatedEvent>;
export type WasCameraUpdatedEventCallback = (event: WasCameraUpdatedEvent) => void;

View file

@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
@ -85,6 +86,9 @@ class IframeListener {
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject(); private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable(); public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly _trackCameraUpdateStream: Subject<LoadSoundEvent> = new Subject();
public readonly trackCameraUpdateStream = this._trackCameraUpdateStream.asObservable();
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject(); private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable(); public readonly setTilesStream = this._setTilesStream.asObservable();
@ -226,6 +230,8 @@ class IframeListener {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true; this.sendPlayerMove = true;
} else if (payload.type == "onCameraUpdate") {
this._trackCameraUpdateStream.next();
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data); this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
@ -442,6 +448,13 @@ class IframeListener {
} }
} }
sendCameraUpdated(event: WasCameraUpdatedEvent) {
this.postMessage({
type: "wasCameraUpdated",
data: event,
});
}
sendButtonClickedEvent(popupId: number, buttonId: number): void { sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({ this.postMessage({
type: "buttonClickedEvent", type: "buttonClickedEvent",

View file

@ -12,6 +12,8 @@ export class EmbeddedWebsite {
private _allow: string; private _allow: string;
private _allowApi: boolean; private _allowApi: boolean;
private _position: Rectangle; private _position: Rectangle;
private readonly origin: "map" | "player" | undefined;
private _scale: number;
constructor(private config: CreateEmbeddedWebsiteEvent) { constructor(private config: CreateEmbeddedWebsiteEvent) {
this.name = config.name; this.name = config.name;
@ -20,6 +22,12 @@ export class EmbeddedWebsite {
this._allow = config.allow ?? ""; this._allow = config.allow ?? "";
this._allowApi = config.allowApi ?? false; this._allowApi = config.allowApi ?? false;
this._position = config.position; this._position = config.position;
this.origin = config.origin;
this._scale = config.scale ?? 1;
}
public get url() {
return this._url;
} }
public set url(url: string) { public set url(url: string) {
@ -33,6 +41,10 @@ export class EmbeddedWebsite {
}); });
} }
public get visible() {
return this._visible;
}
public set visible(visible: boolean) { public set visible(visible: boolean) {
this._visible = visible; this._visible = visible;
sendToWorkadventure({ sendToWorkadventure({
@ -44,6 +56,10 @@ export class EmbeddedWebsite {
}); });
} }
public get x() {
return this._position.x;
}
public set x(x: number) { public set x(x: number) {
this._position.x = x; this._position.x = x;
sendToWorkadventure({ sendToWorkadventure({
@ -55,6 +71,10 @@ export class EmbeddedWebsite {
}); });
} }
public get y() {
return this._position.y;
}
public set y(y: number) { public set y(y: number) {
this._position.y = y; this._position.y = y;
sendToWorkadventure({ sendToWorkadventure({
@ -66,6 +86,10 @@ export class EmbeddedWebsite {
}); });
} }
public get width() {
return this._position.width;
}
public set width(width: number) { public set width(width: number) {
this._position.width = width; this._position.width = width;
sendToWorkadventure({ sendToWorkadventure({
@ -77,6 +101,10 @@ export class EmbeddedWebsite {
}); });
} }
public get height() {
return this._position.height;
}
public set height(height: number) { public set height(height: number) {
this._position.height = height; this._position.height = height;
sendToWorkadventure({ sendToWorkadventure({
@ -87,4 +115,19 @@ export class EmbeddedWebsite {
}, },
}); });
} }
public get scale(): number {
return this._scale;
}
public set scale(scale: number) {
this._scale = scale;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
scale: this._scale,
},
});
}
} }

View file

@ -26,7 +26,7 @@ export class ActionMessage {
this.message = actionMessageOptions.message; this.message = actionMessageOptions.message;
this.type = actionMessageOptions.type ?? "message"; this.type = actionMessageOptions.type ?? "message";
this.callback = actionMessageOptions.callback; this.callback = actionMessageOptions.callback;
this.create(); this.create().catch((e) => console.error(e));
} }
private async create() { private async create() {

View file

@ -0,0 +1,29 @@
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { Subject } from "rxjs";
import type { WasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
import { apiCallback } from "./registeredCallbacks";
import { isWasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
const moveStream = new Subject<WasCameraUpdatedEvent>();
export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdventureCameraCommands> {
callbacks = [
apiCallback({
type: "wasCameraUpdated",
typeChecker: isWasCameraUpdatedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
},
}),
];
onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
sendToWorkadventure({
type: "onCameraUpdate",
data: null,
});
return moveStream;
}
}
export default new WorkAdventureCameraCommands();

View file

@ -3,6 +3,7 @@ import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
import { createState } from "./state";
const moveStream = new Subject<HasPlayerMovedEvent>(); const moveStream = new Subject<HasPlayerMovedEvent>();
@ -31,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => {
}; };
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> { export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
readonly state = createState("player");
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: "hasPlayerMoved", type: "hasPlayerMoved",
@ -74,6 +77,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
return uuid; return uuid;
} }
async getPosition(): Promise<Position> {
return await queryWorkadventure({
type: "getPlayerPosition",
data: undefined,
});
}
get userRoomToken(): string | undefined { get userRoomToken(): string | undefined {
if (userRoomToken === undefined) { if (userRoomToken === undefined) {
throw new Error( throw new Error(
@ -102,4 +112,9 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
} }
} }
export type Position = {
x: number;
y: number;
};
export default new WorkadventurePlayerCommands(); export default new WorkadventurePlayerCommands();

View file

@ -8,93 +8,101 @@ import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
const setVariableResolvers = new Subject<SetVariableEvent>();
const variables = new Map<string, unknown>();
const variableSubscribers = new Map<string, Subject<unknown>>();
export const initVariables = (_variables: Map<string, unknown>): void => {
for (const [name, value] of _variables.entries()) {
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
if (!variables.has(name)) {
variables.set(name, value);
}
}
};
setVariableResolvers.subscribe((event) => {
const oldValue = variables.get(event.key);
// If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return;
}*/
variables.set(event.key, event.value);
const subject = variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> { export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
private setVariableResolvers = new Subject<SetVariableEvent>();
private variables = new Map<string, unknown>();
private variableSubscribers = new Map<string, Subject<unknown>>();
constructor(private target: "global" | "player") {
super();
this.setVariableResolvers.subscribe((event) => {
const oldValue = this.variables.get(event.key);
// If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return;
}*/
this.variables.set(event.key, event.value);
const subject = this.variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
}
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: "setVariable", type: "setVariable",
typeChecker: isSetVariableEvent, typeChecker: isSetVariableEvent,
callback: (payloadData) => { callback: (payloadData) => {
setVariableResolvers.next(payloadData); if (payloadData.target === this.target) {
this.setVariableResolvers.next(payloadData);
}
}, },
}), }),
]; ];
// TODO: see how we can remove this method from types exposed to WA.state object
initVariables(_variables: Map<string, unknown>): void {
for (const [name, value] of _variables.entries()) {
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
if (!this.variables.has(name)) {
this.variables.set(name, value);
}
}
}
saveVariable(key: string, value: unknown): Promise<void> { saveVariable(key: string, value: unknown): Promise<void> {
variables.set(key, value); this.variables.set(key, value);
return queryWorkadventure({ return queryWorkadventure({
type: "setVariable", type: "setVariable",
data: { data: {
key, key,
value, value,
target: this.target,
}, },
}); });
} }
loadVariable(key: string): unknown { loadVariable(key: string): unknown {
return variables.get(key); return this.variables.get(key);
} }
hasVariable(key: string): boolean { hasVariable(key: string): boolean {
return variables.has(key); return this.variables.has(key);
} }
onVariableChange(key: string): Observable<unknown> { onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key); let subject = this.variableSubscribers.get(key);
if (subject === undefined) { if (subject === undefined) {
subject = new Subject<unknown>(); subject = new Subject<unknown>();
variableSubscribers.set(key, subject); this.variableSubscribers.set(key, subject);
} }
return subject.asObservable(); return subject.asObservable();
} }
} }
const proxyCommand = new Proxy(new WorkadventureStateCommands(), { export function createState(target: "global" | "player"): WorkadventureStateCommands & { [key: string]: unknown } {
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { return new Proxy(new WorkadventureStateCommands(target), {
if (p in target) { get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
return Reflect.get(target, p, receiver); if (p in target) {
} return Reflect.get(target, p, receiver);
return target.loadVariable(p.toString()); }
}, return target.loadVariable(p.toString());
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { },
// Note: when using "set", there is no way to wait, so we ignore the return of the promise. set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
// User must use WA.state.saveVariable to have error message. // Note: when using "set", there is no way to wait, so we ignore the return of the promise.
target.saveVariable(p.toString(), value); // User must use WA.state.saveVariable to have error message.
return true; target.saveVariable(p.toString(), value).catch((e) => console.error(e));
},
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
return true; return true;
} },
return target.hasVariable(p.toString()); has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
}, if (p in target) {
}) as WorkadventureStateCommands & { [key: string]: unknown }; return true;
}
export default proxyCommand; return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
}

View file

@ -1,8 +1,4 @@
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { Sound } from "./Sound/Sound";
import { EmbeddedWebsite } from "./Room/EmbeddedWebsite"; import { EmbeddedWebsite } from "./Room/EmbeddedWebsite";
import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent"; import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent";

View file

@ -33,10 +33,10 @@
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte"; import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte"; import VideoOverlay from "./Video/VideoOverlay.svelte";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import AdminMessage from "./TypeMessage/BanMessage.svelte"; import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte"; import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore"; import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore"; import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { warningContainerStore } from "../Stores/MenuStore"; import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte"; import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore"; import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
@ -45,6 +45,9 @@
import AudioManager from "./AudioManager/AudioManager.svelte"; import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore"; import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte"; import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
export let game: Game; export let game: Game;
</script> </script>
@ -75,14 +78,13 @@
<EnableCameraScene {game} /> <EnableCameraScene {game} />
</div> </div>
{/if} {/if}
{#if $banMessageVisibleStore} {#if $banMessageStore.length > 0}
<div> <div>
<AdminMessage /> <BanMessageContainer />
</div> </div>
{/if} {:else if $textMessageStore.length > 0}
{#if $textMessageVisibleStore}
<div> <div>
<TextMessage /> <TextMessageContainer />
</div> </div>
{/if} {/if}
{#if $soundPlayingStore} {#if $soundPlayingStore}
@ -105,6 +107,11 @@
<ReportMenu /> <ReportMenu />
</div> </div>
{/if} {/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<div>
<FollowMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore} {#if $menuIconVisiblilityStore}
<div> <div>
<MenuIcon /> <MenuIcon />

View file

@ -19,12 +19,13 @@
audioManagerVolumeStore.setVolume(volume); audioManagerVolumeStore.setVolume(volume);
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted()); audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
unsubscriberFileStore = audioManagerFileStore.subscribe(() => { unsubscriberFileStore = audioManagerFileStore.subscribe((src) => {
HTMLAudioPlayer.pause(); HTMLAudioPlayer.pause();
HTMLAudioPlayer.src = src;
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop; HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume; HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted; HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted;
HTMLAudioPlayer.play(); void HTMLAudioPlayer.play();
}); });
unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => { unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => {
const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking; const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking;
@ -148,9 +149,7 @@
</label> </label>
<section class="audio-manager-file"> <section class="audio-manager-file">
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}> <audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer} />
<source src={$audioManagerFileStore} />
</audio>
</section> </section>
</div> </div>
</div> </div>

View file

@ -67,6 +67,7 @@
.messagePart { .messagePart {
flex-grow: 1; flex-grow: 1;
max-width: 100%; max-width: 100%;
user-select: text;
span.date { span.date {
font-size: 80%; font-size: 80%;

View file

@ -0,0 +1,197 @@
<!--
vim: ft=typescript
-->
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
const gameScene = gameManager.getCurrentGameScene();
function name(userId: number): string | undefined {
return gameScene.MapPlayersByKey.get(userId)?.PlayerValue;
}
function sendFollowRequest() {
gameScene.CurrentPlayer.sendFollowRequest();
}
function acceptFollowRequest() {
gameScene.CurrentPlayer.startFollowing();
}
function abortEnding() {
followStateStore.set("active");
}
function reset() {
gameScene.connection?.emitFollowAbort();
followUsersStore.stopFollowing();
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
reset();
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if $followStateStore === "requesting" && $followRoleStore === "follower"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Do you want to follow {name($followUsersStore[0])}?</h2>
</section>
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}>Yes</button>
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button>
</section>
</div>
{/if}
{#if $followStateStore === "ending"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Interaction</h2>
</section>
{#if $followRoleStore === "follower"}
<section class="interact-menu-question">
<p>Do you want to stop following {name($followUsersStore[0])}?</p>
</section>
{:else if $followRoleStore === "leader"}
<section class="interact-menu-question">
<p>Do you want to stop leading the way?</p>
</section>
{/if}
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={reset}>Yes</button>
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}>No</button>
</section>
</div>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
<div class="interact-status nes-container is-rounded">
<section class="interact-status">
{#if $followRoleStore === "follower"}
<p>Following {name($followUsersStore[0])}</p>
{:else if $followUsersStore.length === 0}
<p>Waiting for followers' confirmation</p>
{:else if $followUsersStore.length === 1}
<p>{name($followUsersStore[0])} is following you</p>
{:else if $followUsersStore.length === 2}
<p>{name($followUsersStore[0])} and {name($followUsersStore[1])} are following you</p>
{:else}
<p>
{$followUsersStore.slice(0, -1).map(name).join(", ")} and {name(
$followUsersStore[$followUsersStore.length - 1]
)} are following you
</p>
{/if}
</section>
</div>
{/if}
{#if $followStateStore === "off"}
<button
type="button"
class="nes-btn is-primary follow-menu-button"
on:click|preventDefault={sendFollowRequest}
title="Ask others to follow"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
{#if $followRoleStore === "follower"}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop following"><img class="background-img" src={followImg} alt="" /></button
>
{:else}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop leading the way"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{/if}
<style lang="scss">
.nes-container {
padding: 5px;
}
div.interact-status {
background-color: #333333;
color: whitesmoke;
position: relative;
height: 2.7em;
width: 40vw;
top: 87vh;
margin: auto;
text-align: center;
}
div.interact-menu {
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
width: 60vw;
top: 60vh;
margin: auto;
section.interact-menu-title {
margin-bottom: 20px;
display: flex;
justify-content: center;
}
section.interact-menu-question {
margin: 4px;
margin-bottom: 20px;
p {
font-size: 1.05em;
font-weight: bold;
}
}
section.interact-menu-action {
display: grid;
grid-gap: 10%;
grid-template-columns: 45% 45%;
margin-bottom: 20px;
margin-left: 5%;
margin-right: 5%;
}
}
.follow-menu-button {
position: absolute;
bottom: 10px;
left: 10px;
pointer-events: all;
}
@media only screen and (max-width: 800px) {
div.interact-status {
width: 100vw;
top: 78vh;
font-size: 0.75em;
}
div.interact-menu {
height: 21vh;
width: 100vw;
font-size: 0.75em;
}
}
</style>

View file

@ -19,12 +19,12 @@
uploadAudioActive = true; uploadAudioActive = true;
} }
function send() { async function send(): Promise<void> {
if (inputSendTextActive) { if (inputSendTextActive) {
handleSendText.sendTextMessage(broadcastToWorld); return handleSendText.sendTextMessage(broadcastToWorld);
} }
if (uploadAudioActive) { if (uploadAudioActive) {
handleSendAudio.sendAudioMessage(broadcastToWorld); return handleSendAudio.sendAudioMessage(broadcastToWorld);
} }
} }
</script> </script>

View file

@ -41,10 +41,10 @@
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
} }
function logOut() { async function logOut() {
disableMenuStores(); disableMenuStores();
loginSceneVisibleStore.set(true); loginSceneVisibleStore.set(true);
connectionManager.logout(); return connectionManager.logout();
} }
function getProfileUrl() { function getProfileUrl() {

View file

@ -8,6 +8,7 @@
let fullscreen: boolean = localUserStore.getFullscreen(); let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted"; let notification: boolean = localUserStore.getNotification() === "granted";
let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger(); let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger();
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
let valueGame: number = localUserStore.getGameQualityValue(); let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue(); let valueVideo: number = localUserStore.getVideoQualityValue();
let previewValueGame = valueGame; let previewValueGame = valueGame;
@ -32,9 +33,9 @@
const body = HtmlUtils.querySelectorOrFail("body"); const body = HtmlUtils.querySelectorOrFail("body");
if (body) { if (body) {
if (document.fullscreenElement !== null && !fullscreen) { if (document.fullscreenElement !== null && !fullscreen) {
document.exitFullscreen(); document.exitFullscreen().catch((e) => console.error(e));
} else { } else {
body.requestFullscreen(); body.requestFullscreen().catch((e) => console.error(e));
} }
localUserStore.setFullscreen(fullscreen); localUserStore.setFullscreen(fullscreen);
} }
@ -44,14 +45,16 @@
if (Notification.permission === "granted") { if (Notification.permission === "granted") {
localUserStore.setNotification(notification ? "granted" : "denied"); localUserStore.setNotification(notification ? "granted" : "denied");
} else { } else {
Notification.requestPermission().then((response) => { Notification.requestPermission()
if (response === "granted") { .then((response) => {
localUserStore.setNotification(notification ? "granted" : "denied"); if (response === "granted") {
} else { localUserStore.setNotification(notification ? "granted" : "denied");
localUserStore.setNotification("denied"); } else {
notification = false; localUserStore.setNotification("denied");
} notification = false;
}); }
})
.catch((e) => console.error(e));
} }
} }
@ -59,6 +62,10 @@
localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger); localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger);
} }
function changeIgnoreFollowRequests() {
localUserStore.setIgnoreFollowRequests(ignoreFollowRequests);
}
function closeMenu() { function closeMenu() {
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
} }
@ -123,6 +130,15 @@
/> />
<span>Always ask before opening websites and Jitsi Meet rooms</span> <span>Always ask before opening websites and Jitsi Meet rooms</span>
</label> </label>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={ignoreFollowRequests}
on:change={changeIgnoreFollowRequests}
/>
<span>Ignore requests to follow other users</span>
</label>
</section> </section>
</div> </div>

View file

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { fly } from "svelte/transition"; import { fly, fade } from "svelte/transition";
import { banMessageVisibleStore, banMessageContentStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
export let message: Message;
let text: string;
$: {
text = $banMessageContentStore;
}
const NAME_BUTTON = "Ok"; const NAME_BUTTON = "Ok";
let nbSeconds = 10; let nbSeconds = 10;
let nameButton = ""; let nameButton = "";
@ -28,17 +27,21 @@
} }
function closeBanMessage() { function closeBanMessage() {
banMessageVisibleStore.set(false); banMessageStore.clearMessageById(message.id);
} }
</script> </script>
<div class="main-ban-message nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}> <div
class="main-ban-message nes-container is-rounded"
in:fly={{ y: -1000, duration: 500, delay: 250 }}
out:fade={{ duration: 200 }}
>
<h2 class="title-ban-message"> <h2 class="title-ban-message">
<img src="resources/logos/report.svg" alt="***" /> Important message <img src="resources/logos/report.svg" alt="***" /> Important message
<img src="resources/logos/report.svg" alt="***" /> <img src="resources/logos/report.svg" alt="***" />
</h2> </h2>
<div class="content-ban-message"> <div class="content-ban-message">
<p>{text}</p> <p>{message.text}</p>
</div> </div>
<div class="footer-ban-message"> <div class="footer-ban-message">
<button <button

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { flip } from "svelte/animate";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import BanMessage from "./BanMessage.svelte";
</script>
<div class="main-ban-message-container">
{#each $banMessageStore.slice(0, 1) as message (message.id)}
<div animate:flip={{ duration: 250 }}>
<BanMessage {message} />
</div>
{/each}
</div>

View file

@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import { fly } from "svelte/transition"; import { fly, fade } from "svelte/transition";
import { textMessageContentStore, textMessageVisibleStore } from "../../Stores/TypeMessageStore/TextMessageStore";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html"; import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { textMessageStore } from "../../Stores/TypeMessageStore/TextMessageStore";
let converter: QuillDeltaToHtmlConverter; export let message: Message;
$: {
const content = JSON.parse($textMessageContentStore); const content = JSON.parse(message.text);
converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true }); const converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
}
const NAME_BUTTON = "Ok"; const NAME_BUTTON = "Ok";
function closeTextMessage() { function closeTextMessage() {
textMessageVisibleStore.set(false); textMessageStore.clearMessageById(message.id);
} }
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
@ -23,7 +23,11 @@
<svelte:window on:keydown={onKeyDown} /> <svelte:window on:keydown={onKeyDown} />
<div class="main-text-message nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}> <div
class="main-text-message nes-container is-rounded"
in:fly={{ x: -1000, duration: 500, delay: 250 }}
out:fade={{ duration: 250 }}
>
<div class="content-text-message"> <div class="content-text-message">
{@html converter.convert()} {@html converter.convert()}
</div> </div>
@ -43,6 +47,8 @@
width: 80vw; width: 80vw;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
margin-bottom: 16px;
margin-top: 0;
padding-bottom: 0; padding-bottom: 0;
pointer-events: auto; pointer-events: auto;

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { flip } from "svelte/animate";
import TextMessage from "./TextMessage.svelte";
import { textMessageStore } from "../../Stores/TypeMessageStore/TextMessageStore";
const MAX_MESSAGES = 3;
</script>
<div class="main-text-message-container">
{#each $textMessageStore.slice(0, MAX_MESSAGES) as message (message.id)}
<div animate:flip={{ duration: 250 }}>
<TextMessage {message} />
</div>
{/each}
</div>
<style lang="scss">
div.main-text-message-container {
padding-top: 16px;
}
</style>

View file

@ -12,7 +12,7 @@
} }
afterUpdate(() => { afterUpdate(() => {
audio.play(); audio.play().catch((e) => console.error(e));
}); });
</script> </script>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M9.5,5.5c1.1,0,2-0.9,2-2s-0.9-2-2-2s-2,0.9-2,2S8.4,5.5,9.5,5.5z M5.75,8.9L3,23h2.1l1.75-8L9,17v6h2v-7.55L8.95,13.4 l0.6-3C10.85,12,12.8,13,15,13v-2c-1.85,0-3.45-1-4.35-2.45L9.7,6.95C9.35,6.35,8.7,6,8,6C7.75,6,7.5,6.05,7.25,6.15L2,8.3V13h2 V9.65L5.75,8.9 M13,2v7h3.75v14h1.5V9H22V2H13z M18.01,8V6.25H14.5v-1.5h3.51V3l2.49,2.5L18.01,8z"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View file

@ -1,5 +1,5 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import type { BanUserMessage, SendUserMessage } from "../Messages/generated/messages_pb"; import type { BanUserMessage, SendUserMessage } from "../Messages/ts-proto-generated/messages";
export enum AdminMessageEventTypes { export enum AdminMessageEventTypes {
admin = "message", admin = "message",
@ -26,8 +26,8 @@ class AdminMessagesService {
onSendusermessage(message: SendUserMessage | BanUserMessage) { onSendusermessage(message: SendUserMessage | BanUserMessage) {
this._messageStream.next({ this._messageStream.next({
type: message.getType() as unknown as AdminMessageEventTypes, type: message.type as unknown as AdminMessageEventTypes,
text: message.getMessage(), text: message.message,
}); });
} }
} }

View file

@ -1,5 +1,5 @@
import Axios from "axios"; import Axios from "axios";
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable"; import { PUSHER_URL } from "../Enum/EnvironmentVariable";
import { RoomConnection } from "./RoomConnection"; import { RoomConnection } from "./RoomConnection";
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels"; import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
import { GameConnexionTypes, urlManager } from "../Url/UrlManager"; import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
@ -191,7 +191,7 @@ class ConnectionManager {
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API) //Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value //use href to keep # value
localUserStore.setLastRoomUrl(this._currentRoom.href); await localUserStore.setLastRoomUrl(this._currentRoom.href);
//todo: add here some kind of warning if authToken has expired. //todo: add here some kind of warning if authToken has expired.
if (!this.authToken && !this._currentRoom.authenticationMandatory) { if (!this.authToken && !this._currentRoom.authenticationMandatory) {
@ -294,7 +294,7 @@ class ConnectionManager {
reject(error); reject(error);
}); });
connection.onConnectingError((event: CloseEvent) => { connection.connectionErrorStream.subscribe((event: CloseEvent) => {
console.log("An error occurred while connecting to socket server. Retrying"); console.log("An error occurred while connecting to socket server. Retrying");
reject( reject(
new Error( new Error(
@ -306,7 +306,7 @@ class ConnectionManager {
); );
}); });
connection.onConnect((connect: OnConnectInterface) => { connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => {
resolve(connect); resolve(connect);
}); });
}).catch((err) => { }).catch((err) => {
@ -315,7 +315,7 @@ class ConnectionManager {
this.reconnectingTimeout = setTimeout(() => { this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion? //todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then( void this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
(connection) => resolve(connection) (connection) => resolve(connection)
); );
}, 4000 + Math.floor(Math.random() * 2000)); }, 4000 + Math.floor(Math.random() * 2000));

View file

@ -1,44 +1,12 @@
import type { SignalData } from "simple-peer"; import type { SignalData } from "simple-peer";
import type { RoomConnection } from "./RoomConnection"; import type { RoomConnection } from "./RoomConnection";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
export enum EventMessage {
CONNECT = "connect",
WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
WEBRTC_START = "webrtc-start",
//START_ROOM = "start-room", // From server to client: list of all room users/groups/items
JOIN_ROOM = "join-room", // bi-directional
USER_POSITION = "user-position", // From client to server
USER_MOVED = "user-moved", // From server to client
USER_LEFT = "user-left", // From server to client
MESSAGE_ERROR = "message-error",
WEBRTC_DISCONNECT = "webrtc-disconect",
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",
USER_DETAILS_UPDATED = "user-details-updated",
CONNECT_ERROR = "connect_error",
CONNECTING_ERROR = "connecting_error",
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
SET_VIEWPORT = "set-viewport",
BATCH = "batch",
PLAY_GLOBAL_MESSAGE = "play-global-message",
STOP_GLOBAL_MESSAGE = "stop-global-message",
TELEPORT = "teleport",
USER_MESSAGE = "user-message",
START_JITSI_ROOM = "start-jitsi-room",
SET_VARIABLE = "set-variable",
}
export interface PointInterface { export interface PointInterface {
x: number; x: number;
y: number; y: number;
direction: string; direction: string; // TODO: modify this to the enum from ts-proto
moving: boolean; moving: boolean;
} }

View file

@ -1,17 +0,0 @@
import { Subject } from "rxjs";
interface EmoteEvent {
userId: number;
emote: string;
}
class EmoteEventStream {
private _stream: Subject<EmoteEvent> = new Subject();
public stream = this._stream.asObservable();
fire(userId: number, emote: string) {
this._stream.next({ userId, emote });
}
}
export const emoteEventStream = new EmoteEventStream();

View file

@ -14,6 +14,7 @@ const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = "helpCameraSettingsShown"; const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen"; const fullscreenKey = "fullscreen";
const forceCowebsiteTriggerKey = "forceCowebsiteTrigger"; const forceCowebsiteTriggerKey = "forceCowebsiteTrigger";
const ignoreFollowRequests = "ignoreFollowRequests";
const lastRoomUrl = "lastRoomUrl"; const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken"; const authToken = "authToken";
const state = "state"; const state = "state";
@ -21,8 +22,8 @@ const nonce = "nonce";
const notification = "notificationPermission"; const notification = "notificationPermission";
const code = "code"; const code = "code";
const cameraSetup = "cameraSetup"; const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache"; const cacheAPIIndex = "workavdenture-cache";
const userProperties = "user-properties";
class LocalUserStore { class LocalUserStore {
saveUser(localUser: LocalUser) { saveUser(localUser: LocalUser) {
@ -128,13 +129,19 @@ class LocalUserStore {
return localStorage.getItem(forceCowebsiteTriggerKey) === "true"; return localStorage.getItem(forceCowebsiteTriggerKey) === "true";
} }
setLastRoomUrl(roomUrl: string): void { setIgnoreFollowRequests(value: boolean): void {
localStorage.setItem(ignoreFollowRequests, value.toString());
}
getIgnoreFollowRequests(): boolean {
return localStorage.getItem(ignoreFollowRequests) === "true";
}
async setLastRoomUrl(roomUrl: string): Promise<void> {
localStorage.setItem(lastRoomUrl, roomUrl.toString()); localStorage.setItem(lastRoomUrl, roomUrl.toString());
if ("caches" in window) { if ("caches" in window) {
caches.open(cacheAPIIndex).then((cache) => { const cache = await caches.open(cacheAPIIndex);
const stringResponse = new Response(JSON.stringify({ roomUrl })); const stringResponse = new Response(JSON.stringify({ roomUrl }));
cache.put(`/${lastRoomUrl}`, stringResponse); await cache.put(`/${lastRoomUrl}`, stringResponse);
});
} }
} }
getLastRoomUrl(): string { getLastRoomUrl(): string {
@ -212,6 +219,27 @@ class LocalUserStore {
const cameraSetupValues = localStorage.getItem(cameraSetup); const cameraSetupValues = localStorage.getItem(cameraSetup);
return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined;
} }
getAllUserProperties(): Map<string, unknown> {
const result = new Map<string, string>();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
if (key.startsWith(userProperties + "_")) {
const value = localStorage.getItem(key);
if (value) {
const userKey = key.substr((userProperties + "_").length);
result.set(userKey, JSON.parse(value));
}
}
}
}
return result;
}
setUserProperty(name: string, value: unknown): void {
localStorage.setItem(userProperties + "_" + name, JSON.stringify(value));
}
} }
export const localUserStore = new LocalUserStore(); export const localUserStore = new LocalUserStore();

View file

@ -104,9 +104,13 @@ export class Room {
const data = result.data; const data = result.data;
if (isRoomRedirect(data.redirectUrl)) { if (data.authenticationMandatory !== undefined) {
data.authenticationMandatory = Boolean(data.authenticationMandatory);
}
if (isRoomRedirect(data)) {
return { return {
redirectUrl: data.redirectUrl as string, redirectUrl: data.redirectUrl,
}; };
} else if (isMapDetailsData(data)) { } else if (isMapDetailsData(data)) {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl); console.log("Map ", this.id, " resolves to URL ", data.mapUrl);

File diff suppressed because it is too large Load diff

View file

@ -1,12 +0,0 @@
import { Subject } from "rxjs";
class WorldFullMessageStream {
private _stream: Subject<string | null> = new Subject<string | null>();
public stream = this._stream.asObservable();
onMessage(message?: string) {
this._stream.next(message);
}
}
export const worldFullMessageStream = new WorldFullMessageStream();

View file

@ -1 +0,0 @@
/generated/

View file

@ -0,0 +1 @@
*

View file

@ -1,21 +1,21 @@
import { PositionMessage } from "../Messages/generated/messages_pb"; import { PositionMessage, PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
import Direction = PositionMessage.Direction;
import type { PointInterface } from "../Connexion/ConnexionModels"; import type { PointInterface } from "../Connexion/ConnexionModels";
export class ProtobufClientUtils { export class ProtobufClientUtils {
public static toPointInterface(position: PositionMessage): PointInterface { public static toPointInterface(position: PositionMessage): PointInterface {
let direction: string; let direction: string;
switch (position.getDirection()) { switch (position.direction) {
case Direction.UP: case PositionMessage_Direction.UP:
direction = "up"; direction = "up";
break; break;
case Direction.DOWN: case PositionMessage_Direction.DOWN:
direction = "down"; direction = "down";
break; break;
case Direction.LEFT: case PositionMessage_Direction.LEFT:
direction = "left"; direction = "left";
break; break;
case Direction.RIGHT: case PositionMessage_Direction.RIGHT:
direction = "right"; direction = "right";
break; break;
default: default:
@ -24,10 +24,10 @@ export class ProtobufClientUtils {
// sending to all clients in room except sender // sending to all clients in room except sender
return { return {
x: position.getX(), x: position.x,
y: position.getY(), y: position.y,
direction, direction,
moving: position.getMoving(), moving: position.moving,
}; };
} }
} }

View file

@ -41,13 +41,15 @@ export class Companion extends Container {
this.companionName = name; this.companionName = name;
this._pictureStore = writable(undefined); this._pictureStore = writable(undefined);
texturePromise.then((resource) => { texturePromise
this.addResource(resource); .then((resource) => {
this.invisible = false; this.addResource(resource);
return this.getSnapshot().then((htmlImageElementSrc) => { this.invisible = false;
this._pictureStore.set(htmlImageElementSrc); return this.getSnapshot().then((htmlImageElementSrc) => {
}); this._pictureStore.set(htmlImageElementSrc);
}); });
})
.catch((e) => console.error(e));
this.scene.physics.world.enableBody(this); this.scene.physics.world.enableBody(this);

View file

@ -3,7 +3,7 @@ import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./Co
export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => { export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => {
COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => { COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => {
lazyLoadCompanionResource(loader, resource.name); lazyLoadCompanionResource(loader, resource.name).catch((e) => console.error(e));
}); });
return COMPANION_RESOURCES; return COMPANION_RESOURCES;

View file

@ -72,9 +72,11 @@ export class Loader {
if (this.loadingText) { if (this.loadingText) {
this.loadingText.destroy(); this.loadingText.destroy();
} }
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => { promiseLoadLogoTexture
resLoadingImage.destroy(); .then((resLoadingImage: Phaser.GameObjects.Image) => {
}); resLoadingImage.destroy();
})
.catch((e) => console.error(e));
this.progress.destroy(); this.progress.destroy();
this.progressContainer.destroy(); this.progressContainer.destroy();
if (this.scene instanceof DirtyScene) { if (this.scene instanceof DirtyScene) {

View file

@ -33,7 +33,7 @@ export abstract class Character extends Container {
private readonly playerName: Text; private readonly playerName: Text;
public PlayerValue: string; public PlayerValue: string;
public sprites: Map<string, Sprite>; public sprites: Map<string, Sprite>;
private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
//private teleportation: Sprite; //private teleportation: Sprite;
private invisible: boolean; private invisible: boolean;
public companion?: Companion; public companion?: Companion;
@ -277,24 +277,20 @@ export abstract class Character extends Container {
body.setVelocity(x, y); body.setVelocity(x, y);
// up or down animations are prioritized over left and right if (Math.abs(body.velocity.x) > Math.abs(body.velocity.y)) {
if (body.velocity.y < 0) { if (body.velocity.x < 0) {
//moving up this.lastDirection = PlayerAnimationDirections.Left;
this.lastDirection = PlayerAnimationDirections.Up; } else if (body.velocity.x > 0) {
this.playAnimation(PlayerAnimationDirections.Up, true); this.lastDirection = PlayerAnimationDirections.Right;
} else if (body.velocity.y > 0) { }
//moving down } else {
this.lastDirection = PlayerAnimationDirections.Down; if (body.velocity.y < 0) {
this.playAnimation(PlayerAnimationDirections.Down, true); this.lastDirection = PlayerAnimationDirections.Up;
} else if (body.velocity.x > 0) { } else if (body.velocity.y > 0) {
//moving right this.lastDirection = PlayerAnimationDirections.Down;
this.lastDirection = PlayerAnimationDirections.Right; }
this.playAnimation(PlayerAnimationDirections.Right, true);
} else if (body.velocity.x < 0) {
//moving left
this.lastDirection = PlayerAnimationDirections.Left;
this.playAnimation(PlayerAnimationDirections.Left, true);
} }
this.playAnimation(this.lastDirection, true);
this.setDepth(this.y); this.setDepth(this.y);

View file

@ -16,7 +16,8 @@ export class EmbeddedWebsiteManager {
if (website === undefined) { if (website === undefined) {
throw new Error('Cannot find embedded website with name "' + name + '"'); throw new Error('Cannot find embedded website with name "' + name + '"');
} }
const rect = website.iframe.getBoundingClientRect();
const scale = website.scale ?? 1;
return { return {
url: website.url, url: website.url,
name: website.name, name: website.name,
@ -26,9 +27,11 @@ export class EmbeddedWebsiteManager {
position: { position: {
x: website.phaserObject.x, x: website.phaserObject.x,
y: website.phaserObject.y, y: website.phaserObject.y,
width: rect["width"], width: website.phaserObject.width * scale,
height: rect["height"], height: website.phaserObject.height * scale,
}, },
origin: website.origin,
scale: website.scale,
}; };
}); });
@ -59,7 +62,9 @@ export class EmbeddedWebsiteManager {
createEmbeddedWebsiteEvent.position.height, createEmbeddedWebsiteEvent.position.height,
createEmbeddedWebsiteEvent.visible ?? true, createEmbeddedWebsiteEvent.visible ?? true,
createEmbeddedWebsiteEvent.allowApi ?? false, createEmbeddedWebsiteEvent.allowApi ?? false,
createEmbeddedWebsiteEvent.allow ?? "" createEmbeddedWebsiteEvent.allow ?? "",
createEmbeddedWebsiteEvent.origin ?? "map",
createEmbeddedWebsiteEvent.scale ?? 1
); );
} }
); );
@ -107,10 +112,18 @@ export class EmbeddedWebsiteManager {
website.phaserObject.y = embeddedWebsiteEvent.y; website.phaserObject.y = embeddedWebsiteEvent.y;
} }
if (embeddedWebsiteEvent?.width !== undefined) { if (embeddedWebsiteEvent?.width !== undefined) {
website.iframe.style.width = embeddedWebsiteEvent.width + "px"; website.position.width = embeddedWebsiteEvent.width;
website.iframe.style.width = embeddedWebsiteEvent.width / website.phaserObject.scale + "px";
} }
if (embeddedWebsiteEvent?.height !== undefined) { if (embeddedWebsiteEvent?.height !== undefined) {
website.iframe.style.height = embeddedWebsiteEvent.height + "px"; website.position.height = embeddedWebsiteEvent.height;
website.iframe.style.height = embeddedWebsiteEvent.height / website.phaserObject.scale + "px";
}
if (embeddedWebsiteEvent?.scale !== undefined) {
website.phaserObject.scale = embeddedWebsiteEvent.scale;
website.iframe.style.width = website.position.width / embeddedWebsiteEvent.scale + "px";
website.iframe.style.height = website.position.height / embeddedWebsiteEvent.scale + "px";
} }
} }
); );
@ -125,7 +138,9 @@ export class EmbeddedWebsiteManager {
height: number, height: number,
visible: boolean, visible: boolean,
allowApi: boolean, allowApi: boolean,
allow: string allow: string,
origin: "map" | "player" | undefined,
scale: number | undefined
): void { ): void {
if (this.embeddedWebsites.has(name)) { if (this.embeddedWebsites.has(name)) {
throw new Error('An embedded website with the name "' + name + '" already exists in your map'); throw new Error('An embedded website with the name "' + name + '" already exists in your map');
@ -135,9 +150,9 @@ export class EmbeddedWebsiteManager {
name, name,
url, url,
/*x, /*x,
y, y,
width, width,
height,*/ height,*/
allow, allow,
allowApi, allowApi,
visible, visible,
@ -147,6 +162,8 @@ export class EmbeddedWebsiteManager {
width, width,
height, height,
}, },
origin,
scale,
}; };
const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible); const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible);
@ -161,22 +178,43 @@ export class EmbeddedWebsiteManager {
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString(); const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
const scale = embeddedWebsiteEvent.scale ?? 1;
iframe.src = absoluteUrl; iframe.src = absoluteUrl;
iframe.tabIndex = -1; iframe.tabIndex = -1;
iframe.style.width = embeddedWebsiteEvent.position.width + "px"; iframe.style.width = embeddedWebsiteEvent.position.width / scale + "px";
iframe.style.height = embeddedWebsiteEvent.position.height + "px"; iframe.style.height = embeddedWebsiteEvent.position.height / scale + "px";
iframe.style.margin = "0"; iframe.style.margin = "0";
iframe.style.padding = "0"; iframe.style.padding = "0";
iframe.style.border = "none"; iframe.style.border = "none";
const domElement = new DOMElement(
this.gameScene,
embeddedWebsiteEvent.position.x,
embeddedWebsiteEvent.position.y,
iframe
);
domElement.setOrigin(0, 0);
if (embeddedWebsiteEvent.scale) {
domElement.scale = embeddedWebsiteEvent.scale;
}
domElement.setVisible(visible);
switch (embeddedWebsiteEvent.origin) {
case "player":
this.gameScene.CurrentPlayer.add(domElement);
break;
case "map":
default:
this.gameScene.add.existing(domElement);
}
const embeddedWebsite = { const embeddedWebsite = {
...embeddedWebsiteEvent, ...embeddedWebsiteEvent,
phaserObject: this.gameScene.add phaserObject: domElement,
.dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe)
.setVisible(visible)
.setOrigin(0, 0),
iframe: iframe, iframe: iframe,
}; };
if (embeddedWebsiteEvent.allowApi) { if (embeddedWebsiteEvent.allowApi) {
iframeListener.registerIframe(iframe); iframeListener.registerIframe(iframe);
} }

View file

@ -1,13 +1,13 @@
import { emoteEventStream } from "../../Connexion/EmoteEventStream";
import type { GameScene } from "./GameScene"; import type { GameScene } from "./GameScene";
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import type { RoomConnection } from "../../Connexion/RoomConnection";
export class EmoteManager { export class EmoteManager {
private subscription: Subscription; private subscription: Subscription;
constructor(private scene: GameScene) { constructor(private scene: GameScene, private connection: RoomConnection) {
this.subscription = emoteEventStream.stream.subscribe((event) => { this.subscription = connection.emoteEventMessageStream.subscribe((event) => {
const actor = this.scene.MapPlayersByKey.get(event.userId); const actor = this.scene.MapPlayersByKey.get(event.actorUserId);
if (actor) { if (actor) {
actor.playEmote(event.emote); actor.playEmote(event.emote);
} }

View file

@ -81,7 +81,14 @@ export class GameMap {
let depth = -2; let depth = -2;
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if (layer.type === "tilelayer") { if (layer.type === "tilelayer") {
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); this.phaserLayers.push(
phaserMap
.createLayer(layer.name, terrains, (layer.x || 0) * 32, (layer.y || 0) * 32)
.setDepth(depth)
.setAlpha(layer.opacity)
.setVisible(layer.visible)
.setSize(layer.width, layer.height)
);
} }
if (layer.type === "objectgroup" && layer.name === "floorLayer") { if (layer.type === "objectgroup" && layer.name === "floorLayer") {
depth = DEPTH_OVERLAY_INDEX; depth = DEPTH_OVERLAY_INDEX;

View file

@ -123,7 +123,7 @@ export class GameMapPropertiesListener {
.then((coWebsite) => { .then((coWebsite) => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) { if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
coWebsiteManager.closeCoWebsite(coWebsite); coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e));
this.coWebsitesOpenByLayer.delete(layer); this.coWebsitesOpenByLayer.delete(layer);
this.coWebsitesActionTriggerByLayer.delete(layer); this.coWebsitesActionTriggerByLayer.delete(layer);
} else { } else {
@ -132,7 +132,8 @@ export class GameMapPropertiesListener {
state: OpenCoWebsiteState.OPENED, state: OpenCoWebsiteState.OPENED,
}); });
} }
}); })
.catch((e) => console.error(e));
layoutManagerActionStore.removeAction(actionUuid); layoutManagerActionStore.removeAction(actionUuid);
}; };
@ -198,7 +199,7 @@ export class GameMapPropertiesListener {
} }
if (coWebsiteOpen.coWebsite !== undefined) { if (coWebsiteOpen.coWebsite !== undefined) {
coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite); coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e));
} }
this.coWebsitesOpenByLayer.delete(layer); this.coWebsitesOpenByLayer.delete(layer);

View file

@ -1,7 +1,7 @@
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import AnimatedTiles from "phaser-animated-tiles"; import AnimatedTiles from "phaser-animated-tiles";
import { Queue } from "queue-typescript"; import { Queue } from "queue-typescript";
import { get } from "svelte/store"; import { get, Unsubscriber } from "svelte/store";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
@ -40,7 +40,6 @@ import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { GameMap } from "./GameMap"; import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement"; import { PlayerMovement } from "./PlayerMovement";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { DirtyScene } from "./DirtyScene"; import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils"; import { TextUtils } from "../Components/TextUtils";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
@ -60,7 +59,6 @@ import type {
PositionInterface, PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import type { RoomConnection } from "../../Connexion/RoomConnection"; import type { RoomConnection } from "../../Connexion/RoomConnection";
import type { ActionableItem } from "../Items/ActionableItem"; import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
@ -90,7 +88,10 @@ import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
import { deepCopy } from "deep-copy-ts"; import { deepCopy } from "deep-copy-ts";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore"; import { MapStore } from "../../Stores/Utils/MapStore";
import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb"; import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore";
import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator";
import Camera = Phaser.Cameras.Scene2D.Camera;
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -165,9 +166,11 @@ export class GameScene extends DirtyScene {
private createPromise: Promise<void>; private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void; private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private iframeSubscriptionList!: Array<Subscription>; private iframeSubscriptionList!: Array<Subscription>;
private peerStoreUnsubscribe!: () => void; private peerStoreUnsubscribe!: Unsubscriber;
private emoteUnsubscribe!: () => void; private emoteUnsubscribe!: Unsubscriber;
private emoteMenuUnsubscribe!: () => void; private emoteMenuUnsubscribe!: Unsubscriber;
private followUsersColorStoreUnsubscribe!: Unsubscriber;
private biggestAvailableAreaStoreUnsubscribe!: () => void; private biggestAvailableAreaStoreUnsubscribe!: () => void;
MapUrlFile: string; MapUrlFile: string;
roomUrl: string; roomUrl: string;
@ -206,6 +209,8 @@ export class GameScene extends DirtyScene {
private objectsByType = new Map<string, ITiledMapObject[]>(); private objectsByType = new Map<string, ITiledMapObject[]>();
private embeddedWebsiteManager!: EmbeddedWebsiteManager; private embeddedWebsiteManager!: EmbeddedWebsiteManager;
private loader: Loader; private loader: Loader;
private lastCameraEvent: WasCameraUpdatedEvent | undefined;
private firstCameraUpdateSent: boolean = false;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({ super({
@ -236,7 +241,7 @@ export class GameScene extends DirtyScene {
const textures = localUser?.textures; const textures = localUser?.textures;
if (textures) { if (textures) {
for (const texture of textures) { for (const texture of textures) {
loadCustomTexture(this.load, texture); loadCustomTexture(this.load, texture).catch((e) => console.error(e));
} }
} }
@ -263,7 +268,7 @@ export class GameScene extends DirtyScene {
this.load.on( this.load.on(
"filecomplete-tilemapJSON-" + this.MapUrlFile, "filecomplete-tilemapJSON-" + this.MapUrlFile,
(key: string, type: string, data: unknown) => { (key: string, type: string, data: unknown) => {
this.onMapLoad(data); this.onMapLoad(data).catch((e) => console.error(e));
} }
); );
return; return;
@ -287,14 +292,14 @@ export class GameScene extends DirtyScene {
this.load.on( this.load.on(
"filecomplete-tilemapJSON-" + this.MapUrlFile, "filecomplete-tilemapJSON-" + this.MapUrlFile,
(key: string, type: string, data: unknown) => { (key: string, type: string, data: unknown) => {
this.onMapLoad(data); this.onMapLoad(data).catch((e) => console.error(e));
} }
); );
// If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered.
// In this case, we check in the cache to see if the map is here and trigger the event manually. // In this case, we check in the cache to see if the map is here and trigger the event manually.
if (this.cache.tilemap.exists(this.MapUrlFile)) { if (this.cache.tilemap.exists(this.MapUrlFile)) {
const data = this.cache.tilemap.get(this.MapUrlFile); const data = this.cache.tilemap.get(this.MapUrlFile);
this.onMapLoad(data); this.onMapLoad(data).catch((e) => console.error(e));
} }
return; return;
} }
@ -315,7 +320,7 @@ export class GameScene extends DirtyScene {
}); });
this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles"); this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles");
this.load.on("filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.load.on("filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => {
this.onMapLoad(data); this.onMapLoad(data).catch((e) => console.error(e));
}); });
//TODO strategy to add access token //TODO strategy to add access token
this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile);
@ -323,7 +328,7 @@ export class GameScene extends DirtyScene {
// In this case, we check in the cache to see if the map is here and trigger the event manually. // In this case, we check in the cache to see if the map is here and trigger the event manually.
if (this.cache.tilemap.exists(this.MapUrlFile)) { if (this.cache.tilemap.exists(this.MapUrlFile)) {
const data = this.cache.tilemap.get(this.MapUrlFile); const data = this.cache.tilemap.get(this.MapUrlFile);
this.onMapLoad(data); this.onMapLoad(data).catch((e) => console.error(e));
} }
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -401,21 +406,23 @@ export class GameScene extends DirtyScene {
this.load.on("complete", () => { this.load.on("complete", () => {
// FIXME: the factory might fail because the resources might not be loaded yet... // FIXME: the factory might fail because the resources might not be loaded yet...
// We would need to add a loader ended event in addition to the createPromise // We would need to add a loader ended event in addition to the createPromise
this.createPromise.then(async () => { this.createPromise
itemFactory.create(this); .then(async () => {
itemFactory.create(this);
const roomJoinedAnswer = await this.connectionAnswerPromise; const roomJoinedAnswer = await this.connectionAnswerPromise;
for (const object of objectsOfType) { for (const object of objectsOfType) {
// TODO: we should pass here a factory to create sprites (maybe?) // TODO: we should pass here a factory to create sprites (maybe?)
// Do we have a state for this object? // Do we have a state for this object?
const state = roomJoinedAnswer.items[object.id]; const state = roomJoinedAnswer.items[object.id];
const actionableItem = itemFactory.factory(this, object, state); const actionableItem = itemFactory.factory(this, object, state);
this.actionableItems.set(actionableItem.getId(), actionableItem); this.actionableItems.set(actionableItem.getId(), actionableItem);
} }
}); })
.catch((e) => console.error(e));
}); });
} }
} }
@ -444,10 +451,6 @@ export class GameScene extends DirtyScene {
this.pinchManager = new PinchManager(this); this.pinchManager = new PinchManager(this);
} }
this.messageSubscription = worldFullMessageStream.stream.subscribe((message) =>
this.showWorldFullError(message)
);
const playerName = gameManager.getPlayerName(); const playerName = gameManager.getPlayerName();
if (!playerName) { if (!playerName) {
throw "playerName is not set"; throw "playerName is not set";
@ -485,11 +488,11 @@ export class GameScene extends DirtyScene {
if (exitSceneUrl !== undefined) { if (exitSceneUrl !== undefined) {
this.loadNextGame( this.loadNextGame(
Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile) Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
); ).catch((e) => console.error(e));
} }
const exitUrl = this.getExitUrl(layer); const exitUrl = this.getExitUrl(layer);
if (exitUrl !== undefined) { if (exitUrl !== undefined) {
this.loadNextGameFromExitUrl(exitUrl); this.loadNextGameFromExitUrl(exitUrl).catch((e) => console.error(e));
} }
} }
if (layer.type === "objectgroup") { if (layer.type === "objectgroup") {
@ -519,7 +522,9 @@ export class GameScene extends DirtyScene {
object.height, object.height,
object.visible, object.visible,
allowApi ?? false, allowApi ?? false,
"" "",
"map",
1
); );
} }
} }
@ -527,7 +532,7 @@ export class GameScene extends DirtyScene {
} }
this.gameMap.exitUrls.forEach((exitUrl) => { this.gameMap.exitUrls.forEach((exitUrl) => {
this.loadNextGameFromExitUrl(exitUrl); this.loadNextGameFromExitUrl(exitUrl).catch((e) => console.error(e));
}); });
this.startPositionCalculator = new StartPositionCalculator( this.startPositionCalculator = new StartPositionCalculator(
@ -548,7 +553,10 @@ export class GameScene extends DirtyScene {
mediaManager.setUserInputManager(this.userInputManager); mediaManager.setUserInputManager(this.userInputManager);
if (localUserStore.getFullscreen()) { if (localUserStore.getFullscreen()) {
document.querySelector("body")?.requestFullscreen(); document
.querySelector("body")
?.requestFullscreen()
.catch((e) => console.error(e));
} }
//notify game manager can to create currentUser in map //notify game manager can to create currentUser in map
@ -613,8 +621,6 @@ export class GameScene extends DirtyScene {
this.connect(); this.connect();
} }
this.emoteManager = new EmoteManager(this);
let oldPeerNumber = 0; let oldPeerNumber = 0;
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
const newPeerNumber = peers.size; const newPeerNumber = peers.size;
@ -646,9 +652,26 @@ export class GameScene extends DirtyScene {
} }
}); });
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => { this.followUsersColorStoreUnsubscribe = followUsersColorStore.subscribe((color) => {
this.scene.wake(); if (color !== undefined) {
this.CurrentPlayer.setOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color);
} else {
this.CurrentPlayer.removeOutlineColor();
this.connection?.emitPlayerOutlineColor(null);
}
}); });
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises])
.then(() => {
this.scene.wake();
})
.catch((e) =>
console.error(
"Some scripts failed to load ot the connection failed to establish to WorkAdventure server",
e
)
);
} }
/** /**
@ -679,7 +702,7 @@ export class GameScene extends DirtyScene {
playersStore.connectToRoomConnection(this.connection); playersStore.connectToRoomConnection(this.connection);
userIsAdminStore.set(this.connection.hasTag("admin")); userIsAdminStore.set(this.connection.hasTag("admin"));
this.connection.onUserJoins((message: MessageUserJoined) => { this.connection.userJoinedMessageStream.subscribe((message) => {
const userMessage: AddPlayerInterface = { const userMessage: AddPlayerInterface = {
userId: message.userId, userId: message.userId,
characterLayers: message.characterLayers, characterLayers: message.characterLayers,
@ -693,31 +716,33 @@ export class GameScene extends DirtyScene {
this.addPlayer(userMessage); this.addPlayer(userMessage);
}); });
this.connection.onUserMoved((message: UserMovedMessage) => { this.connection.userMovedMessageStream.subscribe((message) => {
const position = message.getPosition(); const position = message.position;
if (position === undefined) { if (position === undefined) {
throw new Error("Position missing from UserMovedMessage"); throw new Error("Position missing from UserMovedMessage");
} }
const messageUserMoved: MessageUserMovedInterface = { const messageUserMoved: MessageUserMovedInterface = {
userId: message.getUserid(), userId: message.userId,
position: ProtobufClientUtils.toPointInterface(position), position: ProtobufClientUtils.toPointInterface(position),
}; };
this.updatePlayerPosition(messageUserMoved); this.updatePlayerPosition(messageUserMoved);
}); });
this.connection.onUserLeft((userId: number) => { this.connection.userLeftMessageStream.subscribe((message) => {
this.removePlayer(userId); this.removePlayer(message.userId);
}); });
this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { this.connection.groupUpdateMessageStream.subscribe(
this.shareGroupPosition(groupPositionMessage); (groupPositionMessage: GroupCreatedUpdatedMessageInterface) => {
}); this.shareGroupPosition(groupPositionMessage);
}
);
this.connection.onGroupDeleted((groupId: number) => { this.connection.groupDeleteMessageStream.subscribe((message) => {
try { try {
this.deleteGroup(groupId); this.deleteGroup(message.groupId);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -729,7 +754,7 @@ export class GameScene extends DirtyScene {
this.createSuccessorGameScene(true, true); this.createSuccessorGameScene(true, true);
}); });
this.connection.onActionableEvent((message) => { this.connection.itemEventMessageStream.subscribe((message) => {
const item = this.actionableItems.get(message.itemId); const item = this.actionableItems.get(message.itemId);
if (item === undefined) { if (item === undefined) {
console.warn( console.warn(
@ -742,18 +767,29 @@ export class GameScene extends DirtyScene {
item.fire(message.event, message.state, message.parameters); item.fire(message.event, message.state, message.parameters);
}); });
this.connection.onPlayerDetailsUpdated((message) => { this.connection.playerDetailsUpdatedMessageStream.subscribe((message) => {
if (message.details === undefined) {
throw new Error("Malformed message. Missing details in PlayerDetailsUpdatedMessage");
}
this.pendingEvents.enqueue({ this.pendingEvents.enqueue({
type: "PlayerDetailsUpdated", type: "PlayerDetailsUpdated",
details: message, details: {
userId: message.userId,
outlineColor: message.details.outlineColor,
removeOutlineColor: message.details.removeOutlineColor,
},
}); });
}); });
/** /**
* Triggered when we receive the JWT token to connect to Jitsi * Triggered when we receive the JWT token to connect to Jitsi
*/ */
this.connection.onStartJitsiRoom((jwt, room) => { this.connection.sendJitsiJwtMessageStream.subscribe((message) => {
this.startJitsi(room, jwt); this.startJitsi(message.jitsiRoom, message.jwt);
});
this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => {
this.showWorldFullError(message);
}); });
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
@ -828,12 +864,15 @@ export class GameScene extends DirtyScene {
}); });
}); });
this.emoteManager = new EmoteManager(this, this.connection);
// this.gameMap.onLeaveLayer((layers) => { // this.gameMap.onLeaveLayer((layers) => {
// layers.forEach((layer) => { // layers.forEach((layer) => {
// iframeListener.sendLeaveLayerEvent(layer.name); // iframeListener.sendLeaveLayerEvent(layer.name);
// }); // });
// }); // });
}); })
.catch((e) => console.error(e));
} }
//todo: into dedicated classes //todo: into dedicated classes
@ -886,7 +925,7 @@ export class GameScene extends DirtyScene {
if (newValue) { if (newValue) {
this.onMapExit( this.onMapExit(
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile) Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
); ).catch((e) => console.error(e));
} else { } else {
setTimeout(() => { setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied"); layoutManagerActionStore.removeAction("roomAccessDenied");
@ -895,7 +934,9 @@ export class GameScene extends DirtyScene {
}); });
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => { this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) { if (newValue) {
this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())); this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) =>
console.error(e)
);
} else { } else {
setTimeout(() => { setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied"); layoutManagerActionStore.removeAction("roomAccessDenied");
@ -1081,21 +1122,47 @@ ${escapedMessage}
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.playSoundStream.subscribe((playSoundEvent) => { iframeListener.playSoundStream.subscribe((playSoundEvent) => {
const url = new URL(playSoundEvent.url, this.MapUrlFile); const url = new URL(playSoundEvent.url, this.MapUrlFile);
soundManager.playSound(this.load, this.sound, url.toString(), playSoundEvent.config); soundManager
.playSound(this.load, this.sound, url.toString(), playSoundEvent.config)
.catch((e) => console.error(e));
}) })
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.stopSoundStream.subscribe((stopSoundEvent) => { iframeListener.trackCameraUpdateStream.subscribe(() => {
const url = new URL(stopSoundEvent.url, this.MapUrlFile); if (!this.firstCameraUpdateSent) {
soundManager.stopSound(this.sound, url.toString()); this.cameras.main.on("followupdate", (camera: Camera) => {
const cameraEvent: WasCameraUpdatedEvent = {
x: camera.worldView.x,
y: camera.worldView.y,
width: camera.worldView.width,
height: camera.worldView.height,
zoom: camera.scaleManager.zoom,
};
if (
this.lastCameraEvent?.x == cameraEvent.x &&
this.lastCameraEvent?.y == cameraEvent.y &&
this.lastCameraEvent?.width == cameraEvent.width &&
this.lastCameraEvent?.height == cameraEvent.height &&
this.lastCameraEvent?.zoom == cameraEvent.zoom
) {
return;
}
this.lastCameraEvent = cameraEvent;
iframeListener.sendCameraUpdated(cameraEvent);
this.firstCameraUpdateSent = true;
});
iframeListener.sendCameraUpdated(this.cameras.main);
}
}) })
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.loadSoundStream.subscribe((loadSoundEvent) => { iframeListener.loadSoundStream.subscribe((loadSoundEvent) => {
const url = new URL(loadSoundEvent.url, this.MapUrlFile); const url = new URL(loadSoundEvent.url, this.MapUrlFile);
soundManager.loadSound(this.load, this.sound, url.toString()); soundManager.loadSound(this.load, this.sound, url.toString()).catch((e) => console.error(e));
}) })
); );
@ -1106,11 +1173,15 @@ ${escapedMessage}
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.loadPageStream.subscribe((url: string) => { iframeListener.loadPageStream.subscribe((url: string) => {
this.loadNextGameFromExitUrl(url).then(() => { this.loadNextGameFromExitUrl(url)
this.events.once(EVENT_TYPE.POST_UPDATE, () => { .then(() => {
this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString())); this.events.once(EVENT_TYPE.POST_UPDATE, () => {
}); this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString())).catch((e) =>
}); console.error(e)
);
});
})
.catch((e) => console.error(e));
}) })
); );
let scriptedBubbleSprite: Sprite; let scriptedBubbleSprite: Sprite;
@ -1151,6 +1222,12 @@ ${escapedMessage}
}) })
); );
this.iframeSubscriptionList.push(
iframeListener.setPropertyStream.subscribe((setProperty) => {
this.setPropertyLayer(setProperty.layerName, setProperty.propertyName, setProperty.propertyValue);
})
);
iframeListener.registerAnswerer("openCoWebsite", async (openCoWebsite, source) => { iframeListener.registerAnswerer("openCoWebsite", async (openCoWebsite, source) => {
if (!source) { if (!source) {
throw new Error("Unknown query source"); throw new Error("Unknown query source");
@ -1221,6 +1298,7 @@ ${escapedMessage}
roomId: this.roomUrl, roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [], tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables, variables: this.sharedVariablesManager.variables,
playerVariables: localUserStore.getAllUserProperties(),
userRoomToken: this.connection ? this.connection.userRoomToken : "", userRoomToken: this.connection ? this.connection.userRoomToken : "",
}; };
}); });
@ -1311,6 +1389,22 @@ ${escapedMessage}
}) })
); );
iframeListener.registerAnswerer("setVariable", (event, source) => {
switch (event.target) {
case "global": {
this.sharedVariablesManager.setVariable(event, source);
break;
}
case "player": {
localUserStore.setUserProperty(event.key, event.value);
break;
}
default: {
const _exhaustiveCheck: never = event.target;
}
}
});
iframeListener.registerAnswerer("removeActionMessage", (message) => { iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid); layoutManagerActionStore.removeAction(message.uuid);
}); });
@ -1329,6 +1423,13 @@ ${escapedMessage}
this.CurrentPlayer.removeOutlineColor(); this.CurrentPlayer.removeOutlineColor();
this.connection?.emitPlayerOutlineColor(null); this.connection?.emitPlayerOutlineColor(null);
}); });
iframeListener.registerAnswerer("getPlayerPosition", () => {
return {
x: this.CurrentPlayer.x,
y: this.CurrentPlayer.y,
};
});
} }
private setPropertyLayer( private setPropertyLayer(
@ -1337,7 +1438,7 @@ ${escapedMessage}
propertyValue: string | number | boolean | undefined propertyValue: string | number | boolean | undefined
): void { ): void {
if (propertyName === GameMapProperties.EXIT_URL && typeof propertyValue === "string") { if (propertyName === GameMapProperties.EXIT_URL && typeof propertyValue === "string") {
this.loadNextGameFromExitUrl(propertyValue); this.loadNextGameFromExitUrl(propertyValue).catch((e) => console.error(e));
} }
this.gameMap.setLayerProperty(layerName, propertyName, propertyValue); this.gameMap.setLayerProperty(layerName, propertyName, propertyValue);
} }
@ -1422,7 +1523,7 @@ ${escapedMessage}
public cleanupClosingScene(): void { public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi // stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsites(); coWebsiteManager.closeCoWebsites().catch((e) => console.error(e));
// Stop the script, if any // Stop the script, if any
const scripts = this.getScriptUrls(this.mapFile); const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) { for (const script of scripts) {
@ -1443,6 +1544,7 @@ ${escapedMessage}
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.emoteUnsubscribe(); this.emoteUnsubscribe();
this.emoteMenuUnsubscribe(); this.emoteMenuUnsubscribe();
this.followUsersColorStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState"); iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset"); iframeListener.unregisterAnswerer("loadTileset");
@ -1452,6 +1554,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("openCoWebsite");
iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("getCoWebsites");
iframeListener.unregisterAnswerer("setPlayerOutline"); iframeListener.unregisterAnswerer("setPlayerOutline");
iframeListener.unregisterAnswerer("setVariable");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close(); this.embeddedWebsiteManager?.close();
@ -1930,6 +2033,7 @@ ${escapedMessage}
this.loader.resize(); this.loader.resize();
} }
private getObjectLayerData(objectName: string): ITiledMapObject | undefined { private getObjectLayerData(objectName: string): ITiledMapObject | undefined {
for (const layer of this.mapFile.layers) { for (const layer of this.mapFile.layers) {
if (layer.type === "objectgroup" && layer.name === "floorLayer") { if (layer.type === "objectgroup" && layer.name === "floorLayer") {
@ -1942,6 +2046,7 @@ ${escapedMessage}
} }
return undefined; return undefined;
} }
private reposition(): void { private reposition(): void {
// Recompute camera offset if needed // Recompute camera offset if needed
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
@ -1960,7 +2065,9 @@ ${escapedMessage}
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined; const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined;
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth); jitsiFactory
.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth)
.catch((e) => console.error(e));
this.connection?.setSilent(true); this.connection?.setSilent(true);
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
analyticsClient.enteredJitsi(roomName, this.room.id); analyticsClient.enteredJitsi(roomName, this.room.id);

View file

@ -41,7 +41,7 @@ export class PlayerMovement {
oldX: this.startPosition.x, oldX: this.startPosition.x,
oldY: this.startPosition.y, oldY: this.startPosition.y,
direction: this.endPosition.direction, direction: this.endPosition.direction,
moving: true, moving: this.endPosition.moving,
}; };
} }
} }

View file

@ -3,6 +3,7 @@ import { iframeListener } from "../../Api/IframeListener";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent";
interface Variable { interface Variable {
defaultValue: unknown; defaultValue: unknown;
@ -41,58 +42,58 @@ export class SharedVariablesManager {
this._variables.set(name, value); this._variables.set(name, value);
} }
roomConnection.onSetVariable((name, value) => { roomConnection.variableMessageStream.subscribe(({ name, value }) => {
this._variables.set(name, value); this._variables.set(name, value);
// On server change, let's notify the iframes // On server change, let's notify the iframes
iframeListener.setVariable({ iframeListener.setVariable({
key: name, key: name,
value: value, value: value,
target: "global",
}); });
}); });
}
// When a variable is modified from an iFrame public setVariable(event: SetVariableEvent, source: MessageEventSource | null): void {
iframeListener.registerAnswerer("setVariable", (event, source) => { const key = event.key;
const key = event.key;
const object = this.variableObjects.get(key); const object = this.variableObjects.get(key);
if (object === undefined) { if (object === undefined) {
const errMsg = const errMsg =
'A script is trying to modify variable "' + 'A script is trying to modify variable "' +
key + key +
'" but this variable is not defined in the map.' + '" but this variable is not defined in the map.' +
'There should be an object in the map whose name is "' + 'There should be an object in the map whose name is "' +
key + key +
'" and whose type is "variable"'; '" and whose type is "variable"';
console.error(errMsg); console.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) { if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
const errMsg = const errMsg =
'A script is trying to modify variable "' + 'A script is trying to modify variable "' +
key + key +
'" but this variable is only writable for users with tag "' + '" but this variable is only writable for users with tag "' +
object.writableBy + object.writableBy +
'".'; '".';
console.error(errMsg); console.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
// Let's stop any propagation of the value we set is the same as the existing value. // Let's stop any propagation of the value we set is the same as the existing value.
if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) { if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
return; return;
} }
this._variables.set(key, event.value); this._variables.set(key, event.value);
// Dispatch to the room connection. // Dispatch to the room connection.
this.roomConnection.emitSetVariableEvent(key, event.value); this.roomConnection.emitSetVariableEvent(key, event.value);
// Dispatch to other iframes // Dispatch to other iframes
iframeListener.dispatchVariableToOtherIframes(key, event.value, source); iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
});
} }
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> { private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {

View file

@ -40,19 +40,21 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
preload() { preload() {
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => { this.loadCustomSceneSelectCharacters()
bodyResourceDescriptions.forEach((bodyResourceDescription) => { .then((bodyResourceDescriptions) => {
if ( bodyResourceDescriptions.forEach((bodyResourceDescription) => {
bodyResourceDescription.level == undefined || if (
bodyResourceDescription.level < 0 || bodyResourceDescription.level == undefined ||
bodyResourceDescription.level > 5 bodyResourceDescription.level < 0 ||
) { bodyResourceDescription.level > 5
throw "Texture level is null"; ) {
} throw "Texture level is null";
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription); }
}); this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
this.lazyloadingAttempt = true; });
}); this.lazyloadingAttempt = true;
})
.catch((e) => console.error(e));
this.layers = loadAllLayers(this.load); this.layers = loadAllLayers(this.load);
this.lazyloadingAttempt = false; this.lazyloadingAttempt = false;

View file

@ -41,12 +41,14 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
preload() { preload() {
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => { this.loadSelectSceneCharacters()
bodyResourceDescriptions.forEach((bodyResourceDescription) => { .then((bodyResourceDescriptions) => {
this.playerModels.push(bodyResourceDescription); bodyResourceDescriptions.forEach((bodyResourceDescription) => {
}); this.playerModels.push(bodyResourceDescription);
this.lazyloadingAttempt = true; });
}); this.lazyloadingAttempt = true;
})
.catch((e) => console.error(e));
this.playerModels = loadAllDefaultModels(this.load); this.playerModels = loadAllDefaultModels(this.load);
this.lazyloadingAttempt = false; this.lazyloadingAttempt = false;

View file

@ -162,6 +162,7 @@ export interface ITiledTileSet {
imageheight: number; imageheight: number;
imagewidth: number; imagewidth: number;
columns: number;
margin: number; margin: number;
name: string; name: string;
properties?: ITiledMapProperty[]; properties?: ITiledMapProperty[];

View file

@ -1,16 +1,17 @@
import { PlayerAnimationDirections } from "./Animation"; import { PlayerAnimationDirections } from "./Animation";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager"; import { ActiveEventList, UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character"; import { Character } from "../Entity/Character";
import type { RemotePlayer } from "../Entity/RemotePlayer";
import { get } from "svelte/store";
import { userMovingStore } from "../../Stores/GameStore"; import { userMovingStore } from "../../Stores/GameStore";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
export const hasMovedEventName = "hasMoved"; export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote"; export const requestEmoteEventName = "requestEmote";
export class Player extends Character { export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false;
constructor( constructor(
Scene: GameScene, Scene: GameScene,
x: number, x: number,
@ -29,71 +30,105 @@ export class Player extends Character {
this.getBody().setImmovable(false); this.getBody().setImmovable(false);
} }
moveUser(delta: number): void { private inputStep(activeEvents: ActiveEventList, x: number, y: number) {
//if user client on shift, camera and player speed // Process input events
let direction = null;
let moving = false;
const activeEvents = this.userInputManager.getEventListForGameTick();
const speedMultiplier = activeEvents.get(UserInputEvent.SpeedUp) ? 25 : 9;
const moveAmount = speedMultiplier * 20;
let x = 0;
let y = 0;
if (activeEvents.get(UserInputEvent.MoveUp)) { if (activeEvents.get(UserInputEvent.MoveUp)) {
y = -moveAmount; y = y - 1;
direction = PlayerAnimationDirections.Up;
moving = true;
} else if (activeEvents.get(UserInputEvent.MoveDown)) { } else if (activeEvents.get(UserInputEvent.MoveDown)) {
y = moveAmount; y = y + 1;
direction = PlayerAnimationDirections.Down;
moving = true;
} }
if (activeEvents.get(UserInputEvent.MoveLeft)) { if (activeEvents.get(UserInputEvent.MoveLeft)) {
x = -moveAmount; x = x - 1;
direction = PlayerAnimationDirections.Left;
moving = true;
} else if (activeEvents.get(UserInputEvent.MoveRight)) { } else if (activeEvents.get(UserInputEvent.MoveRight)) {
x = moveAmount; x = x + 1;
direction = PlayerAnimationDirections.Right;
moving = true;
} }
moving = moving || activeEvents.get(UserInputEvent.JoystickMove);
if (x !== 0 || y !== 0) { // Compute movement deltas
const followMode = get(followStateStore) !== "off";
const speedup = activeEvents.get(UserInputEvent.SpeedUp) && !followMode ? 25 : 9;
const moveAmount = speedup * 20;
x = x * moveAmount;
y = y * moveAmount;
// Compute moving state
const joystickMovement = activeEvents.get(UserInputEvent.JoystickMove);
const moving = x !== 0 || y !== 0 || joystickMovement;
// Compute direction
let direction = this.lastDirection;
if (moving && !joystickMovement) {
if (Math.abs(x) > Math.abs(y)) {
direction = x < 0 ? PlayerAnimationDirections.Left : PlayerAnimationDirections.Right;
} else {
direction = y < 0 ? PlayerAnimationDirections.Up : PlayerAnimationDirections.Down;
}
}
// Send movement events
const emit = () => this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y });
if (moving) {
this.move(x, y); this.move(x, y);
this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y, oldX: x, oldY: y }); emit();
} else if (this.wasMoving && moving) { } else if (get(userMovingStore)) {
// slow joystick movement
this.move(0, 0);
this.emit(hasMovedEventName, {
moving,
direction: this.previousDirection,
x: this.x,
y: this.y,
oldX: x,
oldY: y,
});
} else if (this.wasMoving && !moving) {
this.stop(); this.stop();
this.emit(hasMovedEventName, { emit();
moving,
direction: this.previousDirection,
x: this.x,
y: this.y,
oldX: x,
oldY: y,
});
} }
if (direction !== null) { // Update state
this.previousDirection = direction;
}
this.wasMoving = moving;
userMovingStore.set(moving); userMovingStore.set(moving);
} }
public isMoving(): boolean { private computeFollowMovement(): number[] {
return this.wasMoving; // Find followed WOKA and abort following if we lost it
const player = this.scene.MapPlayersByKey.get(get(followUsersStore)[0]);
if (!player) {
this.scene.connection?.emitFollowAbort();
followStateStore.set("off");
return [0, 0];
}
// Compute movement direction
const xDistance = player.x - this.x;
const yDistance = player.y - this.y;
const distance = Math.pow(xDistance, 2) + Math.pow(yDistance, 2);
if (distance < 2000) {
return [0, 0];
}
const xMovement = xDistance / Math.sqrt(distance);
const yMovement = yDistance / Math.sqrt(distance);
return [xMovement, yMovement];
}
public moveUser(delta: number): void {
const activeEvents = this.userInputManager.getEventListForGameTick();
const state = get(followStateStore);
const role = get(followRoleStore);
if (activeEvents.get(UserInputEvent.Follow)) {
if (state === "off" && this.scene.groups.size > 0) {
this.sendFollowRequest();
} else if (state === "active") {
followStateStore.set("ending");
}
}
let x = 0;
let y = 0;
if ((state === "active" || state === "ending") && role === "follower") {
[x, y] = this.computeFollowMovement();
}
this.inputStep(activeEvents, x, y);
}
public sendFollowRequest() {
this.scene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
}
public startFollowing() {
followStateStore.set("active");
this.scene.connection?.emitFollowConfirmation();
} }
} }

View file

@ -31,6 +31,10 @@ export class WaScaleManager {
height: height * devicePixelRatio, height: height * devicePixelRatio,
}); });
if (gameSize.width == 0) {
return;
}
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio; this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);

View file

@ -16,6 +16,7 @@ export enum UserInputEvent {
MoveDown, MoveDown,
SpeedUp, SpeedUp,
Interact, Interact,
Follow,
Shout, Shout,
JoystickMove, JoystickMove,
} }
@ -147,6 +148,10 @@ export class UserInputManager {
event: UserInputEvent.Interact, event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false), keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
}, },
{
event: UserInputEvent.Follow,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
{ {
event: UserInputEvent.Shout, event: UserInputEvent.Shout,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false), keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),

View file

@ -0,0 +1,89 @@
import { derived, writable } from "svelte/store";
import { getColorRgbFromHue } from "../WebRtc/ColorGenerator";
import { gameManager } from "../Phaser/Game/GameManager";
type FollowState = "off" | "requesting" | "active" | "ending";
type FollowRole = "leader" | "follower";
export const followStateStore = writable<FollowState>("off");
export const followRoleStore = writable<FollowRole>("leader");
function createFollowUsersStore() {
const { subscribe, update, set } = writable<number[]>([]);
return {
subscribe,
addFollowRequest(leader: number): void {
followStateStore.set("requesting");
followRoleStore.set("follower");
set([leader]);
},
addFollower(user: number): void {
update((followers) => {
followers.push(user);
return followers;
});
},
/**
* Removes the follower from the store.
* Will update followStateStore and followRoleStore if nobody is following anymore.
* @param user
*/
removeFollower(user: number): void {
update((followers) => {
const oldFollowerCount = followers.length;
followers = followers.filter((id) => id !== user);
if (followers.length === 0 && oldFollowerCount > 0) {
followStateStore.set("off");
followRoleStore.set("leader");
}
return followers;
});
},
stopFollowing(): void {
set([]);
followStateStore.set("off");
followRoleStore.set("leader");
},
};
}
export const followUsersStore = createFollowUsersStore();
/**
* This store contains the color of the follow group. It is derived from the ID of the leader.
*/
export const followUsersColorStore = derived(
[followStateStore, followRoleStore, followUsersStore],
([$followStateStore, $followRoleStore, $followUsersStore]) => {
if ($followStateStore !== "active") {
return undefined;
}
if ($followUsersStore.length === 0) {
return undefined;
}
let leaderId: number;
if ($followRoleStore === "leader") {
// Let's get my ID by a quite complicated way....
leaderId = gameManager.getCurrentGameScene().connection?.getUserId() ?? 0;
} else {
leaderId = $followUsersStore[0];
}
// Let's compute a random hue between 0 and 1 that varies enough to be interesting
const hue = ((leaderId * 197) % 255) / 255;
let { r, g, b } = getColorRgbFromHue(hue);
if ($followRoleStore === "follower") {
// Let's make the followers very slightly darker
r *= 0.9;
g *= 0.9;
b *= 0.9;
}
return (Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255);
}
);

View file

@ -360,32 +360,27 @@ const implementCorrectTrackBehavior = getNavigatorType() === NavigatorType.firef
/** /**
* Stops the camera from filming * Stops the camera from filming
*/ */
function applyCameraConstraints(currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean): void { async function applyCameraConstraints(
currentStream: MediaStream | null,
constraints: MediaTrackConstraints | boolean
): Promise<void[]> {
if (!currentStream) { if (!currentStream) {
return; return [];
}
for (const track of currentStream.getVideoTracks()) {
toggleConstraints(track, constraints).catch((e) =>
console.error("Error while setting new camera constraints:", e)
);
} }
return Promise.all(currentStream.getVideoTracks().map((track) => toggleConstraints(track, constraints)));
} }
/** /**
* Stops the microphone from listening * Stops the microphone from listening
*/ */
function applyMicrophoneConstraints( async function applyMicrophoneConstraints(
currentStream: MediaStream | null, currentStream: MediaStream | null,
constraints: MediaTrackConstraints | boolean constraints: MediaTrackConstraints | boolean
): void { ): Promise<void[]> {
if (!currentStream) { if (!currentStream) {
return; return [];
}
for (const track of currentStream.getAudioTracks()) {
toggleConstraints(track, constraints).catch((e) =>
console.error("Error while setting new audio constraints:", e)
);
} }
return Promise.all(currentStream.getAudioTracks().map((track) => toggleConstraints(track, constraints)));
} }
async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): Promise<void> { async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): Promise<void> {
@ -477,8 +472,8 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
} }
} }
applyMicrophoneConstraints(currentStream, constraints.audio || false); applyMicrophoneConstraints(currentStream, constraints.audio || false).catch((e) => console.error(e));
applyCameraConstraints(currentStream, constraints.video || false); applyCameraConstraints(currentStream, constraints.video || false).catch((e) => console.error(e));
if (implementCorrectTrackBehavior) { if (implementCorrectTrackBehavior) {
//on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed //on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed

View file

@ -3,6 +3,7 @@ import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection"; import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator"; import { getRandomColor } from "../WebRtc/ColorGenerator";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import room from "../Api/iframe/room";
let idCount = 0; let idCount = 0;
@ -19,7 +20,8 @@ function createPlayersStore() {
connectToRoomConnection: (roomConnection: RoomConnection) => { connectToRoomConnection: (roomConnection: RoomConnection) => {
players = new Map<number, PlayerInterface>(); players = new Map<number, PlayerInterface>();
set(players); set(players);
roomConnection.onUserJoins((message) => { // TODO: it would be cool to unsubscribe properly here
roomConnection.userJoinedMessageStream.subscribe((message) => {
update((users) => { update((users) => {
users.set(message.userId, { users.set(message.userId, {
userId: message.userId, userId: message.userId,
@ -33,9 +35,9 @@ function createPlayersStore() {
return users; return users;
}); });
}); });
roomConnection.onUserLeft((userId) => { roomConnection.userLeftMessageStream.subscribe((message) => {
update((users) => { update((users) => {
users.delete(userId); users.delete(message.userId);
return users; return users;
}); });
}); });

View file

@ -156,7 +156,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
error: e instanceof Error ? e : new Error("An unknown error happened"), error: e instanceof Error ? e : new Error("An unknown error happened"),
}); });
} }
})(); })().catch((e) => console.error(e));
} }
); );

View file

@ -1,5 +1,3 @@
import { writable } from "svelte/store"; import { createMessageStore } from "./MessageStore";
export const banMessageVisibleStore = writable(false); export const banMessageStore = createMessageStore();
export const banMessageContentStore = writable("");

View file

@ -0,0 +1,29 @@
import { writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid";
export interface Message {
id: string;
text: string;
}
/**
* A store that contains a list of messages to be displayed.
*/
export function createMessageStore() {
const { subscribe, update } = writable<Message[]>([]);
return {
subscribe,
addMessage: (text: string): void => {
update((messages: Message[]) => {
return [...messages, { id: uuidv4(), text }];
});
},
clearMessageById: (id: string): void => {
update((messages: Message[]) => {
messages = messages.filter((message) => message.id !== id);
return messages;
});
},
};
}

View file

@ -1,5 +1,3 @@
import { writable } from "svelte/store"; import { createMessageStore } from "./MessageStore";
export const textMessageVisibleStore = writable(false); export const textMessageStore = createMessageStore();
export const textMessageContentStore = writable("");

View file

@ -44,7 +44,7 @@ class UrlManager {
if (window.location.pathname === room.id) return; if (window.location.pathname === room.id) return;
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API) //Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value //use href to keep # value
localUserStore.setLastRoomUrl(room.href); localUserStore.setLastRoomUrl(room.href).catch((e) => console.error(e));
const hash = window.location.hash; const hash = window.location.hash;
const search = room.search.toString(); const search = room.search.toString();
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash); history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);

View file

@ -149,7 +149,7 @@ class CoWebsiteManager {
} }
buttonCloseCoWebsites.blur(); buttonCloseCoWebsites.blur();
this.closeCoWebsites(); this.closeCoWebsites().catch((e) => console.error(e));
}); });
const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId); const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
@ -515,70 +515,72 @@ class CoWebsiteManager {
throw new Error("Too many we"); throw new Error("Too many we");
} }
Promise.resolve(callback(this.cowebsiteBufferDom)).then((iframe) => { Promise.resolve(callback(this.cowebsiteBufferDom))
iframe?.classList.add("pixel"); .then((iframe) => {
iframe?.classList.add("pixel");
if (!iframe.id) { if (!iframe.id) {
do { do {
iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7);
} while (this.getCoWebsiteById(iframe.id)); } while (this.getCoWebsiteById(iframe.id));
}
const onloadPromise = new Promise<void>((resolve) => {
iframe.onload = () => resolve();
});
const icon = this.generateCoWebsiteIcon(iframe);
const coWebsite = {
iframe,
icon,
position: position ?? this.coWebsites.length,
};
// Iframe management on mobile
icon.addEventListener("click", () => {
if (this.isSmallScreen()) {
this.moveRightPreviousCoWebsite(coWebsite, 0);
} }
});
this.coWebsites.push(coWebsite); const onloadPromise = new Promise<void>((resolve) => {
this.cowebsiteSubIconsDom.appendChild(icon); iframe.onload = () => resolve();
});
const onTimeoutPromise = new Promise<void>((resolve) => { const icon = this.generateCoWebsiteIcon(iframe);
setTimeout(() => resolve(), 2000);
});
this.currentOperationPromise = this.currentOperationPromise const coWebsite = {
.then(() => Promise.race([onloadPromise, onTimeoutPromise])) iframe,
.then(() => { icon,
if (coWebsite.position === 0) { position: position ?? this.coWebsites.length,
this.openMain(); };
if (widthPercent) {
this.widthPercent = widthPercent;
}
setTimeout(() => { // Iframe management on mobile
this.fire(); icon.addEventListener("click", () => {
if (this.isSmallScreen()) {
this.moveRightPreviousCoWebsite(coWebsite, 0);
}
});
this.coWebsites.push(coWebsite);
this.cowebsiteSubIconsDom.appendChild(icon);
const onTimeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 2000);
});
this.currentOperationPromise = this.currentOperationPromise
.then(() => Promise.race([onloadPromise, onTimeoutPromise]))
.then(() => {
if (coWebsite.position === 0) {
this.openMain();
if (widthPercent) {
this.widthPercent = widthPercent;
}
setTimeout(() => {
this.fire();
position !== undefined
? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
: this.moveCoWebsite(coWebsite, coWebsite.position);
}, animationTime);
} else {
position !== undefined position !== undefined
? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
: this.moveCoWebsite(coWebsite, coWebsite.position); : this.moveCoWebsite(coWebsite, coWebsite.position);
}, animationTime); }
} else {
position !== undefined
? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
: this.moveCoWebsite(coWebsite, coWebsite.position);
}
return resolve(coWebsite); return resolve(coWebsite);
}) })
.catch((err) => { .catch((err) => {
console.error("Error loadCoWebsite => ", err); console.error("Error loadCoWebsite => ", err);
this.removeCoWebsiteFromStack(coWebsite); this.removeCoWebsiteFromStack(coWebsite);
return reject(); return reject();
}); });
}); })
.catch((e) => console.error("Error loadCoWebsite >=> ", e));
}); });
} }
@ -603,17 +605,21 @@ class CoWebsiteManager {
return this.currentOperationPromise; return this.currentOperationPromise;
} }
public closeJitsi() { public async closeJitsi() {
const jitsi = this.searchJitsi(); const jitsi = this.searchJitsi();
if (jitsi) { if (jitsi) {
this.closeCoWebsite(jitsi); return this.closeCoWebsite(jitsi);
} }
} }
public closeCoWebsites(): Promise<void> { public closeCoWebsites(): Promise<void> {
this.currentOperationPromise = this.currentOperationPromise.then(() => { this.currentOperationPromise = this.currentOperationPromise.then(() => {
const promises: Promise<void>[] = [];
this.coWebsites.forEach((coWebsite: CoWebsite) => { this.coWebsites.forEach((coWebsite: CoWebsite) => {
this.closeCoWebsite(coWebsite); promises.push(this.closeCoWebsite(coWebsite));
});
return Promise.all(promises).then(() => {
return;
}); });
}); });
return this.currentOperationPromise; return this.currentOperationPromise;

View file

@ -1,13 +1,29 @@
export function getRandomColor(): string { export function getRandomColor(): string {
const { r, g, b } = getColorRgbFromHue(Math.random());
return toHexa(r, g, b);
}
function toHexa(r: number, g: number, b: number): string {
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
}
export function getColorRgbFromHue(hue: number): { r: number; g: number; b: number } {
const golden_ratio_conjugate = 0.618033988749895; const golden_ratio_conjugate = 0.618033988749895;
let hue = Math.random();
hue += golden_ratio_conjugate; hue += golden_ratio_conjugate;
hue %= 1; hue %= 1;
return hsv_to_rgb(hue, 0.5, 0.95); return hsv_to_rgb(hue, 0.5, 0.95);
} }
function stringToDouble(string: string): number {
let num = 1;
for (const char of string.split("")) {
num *= char.charCodeAt(0);
}
return (num % 255) / 255;
}
//todo: test this. //todo: test this.
function hsv_to_rgb(hue: number, saturation: number, brightness: number): string { function hsv_to_rgb(hue: number, saturation: number, brightness: number): { r: number; g: number; b: number } {
const h_i = Math.floor(hue * 6); const h_i = Math.floor(hue * 6);
const f = hue * 6 - h_i; const f = hue * 6 - h_i;
const p = brightness * (1 - saturation); const p = brightness * (1 - saturation);
@ -48,5 +64,9 @@ function hsv_to_rgb(hue: number, saturation: number, brightness: number): string
default: default:
throw "h_i cannot be " + h_i; throw "h_i cannot be " + h_i;
} }
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16); return {
r,
g,
b,
};
} }

View file

@ -1,5 +1,5 @@
import { JITSI_URL } from "../Enum/EnvironmentVariable"; import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { coWebsiteManager } from "./CoWebsiteManager"; import { CoWebsite, coWebsiteManager } from "./CoWebsiteManager";
import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
@ -140,8 +140,8 @@ class JitsiFactory {
interfaceConfig?: object, interfaceConfig?: object,
jitsiUrl?: string, jitsiUrl?: string,
jitsiWidth?: number jitsiWidth?: number
): void { ): Promise<CoWebsite> {
coWebsiteManager.addCoWebsite( return coWebsiteManager.addCoWebsite(
async (cowebsiteDiv) => { async (cowebsiteDiv) => {
// Jitsi meet external API maintains some data in local storage // Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a // which is sent via the appData URL parameter when joining a
@ -200,7 +200,7 @@ class JitsiFactory {
const jitsiCoWebsite = coWebsiteManager.searchJitsi(); const jitsiCoWebsite = coWebsiteManager.searchJitsi();
if (jitsiCoWebsite) { if (jitsiCoWebsite) {
coWebsiteManager.closeJitsi(); coWebsiteManager.closeJitsi().catch((e) => console.error(e));
} }
this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback); this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);

View file

@ -75,23 +75,25 @@ export class SimplePeer {
*/ */
private initialise() { private initialise() {
//receive signal by gemer //receive signal by gemer
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => { this.Connection.webRtcSignalToClientMessageStream.subscribe((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcSignal(message); this.receiveWebrtcSignal(message);
}); });
//receive signal by gemer //receive signal by gemer
this.Connection.receiveWebrtcScreenSharingSignal((message: WebRtcSignalReceivedMessageInterface) => { this.Connection.webRtcScreenSharingSignalToClientMessageStream.subscribe(
this.receiveWebrtcScreenSharingSignal(message); (message: WebRtcSignalReceivedMessageInterface) => {
}); this.receiveWebrtcScreenSharingSignal(message);
}
);
mediaManager.showGameOverlay(); mediaManager.showGameOverlay();
//receive message start //receive message start
this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => { this.Connection.webRtcStartMessageStream.subscribe((message: UserSimplePeerInterface) => {
this.receiveWebrtcStart(message); this.receiveWebrtcStart(message);
}); });
this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => { this.Connection.webRtcDisconnectMessageStream.subscribe((data: WebRtcDisconnectMessageInterface): void => {
this.closeConnection(data.userId); this.closeConnection(data.userId);
}); });
} }

View file

@ -9,30 +9,34 @@ import {
} from "./Api/Events/IframeEvent"; } from "./Api/Events/IframeEvent";
import chat from "./Api/iframe/chat"; import chat from "./Api/iframe/chat";
import type { IframeCallback } from "./Api/iframe/IframeApiContribution"; import type { IframeCallback } from "./Api/iframe/IframeApiContribution";
import nav from "./Api/iframe/nav"; import nav, { CoWebsite } from "./Api/iframe/nav";
import controls from "./Api/iframe/controls"; import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui"; import ui from "./Api/iframe/ui";
import sound from "./Api/iframe/sound"; import sound from "./Api/iframe/sound";
import room, { setMapURL, setRoomId } from "./Api/iframe/room"; import room, { setMapURL, setRoomId } from "./Api/iframe/room";
import state, { initVariables } from "./Api/iframe/state"; import { createState } from "./Api/iframe/state";
import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player"; import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound"; import type { Sound } from "./Api/iframe/Sound/Sound";
import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution"; import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution";
import camera from "./Api/iframe/camera";
const globalState = createState("global");
// Notify WorkAdventure that we are ready to receive data // Notify WorkAdventure that we are ready to receive data
const initPromise = queryWorkadventure({ const initPromise = queryWorkadventure({
type: "getState", type: "getState",
data: undefined, data: undefined,
}).then((state) => { }).then((gameState) => {
setPlayerName(state.nickname); setPlayerName(gameState.nickname);
setRoomId(state.roomId); setRoomId(gameState.roomId);
setMapURL(state.mapUrl); setMapURL(gameState.mapUrl);
setTags(state.tags); setTags(gameState.tags);
setUuid(state.uuid); setUuid(gameState.uuid);
initVariables(state.variables as Map<string, unknown>); globalState.initVariables(gameState.variables as Map<string, unknown>);
setUserRoomToken(state.userRoomToken); player.state.initVariables(gameState.playerVariables as Map<string, unknown>);
setUserRoomToken(gameState.userRoomToken);
}); });
const wa = { const wa = {
@ -43,7 +47,8 @@ const wa = {
sound, sound,
room, room,
player, player,
state, camera,
state: globalState,
onInit(): Promise<void> { onInit(): Promise<void> {
return initPromise; return initPromise;
@ -131,17 +136,17 @@ const wa = {
/** /**
* @deprecated Use WA.nav.openCoWebSite instead * @deprecated Use WA.nav.openCoWebSite instead
*/ */
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void { openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): Promise<CoWebsite> {
console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead"); console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead");
nav.openCoWebSite(url, allowApi, allowPolicy); return nav.openCoWebSite(url, allowApi, allowPolicy);
}, },
/** /**
* @deprecated Use WA.nav.closeCoWebSite instead * @deprecated Use WA.nav.closeCoWebSite instead
*/ */
closeCoWebSite(): void { closeCoWebSite(): Promise<void> {
console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead"); console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead");
nav.closeCoWebSite(); return nav.closeCoWebSite();
}, },
/** /**
@ -225,7 +230,5 @@ window.addEventListener(
callback?.callback(payloadData); callback?.callback(payloadData);
} }
} }
// ...
} }
); );

View file

@ -1066,6 +1066,7 @@ div.action.danger p.action-body{
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
user-select: none;
& > div { & > div {
position: relative; position: relative;

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