merged develop
4
.github/workflows/continuous_integration.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-front
|
||||
run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Create index.html"
|
||||
@ -97,7 +97,7 @@ jobs:
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-pusher
|
||||
run: yarn run proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build"
|
||||
|
59
.github/workflows/end_to_end_tests.yml
vendored
@ -11,10 +11,43 @@ on:
|
||||
|
||||
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:
|
||||
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:
|
||||
- name: "Checkout"
|
||||
@ -67,3 +100,27 @@ jobs:
|
||||
- name: Display logs
|
||||
if: ${{ failure() }}
|
||||
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 }}
|
||||
|
2
.github/workflows/push-to-npm.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-front
|
||||
run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Create index.html"
|
||||
|
@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
(
|
||||
cd messages || exit
|
||||
yarn run precommit
|
||||
)
|
||||
(
|
||||
cd front || exit
|
||||
yarn run precommit
|
||||
|
@ -25,6 +25,7 @@
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error"
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"no-throw-literal": "error"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
# 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
|
||||
COPY messages .
|
||||
RUN yarn install && yarn proto
|
||||
|
||||
# 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
|
||||
COPY back/yarn.lock back/package.json ./
|
||||
RUN yarn install
|
||||
@ -15,7 +15,7 @@ ENV NODE_ENV=production
|
||||
RUN yarn run tsc
|
||||
|
||||
# final production image
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
|
||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d
|
||||
WORKDIR /usr/src
|
||||
COPY back/yarn.lock back/package.json ./
|
||||
COPY --from=builder2 /usr/src/dist /usr/src/dist
|
||||
|
@ -68,14 +68,14 @@
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/redis": "^2.8.31",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
"eslint": "^6.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
||||
"@typescript-eslint/parser": "^5.8.0",
|
||||
"eslint": "^8.5.0",
|
||||
"jasmine": "^3.5.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.1",
|
||||
"ts-node-dev": "^1.0.0-pre.44",
|
||||
"typescript": "^3.8.3"
|
||||
"ts-node-dev": "^1.1.8",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
|
@ -1,15 +1,15 @@
|
||||
// lib/server.ts
|
||||
import App from "./src/App";
|
||||
import grpc from "grpc";
|
||||
import {roomManager} from "./src/RoomManager";
|
||||
import {IRoomManagerServer, RoomManagerService} from "./src/Messages/generated/messages_grpc_pb";
|
||||
import {HTTP_PORT, GRPC_PORT} from "./src/Enum/EnvironmentVariable";
|
||||
import { roomManager } from "./src/RoomManager";
|
||||
import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
|
||||
import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
|
||||
|
||||
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT))
|
||||
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
|
||||
|
||||
const server = new grpc.Server();
|
||||
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();
|
||||
console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT);
|
||||
console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT);
|
||||
|
@ -36,9 +36,11 @@ export class DebugController {
|
||||
return "BatchedMessages";
|
||||
}
|
||||
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()) {
|
||||
obj[mapKey] = mapValue;
|
||||
if (typeof mapKey === "number" || typeof mapKey === "string") {
|
||||
obj[mapKey] = mapValue;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
} else if (value instanceof Set) {
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { App } from "../Server/sifrr.server";
|
||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||
const register = require("prom-client").register;
|
||||
const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
|
||||
import { register, collectDefaultMetrics } from "prom-client";
|
||||
|
||||
export class PrometheusController {
|
||||
constructor(private App: App) {
|
||||
collectDefaultMetrics({
|
||||
timeout: 10000,
|
||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
||||
});
|
||||
|
||||
|
@ -2,7 +2,13 @@ import { PointInterface } from "./Websocket/PointInterface";
|
||||
import { Group } from "./Group";
|
||||
import { User, UserSocket } from "./User";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||
import {
|
||||
EmoteCallback,
|
||||
EntersCallback,
|
||||
LeavesCallback,
|
||||
MovesCallback,
|
||||
PlayerDetailsUpdatedCallback,
|
||||
} from "_Model/Zone";
|
||||
import { PositionNotifier } from "./PositionNotifier";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import {
|
||||
@ -11,9 +17,11 @@ import {
|
||||
EmoteEventMessage,
|
||||
ErrorMessage,
|
||||
JoinRoomMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SubToPusherRoomMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
ServerToClientMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||
@ -27,6 +35,7 @@ import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||
import { VariableError } from "../Services/VariableError";
|
||||
import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect";
|
||||
|
||||
export type ConnectCallback = (user: User, group: Group) => void;
|
||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||
@ -56,10 +65,19 @@ export class GameRoom {
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
onEmote: EmoteCallback,
|
||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
) {
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||
this.positionNotifier = new PositionNotifier(
|
||||
320,
|
||||
320,
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote,
|
||||
onPlayerDetailsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
public static async create(
|
||||
@ -71,7 +89,8 @@ export class GameRoom {
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
onEmote: EmoteCallback,
|
||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
): Promise<GameRoom> {
|
||||
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||
|
||||
@ -85,16 +104,13 @@ export class GameRoom {
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote
|
||||
onEmote,
|
||||
onPlayerDetailsUpdated
|
||||
);
|
||||
|
||||
return gameRoom;
|
||||
}
|
||||
|
||||
public getGroups(): Group[] {
|
||||
return Array.from(this.groups.values());
|
||||
}
|
||||
|
||||
public getUsers(): Map<number, User> {
|
||||
return this.users;
|
||||
}
|
||||
@ -157,6 +173,14 @@ export class GameRoom {
|
||||
if (userObj !== undefined && typeof userObj.group !== "undefined") {
|
||||
this.leaveGroup(userObj);
|
||||
}
|
||||
|
||||
if (user.hasFollowers()) {
|
||||
user.stopLeading();
|
||||
}
|
||||
if (user.following) {
|
||||
user.following.delFollower(user);
|
||||
}
|
||||
|
||||
this.users.delete(user.id);
|
||||
this.usersByUuid.delete(user.uuid);
|
||||
|
||||
@ -180,6 +204,14 @@ export class GameRoom {
|
||||
this.updateUserGroup(user);
|
||||
}
|
||||
|
||||
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
if (playerDetailsMessage.getRemoveoutlinecolor()) {
|
||||
user.outlineColor = undefined;
|
||||
} else {
|
||||
user.outlineColor = playerDetailsMessage.getOutlinecolor();
|
||||
}
|
||||
}
|
||||
|
||||
private updateUserGroup(user: User): void {
|
||||
user.group?.updatePosition();
|
||||
user.group?.searchForNearbyUsers();
|
||||
@ -187,8 +219,8 @@ export class GameRoom {
|
||||
if (user.silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.group === undefined) {
|
||||
const group = user.group;
|
||||
if (group === undefined) {
|
||||
// If the user is not part of a group:
|
||||
// should he join a group?
|
||||
|
||||
@ -219,13 +251,40 @@ export class GameRoom {
|
||||
} else {
|
||||
// If the user is part of a group:
|
||||
// should he leave the group?
|
||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition());
|
||||
if (distance > this.groupRadius) {
|
||||
this.leaveGroup(user);
|
||||
let noOneOutOfBounds = true;
|
||||
group.getUsers().forEach((foreignUser: 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) {
|
||||
if (user.silent === silent) {
|
||||
return;
|
||||
@ -253,12 +312,9 @@ export class GameRoom {
|
||||
}
|
||||
group.leave(user);
|
||||
if (group.isEmpty()) {
|
||||
this.positionNotifier.leave(group);
|
||||
group.destroy();
|
||||
if (!this.groups.has(group)) {
|
||||
throw new Error(
|
||||
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
|
||||
);
|
||||
throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`);
|
||||
}
|
||||
this.groups.delete(group);
|
||||
//todo: is the group garbage collected?
|
||||
@ -459,9 +515,9 @@ export class GameRoom {
|
||||
}
|
||||
|
||||
const result = await adminApi.fetchMapDetails(roomUrl);
|
||||
if (!isMapDetailsData(result)) {
|
||||
console.error("Unexpected room details received from server", result);
|
||||
throw new Error("Unexpected room details received from server");
|
||||
if (isRoomRedirect(result)) {
|
||||
console.error("Unexpected room redirect received while querying map details", result);
|
||||
throw new Error("Unexpected room redirect received while querying map details");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -16,6 +16,10 @@ export class Group implements Movable {
|
||||
private wasDestroyed: boolean = false;
|
||||
private roomId: string;
|
||||
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(
|
||||
roomId: string,
|
||||
@ -78,6 +82,10 @@ export class Group implements Movable {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
if (this.outOfBounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldX === undefined) {
|
||||
this.currentZone = this.positionNotifier.enter(this);
|
||||
} else {
|
||||
@ -116,7 +124,7 @@ export class Group implements Movable {
|
||||
leave(user: User): void {
|
||||
const success = this.users.delete(user);
|
||||
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;
|
||||
|
||||
@ -133,6 +141,10 @@ export class Group implements Movable {
|
||||
* Usually used when there is only one user left.
|
||||
*/
|
||||
destroy(): void {
|
||||
if (!this.outOfBounds) {
|
||||
this.positionNotifier.leave(this);
|
||||
}
|
||||
|
||||
for (const user of this.users) {
|
||||
this.leave(user);
|
||||
}
|
||||
@ -142,4 +154,26 @@ export class Group implements Movable {
|
||||
get getSize() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,19 @@
|
||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||
* number of players around the current player.
|
||||
*/
|
||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
|
||||
import {
|
||||
EmoteCallback,
|
||||
EntersCallback,
|
||||
LeavesCallback,
|
||||
MovesCallback,
|
||||
PlayerDetailsUpdatedCallback,
|
||||
Zone,
|
||||
} from "./Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { User } from "../Model/User";
|
||||
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
|
||||
|
||||
interface ZoneDescriptor {
|
||||
i: number;
|
||||
@ -42,7 +49,8 @@ export class PositionNotifier {
|
||||
private onUserEnters: EntersCallback,
|
||||
private onUserMoves: MovesCallback,
|
||||
private onUserLeaves: LeavesCallback,
|
||||
private onEmote: EmoteCallback
|
||||
private onEmote: EmoteCallback,
|
||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
) {}
|
||||
|
||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||
@ -98,7 +106,15 @@ export class PositionNotifier {
|
||||
|
||||
let zone = this.zones[j][i];
|
||||
if (zone === undefined) {
|
||||
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j);
|
||||
zone = new Zone(
|
||||
this.onUserEnters,
|
||||
this.onUserMoves,
|
||||
this.onUserLeaves,
|
||||
this.onEmote,
|
||||
this.onPlayerDetailsUpdated,
|
||||
i,
|
||||
j
|
||||
);
|
||||
this.zones[j][i] = zone;
|
||||
}
|
||||
return zone;
|
||||
@ -132,4 +148,11 @@ export class PositionNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
|
||||
const position = user.getPosition();
|
||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||
zone.updatePlayerDetails(user, playerDetails);
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,11 @@ import { ServerDuplexStream } from "grpc";
|
||||
import {
|
||||
BatchMessage,
|
||||
CompanionMessage,
|
||||
FollowAbortMessage,
|
||||
FollowConfirmationMessage,
|
||||
PusherToBackMessage,
|
||||
ServerToClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SubMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||
@ -18,6 +21,8 @@ export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientM
|
||||
export class User implements Movable {
|
||||
public listenedZones: Set<Zone>;
|
||||
public group?: Group;
|
||||
private _following: User | undefined;
|
||||
private followedBy: Set<User> = new Set<User>();
|
||||
|
||||
public constructor(
|
||||
public id: number,
|
||||
@ -31,7 +36,8 @@ export class User implements Movable {
|
||||
public readonly visitCardUrl: string | null,
|
||||
public readonly name: string,
|
||||
public readonly characterLayers: CharacterLayer[],
|
||||
public readonly companion?: CompanionMessage
|
||||
public readonly companion?: CompanionMessage,
|
||||
private _outlineColor?: number | undefined
|
||||
) {
|
||||
this.listenedZones = new Set<Zone>();
|
||||
|
||||
@ -48,6 +54,45 @@ export class User implements Movable {
|
||||
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 batchTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
@ -69,4 +114,17 @@ export class User implements Movable {
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
public set outlineColor(value: number | undefined) {
|
||||
this._outlineColor = value;
|
||||
|
||||
const playerDetails = new SetPlayerDetailsMessage();
|
||||
if (value === undefined) {
|
||||
playerDetails.setRemoveoutlinecolor(true);
|
||||
} else {
|
||||
playerDetails.setOutlinecolor(value);
|
||||
}
|
||||
|
||||
this.positionNotifier.updatePlayerDetails(this, playerDetails);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,20 @@ import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { Movable } from "./Movable";
|
||||
import { Group } from "./Group";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||
import {
|
||||
EmoteEventMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
PlayerDetailsUpdatedMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
|
||||
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
|
||||
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
||||
export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
|
||||
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
|
||||
export type PlayerDetailsUpdatedCallback = (
|
||||
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
|
||||
listener: ZoneSocket
|
||||
) => void;
|
||||
|
||||
export class Zone {
|
||||
private things: Set<Movable> = new Set<Movable>();
|
||||
@ -19,6 +27,7 @@ export class Zone {
|
||||
private onMoves: MovesCallback,
|
||||
private onLeaves: LeavesCallback,
|
||||
private onEmote: EmoteCallback,
|
||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
|
||||
public readonly x: number,
|
||||
public readonly y: number
|
||||
) {}
|
||||
@ -30,21 +39,13 @@ export class Zone {
|
||||
const result = this.things.delete(thing);
|
||||
if (!result) {
|
||||
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) {
|
||||
throw new Error(
|
||||
"Could not find group " +
|
||||
thing.getId() +
|
||||
" in zone (" +
|
||||
this.x +
|
||||
"," +
|
||||
this.y +
|
||||
"). Position of group: (" +
|
||||
thing.getPosition().x +
|
||||
"," +
|
||||
thing.getPosition().y +
|
||||
")"
|
||||
`Could not find group ${thing.getId()} in zone (${this.x},${this.y}). Position of group: (${
|
||||
thing.getPosition().x
|
||||
},${thing.getPosition().y})`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -106,4 +107,14 @@ export class Zone {
|
||||
this.onEmote(emoteEventMessage, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
|
||||
const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage();
|
||||
playerDetailsUpdatedMessage.setUserid(user.id);
|
||||
playerDetailsUpdatedMessage.setDetails(playerDetails);
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,13 @@ import {
|
||||
AdminPusherToBackMessage,
|
||||
AdminRoomMessage,
|
||||
BanMessage,
|
||||
BanUserMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmotePromptMessage,
|
||||
FollowRequestMessage,
|
||||
FollowConfirmationMessage,
|
||||
FollowAbortMessage,
|
||||
EmptyMessage,
|
||||
ItemEventMessage,
|
||||
JoinRoomMessage,
|
||||
@ -16,7 +20,9 @@ import {
|
||||
QueryJitsiJwtMessage,
|
||||
RefreshRoomPromptMessage,
|
||||
RoomMessage,
|
||||
SendUserMessage,
|
||||
ServerToAdminClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SilentMessage,
|
||||
UserMovesMessage,
|
||||
VariableMessage,
|
||||
@ -100,11 +106,6 @@ const roomManager: IRoomManagerServer = {
|
||||
user,
|
||||
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||
);
|
||||
} else if (message.hasPlayglobalmessage()) {
|
||||
socketManager.emitPlayGlobalMessage(
|
||||
room,
|
||||
message.getPlayglobalmessage() as PlayGlobalMessage
|
||||
);
|
||||
} else if (message.hasQueryjitsijwtmessage()) {
|
||||
socketManager.handleQueryJitsiJwtMessage(
|
||||
user,
|
||||
@ -116,16 +117,37 @@ const roomManager: IRoomManagerServer = {
|
||||
user,
|
||||
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()) {
|
||||
const sendUserMessage = message.getSendusermessage();
|
||||
if (sendUserMessage !== undefined) {
|
||||
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
||||
}
|
||||
socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
|
||||
} else if (message.hasBanusermessage()) {
|
||||
const banUserMessage = message.getBanusermessage();
|
||||
if (banUserMessage !== undefined) {
|
||||
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
||||
}
|
||||
socketManager.handlerBanUserMessage(room, user, banUserMessage as BanUserMessage);
|
||||
} else if (message.hasSetplayerdetailsmessage()) {
|
||||
const setPlayerDetailsMessage = message.getSetplayerdetailsmessage();
|
||||
socketManager.handleSetPlayerDetails(
|
||||
room,
|
||||
user,
|
||||
setPlayerDetailsMessage as SetPlayerDetailsMessage
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unhandled message type");
|
||||
}
|
||||
@ -160,7 +182,7 @@ const roomManager: IRoomManagerServer = {
|
||||
socketManager
|
||||
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => {
|
||||
emitErrorOnZoneSocket(call, e.toString());
|
||||
emitErrorOnZoneSocket(call, e);
|
||||
});
|
||||
|
||||
call.on("cancelled", () => {
|
||||
@ -190,7 +212,7 @@ const roomManager: IRoomManagerServer = {
|
||||
const roomMessage = call.request;
|
||||
|
||||
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
||||
emitErrorOnRoomSocket(call, e.toString());
|
||||
emitErrorOnRoomSocket(call, e);
|
||||
});
|
||||
|
||||
call.on("cancelled", () => {
|
||||
@ -251,7 +273,12 @@ const roomManager: IRoomManagerServer = {
|
||||
},
|
||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
socketManager
|
||||
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||
.sendAdminMessage(
|
||||
call.request.getRoomid(),
|
||||
call.request.getRecipientuuid(),
|
||||
call.request.getMessage(),
|
||||
call.request.getType()
|
||||
)
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
callback(null, new EmptyMessage());
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { Readable } from "stream";
|
||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { createWriteStream } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import Busboy from "busboy";
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { ReadStream } from "fs";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import { MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||
import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||
import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||
import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||
|
||||
class AdminApi {
|
||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||
@ -17,6 +17,12 @@ class AdminApi {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
const EventEmitter = require("events");
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
const clientJoinEvent = "clientJoin";
|
||||
const clientLeaveEvent = "clientLeave";
|
||||
|
@ -32,7 +32,7 @@ class MapFetcher {
|
||||
//throw new 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;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,19 @@ import {
|
||||
import { UserSocket } from "_Model/User";
|
||||
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();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
@ -23,8 +35,9 @@ export function emitError(Client: UserSocket, message: string): void {
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void {
|
||||
console.error(message);
|
||||
export function emitErrorOnRoomSocket(Client: RoomSocket, error: unknown): void {
|
||||
console.error(error);
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
@ -41,8 +54,9 @@ export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void {
|
||||
console.error(message);
|
||||
export function emitErrorOnZoneSocket(Client: ZoneSocket, error: unknown): void {
|
||||
console.error(error);
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
@ -30,9 +30,14 @@ import {
|
||||
BanUserMessage,
|
||||
RefreshRoomMessage,
|
||||
EmotePromptMessage,
|
||||
FollowRequestMessage,
|
||||
FollowConfirmationMessage,
|
||||
FollowAbortMessage,
|
||||
VariableMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
PlayerDetailsUpdatedMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { User, UserSocket } from "../Model/User";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
@ -151,20 +156,9 @@ export class SocketManager {
|
||||
//room.setViewport(client, client.viewport);
|
||||
}
|
||||
|
||||
// Useless now, will be useful again if we allow editing details in game
|
||||
/*handleSetPlayerDetails(client: UserSocket, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
const playerDetails = {
|
||||
name: playerDetailsMessage.getName(),
|
||||
characterLayers: playerDetailsMessage.getCharacterlayersList()
|
||||
};
|
||||
//console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||
emitError(client, 'Invalid SET_PLAYER_DETAILS message received: ');
|
||||
return;
|
||||
}
|
||||
client.name = playerDetails.name;
|
||||
client.characterLayers = SocketManager.mergeCharacterLayersAndCustomTextures(playerDetails.characterLayers, client.textures);
|
||||
}*/
|
||||
handleSetPlayerDetails(room: GameRoom, user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
room.updatePlayerDetails(user, playerDetailsMessage);
|
||||
}
|
||||
|
||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
||||
room.setSilent(user, silentMessage.getSilent());
|
||||
@ -206,7 +200,7 @@ export class SocketManager {
|
||||
webrtcSignalToClient.setSignal(data.getSignal());
|
||||
// TODO: only compute credentials if data.signal.type === "offer"
|
||||
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.setWebrtcpassword(password);
|
||||
}
|
||||
@ -236,7 +230,7 @@ export class SocketManager {
|
||||
webrtcSignalToClient.setSignal(data.getSignal());
|
||||
// TODO: only compute credentials if data.signal.type === "offer"
|
||||
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.setWebrtcpassword(password);
|
||||
}
|
||||
@ -282,7 +276,9 @@ export class SocketManager {
|
||||
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
this.onEmote(emoteEventMessage, listener),
|
||||
(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) =>
|
||||
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener)
|
||||
)
|
||||
.then((gameRoom) => {
|
||||
gaugeManager.incNbRoomGauge();
|
||||
@ -317,7 +313,7 @@ export class SocketManager {
|
||||
if (thing instanceof User) {
|
||||
const userJoinedZoneMessage = new UserJoinedZoneMessage();
|
||||
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.setUseruuid(thing.uuid);
|
||||
@ -329,6 +325,12 @@ export class SocketManager {
|
||||
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
|
||||
}
|
||||
userJoinedZoneMessage.setCompanion(thing.companion);
|
||||
if (thing.outlineColor === undefined) {
|
||||
userJoinedZoneMessage.setHasoutline(false);
|
||||
} else {
|
||||
userJoinedZoneMessage.setHasoutline(true);
|
||||
userJoinedZoneMessage.setOutlinecolor(thing.outlineColor);
|
||||
}
|
||||
|
||||
const subMessage = new SubToPusherMessage();
|
||||
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
|
||||
@ -378,6 +380,13 @@ export class SocketManager {
|
||||
emitZoneMessage(subMessage, client);
|
||||
}
|
||||
|
||||
private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) {
|
||||
const subMessage = new SubToPusherMessage();
|
||||
subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage);
|
||||
|
||||
emitZoneMessage(subMessage, client);
|
||||
}
|
||||
|
||||
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
|
||||
const position = group.getPosition();
|
||||
const pointMessage = new PointMessage();
|
||||
@ -440,7 +449,10 @@ export class SocketManager {
|
||||
webrtcStartMessage1.setUserid(otherUser.id);
|
||||
webrtcStartMessage1.setInitiator(true);
|
||||
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.setWebrtcpassword(password);
|
||||
}
|
||||
@ -454,7 +466,7 @@ export class SocketManager {
|
||||
webrtcStartMessage2.setUserid(user.id);
|
||||
webrtcStartMessage2.setInitiator(false);
|
||||
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.setWebrtcpassword(password);
|
||||
}
|
||||
@ -478,7 +490,7 @@ export class SocketManager {
|
||||
hmac.setEncoding("base64");
|
||||
hmac.write(username);
|
||||
hmac.end();
|
||||
const password = hmac.read();
|
||||
const password = hmac.read() as string;
|
||||
return {
|
||||
username: username,
|
||||
password: password,
|
||||
@ -519,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>> {
|
||||
return this.roomsPromises;
|
||||
}
|
||||
@ -572,7 +575,7 @@ export class SocketManager {
|
||||
user.socket.write(serverToClientMessage);
|
||||
}
|
||||
|
||||
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
||||
public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
||||
const sendUserMessage = new SendUserMessage();
|
||||
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
||||
sendUserMessage.setType(sendUserMessageToSend.getType());
|
||||
@ -691,7 +694,7 @@ export class SocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string, type: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error(
|
||||
@ -715,7 +718,7 @@ export class SocketManager {
|
||||
for (const recipient of recipients) {
|
||||
const sendUserMessage = new SendUserMessage();
|
||||
sendUserMessage.setMessage(message);
|
||||
sendUserMessage.setType("ban"); //todo: is the type correct?
|
||||
sendUserMessage.setType(type);
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setSendusermessage(sendUserMessage);
|
||||
@ -833,6 +836,39 @@ export class SocketManager {
|
||||
emoteEventMessage.setActoruserid(user.id);
|
||||
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();
|
||||
|
@ -101,11 +101,11 @@ export class VariablesManager {
|
||||
}
|
||||
|
||||
// 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") {
|
||||
for (const innerLayer of layer.layers) {
|
||||
for (const innerLayer of layer.layers as ITiledMapLayer[]) {
|
||||
this.recursiveFindVariablesInLayer(innerLayer, objects);
|
||||
}
|
||||
}
|
||||
@ -116,7 +116,7 @@ export class VariablesManager {
|
||||
|
||||
if (object.properties) {
|
||||
for (const property of object.properties) {
|
||||
const value = property.value;
|
||||
const value = property.value as unknown;
|
||||
switch (property.name) {
|
||||
case "default":
|
||||
variable.defaultValue = JSON.stringify(value);
|
||||
|
@ -51,7 +51,8 @@ describe("GameRoom", () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
@ -86,7 +87,8 @@ describe("GameRoom", () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
@ -125,7 +127,8 @@ describe("GameRoom", () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
@ -1,11 +1,10 @@
|
||||
import "jasmine";
|
||||
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
||||
import {User, UserSocket} from "../src/Model/User";
|
||||
import {Zone} from "_Model/Zone";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {ZoneSocket} from "../src/RoomManager";
|
||||
|
||||
import { PositionNotifier } from "../src/Model/PositionNotifier";
|
||||
import { User, UserSocket } from "../src/Model/User";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { ZoneSocket } from "../src/RoomManager";
|
||||
|
||||
describe("PositionNotifier", () => {
|
||||
it("should receive notifications when player moves", () => {
|
||||
@ -13,27 +12,59 @@ describe("PositionNotifier", () => {
|
||||
let moveTriggered = false;
|
||||
let leaveTriggered = false;
|
||||
|
||||
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
|
||||
enterTriggered = true;
|
||||
}, (thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
}, (thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
}, () => {});
|
||||
const positionNotifier = new PositionNotifier(
|
||||
300,
|
||||
300,
|
||||
(thing: Movable) => {
|
||||
enterTriggered = true;
|
||||
},
|
||||
(thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
},
|
||||
(thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = new User(1, 'test', '10.0.0.2', {
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||
const user1 = new User(
|
||||
1,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
const user2 = new User(2, 'test', '10.0.0.2', {
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||
const user2 = new User(
|
||||
2,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
||||
@ -46,21 +77,21 @@ describe("PositionNotifier", () => {
|
||||
bottom: 500
|
||||
});*/
|
||||
|
||||
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
|
||||
// Move inside the zone
|
||||
user2.setPosition({x:501, y:500, direction: 'down', moving: false});
|
||||
user2.setPosition({ x: 501, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(true);
|
||||
moveTriggered = false;
|
||||
|
||||
// Move out of the zone in a zone that we don't track
|
||||
user2.setPosition({x: 901, y: 500, direction: 'down', moving: false});
|
||||
user2.setPosition({ x: 901, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
@ -68,7 +99,7 @@ describe("PositionNotifier", () => {
|
||||
leaveTriggered = false;
|
||||
|
||||
// Move back in
|
||||
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
@ -88,27 +119,59 @@ describe("PositionNotifier", () => {
|
||||
let moveTriggered = false;
|
||||
let leaveTriggered = false;
|
||||
|
||||
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable, fromZone: Zone|null ) => {
|
||||
enterTriggered = true;
|
||||
}, (thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
}, (thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
}, () => {});
|
||||
const positionNotifier = new PositionNotifier(
|
||||
300,
|
||||
300,
|
||||
(thing: Movable, fromZone: Zone | null) => {
|
||||
enterTriggered = true;
|
||||
},
|
||||
(thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
},
|
||||
(thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = new User(1, 'test', '10.0.0.2', {
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||
const user1 = new User(
|
||||
1,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
const user2 = new User(2, 'test', '10.0.0.2', {
|
||||
x: 0,
|
||||
y: 0,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||
const user2 = new User(
|
||||
2,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
const listener = {} as ZoneSocket;
|
||||
positionNotifier.addZoneListener(listener, 0, 0);
|
||||
@ -124,14 +187,12 @@ describe("PositionNotifier", () => {
|
||||
positionNotifier.enter(user1);
|
||||
positionNotifier.enter(user2);
|
||||
|
||||
|
||||
//expect(newUsers.length).toBe(2);
|
||||
expect(enterTriggered).toBe(true);
|
||||
enterTriggered = false;
|
||||
|
||||
|
||||
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
|
||||
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
@ -182,4 +243,4 @@ describe("PositionNotifier", () => {
|
||||
enterTriggered = false;
|
||||
//expect(newUsers.length).toBe(2);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
2668
back/yarn.lock
@ -73,7 +73,7 @@ services:
|
||||
DEBUG: "socket:*"
|
||||
STARTUP_COMMAND_1: yarn install
|
||||
# wait for files generated by "messages" container to exists
|
||||
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||
STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||
SECRET_KEY: yourSecretKey
|
||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||
@ -132,7 +132,7 @@ services:
|
||||
DEBUG: "*"
|
||||
STARTUP_COMMAND_1: yarn install
|
||||
# wait for files generated by "messages" container to exists
|
||||
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||
STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||
SECRET_KEY: yourSecretKey
|
||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||
ALLOW_ARTILLERY: "true"
|
||||
|
@ -4,7 +4,7 @@
|
||||
### Listen to camera updates
|
||||
|
||||
```
|
||||
WA.camera.onCameraUpdate(callback: WasCameraUpdatedEventCallback): void
|
||||
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.
|
||||
@ -19,5 +19,6 @@ The event has the following attributes :
|
||||
|
||||
Example :
|
||||
```javascript
|
||||
WA.camera.onCameraUpdate((worldView) => console.log(worldView));
|
||||
```
|
||||
const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView));
|
||||
//later...
|
||||
subscription.unsubscribe();
|
||||
|
@ -107,6 +107,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
|
||||
```
|
||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||
@ -151,3 +172,25 @@ Example:
|
||||
```javascript
|
||||
WA.player.state.toto //will retrieve the variable
|
||||
```
|
||||
|
||||
### Set the outline color of the player
|
||||
```
|
||||
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;
|
||||
WA.player.removeOutlineColor(): Promise<void>;
|
||||
```
|
||||
|
||||
You can display a thin line around your player's name (the "outline").
|
||||
|
||||
Use `setOutlineColor` to set the outline and `removeOutlineColor` to remove it.
|
||||
|
||||
Colors are expressed in RGB. Each parameter is an integer between 0 and 255.
|
||||
|
||||
```typescript
|
||||
// Let's add a red outline to our player
|
||||
WA.player.setOutlineColor(255, 0, 0);
|
||||
```
|
||||
|
||||
When you set the outline on your player, other players will see the outline too (the outline color is shared across
|
||||
browsers automatically).
|
||||
|
||||
![](images/outlines.png)
|
||||
|
@ -1,8 +1,11 @@
|
||||
{.section-title.accent.text-primary}
|
||||
|
||||
# API Room functions Reference
|
||||
|
||||
### 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 :
|
||||
<div class="row">
|
||||
@ -12,6 +15,7 @@ Example :
|
||||
</div>
|
||||
|
||||
The name of the layers of this map are :
|
||||
|
||||
* `entries/start`
|
||||
* `bottom/ground/under`
|
||||
* `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.
|
||||
|
||||
* **name**: the name of the layer who as defined in Tiled.
|
||||
* **name**: the name of the layer who as defined in Tiled.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
WA.room.onEnterLayer('myLayer').subscribe(() => {
|
||||
WA.chat.sendChatMessage("Hello!", 'Mr Robot');
|
||||
WA.chat.sendChatMessage("Hello!", 'Mr Robot');
|
||||
});
|
||||
|
||||
WA.room.onLeaveLayer('myLayer').subscribe(() => {
|
||||
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
|
||||
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
|
||||
});
|
||||
```
|
||||
|
||||
### Show / Hide a layer
|
||||
|
||||
```
|
||||
WA.room.showLayer(layerName : string): void
|
||||
WA.room.hideLayer(layerName : string) : void
|
||||
```
|
||||
These 2 methods can be used to show and hide a layer.
|
||||
if `layerName` is the name of a group layer, show/hide all the layer in that group layer.
|
||||
|
||||
These 2 methods can be used to show and hide a layer. if `layerName` is the name of a group layer, show/hide all the
|
||||
layer in that group layer.
|
||||
|
||||
Example :
|
||||
|
||||
```javascript
|
||||
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;
|
||||
```
|
||||
|
||||
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
|
||||
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist,
|
||||
create the property `propertyName` and set the value of the property at `propertyValue`.
|
||||
|
||||
Note :
|
||||
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
|
||||
|
||||
Example :
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.room.id`
|
||||
{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.id`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
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"
|
||||
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"
|
||||
})
|
||||
```
|
||||
|
||||
@ -97,19 +105,17 @@ WA.room.mapURL: string;
|
||||
|
||||
The URL of the map is available from the `WA.room.mapURL` property.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.room.mapURL`
|
||||
{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.mapURL`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Map URL: ', WA.room.mapURL);
|
||||
// Will output something like: 'https://mymap.org/map.json"
|
||||
console.log('Map URL: ', WA.room.mapURL);
|
||||
// Will output something like: 'https://mymap.org/map.json"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Getting map data
|
||||
|
||||
```
|
||||
WA.room.getTiledMap(): Promise<ITiledMap>
|
||||
```
|
||||
@ -121,12 +127,16 @@ const map = await WA.room.getTiledMap();
|
||||
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
|
||||
|
||||
```
|
||||
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`.
|
||||
|
||||
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>
|
||||
|
||||
`TileDescriptor` has the following attributes :
|
||||
|
||||
* **x (number) :** The coordinate x of the tile that you want to replace.
|
||||
* **y (number) :** The coordinate y of the tile that you want to replace.
|
||||
* **tile (number | string) :** The id of the tile that will be placed in the map.
|
||||
* **layer (string) :** The name of the layer where the tile will be placed.
|
||||
|
||||
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor.
|
||||
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want
|
||||
to the id of the tile in Tiled Editor.
|
||||
|
||||
Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
|
||||
|
||||
Example :
|
||||
|
||||
```javascript
|
||||
WA.room.setTiles([
|
||||
{x: 6, y: 4, tile: 'blue', layer: 'setTiles'},
|
||||
{x: 7, y: 4, tile: 109, layer: 'setTiles'},
|
||||
{x: 8, y: 4, tile: 109, layer: 'setTiles'},
|
||||
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
|
||||
]);
|
||||
{ x: 6, y: 4, tile: 'blue', layer: 'setTiles' },
|
||||
{ x: 7, y: 4, tile: 109, layer: 'setTiles' },
|
||||
{ x: 8, y: 4, tile: 109, layer: 'setTiles' },
|
||||
{ x: 9, y: 4, tile: 'blue', layer: 'setTiles' }
|
||||
]);
|
||||
```
|
||||
|
||||
### Loading a tileset
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
You can create a tileset file in Tile Editor.
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@ -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>
|
||||
```
|
||||
|
||||
You can get an instance of an embedded website by using the `WA.room.website.get()` method.
|
||||
It returns a promise of an `EmbeddedWebsite` instance.
|
||||
You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of
|
||||
an `EmbeddedWebsite` instance.
|
||||
|
||||
```javascript
|
||||
// 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;
|
||||
```
|
||||
|
||||
|
||||
### Adding a new website in a map
|
||||
|
||||
```
|
||||
@ -201,34 +215,38 @@ interface CreateEmbeddedWebsiteEvent {
|
||||
name: string; // A unique name for this iframe
|
||||
url: string; // The URL the iframe points to.
|
||||
position: {
|
||||
x: number, // In pixels, relative to the map coordinates
|
||||
y: number, // In pixels, relative to the map coordinates
|
||||
width: number, // In pixels, sensitive to zoom level
|
||||
height: number, // In pixels, sensitive to zoom level
|
||||
x: number, // In "game" pixels, relative to the map or player coordinates, depending on origin
|
||||
y: number, // In "game" pixels, relative to the map or player coordinates, depending on origin
|
||||
width: number, // In "game" pixels
|
||||
height: number, // In "game" pixels
|
||||
},
|
||||
visible?: boolean, // Whether to display the iframe or not
|
||||
allowApi?: boolean, // Whether the scripting API should be available to the iframe
|
||||
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.
|
||||
It returns an `EmbeddedWebsite` instance.
|
||||
You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns
|
||||
an `EmbeddedWebsite` instance.
|
||||
|
||||
```javascript
|
||||
// Create a new website object
|
||||
const website = WA.room.website.create({
|
||||
name: "my_website",
|
||||
url: "https://example.com",
|
||||
position: {
|
||||
x: 64,
|
||||
y: 128,
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
visible: true,
|
||||
allowApi: true,
|
||||
allow: "fullscreen",
|
||||
name: "my_website",
|
||||
url: "https://example.com",
|
||||
position: {
|
||||
x: 64,
|
||||
y: 128,
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
visible: true,
|
||||
allowApi: true,
|
||||
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.
|
||||
|
||||
|
||||
### The EmbeddedWebsite class
|
||||
|
||||
Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
|
||||
|
||||
```typescript
|
||||
class EmbeddedWebsite {
|
||||
readonly name: string;
|
||||
url: string;
|
||||
visible: boolean;
|
||||
allow: string;
|
||||
allowApi: boolean;
|
||||
x: number; // In pixels, relative to the map coordinates
|
||||
y: number; // In pixels, relative to the map coordinates
|
||||
width: number; // In pixels, sensitive to zoom level
|
||||
height: number; // In pixels, sensitive to zoom level
|
||||
readonly name: string;
|
||||
url: string;
|
||||
visible: boolean;
|
||||
allow: string;
|
||||
allowApi: boolean;
|
||||
x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
|
||||
y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
|
||||
width: number; // In "game" pixels
|
||||
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.
|
||||
|
||||
|
||||
{.alert.alert-warning}
|
||||
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.
|
||||
{.alert.alert-warning} 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.
|
||||
|
||||
|
@ -29,22 +29,28 @@ It is possible to define special regions on the map that can make the camera zoo
|
||||
<img class="document-img" src="images/camera/3_define_new_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
4. Edit this new object and click on **Add Property**, like this:
|
||||
4. Make sure your object is of type "zone"!
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/4_click_add_property.png" alt="" />
|
||||
<img class="document-img" src="images/camera/4_add_zone_type.png" alt="" />
|
||||
</div>
|
||||
|
||||
5. Add a **bool** property of name *focusable*:
|
||||
5. Edit this new object and click on **Add Property**, like this:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/5_add_focusable_prop.png" alt="" />
|
||||
<img class="document-img" src="images/camera/5_click_add_property.png" alt="" />
|
||||
</div>
|
||||
|
||||
6. Make sure it's checked! :)
|
||||
6. Add a **bool** property of name *focusable*:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/6_make_sure_checked.png" alt="" />
|
||||
<img class="document-img" src="images/camera/6_add_focusable_prop.png" alt="" />
|
||||
</div>
|
||||
|
||||
7. Make sure it's checked! :)
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/7_make_sure_checked.png" alt="" />
|
||||
</div>
|
||||
|
||||
All should be set up now and your new **Focusable Zone** should be working fine!
|
||||
@ -56,19 +62,19 @@ If you want, you can add an additional property to control how much should the c
|
||||
1. Like before, click on **Add Property**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/4_click_add_property.png" alt="" />
|
||||
<img class="document-img" src="images/camera/5_click_add_property.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Add a **float** property of name *zoom_margin*:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/7_add_zoom_margin.png" alt="" />
|
||||
<img class="document-img" src="images/camera/8_add_zoom_margin.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Define how much (in percentage value) should the zoom be decreased:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/8_optional_zoom_margin_defined.png" alt="" />
|
||||
<img class="document-img" src="images/camera/9_optional_zoom_margin_defined.png" alt="" />
|
||||
</div>
|
||||
|
||||
For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300.
|
||||
|
BIN
docs/maps/images/camera/4_add_zone_type.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@ -60,7 +60,7 @@ WA.chat.sendChatMessage('Hello world', 'Mr Robot');
|
||||
|
||||
The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.chat.sendChatMessage` opens the chat and adds a message in it.
|
||||
|
||||
In your browser console, when you open the map, the chat message should be displayed right away.
|
||||
The message should be displayed in the chat history as soon as you enter the room.
|
||||
|
||||
## Adding a script in an iFrame
|
||||
|
||||
|
@ -98,13 +98,14 @@ The exception is the "collides" property that can only be set on tiles, but not
|
||||
By setting properties on the map itself, you can help visitors know more about the creators of the map.
|
||||
|
||||
The following *map* properties are supported:
|
||||
* `mapName` (string)
|
||||
* `mapDescription` (string)
|
||||
* `mapCopyright` (string)
|
||||
* `mapName` (string): The name of your map
|
||||
* `mapLink` (string): A link to your map, for example a repository
|
||||
* `mapDescription` (string): A short description of your map
|
||||
* `mapCopyright` (string): Copyright notice
|
||||
|
||||
And *each tileset* can also have a property called `tilesetCopyright` (string).
|
||||
Each *tileset* can also have a property called `tilesetCopyright` (string).
|
||||
If you are using audio files in your map, you can declare a layer property `audioCopyright` (string).
|
||||
|
||||
Resulting in a "credit" page in the menu looking like this:
|
||||
|
||||
![](images/mapProperties.png){.document-img}
|
||||
|
||||
|
@ -34,14 +34,15 @@ module.exports = {
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"no-throw-literal": "error",
|
||||
// TODO: remove those ignored rules and write a stronger code!
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off"
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
},
|
||||
"settings": {
|
||||
"svelte3/typescript": true,
|
||||
|
@ -1 +1,2 @@
|
||||
src/Messages/generated
|
||||
src/Messages/JsonMessages
|
||||
|
@ -1,13 +1,15 @@
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
|
||||
WORKDIR /usr/src
|
||||
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
|
||||
FROM thecodingmachine/nodejs:14-apache
|
||||
|
||||
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
|
||||
|
||||
# Removing the iframe.html file from the final image as this adds a XSS attack.
|
||||
# iframe.html is only in dev mode to circumvent a limitation
|
||||
|
2
front/dist/.htaccess
vendored
@ -20,7 +20,7 @@ RewriteBase /
|
||||
# We only want to let Apache serve files and not directories.
|
||||
# Rewrite all other queries starting with _ to index.ts.
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule "^[_@]/" "/index.html" [L]
|
||||
RewriteRule "^[_@*]/" "/index.html" [L]
|
||||
RewriteRule "^register/" "/index.html" [L]
|
||||
RewriteRule "^login" "/index.html" [L]
|
||||
RewriteRule "^jwt" "/index.html" [L]
|
||||
|
@ -12,12 +12,12 @@
|
||||
"@types/quill": "^1.3.7",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@types/webpack-dev-server": "^3.11.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"css-loader": "^5.2.4",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-plugin-svelte3": "^3.2.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.9",
|
||||
"fork-ts-checker-webpack-plugin": "^6.5.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"jasmine": "^3.5.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
@ -32,10 +32,10 @@
|
||||
"svelte-check": "^2.1.0",
|
||||
"svelte-loader": "^3.1.1",
|
||||
"svelte-preprocess": "^4.7.3",
|
||||
"ts-loader": "^9.1.2",
|
||||
"ts-node": "^9.1.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "^4.2.4",
|
||||
"typescript": "^4.5.3",
|
||||
"webpack": "^5.37.0",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
@ -47,6 +47,7 @@
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"axios": "^0.21.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-copy-ts": "^0.5.0",
|
||||
"generic-type-guard": "^3.2.0",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"phaser": "^3.54.0",
|
||||
@ -61,6 +62,7 @@
|
||||
"simple-peer": "^9.11.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"standardized-audio-context": "^25.2.4",
|
||||
"ts-proto": "^1.96.0",
|
||||
"uuidv4": "^6.2.10"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -18,64 +18,84 @@ class AnalyticsClient {
|
||||
}
|
||||
|
||||
identifyUser(uuid: string, email: string | null) {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.identify(uuid, { uuid, email, wa: true });
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.identify(uuid, { uuid, email, wa: true });
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
loggedWithSso() {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-logged-sso");
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-logged-sso");
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
loggedWithToken() {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-logged-token");
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-logged-token");
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
enteredRoom(roomId: string, roomGroup: string | null) {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("$pageView", { roomId, roomGroup });
|
||||
posthog.capture("enteredRoom");
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("$pageView", { roomId, roomGroup });
|
||||
posthog.capture("enteredRoom");
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
openedMenu() {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-opened-menu");
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-opened-menu");
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
launchEmote(emote: string) {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-emote-launch", { emote });
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-emote-launch", { emote });
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
enteredJitsi(roomName: string, roomId: string) {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-entered-jitsi", { roomName, roomId });
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-entered-jitsi", { roomName, roomId });
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
validationName() {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-name-validation");
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-name-validation");
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
validationWoka(scene: string) {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-woka-validation", { scene });
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-woka-validation", { scene });
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
validationVideo() {
|
||||
this.posthogPromise?.then((posthog) => {
|
||||
posthog.capture("wa-video-validation");
|
||||
});
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-video-validation");
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
export const analyticsClient = new AnalyticsClient();
|
||||
|
@ -1,27 +1,22 @@
|
||||
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 { UPLOADER_URL } from "../Enum/EnvironmentVariable";
|
||||
import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
|
||||
class UserMessageManager {
|
||||
receiveBannedMessageListener!: Function;
|
||||
|
||||
constructor() {
|
||||
adminMessagesService.messageStream.subscribe((event) => {
|
||||
textMessageVisibleStore.set(false);
|
||||
banMessageVisibleStore.set(false);
|
||||
if (event.type === AdminMessageEventTypes.admin) {
|
||||
textMessageContentStore.set(event.text);
|
||||
textMessageVisibleStore.set(true);
|
||||
textMessageStore.addMessage(event.text);
|
||||
} else if (event.type === AdminMessageEventTypes.audio) {
|
||||
soundPlayingStore.playSound(UPLOADER_URL + event.text);
|
||||
} else if (event.type === AdminMessageEventTypes.ban) {
|
||||
banMessageContentStore.set(event.text);
|
||||
banMessageVisibleStore.set(true);
|
||||
banMessageStore.addMessage(event.text);
|
||||
} else if (event.type === AdminMessageEventTypes.banned) {
|
||||
banMessageContentStore.set(event.text);
|
||||
banMessageVisibleStore.set(true);
|
||||
banMessageStore.addMessage(event.text);
|
||||
this.receiveBannedMessageListener();
|
||||
}
|
||||
});
|
||||
|
13
front/src/Api/Events/ColorEvent.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isColorEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
red: tg.isNumber,
|
||||
green: tg.isNumber,
|
||||
blue: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to dynamically set the outline of the player.
|
||||
*/
|
||||
export type ColorEvent = tg.GuardedType<typeof isColorEvent>;
|
@ -34,6 +34,7 @@ import type { ChangeZoneEvent } from "./ChangeZoneEvent";
|
||||
import type { CameraSetPositionEvent } from "./CameraSetPositionEvent";
|
||||
import type { CameraFocusOnEvent } from "./CameraFocusOnEvent";
|
||||
import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
|
||||
import { isColorEvent } from "./ColorEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
@ -162,6 +163,14 @@ export const iframeQueryMapTypeGuards = {
|
||||
query: isCreateEmbeddedWebsiteEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
setPlayerOutline: {
|
||||
query: isColorEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
removePlayerOutline: {
|
||||
query: tg.isUndefined,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
getPlayerPosition: {
|
||||
query: tg.isUndefined,
|
||||
answer: isPlayerPosition,
|
||||
|
@ -26,7 +26,7 @@ export class ActionMessage {
|
||||
this.message = actionMessageOptions.message;
|
||||
this.type = actionMessageOptions.type ?? "message";
|
||||
this.callback = actionMessageOptions.callback;
|
||||
this.create();
|
||||
this.create().catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
private async create() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { Subject } from "rxjs";
|
||||
import type { WasCameraUpdatedEvent, WasCameraUpdatedEventCallback } from "../Events/WasCameraUpdatedEvent";
|
||||
import type { WasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { isWasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
|
||||
|
||||
@ -38,12 +38,12 @@ export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdven
|
||||
});
|
||||
}
|
||||
|
||||
onCameraUpdate(callback: WasCameraUpdatedEventCallback): void {
|
||||
moveStream.subscribe(callback);
|
||||
onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
|
||||
sendToWorkadventure({
|
||||
type: "onCameraUpdate",
|
||||
data: null,
|
||||
});
|
||||
return moveStream;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +92,24 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
||||
}
|
||||
return userRoomToken;
|
||||
}
|
||||
|
||||
public setOutlineColor(red: number, green: number, blue: number): Promise<void> {
|
||||
return queryWorkadventure({
|
||||
type: "setPlayerOutline",
|
||||
data: {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public removeOutlineColor(): Promise<void> {
|
||||
return queryWorkadventure({
|
||||
type: "removePlayerOutline",
|
||||
data: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type Position = {
|
||||
|
@ -95,7 +95,7 @@ export function createState(target: "global" | "player"): WorkadventureStateComm
|
||||
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.
|
||||
// User must use WA.state.saveVariable to have error message.
|
||||
target.saveVariable(p.toString(), value);
|
||||
target.saveVariable(p.toString(), value).catch((e) => console.error(e));
|
||||
return true;
|
||||
},
|
||||
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
|
||||
|
@ -23,6 +23,9 @@
|
||||
import { chatVisibilityStore } from "../Stores/ChatStore";
|
||||
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
|
||||
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
|
||||
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
|
||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
|
||||
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
||||
@ -30,10 +33,10 @@
|
||||
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
|
||||
import VideoOverlay from "./Video/VideoOverlay.svelte";
|
||||
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
||||
import AdminMessage from "./TypeMessage/BanMessage.svelte";
|
||||
import TextMessage from "./TypeMessage/TextMessage.svelte";
|
||||
import { banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
import { textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
|
||||
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
|
||||
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { warningContainerStore } from "../Stores/MenuStore";
|
||||
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
|
||||
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
|
||||
@ -42,6 +45,9 @@
|
||||
import AudioManager from "./AudioManager/AudioManager.svelte";
|
||||
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
|
||||
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;
|
||||
</script>
|
||||
@ -72,14 +78,13 @@
|
||||
<EnableCameraScene {game} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $banMessageVisibleStore}
|
||||
{#if $banMessageStore.length > 0}
|
||||
<div>
|
||||
<AdminMessage />
|
||||
<BanMessageContainer />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $textMessageVisibleStore}
|
||||
{:else if $textMessageStore.length > 0}
|
||||
<div>
|
||||
<TextMessage />
|
||||
<TextMessageContainer />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $soundPlayingStore}
|
||||
@ -102,6 +107,11 @@
|
||||
<ReportMenu />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $followStateStore !== "off" || $peerStore.size > 0}
|
||||
<div>
|
||||
<FollowMenu />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $menuIconVisiblilityStore}
|
||||
<div>
|
||||
<MenuIcon />
|
||||
@ -129,6 +139,16 @@
|
||||
<HelpCameraSettingsPopup />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $showLimitRoomModalStore}
|
||||
<div>
|
||||
<LimitRoomModal />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $showShareLinkMapModalStore}
|
||||
<div>
|
||||
<ShareLinkMapModal />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $requestVisitCardsStore}
|
||||
<VisitCard visitCardUrl={$requestVisitCardsStore} />
|
||||
{/if}
|
||||
|
@ -19,12 +19,13 @@
|
||||
audioManagerVolumeStore.setVolume(volume);
|
||||
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
|
||||
|
||||
unsubscriberFileStore = audioManagerFileStore.subscribe(() => {
|
||||
unsubscriberFileStore = audioManagerFileStore.subscribe((src) => {
|
||||
HTMLAudioPlayer.pause();
|
||||
HTMLAudioPlayer.src = src;
|
||||
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
|
||||
HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
|
||||
HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted;
|
||||
HTMLAudioPlayer.play();
|
||||
void HTMLAudioPlayer.play();
|
||||
});
|
||||
unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => {
|
||||
const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking;
|
||||
@ -148,9 +149,7 @@
|
||||
</label>
|
||||
<section class="audio-manager-file">
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
|
||||
<source src={$audioManagerFileStore} />
|
||||
</audio>
|
||||
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer} />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,6 +67,7 @@
|
||||
.messagePart {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
user-select: text;
|
||||
|
||||
span.date {
|
||||
font-size: 80%;
|
||||
|
197
front/src/Components/FollowMenu/FollowMenu.svelte
Normal 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>
|
@ -6,11 +6,14 @@
|
||||
|
||||
let expandedMapCopyright = false;
|
||||
let expandedTilesetCopyright = false;
|
||||
let expandedAudioCopyright = false;
|
||||
|
||||
let mapName: string = "";
|
||||
let mapLink: string = "";
|
||||
let mapDescription: string = "";
|
||||
let mapCopyright: string = "The map creator did not declare a copyright for the map.";
|
||||
let tilesetCopyright: string[] = [];
|
||||
let audioCopyright: string[] = [];
|
||||
|
||||
onMount(() => {
|
||||
if (gameScene.mapFile.properties !== undefined) {
|
||||
@ -18,6 +21,10 @@
|
||||
if (propertyName !== undefined && typeof propertyName.value === "string") {
|
||||
mapName = propertyName.value;
|
||||
}
|
||||
const propertyLink = gameScene.mapFile.properties.find((property) => property.name === "mapLink");
|
||||
if (propertyLink !== undefined && typeof propertyLink.value === "string") {
|
||||
mapLink = propertyLink.value;
|
||||
}
|
||||
const propertyDescription = gameScene.mapFile.properties.find(
|
||||
(property) => property.name === "mapDescription"
|
||||
);
|
||||
@ -36,7 +43,18 @@
|
||||
(property) => property.name === "tilesetCopyright"
|
||||
);
|
||||
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") {
|
||||
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
|
||||
// Assignment needed to trigger Svelte's reactivity
|
||||
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of gameScene.mapFile.layers) {
|
||||
if (layer.type && layer.type === "tilelayer" && layer.properties) {
|
||||
const propertyAudioCopyright = layer.properties.find((property) => property.name === "audioCopyright");
|
||||
if (propertyAudioCopyright !== undefined && typeof propertyAudioCopyright.value === "string") {
|
||||
// Assignment needed to trigger Svelte's reactivity
|
||||
audioCopyright = [...audioCopyright, propertyAudioCopyright.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -48,6 +66,9 @@
|
||||
<section class="container-overflow">
|
||||
<h3>{mapName}</h3>
|
||||
<p class="string-HTML">{mapDescription}</p>
|
||||
{#if mapLink}
|
||||
<p class="string-HTML">> <a href={mapLink} target="_blank">link to this map</a> <</p>
|
||||
{/if}
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
|
||||
Copyrights of the map
|
||||
</h3>
|
||||
@ -60,8 +81,21 @@
|
||||
<p class="string-HTML">{copyright}</p>
|
||||
{:else}
|
||||
<p>
|
||||
The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those
|
||||
tilesets have no license.
|
||||
The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets
|
||||
have no license.
|
||||
</p>
|
||||
{/each}
|
||||
</section>
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
|
||||
Copyrights of audio files
|
||||
</h3>
|
||||
<section hidden={!expandedAudioCopyright}>
|
||||
{#each audioCopyright as copyright}
|
||||
<p class="string-HTML">{copyright}</p>
|
||||
{:else}
|
||||
<p>
|
||||
The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
|
||||
have no license.
|
||||
</p>
|
||||
{/each}
|
||||
</section>
|
||||
|
@ -26,7 +26,7 @@
|
||||
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
|
||||
if (!selectedFile) {
|
||||
errorFile = true;
|
||||
throw "no file selected";
|
||||
throw new Error("no file selected");
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
|
@ -19,12 +19,12 @@
|
||||
uploadAudioActive = true;
|
||||
}
|
||||
|
||||
function send() {
|
||||
async function send(): Promise<void> {
|
||||
if (inputSendTextActive) {
|
||||
handleSendText.sendTextMessage(broadcastToWorld);
|
||||
return handleSendText.sendTextMessage(broadcastToWorld);
|
||||
}
|
||||
if (uploadAudioActive) {
|
||||
handleSendAudio.sendAudioMessage(broadcastToWorld);
|
||||
return handleSendAudio.sendAudioMessage(broadcastToWorld);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -21,12 +21,12 @@
|
||||
<div class="guest-main">
|
||||
<section class="container-overflow">
|
||||
<section class="share-url not-mobile">
|
||||
<h3>Share the link of the room !</h3>
|
||||
<h3>Share the link of the room!</h3>
|
||||
<input type="text" readonly id="input-share-link" value={location.toString()} />
|
||||
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
|
||||
</section>
|
||||
<section class="is-mobile">
|
||||
<h3>Share the link of the room !</h3>
|
||||
<h3>Share the link of the room!</h3>
|
||||
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
|
||||
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
|
||||
</section>
|
||||
|
@ -76,7 +76,7 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else throw "There is no menu called " + menu;
|
||||
} else throw new Error("There is no menu called " + menu);
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
|
@ -1,9 +1,14 @@
|
||||
<script lang="typescript">
|
||||
import logoTalk from "../images/logo-message-pixel.png";
|
||||
import logoWA from "../images/logo-WA-pixel.png";
|
||||
import logoInvite from "../images/logo-invite-pixel.png";
|
||||
import logoRegister from "../images/logo-register-pixel.png";
|
||||
import { menuVisiblilityStore } from "../../Stores/MenuStore";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { limitMapStore } from "../../Stores/GameStore";
|
||||
import { get } from "svelte/store";
|
||||
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
|
||||
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
|
||||
|
||||
function showMenu() {
|
||||
menuVisiblilityStore.set(!get(menuVisiblilityStore));
|
||||
@ -11,13 +16,25 @@
|
||||
function showChat() {
|
||||
chatVisibilityStore.set(true);
|
||||
}
|
||||
|
||||
function register() {
|
||||
window.open(`${ADMIN_URL}/second-step-register`, "_self");
|
||||
}
|
||||
function showInvite() {
|
||||
showShareLinkMapModalStore.set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window />
|
||||
|
||||
<main class="menuIcon">
|
||||
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
|
||||
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
|
||||
{#if $limitMapStore}
|
||||
<img src={logoInvite} alt="open menu" class="nes-pointer" on:click|preventDefault={showInvite} />
|
||||
<img src={logoRegister} alt="open menu" class="nes-pointer" on:click|preventDefault={register} />
|
||||
{:else}
|
||||
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
|
||||
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -41,10 +41,10 @@
|
||||
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
|
||||
}
|
||||
|
||||
function logOut() {
|
||||
async function logOut() {
|
||||
disableMenuStores();
|
||||
loginSceneVisibleStore.set(true);
|
||||
connectionManager.logout();
|
||||
return connectionManager.logout();
|
||||
}
|
||||
|
||||
function getProfileUrl() {
|
||||
|
@ -8,6 +8,7 @@
|
||||
let fullscreen: boolean = localUserStore.getFullscreen();
|
||||
let notification: boolean = localUserStore.getNotification() === "granted";
|
||||
let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger();
|
||||
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
|
||||
let valueGame: number = localUserStore.getGameQualityValue();
|
||||
let valueVideo: number = localUserStore.getVideoQualityValue();
|
||||
let previewValueGame = valueGame;
|
||||
@ -32,9 +33,9 @@
|
||||
const body = HtmlUtils.querySelectorOrFail("body");
|
||||
if (body) {
|
||||
if (document.fullscreenElement !== null && !fullscreen) {
|
||||
document.exitFullscreen();
|
||||
document.exitFullscreen().catch((e) => console.error(e));
|
||||
} else {
|
||||
body.requestFullscreen();
|
||||
body.requestFullscreen().catch((e) => console.error(e));
|
||||
}
|
||||
localUserStore.setFullscreen(fullscreen);
|
||||
}
|
||||
@ -44,14 +45,16 @@
|
||||
if (Notification.permission === "granted") {
|
||||
localUserStore.setNotification(notification ? "granted" : "denied");
|
||||
} else {
|
||||
Notification.requestPermission().then((response) => {
|
||||
if (response === "granted") {
|
||||
localUserStore.setNotification(notification ? "granted" : "denied");
|
||||
} else {
|
||||
localUserStore.setNotification("denied");
|
||||
notification = false;
|
||||
}
|
||||
});
|
||||
Notification.requestPermission()
|
||||
.then((response) => {
|
||||
if (response === "granted") {
|
||||
localUserStore.setNotification(notification ? "granted" : "denied");
|
||||
} else {
|
||||
localUserStore.setNotification("denied");
|
||||
notification = false;
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +62,10 @@
|
||||
localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger);
|
||||
}
|
||||
|
||||
function changeIgnoreFollowRequests() {
|
||||
localUserStore.setIgnoreFollowRequests(ignoreFollowRequests);
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuVisiblilityStore.set(false);
|
||||
}
|
||||
@ -123,6 +130,15 @@
|
||||
/>
|
||||
<span>Always ask before opening websites and Jitsi Meet rooms</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
47
front/src/Components/Modal/LimitRoomModal.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from "svelte/transition";
|
||||
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
function register() {
|
||||
window.open(`${ADMIN_URL}/second-step-register`, "_self");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="limit-map nes-container" transition:fly={{ y: -900, duration: 500 }}>
|
||||
<section>
|
||||
<h2>Limit of your room</h2>
|
||||
<p>Register your account!</p>
|
||||
<p>
|
||||
This map is limited in the time and to continue to use WorkAdventure, you must register your account in our
|
||||
back office.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<button class="nes-btn is-primary" on:click|preventDefault={register}>Register</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.limit-map {
|
||||
pointer-events: auto;
|
||||
background: #eceeee;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 10vh;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
|
||||
section {
|
||||
p {
|
||||
margin: 15px;
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
90
front/src/Components/Modal/ShareLinkMapModal.svelte
Normal file
@ -0,0 +1,90 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from "svelte/transition";
|
||||
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
|
||||
|
||||
interface ExtNavigator extends Navigator {
|
||||
canShare?(data?: ShareData): Promise<boolean>;
|
||||
}
|
||||
|
||||
const myNavigator: ExtNavigator = window.navigator;
|
||||
const haveNavigatorSharingFeature: boolean =
|
||||
myNavigator && myNavigator.canShare != null && myNavigator.share != null;
|
||||
|
||||
let copied: boolean = false;
|
||||
|
||||
function copyLink() {
|
||||
try {
|
||||
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
|
||||
input.focus();
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
copied = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
copied = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function shareLink() {
|
||||
const shareData = { url: location.toString() };
|
||||
|
||||
try {
|
||||
await myNavigator.share(shareData);
|
||||
} catch (err) {
|
||||
console.error("Error: " + err);
|
||||
copyLink();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
showShareLinkMapModalStore.set(false);
|
||||
copied = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="share-link-map nes-container" transition:fly={{ y: -900, duration: 500 }}>
|
||||
<section>
|
||||
<h2>Invite your friends or colleagues</h2>
|
||||
<p>Share the link of the room!</p>
|
||||
</section>
|
||||
<section>
|
||||
{#if haveNavigatorSharingFeature}
|
||||
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
|
||||
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
|
||||
{:else}
|
||||
<input type="text" readonly id="input-share-link" value={location.toString()} />
|
||||
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
|
||||
{/if}
|
||||
{#if copied}
|
||||
<p>Copied!</p>
|
||||
{/if}
|
||||
</section>
|
||||
<section>
|
||||
<button class="nes-btn" on:click|preventDefault={close}>Close</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.share-link-map {
|
||||
pointer-events: auto;
|
||||
background: #eceeee;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 10vh;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
|
||||
section {
|
||||
p {
|
||||
margin: 15px;
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -6,12 +6,11 @@
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { GameConnexionTypes } from "../../Url/UrlManager";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
let blockActive = true;
|
||||
let reportActive = !blockActive;
|
||||
let anonymous: boolean = false;
|
||||
let disableReport: boolean = false;
|
||||
let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid;
|
||||
let userName = "No name";
|
||||
let unsubscriber: Unsubscriber;
|
||||
@ -26,7 +25,7 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous;
|
||||
disableReport = !connectionManager.currentRoom?.canReport ?? true;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@ -65,7 +64,7 @@
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
|
||||
</section>
|
||||
</section>
|
||||
<section class="report-menu-action {anonymous ? 'hidden' : ''}">
|
||||
<section class="report-menu-action {disableReport ? 'hidden' : ''}">
|
||||
<section class="justify-center">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import { banMessageVisibleStore, banMessageContentStore } from "../../Stores/TypeMessageStore/BanMessageStore";
|
||||
import { fly, fade } from "svelte/transition";
|
||||
import { onMount } from "svelte";
|
||||
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
|
||||
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
|
||||
|
||||
export let message: Message;
|
||||
|
||||
const text = $banMessageContentStore;
|
||||
const NAME_BUTTON = "Ok";
|
||||
let nbSeconds = 10;
|
||||
let nameButton = "";
|
||||
@ -25,17 +27,21 @@
|
||||
}
|
||||
|
||||
function closeBanMessage() {
|
||||
banMessageVisibleStore.set(false);
|
||||
banMessageStore.clearMessageById(message.id);
|
||||
}
|
||||
</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">
|
||||
<img src="resources/logos/report.svg" alt="***" /> Important message
|
||||
<img src="resources/logos/report.svg" alt="***" />
|
||||
</h2>
|
||||
<div class="content-ban-message">
|
||||
<p>{text}</p>
|
||||
<p>{message.text}</p>
|
||||
</div>
|
||||
<div class="footer-ban-message">
|
||||
<button
|
||||
|
13
front/src/Components/TypeMessage/BanMessageContainer.svelte
Normal 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>
|
@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import { textMessageContentStore, textMessageVisibleStore } from "../../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { fly, fade } from "svelte/transition";
|
||||
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
|
||||
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
|
||||
import { textMessageStore } from "../../Stores/TypeMessageStore/TextMessageStore";
|
||||
|
||||
const content = JSON.parse($textMessageContentStore);
|
||||
export let message: Message;
|
||||
|
||||
const content = JSON.parse(message.text);
|
||||
const converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
|
||||
const NAME_BUTTON = "Ok";
|
||||
|
||||
function closeTextMessage() {
|
||||
textMessageVisibleStore.set(false);
|
||||
textMessageStore.clearMessageById(message.id);
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
@ -20,7 +23,11 @@
|
||||
|
||||
<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">
|
||||
{@html converter.convert()}
|
||||
</div>
|
||||
@ -40,6 +47,8 @@
|
||||
width: 80vw;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
pointer-events: auto;
|
||||
|
21
front/src/Components/TypeMessage/TextMessageContainer.svelte
Normal 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>
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
audio.play();
|
||||
audio.play().catch((e) => console.error(e));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,20 +1,26 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from "svelte/transition";
|
||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||
import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore";
|
||||
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
const upgradeLink = ADMIN_URL + "/pricing";
|
||||
const registerLink = ADMIN_URL + "/second-step-register";
|
||||
</script>
|
||||
|
||||
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
|
||||
<h2>Warning!</h2>
|
||||
{#if $userIsAdminStore}
|
||||
<h2>Warning!</h2>
|
||||
<p>
|
||||
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
|
||||
>here</a
|
||||
>
|
||||
</p>
|
||||
{:else if $limitMapStore}
|
||||
<p>
|
||||
This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>!
|
||||
</p>
|
||||
{:else}
|
||||
<h2>Warning!</h2>
|
||||
<p>This world is close to its limit!</p>
|
||||
{/if}
|
||||
</main>
|
||||
|
1
front/src/Components/images/follow.svg
Normal 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 |
BIN
front/src/Components/images/logo-invite-pixel.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
front/src/Components/images/logo-register-pixel.png
Normal file
After Width: | Height: | Size: 977 B |
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
admin = "message",
|
||||
@ -26,8 +26,8 @@ class AdminMessagesService {
|
||||
|
||||
onSendusermessage(message: SendUserMessage | BanUserMessage) {
|
||||
this._messageStream.next({
|
||||
type: message.getType() as unknown as AdminMessageEventTypes,
|
||||
text: message.getMessage(),
|
||||
type: message.type as unknown as AdminMessageEventTypes,
|
||||
text: message.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Axios from "axios";
|
||||
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
|
||||
import { PUSHER_URL } from "../Enum/EnvironmentVariable";
|
||||
import { RoomConnection } from "./RoomConnection";
|
||||
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
|
||||
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
|
||||
@ -8,9 +8,14 @@ import { CharacterTexture, LocalUser } from "./LocalUser";
|
||||
import { Room } from "./Room";
|
||||
import { _ServiceWorker } from "../Network/ServiceWorker";
|
||||
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
|
||||
import { userIsConnected } from "../Stores/MenuStore";
|
||||
import { userIsConnected, warningContainerStore } from "../Stores/MenuStore";
|
||||
import { analyticsClient } from "../Administration/AnalyticsClient";
|
||||
import { axiosWithRetry } from "./AxiosUtils";
|
||||
import axios from "axios";
|
||||
import { isRegisterData } from "../Messages/JsonMessages/RegisterData";
|
||||
import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
|
||||
import { limitMapStore } from "../Stores/GameStore";
|
||||
import { showLimitRoomModalStore } from "../Stores/ModalStore";
|
||||
|
||||
class ConnectionManager {
|
||||
private localUser!: LocalUser;
|
||||
@ -101,10 +106,10 @@ class ConnectionManager {
|
||||
const code = urlParams.get("code");
|
||||
const state = urlParams.get("state");
|
||||
if (!state || !localUserStore.verifyState(state)) {
|
||||
throw "Could not validate state!";
|
||||
throw new Error("Could not validate state!");
|
||||
}
|
||||
if (!code) {
|
||||
throw "No Auth code provided";
|
||||
throw new Error("No Auth code provided");
|
||||
}
|
||||
localUserStore.setCode(code);
|
||||
}
|
||||
@ -125,6 +130,10 @@ class ConnectionManager {
|
||||
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
if (!isRegisterData(data)) {
|
||||
console.error("Invalid data received from /register route. Data: ", data);
|
||||
throw new Error("Invalid data received from /register route.");
|
||||
}
|
||||
this.localUser = new LocalUser(data.userUuid, data.textures, data.email);
|
||||
this.authToken = data.authToken;
|
||||
localUserStore.saveUser(this.localUser);
|
||||
@ -145,11 +154,7 @@ class ConnectionManager {
|
||||
)
|
||||
);
|
||||
urlManager.pushRoomIdToUrl(this._currentRoom);
|
||||
} else if (
|
||||
connexionType === GameConnexionTypes.organization ||
|
||||
connexionType === GameConnexionTypes.anonymous ||
|
||||
connexionType === GameConnexionTypes.empty
|
||||
) {
|
||||
} else if (connexionType === GameConnexionTypes.room || connexionType === GameConnexionTypes.empty) {
|
||||
this.authToken = localUserStore.getAuthToken();
|
||||
|
||||
let roomPath: string;
|
||||
@ -163,6 +168,9 @@ class ConnectionManager {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err instanceof Error) {
|
||||
console.error(err.stack);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const query = urlParams.toString();
|
||||
@ -181,7 +189,7 @@ class ConnectionManager {
|
||||
|
||||
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
|
||||
//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.
|
||||
if (!this.authToken && !this._currentRoom.authenticationMandatory) {
|
||||
@ -192,11 +200,13 @@ class ConnectionManager {
|
||||
analyticsClient.loggedWithSso();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
//if user must to be connect in current room or pusher error is not openid provier access error
|
||||
//try to connected with function loadOpenIDScreen
|
||||
// if the user must be connected in the current room or if the pusher error is not openid provider access error
|
||||
// try to connect with function loadOpenIDScreen
|
||||
if (
|
||||
this._currentRoom.authenticationMandatory ||
|
||||
(err.response?.data && err.response.data !== "User cannot to be connected on openid provier")
|
||||
(axios.isAxiosError(err) &&
|
||||
err.response?.data &&
|
||||
err.response.data !== "User cannot to be connected on openid provider")
|
||||
) {
|
||||
this.loadOpenIDScreen();
|
||||
return Promise.reject(new Error("You will be redirect on login page"));
|
||||
@ -228,6 +238,17 @@ class ConnectionManager {
|
||||
analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email);
|
||||
}
|
||||
|
||||
//if limit room active test headband
|
||||
if (this._currentRoom.expireOn !== undefined) {
|
||||
warningContainerStore.activateWarningContainer();
|
||||
limitMapStore.set(true);
|
||||
|
||||
//check time of map
|
||||
if (new Date() > this._currentRoom.expireOn) {
|
||||
showLimitRoomModalStore.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
this.serviceWorker = new _ServiceWorker();
|
||||
return Promise.resolve(this._currentRoom);
|
||||
}
|
||||
@ -271,7 +292,7 @@ class ConnectionManager {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
connection.onConnectingError((event: CloseEvent) => {
|
||||
connection.connectionErrorStream.subscribe((event: CloseEvent) => {
|
||||
console.log("An error occurred while connecting to socket server. Retrying");
|
||||
reject(
|
||||
new Error(
|
||||
@ -283,7 +304,7 @@ class ConnectionManager {
|
||||
);
|
||||
});
|
||||
|
||||
connection.onConnect((connect: OnConnectInterface) => {
|
||||
connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => {
|
||||
resolve(connect);
|
||||
});
|
||||
}).catch((err) => {
|
||||
@ -292,7 +313,7 @@ class ConnectionManager {
|
||||
this.reconnectingTimeout = setTimeout(() => {
|
||||
//todo: allow a way to break recursion?
|
||||
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
||||
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
|
||||
void this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
|
||||
(connection) => resolve(connection)
|
||||
);
|
||||
}, 4000 + Math.floor(Math.random() * 2000));
|
||||
@ -315,15 +336,17 @@ class ConnectionManager {
|
||||
|
||||
if (!token) {
|
||||
if (!state || !localUserStore.verifyState(state)) {
|
||||
throw "Could not validate state!";
|
||||
throw new Error("Could not validate state!");
|
||||
}
|
||||
if (!code) {
|
||||
throw "No Auth code provided";
|
||||
throw new Error("No Auth code provided");
|
||||
}
|
||||
}
|
||||
const { authToken, userUuid, textures, email } = await Axios.get(`${PUSHER_URL}/login-callback`, {
|
||||
params: { code, nonce, token, playUri: this.currentRoom?.key },
|
||||
}).then((res) => res.data);
|
||||
}).then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
localUserStore.setAuthToken(authToken);
|
||||
this.localUser = new LocalUser(userUuid, textures, email);
|
||||
localUserStore.saveUser(this.localUser);
|
||||
|
@ -1,43 +1,12 @@
|
||||
import type { SignalData } from "simple-peer";
|
||||
import type { RoomConnection } from "./RoomConnection";
|
||||
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||
|
||||
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",
|
||||
|
||||
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",
|
||||
}
|
||||
import { PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
|
||||
|
||||
export interface PointInterface {
|
||||
x: number;
|
||||
y: number;
|
||||
direction: string;
|
||||
direction: string; // TODO: modify this to the enum from ts-proto
|
||||
moving: boolean;
|
||||
}
|
||||
|
||||
@ -64,6 +33,7 @@ export interface MessageUserJoined {
|
||||
visitCardUrl: string | null;
|
||||
companion: string | null;
|
||||
userUuid: string;
|
||||
outlineColor: number | undefined;
|
||||
}
|
||||
|
||||
export interface PositionInterface {
|
||||
@ -102,6 +72,12 @@ export interface ItemEventMessageInterface {
|
||||
parameters: unknown;
|
||||
}
|
||||
|
||||
export interface PlayerDetailsUpdatedMessageInterface {
|
||||
userId: number;
|
||||
outlineColor: number;
|
||||
removeOutlineColor: boolean;
|
||||
}
|
||||
|
||||
export interface RoomJoinedMessageInterface {
|
||||
//users: MessageUserPositionInterface[],
|
||||
//groups: GroupCreatedUpdatedMessageInterface[],
|
||||
|
@ -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();
|
@ -14,6 +14,7 @@ const audioPlayerMuteKey = "audioMute";
|
||||
const helpCameraSettingsShown = "helpCameraSettingsShown";
|
||||
const fullscreenKey = "fullscreen";
|
||||
const forceCowebsiteTriggerKey = "forceCowebsiteTrigger";
|
||||
const ignoreFollowRequests = "ignoreFollowRequests";
|
||||
const lastRoomUrl = "lastRoomUrl";
|
||||
const authToken = "authToken";
|
||||
const state = "state";
|
||||
@ -128,13 +129,23 @@ class LocalUserStore {
|
||||
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());
|
||||
if ("caches" in window) {
|
||||
caches.open(cacheAPIIndex).then((cache) => {
|
||||
try {
|
||||
const cache = await caches.open(cacheAPIIndex);
|
||||
const stringResponse = new Response(JSON.stringify({ roomUrl }));
|
||||
cache.put(`/${lastRoomUrl}`, stringResponse);
|
||||
});
|
||||
await cache.put(`/${lastRoomUrl}`, stringResponse);
|
||||
} catch (e) {
|
||||
console.error("Could not store last room url in Browser cache. Are you using private browser mode?", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
getLastRoomUrl(): string {
|
||||
|
@ -5,6 +5,8 @@ import type { CharacterTexture } from "./LocalUser";
|
||||
import { localUserStore } from "./LocalUserStore";
|
||||
import axios from "axios";
|
||||
import { axiosWithRetry } from "./AxiosUtils";
|
||||
import { isMapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||
|
||||
export class MapDetail {
|
||||
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
|
||||
@ -16,7 +18,10 @@ export interface RoomRedirect {
|
||||
|
||||
export class Room {
|
||||
public readonly id: string;
|
||||
public readonly isPublic: boolean;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
private readonly isPublic: boolean;
|
||||
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
|
||||
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
|
||||
private _mapUrl: string | undefined;
|
||||
@ -25,6 +30,8 @@ export class Room {
|
||||
private readonly _search: URLSearchParams;
|
||||
private _contactPage: string | undefined;
|
||||
private _group: string | null = null;
|
||||
private _expireOn: Date | undefined;
|
||||
private _canReport: boolean = false;
|
||||
|
||||
private constructor(private roomUrl: URL) {
|
||||
this.id = roomUrl.pathname;
|
||||
@ -32,7 +39,7 @@ export class Room {
|
||||
if (this.id.startsWith("/")) {
|
||||
this.id = this.id.substr(1);
|
||||
}
|
||||
if (this.id.startsWith("_/")) {
|
||||
if (this.id.startsWith("_/") || this.id.startsWith("*/")) {
|
||||
this.isPublic = true;
|
||||
} else if (this.id.startsWith("@/")) {
|
||||
this.isPublic = false;
|
||||
@ -80,13 +87,11 @@ export class Room {
|
||||
const currentRoom = new Room(baseUrl);
|
||||
let instance: string = "global";
|
||||
if (currentRoom.isPublic) {
|
||||
instance = currentRoom.instance as string;
|
||||
instance = currentRoom.getInstance();
|
||||
}
|
||||
|
||||
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
|
||||
if (absoluteExitSceneUrl.hash) {
|
||||
baseUrl.hash = absoluteExitSceneUrl.hash;
|
||||
}
|
||||
baseUrl.hash = absoluteExitSceneUrl.hash;
|
||||
|
||||
return baseUrl;
|
||||
}
|
||||
@ -101,27 +106,41 @@ export class Room {
|
||||
});
|
||||
|
||||
const data = result.data;
|
||||
if (data.redirectUrl) {
|
||||
return {
|
||||
redirectUrl: data.redirectUrl as string,
|
||||
};
|
||||
|
||||
if (data.authenticationMandatory !== undefined) {
|
||||
data.authenticationMandatory = Boolean(data.authenticationMandatory);
|
||||
}
|
||||
|
||||
if (isRoomRedirect(data)) {
|
||||
return {
|
||||
redirectUrl: data.redirectUrl,
|
||||
};
|
||||
} else if (isMapDetailsData(data)) {
|
||||
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
||||
this._mapUrl = data.mapUrl;
|
||||
this._textures = data.textures;
|
||||
this._group = data.group;
|
||||
this._authenticationMandatory =
|
||||
data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS;
|
||||
this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
|
||||
this._contactPage = data.contactPage || CONTACT_URL;
|
||||
if (data.expireOn) {
|
||||
this._expireOn = new Date(data.expireOn);
|
||||
}
|
||||
this._canReport = data.canReport ?? false;
|
||||
return new MapDetail(data.mapUrl, data.textures);
|
||||
} else {
|
||||
throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
|
||||
}
|
||||
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
||||
this._mapUrl = data.mapUrl;
|
||||
this._textures = data.textures;
|
||||
this._group = data.group;
|
||||
this._authenticationMandatory =
|
||||
data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS;
|
||||
this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
|
||||
this._contactPage = data.contactPage || CONTACT_URL;
|
||||
return new MapDetail(data.mapUrl, data.textures);
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e) && e.response?.status == 401 && e.response?.data === "Token decrypted error") {
|
||||
console.warn("JWT token sent could not be decrypted. Maybe it expired?");
|
||||
localUserStore.setAuthToken(null);
|
||||
window.location.assign("/login");
|
||||
} else {
|
||||
} else if (axios.isAxiosError(e)) {
|
||||
console.error("Error => getMapDetail", e, e.response);
|
||||
} else {
|
||||
console.error("Error => getMapDetail", e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@ -131,6 +150,8 @@ export class Room {
|
||||
* Instance name is:
|
||||
* - In a public URL: the second part of the URL ( _/[instance]/map.json)
|
||||
* - In a private URL: [organizationId/worldId]
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public getInstance(): string {
|
||||
if (this.instance !== undefined) {
|
||||
@ -138,7 +159,7 @@ export class Room {
|
||||
}
|
||||
|
||||
if (this.isPublic) {
|
||||
const match = /_\/([^/]+)\/.+/.exec(this.id);
|
||||
const match = /[_*]\/([^/]+)\/.+/.exec(this.id);
|
||||
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
||||
this.instance = match[1];
|
||||
return this.instance;
|
||||
@ -210,4 +231,12 @@ export class Room {
|
||||
get group(): string | null {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
get expireOn(): Date | undefined {
|
||||
return this._expireOn;
|
||||
}
|
||||
|
||||
get canReport(): boolean {
|
||||
return this._canReport;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
1
front/src/Messages/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/generated/
|
2
front/src/Messages/JsonMessages/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
1
front/src/Messages/ts-proto-generated/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*
|
@ -1,21 +1,21 @@
|
||||
import { PositionMessage } from "../Messages/generated/messages_pb";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import { PositionMessage, PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
|
||||
|
||||
import type { PointInterface } from "../Connexion/ConnexionModels";
|
||||
|
||||
export class ProtobufClientUtils {
|
||||
public static toPointInterface(position: PositionMessage): PointInterface {
|
||||
let direction: string;
|
||||
switch (position.getDirection()) {
|
||||
case Direction.UP:
|
||||
switch (position.direction) {
|
||||
case PositionMessage_Direction.UP:
|
||||
direction = "up";
|
||||
break;
|
||||
case Direction.DOWN:
|
||||
case PositionMessage_Direction.DOWN:
|
||||
direction = "down";
|
||||
break;
|
||||
case Direction.LEFT:
|
||||
case PositionMessage_Direction.LEFT:
|
||||
direction = "left";
|
||||
break;
|
||||
case Direction.RIGHT:
|
||||
case PositionMessage_Direction.RIGHT:
|
||||
direction = "right";
|
||||
break;
|
||||
default:
|
||||
@ -24,10 +24,10 @@ export class ProtobufClientUtils {
|
||||
|
||||
// sending to all clients in room except sender
|
||||
return {
|
||||
x: position.getX(),
|
||||
y: position.getY(),
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
direction,
|
||||
moving: position.getMoving(),
|
||||
moving: position.moving,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -41,13 +41,15 @@ export class Companion extends Container {
|
||||
this.companionName = name;
|
||||
this._pictureStore = writable(undefined);
|
||||
|
||||
texturePromise.then((resource) => {
|
||||
this.addResource(resource);
|
||||
this.invisible = false;
|
||||
return this.getSnapshot().then((htmlImageElementSrc) => {
|
||||
this._pictureStore.set(htmlImageElementSrc);
|
||||
});
|
||||
});
|
||||
texturePromise
|
||||
.then((resource) => {
|
||||
this.addResource(resource);
|
||||
this.invisible = false;
|
||||
return this.getSnapshot().then((htmlImageElementSrc) => {
|
||||
this._pictureStore.set(htmlImageElementSrc);
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
this.scene.physics.world.enableBody(this);
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./Co
|
||||
|
||||
export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => {
|
||||
COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => {
|
||||
lazyLoadCompanionResource(loader, resource.name);
|
||||
lazyLoadCompanionResource(loader, resource.name).catch((e) => console.error(e));
|
||||
});
|
||||
|
||||
return COMPANION_RESOURCES;
|
||||
|
@ -72,9 +72,11 @@ export class Loader {
|
||||
if (this.loadingText) {
|
||||
this.loadingText.destroy();
|
||||
}
|
||||
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
|
||||
resLoadingImage.destroy();
|
||||
});
|
||||
promiseLoadLogoTexture
|
||||
.then((resLoadingImage: Phaser.GameObjects.Image) => {
|
||||
resLoadingImage.destroy();
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
this.progress.destroy();
|
||||
this.progressContainer.destroy();
|
||||
if (this.scene instanceof DirtyScene) {
|
||||
|
@ -13,7 +13,8 @@ import { isSilentStore } from "../../Stores/MediaStore";
|
||||
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
|
||||
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
||||
import type { PictureStore } from "../../Stores/PictureStore";
|
||||
import { Writable, writable } from "svelte/store";
|
||||
import { Unsubscriber, Writable, writable } from "svelte/store";
|
||||
import { createColorStore } from "../../Stores/OutlineColorStore";
|
||||
|
||||
const playerNameY = -25;
|
||||
|
||||
@ -32,7 +33,7 @@ export abstract class Character extends Container {
|
||||
private readonly playerName: Text;
|
||||
public PlayerValue: string;
|
||||
public sprites: Map<string, Sprite>;
|
||||
private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
|
||||
protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
|
||||
//private teleportation: Sprite;
|
||||
private invisible: boolean;
|
||||
public companion?: Companion;
|
||||
@ -40,6 +41,8 @@ export abstract class Character extends Container {
|
||||
private emoteTween: Phaser.Tweens.Tween | null = null;
|
||||
scene: GameScene;
|
||||
private readonly _pictureStore: Writable<string | undefined>;
|
||||
private readonly outlineColorStore = createColorStore();
|
||||
private readonly outlineColorStoreUnsubscribe: Unsubscriber;
|
||||
|
||||
constructor(
|
||||
scene: GameScene,
|
||||
@ -97,18 +100,26 @@ export abstract class Character extends Container {
|
||||
});
|
||||
|
||||
this.on("pointerover", () => {
|
||||
this.getOutlinePlugin()?.add(this.playerName, {
|
||||
thickness: 2,
|
||||
outlineColor: 0xffff00,
|
||||
});
|
||||
this.scene.markDirty();
|
||||
this.outlineColorStore.pointerOver();
|
||||
});
|
||||
this.on("pointerout", () => {
|
||||
this.getOutlinePlugin()?.remove(this.playerName);
|
||||
this.scene.markDirty();
|
||||
this.outlineColorStore.pointerOut();
|
||||
});
|
||||
}
|
||||
|
||||
this.outlineColorStoreUnsubscribe = this.outlineColorStore.subscribe((color) => {
|
||||
if (color === undefined) {
|
||||
this.getOutlinePlugin()?.remove(this.playerName);
|
||||
} else {
|
||||
this.getOutlinePlugin()?.remove(this.playerName);
|
||||
this.getOutlinePlugin()?.add(this.playerName, {
|
||||
thickness: 2,
|
||||
outlineColor: color,
|
||||
});
|
||||
}
|
||||
this.scene.markDirty();
|
||||
});
|
||||
|
||||
scene.add.existing(this);
|
||||
|
||||
this.scene.physics.world.enableBody(this);
|
||||
@ -266,24 +277,20 @@ export abstract class Character extends Container {
|
||||
|
||||
body.setVelocity(x, y);
|
||||
|
||||
// up or down animations are prioritized over left and right
|
||||
if (body.velocity.y < 0) {
|
||||
//moving up
|
||||
this.lastDirection = PlayerAnimationDirections.Up;
|
||||
this.playAnimation(PlayerAnimationDirections.Up, true);
|
||||
} else if (body.velocity.y > 0) {
|
||||
//moving down
|
||||
this.lastDirection = PlayerAnimationDirections.Down;
|
||||
this.playAnimation(PlayerAnimationDirections.Down, true);
|
||||
} else if (body.velocity.x > 0) {
|
||||
//moving right
|
||||
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);
|
||||
if (Math.abs(body.velocity.x) > Math.abs(body.velocity.y)) {
|
||||
if (body.velocity.x < 0) {
|
||||
this.lastDirection = PlayerAnimationDirections.Left;
|
||||
} else if (body.velocity.x > 0) {
|
||||
this.lastDirection = PlayerAnimationDirections.Right;
|
||||
}
|
||||
} else {
|
||||
if (body.velocity.y < 0) {
|
||||
this.lastDirection = PlayerAnimationDirections.Up;
|
||||
} else if (body.velocity.y > 0) {
|
||||
this.lastDirection = PlayerAnimationDirections.Down;
|
||||
}
|
||||
}
|
||||
this.playAnimation(this.lastDirection, true);
|
||||
|
||||
this.setDepth(this.y);
|
||||
|
||||
@ -315,6 +322,7 @@ export abstract class Character extends Container {
|
||||
}
|
||||
}
|
||||
this.list.forEach((objectContaining) => objectContaining.destroy());
|
||||
this.outlineColorStoreUnsubscribe();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@ -401,4 +409,12 @@ export abstract class Character extends Container {
|
||||
public get pictureStore(): PictureStore {
|
||||
return this._pictureStore;
|
||||
}
|
||||
|
||||
public setOutlineColor(color: number): void {
|
||||
this.outlineColorStore.setColor(color);
|
||||
}
|
||||
|
||||
public removeOutlineColor(): void {
|
||||
this.outlineColorStore.removeColor();
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export const getRessourceDescriptor = (
|
||||
const playerResource = LAYERS[i][textureName];
|
||||
if (playerResource !== undefined) return playerResource;
|
||||
}
|
||||
throw "Could not find a data for texture " + textureName;
|
||||
throw new Error("Could not find a data for texture " + textureName);
|
||||
};
|
||||
|
||||
export const createLoadingPromise = (
|
||||
|
@ -44,6 +44,7 @@ export class CameraManager extends Phaser.Events.EventEmitter {
|
||||
private startFollowTween?: Phaser.Tweens.Tween;
|
||||
|
||||
private playerToFollow?: Player;
|
||||
private cameraLocked: boolean;
|
||||
|
||||
constructor(scene: GameScene, cameraBounds: { x: number; y: number }, waScaleManager: WaScaleManager) {
|
||||
super();
|
||||
@ -51,6 +52,7 @@ export class CameraManager extends Phaser.Events.EventEmitter {
|
||||
|
||||
this.camera = scene.cameras.main;
|
||||
this.cameraBounds = cameraBounds;
|
||||
this.cameraLocked = false;
|
||||
|
||||
this.waScaleManager = waScaleManager;
|
||||
|
||||
@ -123,6 +125,8 @@ export class CameraManager extends Phaser.Events.EventEmitter {
|
||||
this.waScaleManager.saveZoom();
|
||||
this.waScaleManager.setFocusTarget(focusOn);
|
||||
|
||||
this.cameraLocked = true;
|
||||
this.unlockCameraWithDelay(duration);
|
||||
this.restoreZoomTween?.stop();
|
||||
this.startFollowTween?.stop();
|
||||
const marginMult = 1 + margin;
|
||||
@ -143,8 +147,9 @@ export class CameraManager extends Phaser.Events.EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
public leaveFocusMode(player: Player, duration: number = 1000): void {
|
||||
public leaveFocusMode(player: Player, duration = 0): void {
|
||||
this.waScaleManager.setFocusTarget();
|
||||
this.unlockCameraWithDelay(duration);
|
||||
this.startFollowPlayer(player, duration);
|
||||
this.restoreZoom(duration);
|
||||
}
|
||||
@ -196,8 +201,14 @@ export class CameraManager extends Phaser.Events.EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
public isCameraZoomLocked(): boolean {
|
||||
return [CameraMode.Focus, CameraMode.Positioned].includes(this.cameraMode);
|
||||
public isCameraLocked(): boolean {
|
||||
return this.cameraLocked;
|
||||
}
|
||||
|
||||
private unlockCameraWithDelay(delay: number): void {
|
||||
this.scene.time.delayedCall(delay, () => {
|
||||
this.cameraLocked = false;
|
||||
});
|
||||
}
|
||||
|
||||
private setCameraMode(mode: CameraMode): void {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { emoteEventStream } from "../../Connexion/EmoteEventStream";
|
||||
import type { GameScene } from "./GameScene";
|
||||
import type { Subscription } from "rxjs";
|
||||
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
||||
|
||||
export class EmoteManager {
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(private scene: GameScene) {
|
||||
this.subscription = emoteEventStream.stream.subscribe((event) => {
|
||||
const actor = this.scene.MapPlayersByKey.get(event.userId);
|
||||
constructor(private scene: GameScene, private connection: RoomConnection) {
|
||||
this.subscription = connection.emoteEventMessageStream.subscribe((event) => {
|
||||
const actor = this.scene.MapPlayersByKey.get(event.actorUserId);
|
||||
if (actor) {
|
||||
actor.playEmote(event.emote);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export class GameManager {
|
||||
|
||||
getCharacterLayers(): string[] {
|
||||
if (!this.characterLayers) {
|
||||
throw "characterLayers are not set";
|
||||
throw new Error("characterLayers are not set");
|
||||
}
|
||||
return this.characterLayers;
|
||||
}
|
||||
@ -119,7 +119,7 @@ export class GameManager {
|
||||
* This will close the socket connections and stop the gameScene, but won't remove it.
|
||||
*/
|
||||
leaveGame(targetSceneName: string, sceneClass: Phaser.Scene): void {
|
||||
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||
if (this.currentGameSceneName === null) throw new Error("No current scene id set!");
|
||||
const gameScene: GameScene = this.scenePlugin.get(this.currentGameSceneName) as GameScene;
|
||||
gameScene.cleanupClosingScene();
|
||||
gameScene.createSuccessorGameScene(false, false);
|
||||
@ -143,7 +143,7 @@ export class GameManager {
|
||||
}
|
||||
|
||||
public getCurrentGameScene(): GameScene {
|
||||
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||
if (this.currentGameSceneName === null) throw new Error("No current scene id set!");
|
||||
return this.scenePlugin.get(this.currentGameSceneName) as GameScene;
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,14 @@ export class GameMap {
|
||||
let depth = -2;
|
||||
for (const layer of this.flatLayers) {
|
||||
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") {
|
||||
depth = DEPTH_OVERLAY_INDEX;
|
||||
|
@ -123,7 +123,7 @@ export class GameMapPropertiesListener {
|
||||
.then((coWebsite) => {
|
||||
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
|
||||
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
|
||||
coWebsiteManager.closeCoWebsite(coWebsite);
|
||||
coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e));
|
||||
this.coWebsitesOpenByLayer.delete(layer);
|
||||
this.coWebsitesActionTriggerByLayer.delete(layer);
|
||||
} else {
|
||||
@ -132,7 +132,8 @@ export class GameMapPropertiesListener {
|
||||
state: OpenCoWebsiteState.OPENED,
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
layoutManagerActionStore.removeAction(actionUuid);
|
||||
};
|
||||
@ -198,7 +199,7 @@ export class GameMapPropertiesListener {
|
||||
}
|
||||
|
||||
if (coWebsiteOpen.coWebsite !== undefined) {
|
||||
coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite);
|
||||
coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
this.coWebsitesOpenByLayer.delete(layer);
|
||||
|