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

This commit is contained in:
_Bastler 2022-04-13 15:35:28 +02:00
commit 12bc22236f
182 changed files with 1413 additions and 2592 deletions

View File

@ -26,6 +26,9 @@
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error"
],
"no-throw-literal": "error" "no-throw-literal": "error"
} }
} }

View File

@ -45,7 +45,6 @@
"busboy": "^0.3.1", "busboy": "^0.3.1",
"circular-json": "^0.5.9", "circular-json": "^0.5.9",
"debug": "^4.3.1", "debug": "^4.3.1",
"generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"grpc": "^1.24.4", "grpc": "^1.24.4",
"ipaddr.js": "^2.0.1", "ipaddr.js": "^2.0.1",
@ -56,7 +55,7 @@
"redis": "^3.1.2", "redis": "^3.1.2",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
"uuidv4": "^6.0.7", "uuidv4": "^6.0.7",
"zod": "^3.12.0" "zod": "^3.14.3"
}, },
"devDependencies": { "devDependencies": {
"@types/busboy": "^0.2.3", "@types/busboy": "^0.2.3",

View File

@ -48,7 +48,7 @@ export class DebugController {
return obj; return obj;
} else if (value instanceof Set) { } else if (value instanceof Set) {
const obj: Array<unknown> = []; const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) { for (const setValue of value.values()) {
obj.push(setValue); obj.push(setValue);
} }
return obj; return obj;

View File

@ -1,5 +1,5 @@
import { App } from "../Server/sifrr.server"; import { App } from "../Server/sifrr.server";
import { HttpRequest, HttpResponse } from "uWebSockets.js"; import { HttpResponse } from "uWebSockets.js";
import { register, collectDefaultMetrics } from "prom-client"; import { register, collectDefaultMetrics } from "prom-client";
export class PrometheusController { export class PrometheusController {
@ -11,7 +11,7 @@ export class PrometheusController {
this.App.get("/metrics", this.metrics.bind(this)); this.App.get("/metrics", this.metrics.bind(this));
} }
private metrics(res: HttpResponse, req: HttpRequest): void { private metrics(res: HttpResponse): void {
res.writeHeader("Content-Type", register.contentType); res.writeHeader("Content-Type", register.contentType);
res.end(register.metrics()); res.end(register.metrics());
} }

View File

@ -1,7 +1,7 @@
import { PointInterface } from "./Websocket/PointInterface"; import { PointInterface } from "./Websocket/PointInterface";
import { Group } from "./Group"; import { Group } from "./Group";
import { User, UserSocket } from "./User"; import { User, UserSocket } from "./User";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
import { import {
EmoteCallback, EmoteCallback,
EntersCallback, EntersCallback,
@ -9,23 +9,20 @@ import {
LockGroupCallback, LockGroupCallback,
MovesCallback, MovesCallback,
PlayerDetailsUpdatedCallback, PlayerDetailsUpdatedCallback,
} from "_Model/Zone"; } from "../Model/Zone";
import { PositionNotifier } from "./PositionNotifier"; import { PositionNotifier } from "./PositionNotifier";
import { Movable } from "_Model/Movable"; import { Movable } from "../Model/Movable";
import { import {
BatchToPusherMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
EmoteEventMessage, EmoteEventMessage,
ErrorMessage,
JoinRoomMessage, JoinRoomMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SubToPusherRoomMessage, SubToPusherRoomMessage,
VariableMessage,
VariableWithTagMessage, VariableWithTagMessage,
ServerToClientMessage, ServerToClientMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager"; import { RoomSocket, ZoneSocket } from "../RoomManager";
import { Admin } from "../Model/Admin"; import { Admin } from "../Model/Admin";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
@ -36,7 +33,6 @@ import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { LocalUrlError } from "../Services/LocalUrlError"; import { LocalUrlError } from "../Services/LocalUrlError";
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers"; import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
import { VariableError } from "../Services/VariableError"; import { VariableError } from "../Services/VariableError";
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
@ -150,6 +146,7 @@ export class GameRoom {
position, position,
false, false,
this.positionNotifier, this.positionNotifier,
joinRoomMessage.getAway(),
socket, socket,
joinRoomMessage.getTagList(), joinRoomMessage.getTagList(),
joinRoomMessage.getVisitcardurl(), joinRoomMessage.getVisitcardurl(),
@ -398,7 +395,7 @@ export class GameRoom {
private searchClosestAvailableUserOrGroup(user: User): User | Group | null { private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
let matchingItem: User | Group | null = null; let matchingItem: User | Group | null = null;
this.users.forEach((currentUser, userId) => { this.users.forEach((currentUser) => {
// Let's only check users that are not part of a group // Let's only check users that are not part of a group
if (typeof currentUser.group !== "undefined") { if (typeof currentUser.group !== "undefined") {
return; return;
@ -584,12 +581,15 @@ export class GameRoom {
}; };
} }
const result = await adminApi.fetchMapDetails(roomUrl); const result = isMapDetailsData.safeParse(await adminApi.fetchMapDetails(roomUrl));
if (isRoomRedirect(result)) {
console.error("Unexpected room redirect received while querying map details", result); if (result.success) {
throw new Error("Unexpected room redirect received while querying map details"); return result.data;
} }
return result;
console.error(result.error.issues);
console.error("Unexpected room redirect received while querying map details", result);
throw new Error("Unexpected room redirect received while querying map details");
} }
private mapPromise: Promise<ITiledMap> | undefined; private mapPromise: Promise<ITiledMap> | undefined;

View File

@ -1,8 +1,8 @@
import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom"; import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
import { User } from "./User"; import { User } from "./User";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
import { Movable } from "_Model/Movable"; import { Movable } from "../Model/Movable";
import { PositionNotifier } from "_Model/PositionNotifier"; import { PositionNotifier } from "../Model/PositionNotifier";
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable"; import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
import type { Zone } from "../Model/Zone"; import type { Zone } from "../Model/Zone";

View File

@ -1,4 +1,4 @@
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
/** /**
* A physical object that can be placed into a Zone * A physical object that can be placed into a Zone

View File

@ -17,8 +17,8 @@ import {
PlayerDetailsUpdatedCallback, PlayerDetailsUpdatedCallback,
Zone, Zone,
} from "./Zone"; } from "./Zone";
import { Movable } from "_Model/Movable"; import { Movable } from "../Model/Movable";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
import { ZoneSocket } from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import { User } from "../Model/User"; import { User } from "../Model/User";
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb"; import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";

View File

@ -1,8 +1,8 @@
import { Group } from "./Group"; import { Group } from "./Group";
import { PointInterface } from "./Websocket/PointInterface"; import { PointInterface } from "./Websocket/PointInterface";
import { Zone } from "_Model/Zone"; import { Zone } from "../Model/Zone";
import { Movable } from "_Model/Movable"; import { Movable } from "../Model/Movable";
import { PositionNotifier } from "_Model/PositionNotifier"; import { PositionNotifier } from "../Model/PositionNotifier";
import { ServerDuplexStream } from "grpc"; import { ServerDuplexStream } from "grpc";
import { import {
BatchMessage, BatchMessage,
@ -14,7 +14,7 @@ import {
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SubMessage, SubMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import { CharacterLayer } from "../Model/Websocket/CharacterLayer";
import { BoolValue, UInt32Value } from "google-protobuf/google/protobuf/wrappers_pb"; import { BoolValue, UInt32Value } from "google-protobuf/google/protobuf/wrappers_pb";
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>; export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -32,6 +32,7 @@ export class User implements Movable {
private position: PointInterface, private position: PointInterface,
public silent: boolean, public silent: boolean,
private positionNotifier: PositionNotifier, private positionNotifier: PositionNotifier,
private away: boolean,
public readonly socket: UserSocket, public readonly socket: UserSocket,
public readonly tags: string[], public readonly tags: string[],
public readonly visitCardUrl: string | null, public readonly visitCardUrl: string | null,
@ -89,6 +90,10 @@ export class User implements Movable {
return this.outlineColor; return this.outlineColor;
} }
public isAway(): boolean {
return this.away;
}
get following(): User | undefined { get following(): User | undefined {
return this._following; return this._following;
} }
@ -129,6 +134,11 @@ export class User implements Movable {
} }
this.voiceIndicatorShown = details.getShowvoiceindicator()?.getValue(); this.voiceIndicatorShown = details.getShowvoiceindicator()?.getValue();
const away = details.getAway();
if (away) {
this.away = away.getValue();
}
const playerDetails = new SetPlayerDetailsMessage(); const playerDetails = new SetPlayerDetailsMessage();
if (this.outlineColor !== undefined) { if (this.outlineColor !== undefined) {
@ -137,6 +147,9 @@ export class User implements Movable {
if (this.voiceIndicatorShown !== undefined) { if (this.voiceIndicatorShown !== undefined) {
playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown)); playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown));
} }
if (details.getAway() !== undefined) {
playerDetails.setAway(new BoolValue().setValue(this.away));
}
this.positionNotifier.updatePlayerDetails(this, playerDetails); this.positionNotifier.updatePlayerDetails(this, playerDetails);
} }

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isItemEventMessageInterface = new tg.IsInterface() export const isItemEventMessageInterface = z.object({
.withProperties({ itemId: z.number(),
itemId: tg.isNumber, event: z.string(),
event: tg.isString, state: z.unknown(),
state: tg.isUnknown, parameters: z.unknown(),
parameters: tg.isUnknown, });
})
.get(); export type ItemEventMessageInterface = z.infer<typeof isItemEventMessageInterface>;
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;

View File

@ -1,18 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
/*export interface PointInterface { export const isPointInterface = z.object({
readonly x: number; x: z.number(),
readonly y: number; y: z.number(),
readonly direction: string; direction: z.string(),
readonly moving: boolean; moving: z.boolean(),
}*/ });
export const isPointInterface = new tg.IsInterface() export type PointInterface = z.infer<typeof isPointInterface>;
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
direction: tg.isString,
moving: tg.isBoolean,
})
.get();
export type PointInterface = tg.GuardedType<typeof isPointInterface>;

View File

@ -5,10 +5,10 @@ import {
PointMessage, PointMessage,
PositionMessage, PositionMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import { CharacterLayer } from "../../Model/Websocket/CharacterLayer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; import { ItemEventMessageInterface } from "../../Model/Websocket/ItemEventMessage";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../../Model/PositionInterface";
export class ProtobufUtils { export class ProtobufUtils {
public static toPositionMessage(point: PointInterface): PositionMessage { public static toPositionMessage(point: PointInterface): PositionMessage {

View File

@ -1,5 +1,5 @@
import { User } from "./User"; import { User } from "./User";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
import { Movable } from "./Movable"; import { Movable } from "./Movable";
import { Group } from "./Group"; import { Group } from "./Group";
import { ZoneSocket } from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
@ -71,6 +71,7 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user entered * Notify listeners of this zone that this user entered
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onEnters(thing, oldZone, listener); this.onEnters(thing, oldZone, listener);

View File

@ -15,7 +15,6 @@ import {
EmptyMessage, EmptyMessage,
ItemEventMessage, ItemEventMessage,
JoinRoomMessage, JoinRoomMessage,
PlayGlobalMessage,
PusherToBackMessage, PusherToBackMessage,
QueryJitsiJwtMessage, QueryJitsiJwtMessage,
RefreshRoomPromptMessage, RefreshRoomPromptMessage,

View File

@ -18,12 +18,21 @@ class AdminApi {
params, params,
}); });
if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) { const mapDetailData = isMapDetailsData.safeParse(res.data);
console.error("Unexpected answer from the /api/map admin endpoint.", res.data); const roomRedirect = isRoomRedirect.safeParse(res.data);
throw new Error("Unexpected answer from the /api/map admin endpoint.");
if (mapDetailData.success) {
return mapDetailData.data;
} }
return res.data; if (roomRedirect.success) {
return roomRedirect.data;
}
console.error(mapDetailData.error.issues);
console.error(roomRedirect.error.issues);
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
throw new Error("Unexpected answer from the /api/map admin endpoint.");
} }
} }

View File

@ -1,5 +1,4 @@
import { import {
BatchMessage,
BatchToPusherMessage, BatchToPusherMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
ErrorMessage, ErrorMessage,
@ -7,7 +6,7 @@ import {
SubToPusherMessage, SubToPusherMessage,
SubToPusherRoomMessage, SubToPusherRoomMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { UserSocket } from "_Model/User"; import { UserSocket } from "../Model/User";
import { RoomSocket, ZoneSocket } from "../RoomManager"; import { RoomSocket, ZoneSocket } from "../RoomManager";
function getMessageFromError(error: unknown): string { function getMessageFromError(error: unknown): string {

View File

@ -4,10 +4,12 @@ import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
* Mock class in charge of NOT saving/loading variables from the data store * Mock class in charge of NOT saving/loading variables from the data store
*/ */
export class VoidVariablesRepository implements VariablesRepositoryInterface { export class VoidVariablesRepository implements VariablesRepositoryInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> { loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
return Promise.resolve({}); return Promise.resolve({});
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
saveVariable(roomUrl: string, key: string, value: string): Promise<number> { saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
return Promise.resolve(0); return Promise.resolve(0);
} }

View File

@ -2,7 +2,6 @@ import { GameRoom } from "../Model/GameRoom";
import { import {
ItemEventMessage, ItemEventMessage,
ItemStateMessage, ItemStateMessage,
PlayGlobalMessage,
PointMessage, PointMessage,
RoomJoinedMessage, RoomJoinedMessage,
ServerToClientMessage, ServerToClientMessage,
@ -35,12 +34,10 @@ import {
FollowAbortMessage, FollowAbortMessage,
VariableMessage, VariableMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
SubToPusherRoomMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
PlayerDetailsUpdatedMessage, PlayerDetailsUpdatedMessage,
GroupUsersUpdateMessage, GroupUsersUpdateMessage,
LockGroupPromptMessage, LockGroupPromptMessage,
RoomMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { User, UserSocket } from "../Model/User"; import { User, UserSocket } from "../Model/User";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
@ -60,9 +57,9 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { clientEventsEmitter } from "./ClientEventsEmitter"; import { clientEventsEmitter } from "./ClientEventsEmitter";
import { gaugeManager } from "./GaugeManager"; import { gaugeManager } from "./GaugeManager";
import { RoomSocket, ZoneSocket } from "../RoomManager"; import { RoomSocket, ZoneSocket } from "../RoomManager";
import { Zone } from "_Model/Zone"; import { Zone } from "../Model/Zone";
import Debug from "debug"; import Debug from "debug";
import { Admin } from "_Model/Admin"; import { Admin } from "../Model/Admin";
import crypto from "crypto"; import crypto from "crypto";
const debug = Debug("sockermanager"); const debug = Debug("sockermanager");
@ -331,6 +328,7 @@ export class SocketManager {
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid); userJoinedZoneMessage.setUseruuid(thing.uuid);
userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setName(thing.name);
userJoinedZoneMessage.setAway(thing.isAway());
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone)); userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
@ -658,6 +656,7 @@ export class SocketManager {
userJoinedMessage.setUserid(thing.id); userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setUseruuid(thing.uuid); userJoinedMessage.setUseruuid(thing.uuid);
userJoinedMessage.setName(thing.name); userJoinedMessage.setName(thing.name);
userJoinedMessage.setAway(thing.isAway());
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
if (thing.visitCardUrl) { if (thing.visitCardUrl) {

View File

@ -2,7 +2,7 @@
* Handles variables shared between the scripting API and the server. * Handles variables shared between the scripting API and the server.
*/ */
import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist"; import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist";
import { User } from "_Model/User"; import { User } from "../Model/User";
import { variablesRepository } from "./Repository/VariablesRepository"; import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient"; import { redisClient } from "./RedisClient";
import { VariableError } from "./VariableError"; import { VariableError } from "./VariableError";

View File

@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import "jasmine"; import "jasmine";
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom"; import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
import { Point } from "../src/Model/Websocket/MessageUserPosition"; import { Point } from "../src/Model/Websocket/MessageUserPosition";
import { Group } from "../src/Model/Group"; import { Group } from "../src/Model/Group";
import { User, UserSocket } from "_Model/User"; import { User, UserSocket } from "../src/Model/User";
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb"; import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import { EmoteCallback } from "_Model/Zone"; import { EmoteCallback } from "../src/Model/Zone";
function createMockUser(userId: number): User { function createMockUser(userId: number): User {
return { return {

View File

@ -1,4 +1,3 @@
import { arrayIntersect } from "../src/Services/ArrayHelper";
import { mapFetcher } from "../src/Services/MapFetcher"; import { mapFetcher } from "../src/Services/MapFetcher";
describe("MapFetcher", () => { describe("MapFetcher", () => {

View File

@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import "jasmine"; import "jasmine";
import { PositionNotifier } from "../src/Model/PositionNotifier"; import { PositionNotifier } from "../src/Model/PositionNotifier";
import { User, UserSocket } from "../src/Model/User"; import { User, UserSocket } from "../src/Model/User";
import { Zone } from "_Model/Zone"; import { Zone } from "../src/Model/Zone";
import { Movable } from "_Model/Movable"; import { Movable } from "../src/Model/Movable";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "../src/Model/PositionInterface";
import { ZoneSocket } from "../src/RoomManager"; import { ZoneSocket } from "../src/RoomManager";
describe("PositionNotifier", () => { describe("PositionNotifier", () => {
@ -41,6 +42,7 @@ describe("PositionNotifier", () => {
}, },
false, false,
positionNotifier, positionNotifier,
false,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,
@ -60,6 +62,7 @@ describe("PositionNotifier", () => {
}, },
false, false,
positionNotifier, positionNotifier,
false,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,
@ -149,6 +152,7 @@ describe("PositionNotifier", () => {
}, },
false, false,
positionNotifier, positionNotifier,
false,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,
@ -168,6 +172,7 @@ describe("PositionNotifier", () => {
}, },
false, false,
positionNotifier, positionNotifier,
false,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,

View File

@ -3,18 +3,18 @@
"experimentalDecorators": true, "experimentalDecorators": true,
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"downlevelIteration": true, "downlevelIteration": true,
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */ // "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */ "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */ "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */ // "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */ "outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */ // "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
@ -23,50 +23,50 @@
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ // Disabled because of sifrr server that is monkey patching HttpResponse "noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ // Disabled because of sifrr server that is monkey patching HttpResponse
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */ // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"paths": { // "paths": {
"_Controller/*": ["src/Controller/*"], // "_Controller/*": [
"_Model/*": ["src/Model/*"], // "src/Controller/*"
"_Enum/*": ["src/Enum/*"] // ],
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "_Model/*": [
// "src/Model/*"
// ],
// "_Enum/*": [
// "src/Enum/*"
// ]
// }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */ /* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */ /* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */ /* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
} }
} }

View File

@ -981,7 +981,7 @@ gauge@^3.0.0:
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
wide-align "^1.1.2" wide-align "^1.1.2"
generic-type-guard@^3.2.0, generic-type-guard@^3.4.1: generic-type-guard@^3.4.1:
version "3.5.0" version "3.5.0"
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.5.0.tgz#39de9f8fceee65d79e7540959f0e7b23210c07b6" resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.5.0.tgz#39de9f8fceee65d79e7540959f0e7b23210c07b6"
integrity sha512-OpgXv/sbRobhFboaSyN/Tsh97Sxt5pcfLLxCiYZgYIIWFFp+kn2EzAXiaQZKEVRlq1rOE/zh8cYhJXEwplbJiQ== integrity sha512-OpgXv/sbRobhFboaSyN/Tsh97Sxt5pcfLLxCiYZgYIIWFFp+kn2EzAXiaQZKEVRlq1rOE/zh8cYhJXEwplbJiQ==
@ -1459,9 +1459,9 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@^1.2.5: minimist@^1.2.5:
version "1.2.5" version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass@^3.0.0: minipass@^3.0.0:
version "3.1.6" version "3.1.6"
@ -2259,7 +2259,7 @@ yn@3.1.1:
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
zod@^3.12.0: zod@^3.14.3:
version "3.14.2" version "3.14.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.2.tgz#0b4ed79085c471adce0e7f2c0a4fbb5ddc516ba2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.3.tgz#60e86341c05883c281fe96a0e79acea48a09f123"
integrity sha512-iF+wrtzz7fQfkmn60PG6XFxaWBhYYKzp2i+nv24WbLUWb2JjymdkHlzBwP0erpc78WotwP5g9AAu7Sk8GWVVNw== integrity sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ==

View File

@ -369,9 +369,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"mkdirp": { "mkdirp": {
"version": "1.0.4", "version": "1.0.4",

View File

@ -268,8 +268,8 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@^1.1.3, minimist@^1.2.5: minimist@^1.1.3, minimist@^1.2.5:
version "1.2.5" version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
mkdirp@^1.0.4: mkdirp@^1.0.4:
version "1.0.4" version "1.0.4"

View File

@ -12,7 +12,7 @@ services:
- --entryPoints.websecure.address=:${HTTPS_PORT} - --entryPoints.websecure.address=:${HTTPS_PORT}
# HTTP challenge # HTTP challenge
- --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL} - --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}
- --certificatesresolvers.myresolver.acme.storage=/acme.json - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
# Let's Encrypt's staging server # Let's Encrypt's staging server
# uncomment during testing to avoid rate limiting # uncomment during testing to avoid rate limiting
@ -22,7 +22,7 @@ services:
- "${HTTPS_PORT}:443" - "${HTTPS_PORT}:443"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ${DATA_DIR}/letsencrypt/acme.json:/acme.json - ${DATA_DIR}/letsencrypt/:/letsencrypt/
restart: ${RESTART_POLICY} restart: ${RESTART_POLICY}

View File

@ -64,6 +64,11 @@
"ADMIN_API_URL": adminUrl, "ADMIN_API_URL": adminUrl,
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
"ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN, "ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN,
"OPID_CLIENT_ID": "auth-code-client",
"OPID_CLIENT_SECRET": "mySecretHydraWA2022",
"OPID_CLIENT_ISSUER": "https://publichydra-"+url,
"OPID_CLIENT_REDIRECT_URL": "https://"+url+"/oauth/hydra",
"OPID_LOGIN_SCREEN_PROVIDER": "https://pusher-"+url+"/login-screen",
} else {}) } else {})
}, },
"front": { "front": {

View File

@ -12,10 +12,10 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"moduleResolution": "node", "moduleResolution": "node",
"baseUrl": ".", //"baseUrl": ".",
"paths": {}, //"paths": {},
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true "skipLibCheck": true
} }
} }

View File

@ -5,7 +5,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "esnext", "module": "esnext",
"resolveJsonModule": true, "resolveJsonModule": true,
"baseUrl": ".", //"baseUrl": ".",
/** /**
* Typecheck JS in `.svelte` and `.js` files by default. * Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS. * Disable checkJs if you'd like to use dynamic types in JS.
@ -15,10 +15,23 @@
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"paths": { "paths": {
"~/*": ["./src/*"], "~/*": [
"@wa-preload-local-app": ["../electron/src/preload-local-app/types.ts"], "./src/*"
],
"@wa-preload-local-app": [
"../electron/src/preload-local-app/types.ts"
],
} }
}, },
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": [
"references": [{ "path": "./tsconfig.node.json" }] "src/**/*.d.ts",
} "src/**/*.ts",
"src/**/*.js",
"src/**/*.svelte"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -114,7 +114,6 @@ services:
#APACHE_EXTENSION_HEADERS: 1 #APACHE_EXTENSION_HEADERS: 1
STARTUP_COMMAND_0: sudo a2enmod headers STARTUP_COMMAND_0: sudo a2enmod headers
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
STARTUP_COMMAND_2: yarn run dev &
volumes: volumes:
- ./maps:/var/www/html - ./maps:/var/www/html
labels: labels:

View File

@ -9,7 +9,7 @@ The [scripting API](https://workadventu.re/map-building/scripting.md) allows map
The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to
offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance, offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance,
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!) you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that
a feature is missing in the API. This article is here to explain to you how to add this feature. a feature is missing in the API. This article is here to explain to you how to add this feature.
@ -35,7 +35,7 @@ directly access Phaser objects (Phaser is the game engine used in WorkAdventure)
can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be
a huge XSS security flaw). a huge XSS security flaw).
Instead, the only way the script can interact with WorkAdventure is by sending messages using the Instead, the only way the script can interact with WorkAdventure is by sending messages using the
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
![](images/scripting_2.svg) ![](images/scripting_2.svg)
@ -103,14 +103,14 @@ All the other files dedicated to the iframe API are located in the `src/Api/ifra
## Utility functions to exchange messages ## Utility functions to exchange messages
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function. [`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function.
Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes. Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes.
We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this. We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this.
Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function. [`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function.
## Types ## Types
@ -122,7 +122,7 @@ Indeed, Typescript interfaces only exist at compilation time but cannot be enfor
is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could
send specially crafted JSON packages to try to hack WA). send specially crafted JSON packages to try to hack WA).
In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package In WorkAdventure, we use the [zod](https://github.com/colinhacks/zod) package. This package
allows us to create interfaces AND custom type guards in one go. allows us to create interfaces AND custom type guards in one go.
Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API: Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API:
@ -140,21 +140,20 @@ sendToWorkadventure({
The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`: The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`:
```typescript ```typescript
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isChatEvent = z.object({
message: z.string(),
author: z.string(),
});
export const isChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
author: tg.isString,
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type ChatEvent = tg.GuardedType<typeof isChatEvent>; export type ChatEvent = z.infer<typeof isChatEvent>;
``` ```
Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`). Using the zod library, we start by writing a type guard function (`isChatEvent`).
From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code. From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code.
The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received
@ -212,7 +211,7 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to
define the type of the query and the type of the response. define the type of the query and the type of the response.
The signature of `queryWorkadventure` is: The signature of `queryWorkadventure` is:
```typescript ```typescript
function queryWorkadventure<T extends keyof IframeQueryMap>( function queryWorkadventure<T extends keyof IframeQueryMap>(
@ -250,12 +249,12 @@ Here is a sample:
```typescript ```typescript
iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => { iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => {
// ... // ...
return /*...*/; return /*...*/;
}); });
``` ```
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`). (the one you defined in the `answer` key of `iframeQueryMapTypeGuards`).
Important: Important:

View File

@ -9,7 +9,7 @@ In order to build your own map for WorkAdventure, you need:
* "tiles" (i.e. images) to create your map * "tiles" (i.e. images) to create your map
* a web-server to serve your map * a web-server to serve your map
WorkAdventure comes with a "map starter kit" that we recommend using to start designing your map quickly. It contains **a good default tileset** for building an office and it proposes to **use Github static pages as a web-server** which is both free and performant. It also comes with a local webserver for testing purpose and with Typescript support (if you are looking to use the [map scripting API]({{url('/map-building/scripting')}}). WorkAdventure comes with a "map starter kit" that we recommend using to start designing your map quickly. It contains **a good default tileset** for building an office and it proposes to **use Github static pages as a web-server** which is both free and performant. It also comes with a local webserver for testing purpose and with Typescript support (if you are looking to use the [map scripting API](scripting.md).
{.alert.alert-info} {.alert.alert-info}
If you are looking to host your maps on your own webserver, be sure to read the [Self-hosting your map](hosting.md) guide. If you are looking to host your maps on your own webserver, be sure to read the [Self-hosting your map](hosting.md) guide.

View File

@ -27,8 +27,8 @@ module.exports = {
], ],
"overrides": [ "overrides": [
{ {
"files": ["*.svelte"], "files": ["*.svelte"],
"processor": "svelte3/svelte3" "processor": "svelte3/svelte3"
} }
], ],
"rules": { "rules": {
@ -36,6 +36,7 @@ module.exports = {
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"no-throw-literal": "error", "no-throw-literal": "error",
"@typescript-eslint/no-unused-vars": ["error"],
// TODO: remove those ignored rules and write a stronger code! // TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off", "@typescript-eslint/restrict-plus-operands": "off",

View File

@ -45,7 +45,6 @@
"deep-copy-ts": "^0.5.0", "deep-copy-ts": "^0.5.0",
"dompurify" : "^2.3.6", "dompurify" : "^2.3.6",
"easystarjs": "^0.4.4", "easystarjs": "^0.4.4",
"generic-type-guard": "^3.4.2",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"phaser": "3.55.1", "phaser": "3.55.1",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
@ -63,7 +62,7 @@
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0", "typesafe-i18n": "^2.59.0",
"uuidv4": "^6.2.10", "uuidv4": "^6.2.10",
"zod": "^3.11.6" "zod": "^3.14.3"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve watch-iframe-api svelte-check-watch typesafe-i18n-watch", "start": "run-p templater serve watch-iframe-api svelte-check-watch typesafe-i18n-watch",

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isActionsMenuActionClickedEvent = new tg.IsInterface() export const isActionsMenuActionClickedEvent = z.object({
.withProperties({ id: z.number(),
id: tg.isNumber, actionName: z.string(),
actionName: tg.isString, });
})
.get();
export type ActionsMenuActionClickedEvent = tg.GuardedType<typeof isActionsMenuActionClickedEvent>; export type ActionsMenuActionClickedEvent = z.infer<typeof isActionsMenuActionClickedEvent>;
export type ActionsMenuActionClickedEventCallback = (event: ActionsMenuActionClickedEvent) => void; export type ActionsMenuActionClickedEventCallback = (event: ActionsMenuActionClickedEvent) => void;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isAddActionsMenuKeyToRemotePlayerEvent = new tg.IsInterface() export const isAddActionsMenuKeyToRemotePlayerEvent = z.object({
.withProperties({ id: z.number(),
id: tg.isNumber, actionKey: z.string(),
actionKey: tg.isString, });
})
.get();
export type AddActionsMenuKeyToRemotePlayerEvent = tg.GuardedType<typeof isAddActionsMenuKeyToRemotePlayerEvent>; export type AddActionsMenuKeyToRemotePlayerEvent = z.infer<typeof isAddActionsMenuKeyToRemotePlayerEvent>;
export type AddActionsMenuKeyToRemotePlayerEventCallback = (event: AddActionsMenuKeyToRemotePlayerEvent) => void; export type AddActionsMenuKeyToRemotePlayerEventCallback = (event: AddActionsMenuKeyToRemotePlayerEvent) => void;

View File

@ -1,14 +1,13 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isButtonClickedEvent = z.object({
popupId: z.number(),
buttonId: z.number(),
input: z.boolean(),
inputValue: z.string(),
});
export const isButtonClickedEvent = new tg.IsInterface()
.withProperties({
popupId: tg.isNumber,
buttonId: tg.isNumber,
input: tg.isBoolean,
inputValue: tg.isString,
})
.get();
/** /**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/ */
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>; export type ButtonClickedEvent = z.infer<typeof isButtonClickedEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isCameraFollowPlayerEvent = z.object({
smooth: z.boolean(),
});
export const isCameraFollowPlayerEvent = new tg.IsInterface()
.withProperties({
smooth: tg.isBoolean,
})
.get();
/** /**
* A message sent from the iFrame to the game to make the camera follow player. * A message sent from the iFrame to the game to make the camera follow player.
*/ */
export type CameraFollowPlayerEvent = tg.GuardedType<typeof isCameraFollowPlayerEvent>; export type CameraFollowPlayerEvent = z.infer<typeof isCameraFollowPlayerEvent>;

View File

@ -1,16 +1,15 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isCameraSetEvent = z.object({
x: z.number(),
y: z.number(),
width: z.optional(z.number()),
height: z.optional(z.number()),
lock: z.boolean(),
smooth: z.boolean(),
});
export const isCameraSetEvent = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isOptional(tg.isNumber),
height: tg.isOptional(tg.isNumber),
lock: tg.isBoolean,
smooth: tg.isBoolean,
})
.get();
/** /**
* A message sent from the iFrame to the game to change the camera position. * A message sent from the iFrame to the game to change the camera position.
*/ */
export type CameraSetEvent = tg.GuardedType<typeof isCameraSetEvent>; export type CameraSetEvent = z.infer<typeof isCameraSetEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isChangeLayerEvent = z.object({
name: z.string(),
});
export const isChangeLayerEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/** /**
* A message sent from the game to the iFrame when a user enters or leaves a layer. * A message sent from the game to the iFrame when a user enters or leaves a layer.
*/ */
export type ChangeLayerEvent = tg.GuardedType<typeof isChangeLayerEvent>; export type ChangeLayerEvent = z.infer<typeof isChangeLayerEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isChangeZoneEvent = z.object({
name: z.string(),
});
export const isChangeZoneEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/** /**
* A message sent from the game to the iFrame when a user enters or leaves a zone. * A message sent from the game to the iFrame when a user enters or leaves a zone.
*/ */
export type ChangeZoneEvent = tg.GuardedType<typeof isChangeZoneEvent>; export type ChangeZoneEvent = z.infer<typeof isChangeZoneEvent>;

View File

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isChatEvent = z.object({
message: z.string(),
author: z.string(),
});
export const isChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
author: tg.isString,
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type ChatEvent = tg.GuardedType<typeof isChatEvent>; export type ChatEvent = z.infer<typeof isChatEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isCloseCoWebsite = new tg.IsInterface() export const isCloseCoWebsite = z.object({
.withProperties({ id: z.optional(z.string()),
id: tg.isOptional(tg.isString), });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type CloseCoWebsiteEvent = tg.GuardedType<typeof isCloseCoWebsite>; export type CloseCoWebsiteEvent = z.infer<typeof isCloseCoWebsite>;

View File

@ -1,13 +1,11 @@
import * as tg from "generic-type-guard"; import { string, z } from "zod";
export const isClosePopupEvent = new tg.IsInterface() export const isClosePopupEvent = z.object({
.withProperties({ popupId: z.number(),
popupId: tg.isNumber, inputValue: z.string(),
inputValue: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>; export type ClosePopupEvent = z.infer<typeof isClosePopupEvent>;

View File

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isColorEvent = z.object({
red: z.number(),
green: z.number(),
blue: z.number(),
});
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. * A message sent from the iFrame to the game to dynamically set the outline of the player.
*/ */
export type ColorEvent = tg.GuardedType<typeof isColorEvent>; export type ColorEvent = z.infer<typeof isColorEvent>;

View File

@ -1,52 +1,43 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isRectangle = new tg.IsInterface() export const isRectangle = z.object({
.withProperties({ x: z.number(),
x: tg.isNumber, y: z.number(),
y: tg.isNumber, width: z.number(),
width: tg.isNumber, height: z.number(),
height: tg.isNumber, });
})
.get();
export const isEmbeddedWebsiteEvent = new tg.IsInterface() // TODO: make a variation that is all optional (except for the name)
.withProperties({ export type Rectangle = z.infer<typeof isRectangle>;
name: tg.isString,
})
.withOptionalProperties({
url: tg.isString,
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
})
.get();
export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface() export const isEmbeddedWebsiteEvent = z.object({
.withProperties({ name: z.string(),
name: tg.isString, url: z.optional(z.string()),
url: tg.isString, visible: z.optional(z.boolean()),
position: isRectangle, allowApi: z.optional(z.boolean()),
}) allow: z.optional(z.string()),
.withOptionalProperties({ x: z.optional(z.number()),
visible: tg.isBoolean, y: z.optional(z.number()),
allowApi: tg.isBoolean, width: z.optional(z.number()),
allow: tg.isString, height: z.optional(z.number()),
origin: tg.isSingletonStringUnion("player", "map"), origin: z.optional(z.enum(["player", "map"])),
scale: tg.isNumber, scale: z.optional(z.number()),
}) });
.get();
/** /**
* A message sent from the iFrame to the game to modify an embedded website * A message sent from the iFrame to the game to modify an embedded website
*/ */
export type ModifyEmbeddedWebsiteEvent = tg.GuardedType<typeof isEmbeddedWebsiteEvent>; export type ModifyEmbeddedWebsiteEvent = z.infer<typeof isEmbeddedWebsiteEvent>;
export type CreateEmbeddedWebsiteEvent = tg.GuardedType<typeof isCreateEmbeddedWebsiteEvent>; export const isCreateEmbeddedWebsiteEvent = z.object({
// TODO: make a variation that is all optional (except for the name) name: z.string(),
export type Rectangle = tg.GuardedType<typeof isRectangle>; url: z.string(),
position: isRectangle,
visible: z.optional(z.boolean()),
allowApi: z.optional(z.boolean()),
allow: z.optional(z.string()),
origin: z.optional(z.enum(["player", "map"])),
scale: z.optional(z.number()),
});
export type CreateEmbeddedWebsiteEvent = z.infer<typeof isCreateEmbeddedWebsiteEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isEnterLeaveEvent = z.object({
name: z.string(),
});
export const isEnterLeaveEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/** /**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/ */
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>; export type EnterLeaveEvent = z.infer<typeof isEnterLeaveEvent>;

View File

@ -1,20 +1,19 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isGameStateEvent = z.object({
roomId: z.string(),
mapUrl: z.string(),
nickname: z.string(),
language: z.optional(z.string()),
uuid: z.optional(z.string()),
startLayerName: z.optional(z.string()),
tags: z.array(z.string()),
variables: z.unknown(), // Todo : Typing
playerVariables: z.unknown(), // Todo : Typing
userRoomToken: z.optional(z.string()),
});
export const isGameStateEvent = new tg.IsInterface()
.withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isString,
language: tg.isUnion(tg.isString, tg.isUndefined),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),
variables: tg.isObject,
playerVariables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
})
.get();
/** /**
* A message sent from the game to the iFrame when the gameState is received by the script * A message sent from the game to the iFrame when the gameState is received by the script
*/ */
export type GameStateEvent = tg.GuardedType<typeof isGameStateEvent>; export type GameStateEvent = z.infer<typeof isGameStateEvent>;

View File

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isGetPropertyEvent = z.object({
layerName: z.string(),
propertyName: z.string(),
propertyValue: z.union([ z.string(), z.number(), z.boolean(), z.undefined() ]),
});
export const isGetPropertyEvent = new tg.IsInterface()
.withProperties({
layerName: tg.isString,
propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
})
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */
export type GetPropertyEvent = tg.GuardedType<typeof isGetPropertyEvent>; export type GetPropertyEvent = z.infer<typeof isGetPropertyEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isGoToPageEvent = new tg.IsInterface() export const isGoToPageEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>; export type GoToPageEvent = z.infer<typeof isGoToPageEvent>;

View File

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

View File

@ -1,45 +1,44 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
import type { ButtonClickedEvent } from "./ButtonClickedEvent"; import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent"; import { isChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent"; import { isClosePopupEvent } from "./ClosePopupEvent";
import type { EnterLeaveEvent } from "./EnterLeaveEvent"; import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { GoToPageEvent } from "./GoToPageEvent"; import { isGoToPageEvent } from "./GoToPageEvent";
import type { LoadPageEvent } from "./LoadPageEvent"; import { isLoadPageEvent } from "./LoadPageEvent";
import { isCoWebsite, isOpenCoWebsiteEvent } from "./OpenCoWebsiteEvent"; import { isCoWebsite, isOpenCoWebsiteEvent } from "./OpenCoWebsiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent"; import { isOpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent"; import { isOpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent"; import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { LayerEvent } from "./LayerEvent"; import { isLayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent"; import { isSetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent"; import { isLoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent"; import { isPlaySoundEvent } from "./PlaySoundEvent";
import { isStopSoundEvent } from "./StopSoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import { isSetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent"; import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent"; import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent"; import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent"; import { isSetVariableEvent } from "./SetVariableEvent";
import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite"; import { isCreateEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent"; import { isLoadTilesetEvent } from "./LoadTilesetEvent";
import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent"; import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import { isPlayerPosition } from "./PlayerPosition"; import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent"; import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import type { CameraSetEvent } from "./CameraSetEvent"; import { isCameraSetEvent } from "./CameraSetEvent";
import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent"; import { isCameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
import { isColorEvent } from "./ColorEvent"; import { isColorEvent } from "./ColorEvent";
import { isMovePlayerToEventConfig } from "./MovePlayerToEvent"; import { isMovePlayerToEventConfig } from "./MovePlayerToEvent";
import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer"; import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer";
import type { RemotePlayerClickedEvent } from "./RemotePlayerClickedEvent"; import type { RemotePlayerClickedEvent } from "./RemotePlayerClickedEvent";
import type { AddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent"; import { isAddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent";
import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent"; import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent";
import type { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent"; import { isRemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent";
import { isGetPropertyEvent } from "./GetPropertyEvent"; import { isGetPropertyEvent } from "./GetPropertyEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
@ -49,45 +48,114 @@ export interface TypedMessageEvent<T> extends MessageEvent {
/** /**
* List event types sent from an iFrame to WorkAdventure * List event types sent from an iFrame to WorkAdventure
*/ */
export type IframeEventMap = { export const isIframeEventWrapper = z.union([
addActionsMenuKeyToRemotePlayer: AddActionsMenuKeyToRemotePlayerEvent; z.object({
removeActionsMenuKeyFromRemotePlayer: RemoveActionsMenuKeyFromRemotePlayerEvent; type: z.literal("addActionsMenuKeyToRemotePlayer"),
loadPage: LoadPageEvent; data: isAddActionsMenuKeyToRemotePlayerEvent,
chat: ChatEvent; }),
cameraFollowPlayer: CameraFollowPlayerEvent; z.object({
cameraSet: CameraSetEvent; type: z.literal("removeActionsMenuKeyFromRemotePlayer"),
openPopup: OpenPopupEvent; data: isRemoveActionsMenuKeyFromRemotePlayerEvent,
closePopup: ClosePopupEvent; }),
openTab: OpenTabEvent; z.object({
goToPage: GoToPageEvent; type: z.literal("loadPage"),
disablePlayerControls: null; data: isLoadPageEvent,
restorePlayerControls: null; }),
displayBubble: null; z.object({
removeBubble: null; type: z.literal("chat"),
onPlayerMove: undefined; data: isChatEvent,
onOpenActionMenu: undefined; }),
onCameraUpdate: undefined; z.object({
showLayer: LayerEvent; type: z.literal("cameraFollowPlayer"),
hideLayer: LayerEvent; data: isCameraFollowPlayerEvent,
setProperty: SetPropertyEvent; }),
loadSound: LoadSoundEvent; z.object({
playSound: PlaySoundEvent; type: z.literal("cameraSet"),
stopSound: null; data: isCameraSetEvent,
getState: undefined; }),
loadTileset: LoadTilesetEvent; z.object({
registerMenu: MenuRegisterEvent; type: z.literal("openPopup"),
unregisterMenu: UnregisterMenuEvent; data: isOpenPopupEvent,
setTiles: SetTilesEvent; }),
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact z.object({
}; type: z.literal("closePopup"),
export interface IframeEvent<T extends keyof IframeEventMap> { data: isClosePopupEvent,
type: T; }),
data: IframeEventMap[T]; z.object({
} type: z.literal("openTab"),
data: isOpenTabEvent,
}),
z.object({
type: z.literal("goToPage"),
data: isGoToPageEvent,
}),
z.object({
type: z.literal("disablePlayerControls"),
data: z.undefined(),
}),
z.object({
type: z.literal("restorePlayerControls"),
data: z.undefined(),
}),
z.object({
type: z.literal("displayBubble"),
data: z.undefined(),
}),
z.object({
type: z.literal("removeBubble"),
data: z.undefined(),
}),
z.object({
type: z.literal("onPlayerMove"),
data: z.undefined(),
}),
z.object({
type: z.literal("onCameraUpdate"),
data: z.undefined(),
}),
z.object({
type: z.literal("showLayer"),
data: isLayerEvent,
}),
z.object({
type: z.literal("hideLayer"),
data: isLayerEvent,
}),
z.object({
type: z.literal("setProperty"),
data: isSetPropertyEvent,
}),
z.object({
type: z.literal("loadSound"),
data: isLoadSoundEvent,
}),
z.object({
type: z.literal("playSound"),
data: isPlaySoundEvent,
}),
z.object({
type: z.literal("stopSound"),
data: isStopSoundEvent,
}),
z.object({
type: z.literal("registerMenu"),
data: isMenuRegisterEvent,
}),
z.object({
type: z.literal("unregisterMenu"),
data: isUnregisterMenuEvent,
}),
z.object({
type: z.literal("setTiles"),
data: isSetTilesEvent,
}),
z.object({
type: z.literal("modifyEmbeddedWebsite"),
data: isEmbeddedWebsiteEvent,
}),
]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any export type IframeEvent = z.infer<typeof isIframeEventWrapper>;
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export interface IframeResponseEventMap { export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent; userInputChat: UserInputChatEvent;
@ -116,13 +184,18 @@ export const isIframeResponseEventWrapper = (event: {
type?: string; type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string"; }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
export const isLookingLikeIframeEventWrapper = z.object({
type: z.string(),
data: z.unknown().optional(),
});
/** /**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame. * List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame.
* Types are defined using Type guards that will actually bused to enforce and check types. * Types are defined using Type guards that will actually bused to enforce and check types.
*/ */
export const iframeQueryMapTypeGuards = { export const iframeQueryMapTypeGuards = {
getState: { getState: {
query: tg.isUndefined, query: z.undefined(),
answer: isGameStateEvent, answer: isGameStateEvent,
}, },
getProperty: { getProperty: {
@ -130,63 +203,63 @@ export const iframeQueryMapTypeGuards = {
answer: isGetPropertyEvent, answer: isGetPropertyEvent,
}, },
getMapData: { getMapData: {
query: tg.isUndefined, query: z.undefined(),
answer: isMapDataEvent, answer: isMapDataEvent,
}, },
setVariable: { setVariable: {
query: isSetVariableEvent, query: isSetVariableEvent,
answer: tg.isUndefined, answer: z.undefined(),
}, },
loadTileset: { loadTileset: {
query: isLoadTilesetEvent, query: isLoadTilesetEvent,
answer: tg.isNumber, answer: z.number(),
}, },
openCoWebsite: { openCoWebsite: {
query: isOpenCoWebsiteEvent, query: isOpenCoWebsiteEvent,
answer: isCoWebsite, answer: isCoWebsite,
}, },
getCoWebsites: { getCoWebsites: {
query: tg.isUndefined, query: z.undefined(),
answer: tg.isArray(isCoWebsite), answer: z.array(isCoWebsite),
}, },
closeCoWebsite: { closeCoWebsite: {
query: tg.isString, query: z.string(),
answer: tg.isUndefined, answer: z.undefined(),
}, },
closeCoWebsites: { closeCoWebsites: {
query: tg.isUndefined, query: z.undefined(),
answer: tg.isUndefined, answer: z.undefined(),
}, },
triggerActionMessage: { triggerActionMessage: {
query: isTriggerActionMessageEvent, query: isTriggerActionMessageEvent,
answer: tg.isUndefined, answer: z.undefined(),
}, },
removeActionMessage: { removeActionMessage: {
query: isMessageReferenceEvent, query: isMessageReferenceEvent,
answer: tg.isUndefined, answer: z.undefined(),
}, },
getEmbeddedWebsite: { getEmbeddedWebsite: {
query: tg.isString, query: z.string(),
answer: isCreateEmbeddedWebsiteEvent, answer: isCreateEmbeddedWebsiteEvent,
}, },
deleteEmbeddedWebsite: { deleteEmbeddedWebsite: {
query: tg.isString, query: z.string(),
answer: tg.isUndefined, answer: z.undefined(),
}, },
createEmbeddedWebsite: { createEmbeddedWebsite: {
query: isCreateEmbeddedWebsiteEvent, query: isCreateEmbeddedWebsiteEvent,
answer: tg.isUndefined, answer: z.undefined(),
}, },
setPlayerOutline: { setPlayerOutline: {
query: isColorEvent, query: isColorEvent,
answer: tg.isUndefined, answer: z.undefined(),
}, },
removePlayerOutline: { removePlayerOutline: {
query: tg.isUndefined, query: z.undefined(),
answer: tg.isUndefined, answer: z.undefined(),
}, },
getPlayerPosition: { getPlayerPosition: {
query: tg.isUndefined, query: z.undefined(),
answer: isPlayerPosition, answer: isPlayerPosition,
}, },
movePlayerTo: { movePlayerTo: {
@ -195,14 +268,13 @@ export const iframeQueryMapTypeGuards = {
}, },
}; };
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards; type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
type UnknownToVoid<T> = undefined extends T ? void : T; type UnknownToVoid<T> = undefined extends T ? void : T;
export type IframeQueryMap = { export type IframeQueryMap = {
[key in keyof IframeQueryMapTypeGuardsType]: { [key in keyof IframeQueryMapTypeGuardsType]: {
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>; query: z.infer<typeof iframeQueryMapTypeGuards[key]["query"]>;
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>; answer: UnknownToVoid<z.infer<typeof iframeQueryMapTypeGuards[key]["answer"]>>;
}; };
}; };
@ -230,11 +302,18 @@ export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQuer
return false; return false;
} }
const result = iframeQueryMapTypeGuards[type].query(event.data); try {
if (!result) { iframeQueryMapTypeGuards[type].query.parse(event.data);
} catch (err) {
if (err instanceof z.ZodError) {
console.error(err.issues);
}
console.warn('Received a query with type "' + type + '" but the payload is invalid.'); console.warn('Received a query with type "' + type + '" but the payload is invalid.');
return false;
} }
return result;
return true;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isLayerEvent = z.object({
name: z.string(),
});
export const isLayerEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/** /**
* A message sent from the iFrame to the game to show/hide a layer. * A message sent from the iFrame to the game to show/hide a layer.
*/ */
export type LayerEvent = tg.GuardedType<typeof isLayerEvent>; export type LayerEvent = z.infer<typeof isLayerEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isLoadPageEvent = new tg.IsInterface() export const isLoadPageEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>; export type LoadPageEvent = z.infer<typeof isLoadPageEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isLoadSoundEvent = new tg.IsInterface() export const isLoadSoundEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>; export type LoadSoundEvent = z.infer<typeof isLoadSoundEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isLoadTilesetEvent = new tg.IsInterface() export const isLoadTilesetEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type LoadTilesetEvent = tg.GuardedType<typeof isLoadTilesetEvent>; export type LoadTilesetEvent = z.infer<typeof isLoadTilesetEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isMapDataEvent = new tg.IsInterface() export const isMapDataEvent = z.object({
.withProperties({ data: z.unknown(), // Todo : Typing
data: tg.isObject, });
})
.get();
/** /**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
*/ */
export type MapDataEvent = tg.GuardedType<typeof isMapDataEvent>; export type MapDataEvent = z.infer<typeof isMapDataEvent>;

View File

@ -1,11 +1,9 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isMovePlayerToEventConfig = new tg.IsInterface() export const isMovePlayerToEventConfig = z.object({
.withProperties({ x: z.number(),
x: tg.isNumber, y: z.number(),
y: tg.isNumber, speed: z.optional(z.number()),
speed: tg.isOptional(tg.isNumber), });
})
.get();
export type MovePlayerToEvent = tg.GuardedType<typeof isMovePlayerToEventConfig>; export type MovePlayerToEvent = z.infer<typeof isMovePlayerToEventConfig>;

View File

@ -1,11 +1,9 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isMovePlayerToEventAnswer = new tg.IsInterface() export const isMovePlayerToEventAnswer = z.object({
.withProperties({ x: z.number(),
x: tg.isNumber, y: z.number(),
y: tg.isNumber, cancelled: z.boolean(),
cancelled: tg.isBoolean, });
})
.get();
export type MovePlayerToEventAnswer = tg.GuardedType<typeof isMovePlayerToEventAnswer>; export type ActionsMenuActionClickedEvent = z.infer<typeof isMovePlayerToEventAnswer>;

View File

@ -1,25 +1,21 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isOpenCoWebsiteEvent = new tg.IsInterface() export const isOpenCoWebsiteEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, allowApi: z.optional(z.boolean()),
allowApi: tg.isOptional(tg.isBoolean), allowPolicy: z.optional(z.string()),
allowPolicy: tg.isOptional(tg.isString), widthPercent: z.optional(z.number()),
widthPercent: tg.isOptional(tg.isNumber), position: z.optional(z.number()),
position: tg.isOptional(tg.isNumber), closable: z.optional(z.boolean()),
closable: tg.isOptional(tg.isBoolean), lazy: z.optional(z.boolean()),
lazy: tg.isOptional(tg.isBoolean), hint: z.optional(z.string()),
hint: tg.isOptional(tg.isString), });
})
.get();
export const isCoWebsite = new tg.IsInterface() export const isCoWebsite = z.object({
.withProperties({ id: z.string(),
id: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type OpenCoWebsiteEvent = tg.GuardedType<typeof isOpenCoWebsiteEvent>; export type OpenCoWebsiteEvent = z.infer<typeof isOpenCoWebsiteEvent>;

View File

@ -1,24 +1,20 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
const isButtonDescriptor = new tg.IsInterface() export const isButtonDescriptor = z.object({
.withProperties({ label: z.string(),
label: tg.isString, className: z.optional(z.string()),
className: tg.isOptional(tg.isString), });
})
.get();
export const isOpenPopupEvent = new tg.IsInterface() export const isOpenPopupEvent = z.object({
.withProperties({ popupId: z.number(),
popupId: tg.isNumber, targetObject: z.string(),
targetObject: tg.isString, message: z.string(),
message: tg.isString, buttons: z.array(isButtonDescriptor),
buttons: tg.isArray(isButtonDescriptor), popupClass: z.string(),
popupClass: tg.isString, input: z.boolean(),
input: tg.isBoolean, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type OpenPopupEvent = tg.GuardedType<typeof isOpenPopupEvent>; export type OpenPopupEvent = z.infer<typeof isOpenPopupEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isOpenTabEvent = new tg.IsInterface() export const isOpenTabEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type OpenTabEvent = tg.GuardedType<typeof isOpenTabEvent>; export type OpenTabEvent = z.infer<typeof isOpenTabEvent>;

View File

@ -1,25 +1,21 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
const isSoundConfig = new tg.IsInterface() export const isSoundConfig = z.object({
.withProperties({ volume: z.optional(z.number()),
volume: tg.isOptional(tg.isNumber), loop: z.optional(z.boolean()),
loop: tg.isOptional(tg.isBoolean), mute: z.optional(z.boolean()),
mute: tg.isOptional(tg.isBoolean), rate: z.optional(z.number()),
rate: tg.isOptional(tg.isNumber), detune: z.optional(z.number()),
detune: tg.isOptional(tg.isNumber), seek: z.optional(z.number()),
seek: tg.isOptional(tg.isNumber), delay: z.optional(z.number()),
delay: tg.isOptional(tg.isNumber), });
})
.get();
export const isPlaySoundEvent = new tg.IsInterface() export const isPlaySoundEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, config: z.optional(isSoundConfig),
config: tg.isOptional(isSoundConfig), });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type PlaySoundEvent = tg.GuardedType<typeof isPlaySoundEvent>; export type PlaySoundEvent = z.infer<typeof isPlaySoundEvent>;

View File

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

View File

@ -1,15 +1,13 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
// TODO: Change for player Clicked, add all neccessary data // TODO: Change for player Clicked, add all neccessary data
export const isRemotePlayerClickedEvent = new tg.IsInterface() export const isRemotePlayerClickedEvent = z.object({
.withProperties({ id: z.number(),
id: tg.isNumber, });
})
.get();
/** /**
* A message sent from the game to the iFrame when RemotePlayer is clicked. * A message sent from the game to the iFrame when RemotePlayer is clicked.
*/ */
export type RemotePlayerClickedEvent = tg.GuardedType<typeof isRemotePlayerClickedEvent>; export type RemotePlayerClickedEvent = z.infer<typeof isRemotePlayerClickedEvent>;
export type RemotePlayerClickedEventCallback = (event: RemotePlayerClickedEvent) => void; export type RemotePlayerClickedEventCallback = (event: RemotePlayerClickedEvent) => void;

View File

@ -1,15 +1,11 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isRemoveActionsMenuKeyFromRemotePlayerEvent = new tg.IsInterface() export const isRemoveActionsMenuKeyFromRemotePlayerEvent = z.object({
.withProperties({ id: z.number(),
id: tg.isNumber, actionKey: z.string(),
actionKey: tg.isString, });
})
.get();
export type RemoveActionsMenuKeyFromRemotePlayerEvent = tg.GuardedType< export type RemoveActionsMenuKeyFromRemotePlayerEvent = z.infer<typeof isRemoveActionsMenuKeyFromRemotePlayerEvent>;
typeof isRemoveActionsMenuKeyFromRemotePlayerEvent
>;
export type RemoveActionsMenuKeyFromRemotePlayerEventCallback = ( export type RemoveActionsMenuKeyFromRemotePlayerEventCallback = (
event: RemoveActionsMenuKeyFromRemotePlayerEvent event: RemoveActionsMenuKeyFromRemotePlayerEvent

View File

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

View File

@ -1,20 +1,17 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isSetVariableEvent = z.object({
key: z.string(),
value: z.unknown(),
target: z.enum(["global", "player"]),
});
export const isSetVariableIframeEvent = z.object({
type: z.enum(["setVariable"]),
data: isSetVariableEvent,
});
export const isSetVariableEvent = new tg.IsInterface()
.withProperties({
key: tg.isString,
value: tg.isUnknown,
target: tg.isSingletonStringUnion("global", "player"),
})
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>; export type SetVariableEvent = z.infer<typeof isSetVariableEvent>;
export const isSetVariableIframeEvent = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString("setVariable"),
data: isSetVariableEvent,
})
.get();

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isStopSoundEvent = new tg.IsInterface() export const isStopSoundEvent = z.object({
.withProperties({ url: z.string(),
url: tg.isString, });
})
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type StopSoundEvent = tg.GuardedType<typeof isStopSoundEvent>; export type StopSoundEvent = z.infer<typeof isStopSoundEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isUserInputChatEvent = z.object({
message: z.string(),
});
export const isUserInputChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
})
.get();
/** /**
* A message sent from the game to the iFrame when a user types a message in the chat. * A message sent from the game to the iFrame when a user types a message in the chat.
*/ */
export type UserInputChatEvent = tg.GuardedType<typeof isUserInputChatEvent>; export type UserInputChatEvent = z.infer<typeof isUserInputChatEvent>;

View File

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

View File

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const isSetPropertyEvent = z.object({
layerName: z.string(),
propertyName: z.string(),
propertyValue: z.optional(z.union([z.string(), z.number(), z.boolean()])),
});
export const isSetPropertyEvent = new tg.IsInterface()
.withProperties({
layerName: tg.isString,
propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
})
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */
export type SetPropertyEvent = tg.GuardedType<typeof isSetPropertyEvent>; export type SetPropertyEvent = z.infer<typeof isSetPropertyEvent>;

View File

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

View File

@ -1,31 +1,25 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
/** /**
* A message sent from a script to the game to remove a custom menu from the menu * A message sent from a script to the game to remove a custom menu from the menu
*/ */
export const isUnregisterMenuEvent = new tg.IsInterface() export const isUnregisterMenuEvent = z.object({
.withProperties({ name: z.string(),
name: tg.isString, });
})
.get();
export type UnregisterMenuEvent = tg.GuardedType<typeof isUnregisterMenuEvent>; export type UnregisterMenuEvent = z.infer<typeof isUnregisterMenuEvent>;
export const isMenuRegisterOptions = new tg.IsInterface() export const isMenuRegisterOptions = z.object({
.withProperties({ allowApi: z.boolean(),
allowApi: tg.isBoolean, });
})
.get();
/** /**
* A message sent from a script to the game to add a custom menu from the menu * A message sent from a script to the game to add a custom menu from the menu
*/ */
export const isMenuRegisterEvent = new tg.IsInterface() export const isMenuRegisterEvent = z.object({
.withProperties({ name: z.string(),
name: tg.isString, iframe: z.optional(z.string()),
iframe: tg.isUnion(tg.isString, tg.isUndefined), options: isMenuRegisterOptions,
options: isMenuRegisterOptions, });
})
.get();
export type MenuRegisterEvent = tg.GuardedType<typeof isMenuRegisterEvent>; export type MenuRegisterEvent = z.infer<typeof isMenuRegisterEvent>;

View File

@ -1,26 +1,22 @@
import * as tg from "generic-type-guard"; import { z } from "zod";
export const triggerActionMessage = "triggerActionMessage"; export const triggerActionMessage = "triggerActionMessage";
export const removeActionMessage = "removeActionMessage"; export const removeActionMessage = "removeActionMessage";
export const isActionMessageType = tg.isSingletonStringUnion("message", "warning"); export const isActionMessageType = z.enum(["message", "warning"]);
export type ActionMessageType = tg.GuardedType<typeof isActionMessageType>; export type ActionMessageType = z.infer<typeof isActionMessageType>;
export const isTriggerActionMessageEvent = new tg.IsInterface() export const isTriggerActionMessageEvent = z.object({
.withProperties({ message: z.string(),
message: tg.isString, uuid: z.string(),
uuid: tg.isString, type: isActionMessageType,
type: isActionMessageType, });
})
.get();
export type TriggerActionMessageEvent = tg.GuardedType<typeof isTriggerActionMessageEvent>; export type TriggerActionMessageEvent = z.infer<typeof isTriggerActionMessageEvent>;
export const isMessageReferenceEvent = new tg.IsInterface() export const isMessageReferenceEvent = z.object({
.withProperties({ uuid: z.string(),
uuid: tg.isString, });
})
.get();
export type MessageReferenceEvent = tg.GuardedType<typeof isMessageReferenceEvent>; export type MessageReferenceEvent = z.infer<typeof isMessageReferenceEvent>;

View File

@ -5,20 +5,16 @@ import {
triggerActionMessage, triggerActionMessage,
} from "./TriggerActionMessageEvent"; } from "./TriggerActionMessageEvent";
import * as tg from "generic-type-guard"; import { z } from "zod";
const isTriggerMessageEventObject = new tg.IsInterface() const isTriggerMessageEventObject = z.object({
.withProperties({ type: z.enum([triggerActionMessage]),
type: tg.isSingletonString(triggerActionMessage), data: isTriggerActionMessageEvent,
data: isTriggerActionMessageEvent, });
})
.get();
const isTriggerMessageRemoveEventObject = new tg.IsInterface() const isTriggerMessageRemoveEventObject = z.object({
.withProperties({ type: z.enum([removeActionMessage]),
type: tg.isSingletonString(removeActionMessage), data: isMessageReferenceEvent,
data: isMessageReferenceEvent, });
})
.get();
export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject); export const isTriggerMessageHandlerEvent = z.union([isTriggerMessageEventObject, isTriggerMessageRemoveEventObject]);

View File

@ -1,13 +1,11 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { isChatEvent } from "./Events/ChatEvent";
import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; import { OpenPopupEvent } from "./Events/OpenPopupEvent";
import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; import { OpenTabEvent } from "./Events/OpenTabEvent";
import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import { ClosePopupEvent } from "./Events/ClosePopupEvent";
import { scriptUtils } from "./ScriptUtils"; import { scriptUtils } from "./ScriptUtils";
import { isGoToPageEvent } from "./Events/GoToPageEvent";
import { import {
IframeErrorAnswerEvent, IframeErrorAnswerEvent,
IframeQueryMap, IframeQueryMap,
@ -15,35 +13,28 @@ import {
IframeResponseEventMap, IframeResponseEventMap,
isIframeEventWrapper, isIframeEventWrapper,
isIframeQueryWrapper, isIframeQueryWrapper,
isLookingLikeIframeEventWrapper,
} from "./Events/IframeEvent"; } from "./Events/IframeEvent";
import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; import { PlaySoundEvent } from "./Events/PlaySoundEvent";
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; import { StopSoundEvent } from "./Events/StopSoundEvent";
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; import { LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"; import { SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import { LayerEvent } from "./Events/LayerEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { SetTilesEvent } from "./Events/SetTilesEvent";
import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { ModifyEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
import { CameraSetEvent, isCameraSetEvent } from "./Events/CameraSetEvent"; import { CameraSetEvent } from "./Events/CameraSetEvent";
import { CameraFollowPlayerEvent, isCameraFollowPlayerEvent } from "./Events/CameraFollowPlayerEvent"; import { CameraFollowPlayerEvent } from "./Events/CameraFollowPlayerEvent";
import type { RemotePlayerClickedEvent } from "./Events/RemotePlayerClickedEvent"; import type { RemotePlayerClickedEvent } from "./Events/RemotePlayerClickedEvent";
import { import { AddActionsMenuKeyToRemotePlayerEvent } from "./Events/AddActionsMenuKeyToRemotePlayerEvent";
AddActionsMenuKeyToRemotePlayerEvent,
isAddActionsMenuKeyToRemotePlayerEvent,
} from "./Events/AddActionsMenuKeyToRemotePlayerEvent";
import type { ActionsMenuActionClickedEvent } from "./Events/ActionsMenuActionClickedEvent"; import type { ActionsMenuActionClickedEvent } from "./Events/ActionsMenuActionClickedEvent";
import { import { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./Events/RemoveActionsMenuKeyFromRemotePlayerEvent";
isRemoveActionsMenuKeyFromRemotePlayerEvent,
RemoveActionsMenuKeyFromRemotePlayerEvent,
} from "./Events/RemoveActionsMenuKeyFromRemotePlayerEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"], query: IframeQueryMap[T]["query"],
@ -153,8 +144,10 @@ class IframeListener {
const payload = message.data; const payload = message.data;
const lookingLikeEvent = isLookingLikeIframeEventWrapper.safeParse(payload);
if (foundSrc === undefined || iframe === undefined) { if (foundSrc === undefined || iframe === undefined) {
if (isIframeEventWrapper(payload)) { if (lookingLikeEvent.success) {
console.warn( console.warn(
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " + "It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
"If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " + "If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " +
@ -224,63 +217,70 @@ class IframeListener {
} catch (reason) { } catch (reason) {
errorHandler(reason); errorHandler(reason);
} }
} else if (isIframeEventWrapper(payload)) { } else if (lookingLikeEvent.success) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) { const iframeEventGuarded = isIframeEventWrapper.safeParse(lookingLikeEvent.data);
this._showLayerStream.next(payload.data);
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) { if (!iframeEventGuarded.success) {
this._hideLayerStream.next(payload.data); console.error(
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { `Invalid event "${lookingLikeEvent.data.type}" received from Iframe: `,
this._setPropertyStream.next(payload.data); lookingLikeEvent.data,
} else if (payload.type === "cameraSet" && isCameraSetEvent(payload.data)) { iframeEventGuarded.error.issues
this._cameraSetStream.next(payload.data); );
} else if (payload.type === "cameraFollowPlayer" && isCameraFollowPlayerEvent(payload.data)) { return;
this._cameraFollowPlayerStream.next(payload.data); }
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
scriptUtils.sendAnonymousChat(payload.data); const iframeEvent = iframeEventGuarded.data;
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data); if (iframeEvent.type === "showLayer") {
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { this._showLayerStream.next(iframeEvent.data);
this._closePopupStream.next(payload.data); } else if (iframeEvent.type === "hideLayer") {
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) { this._hideLayerStream.next(iframeEvent.data);
scriptUtils.openTab(payload.data.url); } else if (iframeEvent.type === "setProperty") {
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) { this._setPropertyStream.next(iframeEvent.data);
scriptUtils.goToPage(payload.data.url); } else if (iframeEvent.type === "cameraSet") {
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) { this._cameraSetStream.next(iframeEvent.data);
this._loadPageStream.next(payload.data.url); } else if (iframeEvent.type === "cameraFollowPlayer") {
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) { this._cameraFollowPlayerStream.next(iframeEvent.data);
this._playSoundStream.next(payload.data); } else if (iframeEvent.type === "chat") {
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) { scriptUtils.sendAnonymousChat(iframeEvent.data);
this._stopSoundStream.next(payload.data); } else if (iframeEvent.type === "openPopup") {
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { this._openPopupStream.next(iframeEvent.data);
this._loadSoundStream.next(payload.data); } else if (iframeEvent.type === "closePopup") {
} else if (payload.type === "disablePlayerControls") { this._closePopupStream.next(iframeEvent.data);
} else if (iframeEvent.type === "openTab") {
scriptUtils.openTab(iframeEvent.data.url);
} else if (iframeEvent.type === "goToPage") {
scriptUtils.goToPage(iframeEvent.data.url);
} else if (iframeEvent.type === "loadPage") {
this._loadPageStream.next(iframeEvent.data.url);
} else if (iframeEvent.type === "playSound") {
this._playSoundStream.next(iframeEvent.data);
} else if (iframeEvent.type === "stopSound") {
this._stopSoundStream.next(iframeEvent.data);
} else if (iframeEvent.type === "loadSound") {
this._loadSoundStream.next(iframeEvent.data);
} else if (iframeEvent.type === "disablePlayerControls") {
this._disablePlayerControlStream.next(); this._disablePlayerControlStream.next();
} else if (payload.type === "restorePlayerControls") { } else if (iframeEvent.type === "restorePlayerControls") {
this._enablePlayerControlStream.next(); this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") { } else if (iframeEvent.type === "displayBubble") {
this._displayBubbleStream.next(); this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") { } else if (iframeEvent.type === "removeBubble") {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (iframeEvent.type == "onPlayerMove") {
this.sendPlayerMove = true; this.sendPlayerMove = true;
} else if ( } else if (iframeEvent.type == "addActionsMenuKeyToRemotePlayer") {
payload.type == "addActionsMenuKeyToRemotePlayer" && this._addActionsMenuKeyToRemotePlayerStream.next(iframeEvent.data);
isAddActionsMenuKeyToRemotePlayerEvent(payload.data) } else if (iframeEvent.type == "removeActionsMenuKeyFromRemotePlayer") {
) { this._removeActionsMenuKeyFromRemotePlayerEvent.next(iframeEvent.data);
this._addActionsMenuKeyToRemotePlayerStream.next(payload.data); } else if (iframeEvent.type == "onCameraUpdate") {
} else if (
payload.type == "removeActionsMenuKeyFromRemotePlayer" &&
isRemoveActionsMenuKeyFromRemotePlayerEvent(payload.data)
) {
this._removeActionsMenuKeyFromRemotePlayerEvent.next(payload.data);
} else if (payload.type == "onCameraUpdate") {
this._trackCameraUpdateStream.next(); this._trackCameraUpdateStream.next();
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { } else if (iframeEvent.type == "setTiles") {
this._setTilesStream.next(payload.data); this._setTilesStream.next(iframeEvent.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { } else if (iframeEvent.type == "modifyEmbeddedWebsite") {
this._modifyEmbeddedWebsiteStream.next(payload.data); this._modifyEmbeddedWebsiteStream.next(iframeEvent.data);
} else if (payload.type == "registerMenu" && isMenuRegisterEvent(payload.data)) { } else if (iframeEvent.type == "registerMenu") {
const dataName = payload.data.name; const dataName = iframeEvent.data.name;
this.iframeCloseCallbacks.get(iframe)?.push(() => { this.iframeCloseCallbacks.get(iframe)?.push(() => {
handleMenuUnregisterEvent(dataName); handleMenuUnregisterEvent(dataName);
}); });
@ -288,13 +288,17 @@ class IframeListener {
foundSrc = this.getBaseUrl(foundSrc, message.source); foundSrc = this.getBaseUrl(foundSrc, message.source);
handleMenuRegistrationEvent( handleMenuRegistrationEvent(
payload.data.name, iframeEvent.data.name,
payload.data.iframe, iframeEvent.data.iframe,
foundSrc, foundSrc,
payload.data.options iframeEvent.data.options
); );
} else if (payload.type == "unregisterMenu" && isUnregisterMenuEvent(payload.data)) { } else if (iframeEvent.type == "unregisterMenu") {
handleMenuUnregisterEvent(payload.data.name); handleMenuUnregisterEvent(iframeEvent.data.name);
} else {
// Keep the line below. It will throw an error if we forget to handle one of the possible values.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveCheck: never = iframeEvent;
} }
} }
}, },
@ -319,7 +323,7 @@ class IframeListener {
} }
registerScript(scriptUrl: string, enableModuleMode: boolean = true): Promise<void> { registerScript(scriptUrl: string, enableModuleMode: boolean = true): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve) => {
console.info("Loading map related script at ", scriptUrl); console.info("Loading map related script at ", scriptUrl);
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");

View File

@ -1,14 +1,8 @@
import type * as tg from "generic-type-guard"; import { z } from "zod";
import type { import type { IframeEvent, IframeQuery, IframeQueryMap, IframeResponseEventMap } from "../Events/IframeEvent";
IframeEvent,
IframeEventMap,
IframeQuery,
IframeQueryMap,
IframeResponseEventMap,
} from "../Events/IframeEvent";
import type { IframeQueryWrapper } from "../Events/IframeEvent"; import type { IframeQueryWrapper } from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) { export function sendToWorkadventure(content: IframeEvent) {
window.parent.postMessage(content, "*"); window.parent.postMessage(content, "*");
} }
@ -48,12 +42,10 @@ export function queryWorkadventure<T extends keyof IframeQueryMap>(
}); });
} }
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never;
export interface IframeCallback< export interface IframeCallback<
Key extends keyof IframeResponseEventMap, Key extends keyof IframeResponseEventMap,
T = IframeResponseEventMap[Key], T = IframeResponseEventMap[Key],
Guard = tg.TypeGuard<T> Guard = z.ZodType<T>
> { > {
typeChecker: Guard; typeChecker: Guard;
callback: (payloadData: T) => void; callback: (payloadData: T) => void;

View File

@ -1,9 +1,5 @@
import { sendToWorkadventure } from "../IframeApiContribution"; import { sendToWorkadventure } from "../IframeApiContribution";
import type { import type { CreateEmbeddedWebsiteEvent, Rectangle } from "../../Events/EmbeddedWebsiteEvent";
CreateEmbeddedWebsiteEvent,
ModifyEmbeddedWebsiteEvent,
Rectangle,
} from "../../Events/EmbeddedWebsiteEvent";
export class EmbeddedWebsite { export class EmbeddedWebsite {
public readonly name: string; public readonly name: string;

View File

@ -41,7 +41,7 @@ export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdven
onCameraUpdate(): Subject<WasCameraUpdatedEvent> { onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
sendToWorkadventure({ sendToWorkadventure({
type: "onCameraUpdate", type: "onCameraUpdate",
data: null, data: undefined,
}); });
return moveStream; return moveStream;
} }

View File

@ -4,11 +4,11 @@ export class WorkadventureControlsCommands extends IframeApiContribution<Workadv
callbacks = []; callbacks = [];
disablePlayerControls(): void { disablePlayerControls(): void {
sendToWorkadventure({ type: "disablePlayerControls", data: null }); sendToWorkadventure({ type: "disablePlayerControls", data: undefined });
} }
restorePlayerControls(): void { restorePlayerControls(): void {
sendToWorkadventure({ type: "restorePlayerControls", data: null }); sendToWorkadventure({ type: "restorePlayerControls", data: undefined });
} }
} }

View File

@ -54,7 +54,7 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
moveStream.subscribe(callback); moveStream.subscribe(callback);
sendToWorkadventure({ sendToWorkadventure({
type: "onPlayerMove", type: "onPlayerMove",
data: null, data: undefined,
}); });
} }

View File

@ -1,7 +1,4 @@
import type { LoadSoundEvent } from "../Events/LoadSoundEvent"; import { IframeApiContribution } from "./IframeApiContribution";
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { Sound } from "./Sound/Sound"; import { Sound } from "./Sound/Sound";
export class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> { export class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {

View File

@ -1,13 +1,9 @@
import { Observable, Subject } from "rxjs"; import { Observable, Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { IframeApiContribution, queryWorkadventure } from "./IframeApiContribution";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent"; import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> { export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
private setVariableResolvers = new Subject<SetVariableEvent>(); private setVariableResolvers = new Subject<SetVariableEvent>();
private variables = new Map<string, unknown>(); private variables = new Map<string, unknown>();
@ -17,7 +13,7 @@ export class WorkadventureStateCommands extends IframeApiContribution<Workadvent
super(); super();
this.setVariableResolvers.subscribe((event) => { this.setVariableResolvers.subscribe((event) => {
const oldValue = this.variables.get(event.key); // const oldValue = this.variables.get(event.key);
// If we are setting the same value, no need to do anything. // If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager // No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) { /*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
@ -92,6 +88,7 @@ export function createState(target: "global" | "player"): WorkadventureStateComm
} }
return target.loadVariable(p.toString()); return target.loadVariable(p.toString());
}, },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { 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. // 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. // User must use WA.state.saveVariable to have error message.

View File

@ -34,14 +34,6 @@ interface MenuDescriptor {
export type MenuOptions = RequireOnlyOne<MenuDescriptor, "callback" | "iframe">; export type MenuOptions = RequireOnlyOne<MenuDescriptor, "callback" | "iframe">;
interface ZonedPopupOptions {
zone: string;
objectLayerName?: string;
popupText: string;
delay?: number;
popupOptions: Array<ButtonDescriptor>;
}
export interface ActionMessageOptions { export interface ActionMessageOptions {
message: string; message: string;
type?: "message" | "warning"; type?: "message" | "warning";
@ -284,11 +276,11 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
} }
public displayBubble(): void { public displayBubble(): void {
sendToWorkadventure({ type: "displayBubble", data: null }); sendToWorkadventure({ type: "displayBubble", data: undefined });
} }
public removeBubble(): void { public removeBubble(): void {
sendToWorkadventure({ type: "removeBubble", data: null }); sendToWorkadventure({ type: "removeBubble", data: undefined });
} }
public displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage { public displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {

View File

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

View File

@ -271,10 +271,6 @@
.cowebsite-thumbnail-hint { .cowebsite-thumbnail-hint {
display: inline-block; display: inline-block;
} }
&.hide {
display: none;
}
} }
} }
</style> </style>

View File

@ -42,4 +42,4 @@ axiosWithRetry.interceptors.response.use((res) => {
return res; return res;
}); });
const interceptorId = rax.attach(axiosWithRetry); rax.attach(axiosWithRetry);

View File

@ -4,7 +4,7 @@ import { RoomConnection } from "./RoomConnection";
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels"; import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
import { GameConnexionTypes, urlManager } from "../Url/UrlManager"; import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
import { localUserStore } from "./LocalUserStore"; import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser"; import { LocalUser } from "./LocalUser";
import { Room } from "./Room"; import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker"; import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore"; import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
@ -13,7 +13,6 @@ import { analyticsClient } from "../Administration/AnalyticsClient";
import { axiosWithRetry } from "./AxiosUtils"; import { axiosWithRetry } from "./AxiosUtils";
import axios from "axios"; import axios from "axios";
import { isRegisterData } from "../Messages/JsonMessages/RegisterData"; import { isRegisterData } from "../Messages/JsonMessages/RegisterData";
import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
import { limitMapStore } from "../Stores/GameStore"; import { limitMapStore } from "../Stores/GameStore";
import { showLimitRoomModalStore } from "../Stores/ModalStore"; import { showLimitRoomModalStore } from "../Stores/ModalStore";
import { gameManager } from "../Phaser/Game/GameManager"; import { gameManager } from "../Phaser/Game/GameManager";
@ -74,9 +73,7 @@ class ConnectionManager {
//Logout user in pusher and hydra //Logout user in pusher and hydra
const token = localUserStore.getAuthToken(); const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/logout-callback`, { params: { token } }).then( await Axios.get(`${PUSHER_URL}/logout-callback`, { params: { token } }).then((res) => res.data);
(res) => res.data
);
localUserStore.setAuthToken(null); localUserStore.setAuthToken(null);
//Go on login page can permit to clear token and start authentication process //Go on login page can permit to clear token and start authentication process
@ -140,13 +137,19 @@ class ConnectionManager {
//@deprecated //@deprecated
else if (this.connexionType === GameConnexionTypes.register) { else if (this.connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken(); const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then( const result = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
(res) => res.data (res) => res.data
); );
if (!isRegisterData(data)) {
console.error("Invalid data received from /register route. Data: ", data); const registerDataChecking = isRegisterData.safeParse(result);
if (!registerDataChecking.success) {
console.error("Invalid data received from /register route. Data: ", result);
throw new Error("Invalid data received from /register route."); throw new Error("Invalid data received from /register route.");
} }
const data = registerDataChecking.data;
this.localUser = new LocalUser(data.userUuid, data.email); this.localUser = new LocalUser(data.userUuid, data.email);
this.authToken = data.authToken; this.authToken = data.authToken;
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
@ -307,9 +310,9 @@ class ConnectionManager {
connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => { connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => {
resolve(connect); resolve(connect);
}); });
}).catch((err) => { }).catch(() => {
// Let's retry in 4-6 seconds // Let's retry in 4-6 seconds
return new Promise<OnConnectInterface>((resolve, reject) => { return new Promise<OnConnectInterface>((resolve) => {
this.reconnectingTimeout = setTimeout(() => { this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion? //todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.

View File

@ -5,7 +5,7 @@ import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTe
export interface PointInterface { export interface PointInterface {
x: number; x: number;
y: number; y: number;
direction: string; // TODO: modify this to the enum from ts-proto direction: "up" | "down" | "left" | "right"; // TODO: modify this to the enum from ts-proto
moving: boolean; moving: boolean;
} }
@ -14,6 +14,7 @@ export interface MessageUserPositionInterface {
name: string; name: string;
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
away: boolean;
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string | null; companion: string | null;
userUuid: string; userUuid: string;
@ -29,6 +30,7 @@ export interface MessageUserJoined {
name: string; name: string;
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
away: boolean;
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string | null; companion: string | null;
userUuid: string; userUuid: string;

View File

@ -1,7 +1,4 @@
import * as rax from "retry-axios";
import Axios from "axios";
import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import type { CharacterTexture } from "./LocalUser";
import { localUserStore } from "./LocalUserStore"; import { localUserStore } from "./LocalUserStore";
import axios from "axios"; import axios from "axios";
import { axiosWithRetry } from "./AxiosUtils"; import { axiosWithRetry } from "./AxiosUtils";
@ -112,11 +109,14 @@ export class Room {
data.authenticationMandatory = Boolean(data.authenticationMandatory); data.authenticationMandatory = Boolean(data.authenticationMandatory);
} }
if (isRoomRedirect(data)) { const roomRedirectChecking = isRoomRedirect.safeParse(data);
const mapDetailsDataChecking = isMapDetailsData.safeParse(data);
if (roomRedirectChecking.success) {
return { return {
redirectUrl: data.redirectUrl, redirectUrl: data.redirectUrl,
}; };
} else if (isMapDetailsData(data)) { } else if (mapDetailsDataChecking.success) {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl); console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl; this._mapUrl = data.mapUrl;
this._group = data.group; this._group = data.group;
@ -132,6 +132,9 @@ export class Room {
this._loginSceneLogo = data.loginSceneLogo ?? undefined; this._loginSceneLogo = data.loginSceneLogo ?? undefined;
return new MapDetail(data.mapUrl); return new MapDetail(data.mapUrl);
} else { } else {
console.log(data);
console.error("roomRedirectChecking", roomRedirectChecking.error.issues);
console.error("mapDetailsDataChecking", mapDetailsDataChecking.error.issues);
throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format."); throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
} }
} catch (e) { } catch (e) {

View File

@ -190,7 +190,7 @@ export class RoomConnection implements RoomConnection {
let interval: ReturnType<typeof setInterval> | undefined = undefined; let interval: ReturnType<typeof setInterval> | undefined = undefined;
this.socket.onopen = (ev) => { this.socket.onopen = () => {
//we manually ping every 20s to not be logged out by the server, even when the game is in background. //we manually ping every 20s to not be logged out by the server, even when the game is in background.
const pingMessage = PingMessageTsProto.encode({}).finish(); const pingMessage = PingMessageTsProto.encode({}).finish();
interval = setInterval(() => this.socket.send(pingMessage), manualPingDelay); interval = setInterval(() => this.socket.send(pingMessage), manualPingDelay);
@ -297,6 +297,7 @@ export class RoomConnection implements RoomConnection {
} }
default: { default: {
// Security check: if we forget a "case", the line below will catch the error at compile-time. // Security check: if we forget a "case", the line below will catch the error at compile-time.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const tmp: never = subMessage; const tmp: never = subMessage;
} }
} }
@ -477,6 +478,7 @@ export class RoomConnection implements RoomConnection {
} }
default: { default: {
// Security check: if we forget a "case", the line below will catch the error at compile-time. // Security check: if we forget a "case", the line below will catch the error at compile-time.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const tmp: never = message; const tmp: never = message;
} }
} }
@ -518,6 +520,20 @@ export class RoomConnection implements RoomConnection {
this.socket.send(bytes); this.socket.send(bytes);
} }
public emitPlayerAway(away: boolean): void {
const message = SetPlayerDetailsMessageTsProto.fromPartial({
away,
});
const bytes = ClientToServerMessageTsProto.encode({
message: {
$case: "setPlayerDetailsMessage",
setPlayerDetailsMessage: message,
},
}).finish();
this.socket.send(bytes);
}
public emitPlayerOutlineColor(color: number | null) { public emitPlayerOutlineColor(color: number | null) {
let message: SetPlayerDetailsMessageTsProto; let message: SetPlayerDetailsMessageTsProto;
if (color === null) { if (color === null) {
@ -654,6 +670,7 @@ export class RoomConnection implements RoomConnection {
characterLayers, characterLayers,
visitCardUrl: message.visitCardUrl, visitCardUrl: message.visitCardUrl,
position: ProtobufClientUtils.toPointInterface(position), position: ProtobufClientUtils.toPointInterface(position),
away: message.away,
companion: companion ? companion.name : null, companion: companion ? companion.name : null,
userUuid: message.userUuid, userUuid: message.userUuid,
outlineColor: message.hasOutline ? message.outlineColor : undefined, outlineColor: message.hasOutline ? message.outlineColor : undefined,

View File

@ -4,7 +4,7 @@ import type { PointInterface } from "../Connexion/ConnexionModels";
export class ProtobufClientUtils { export class ProtobufClientUtils {
public static toPointInterface(position: PositionMessage): PointInterface { public static toPointInterface(position: PositionMessage): PointInterface {
let direction: string; let direction: "up" | "down" | "left" | "right";
switch (position.direction) { switch (position.direction) {
case PositionMessage_Direction.UP: case PositionMessage_Direction.UP:
direction = "up"; direction = "up";

View File

@ -1,9 +1,7 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
import { DirtyScene } from "../Game/DirtyScene"; import { DirtyScene } from "../Game/DirtyScene";
import { gameManager } from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
import CancelablePromise from "cancelable-promise"; import CancelablePromise from "cancelable-promise";
import Image = Phaser.GameObjects.Image;
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
const TextName: string = "Loading..."; const TextName: string = "Loading...";
@ -35,8 +33,6 @@ export class Loader {
const logoResource = gameManager.currentStartedRoom.loadingLogo ?? "static/images/logo.png"; const logoResource = gameManager.currentStartedRoom.loadingLogo ?? "static/images/logo.png";
this.logoNameIndex = "logoLoading" + logoResource; this.logoNameIndex = "logoLoading" + logoResource;
const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3);
//add loading if logo image until logo image is ready //add loading if logo image until logo image is ready
this.loadingText = this.scene.add.text( this.loadingText = this.scene.add.text(
this.scene.game.renderer.width / 2, this.scene.game.renderer.width / 2,

View File

@ -0,0 +1,65 @@
import { Easing } from "../../types";
export class PlayerStatusDot extends Phaser.GameObjects.Container {
private graphics: Phaser.GameObjects.Graphics;
private away: boolean;
private readonly COLORS = {
// online: 0x00ff00,
// away: 0xffff00,
online: 0x8cc43f,
onlineOutline: 0x427a25,
away: 0xf5931e,
awayOutline: 0x875d13,
};
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y);
this.away = false;
this.graphics = this.scene.add.graphics();
this.add(this.graphics);
this.redraw();
this.scene.add.existing(this);
}
public setAway(away: boolean = true, instant: boolean = false): void {
if (this.away === away) {
return;
}
this.away = away;
if (instant) {
this.redraw();
} else {
this.playStatusChangeAnimation();
}
}
private playStatusChangeAnimation(): void {
this.scale = 1;
this.scene.tweens.add({
targets: [this],
duration: 200,
yoyo: true,
ease: Easing.BackEaseIn,
scale: 0,
onYoyo: () => {
this.redraw();
},
onComplete: () => {
this.scale = 1;
},
});
}
private redraw(): void {
this.graphics.clear();
this.graphics.fillStyle(this.away ? this.COLORS.away : this.COLORS.online);
this.graphics.lineStyle(1, this.away ? this.COLORS.awayOutline : this.COLORS.onlineOutline);
this.graphics.fillCircle(0, 0, 3);
this.graphics.strokeCircle(0, 0, 3);
}
}

View File

@ -1,6 +1,5 @@
import type { ITiledMapObject } from "../Map/ITiledMap"; import type { ITiledMapObject } from "../Map/ITiledMap";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import { type } from "os";
import { GameMapProperties } from "../Game/GameMapProperties"; import { GameMapProperties } from "../Game/GameMapProperties";
export class TextUtils { export class TextUtils {

View File

@ -1,9 +1,4 @@
import { import { getPlayerAnimations, PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
AnimationData,
getPlayerAnimations,
PlayerAnimationDirections,
PlayerAnimationTypes,
} from "../Player/Animation";
import { SpeechBubble } from "./SpeechBubble"; import { SpeechBubble } from "./SpeechBubble";
import Text = Phaser.GameObjects.Text; import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container; import Container = Phaser.GameObjects.Container;
@ -24,6 +19,7 @@ import type { OutlineableInterface } from "../Game/OutlineableInterface";
import type CancelablePromise from "cancelable-promise"; import type CancelablePromise from "cancelable-promise";
import { TalkIcon } from "../Components/TalkIcon"; import { TalkIcon } from "../Components/TalkIcon";
import { Deferred } from "ts-deferred"; import { Deferred } from "ts-deferred";
import { PlayerStatusDot } from "../Components/PlayerStatusDot";
const playerNameY = -25; const playerNameY = -25;
const interactiveRadius = 35; const interactiveRadius = 35;
@ -32,6 +28,7 @@ export abstract class Character extends Container implements OutlineableInterfac
private bubble: SpeechBubble | null = null; private bubble: SpeechBubble | null = null;
private readonly playerNameText: Text; private readonly playerNameText: Text;
private readonly talkIcon: TalkIcon; private readonly talkIcon: TalkIcon;
protected readonly statusDot: PlayerStatusDot;
public playerName: string; public playerName: string;
public sprites: Map<string, Sprite>; public sprites: Map<string, Sprite>;
protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
@ -137,7 +134,8 @@ export abstract class Character extends Container implements OutlineableInterfac
}); });
} }
this.playerNameText.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX); this.playerNameText.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerNameText); this.statusDot = new PlayerStatusDot(scene, this.playerNameText.getLeftCenter().x - 6, playerNameY - 1);
this.add([this.playerNameText, this.statusDot]);
this.setClickable(isClickable); this.setClickable(isClickable);
@ -238,6 +236,10 @@ export abstract class Character extends Container implements OutlineableInterfac
this.talkIcon.show(show, forceClose); this.talkIcon.show(show, forceClose);
} }
public setAwayStatus(away: boolean = true, instant: boolean = false): void {
this.statusDot.setAway(away, instant);
}
public addCompanion(name: string, texturePromise?: CancelablePromise<string>): void { public addCompanion(name: string, texturePromise?: CancelablePromise<string>): void {
if (typeof texturePromise !== "undefined") { if (typeof texturePromise !== "undefined") {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);

View File

@ -1,6 +1,5 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type { CharacterTexture } from "../../Connexion/LocalUser"; import { BodyResourceDescriptionInterface, PlayerTextures, PlayerTexturesKey } from "./PlayerTextures";
import { BodyResourceDescriptionInterface, mapLayerToLevel, PlayerTextures, PlayerTexturesKey } from "./PlayerTextures";
import CancelablePromise from "cancelable-promise"; import CancelablePromise from "cancelable-promise";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;

View File

@ -97,6 +97,7 @@ export class CameraManager extends Phaser.Events.EventEmitter {
}); });
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.camera.pan(setTo.x, setTo.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => { this.camera.pan(setTo.x, setTo.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => {
if (this.cameraMode === CameraMode.Positioned) { if (this.cameraMode === CameraMode.Positioned) {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange; this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
@ -138,6 +139,7 @@ export class CameraManager extends Phaser.Events.EventEmitter {
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData()); this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.camera.pan(focusOn.x, focusOn.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => { this.camera.pan(focusOn.x, focusOn.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange; this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
if (progress === 1) { if (progress === 1) {

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