Merge branch 'develop' of github.com:thecodingmachine/workadventure into main
This commit is contained in:
@@ -26,7 +26,6 @@
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
|
||||
// TODO: remove those ignored rules and write a stronger code!
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isRectangle = new tg.IsInterface()
|
||||
.withProperties({
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
width: tg.isNumber,
|
||||
height: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
|
||||
export const isEmbeddedWebsiteEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
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,
|
||||
})
|
||||
.get();
|
||||
|
||||
export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
name: tg.isString,
|
||||
url: tg.isString,
|
||||
position: isRectangle,
|
||||
})
|
||||
.withOptionalProperties({
|
||||
visible: tg.isBoolean,
|
||||
allowApi: tg.isBoolean,
|
||||
allow: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to modify an embedded website
|
||||
*/
|
||||
export type ModifyEmbeddedWebsiteEvent = tg.GuardedType<typeof isEmbeddedWebsiteEvent>;
|
||||
|
||||
export type CreateEmbeddedWebsiteEvent = tg.GuardedType<typeof isCreateEmbeddedWebsiteEvent>;
|
||||
// TODO: make a variation that is all optional (except for the name)
|
||||
export type Rectangle = tg.GuardedType<typeof isRectangle>;
|
||||
@@ -9,6 +9,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
||||
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
||||
import type { MapDataEvent } from "./MapDataEvent";
|
||||
import type { LayerEvent } from "./LayerEvent";
|
||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||
@@ -21,8 +22,17 @@ import type { SetVariableEvent } from "./SetVariableEvent";
|
||||
import { isGameStateEvent } from "./GameStateEvent";
|
||||
import { isMapDataEvent } from "./MapDataEvent";
|
||||
import { isSetVariableEvent } from "./SetVariableEvent";
|
||||
import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite";
|
||||
import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
|
||||
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
|
||||
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
|
||||
import type {
|
||||
MessageReferenceEvent,
|
||||
removeActionMessage,
|
||||
triggerActionMessage,
|
||||
TriggerActionMessageEvent,
|
||||
} from "./ui/TriggerActionMessageEvent";
|
||||
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
@@ -55,6 +65,7 @@ export type IframeEventMap = {
|
||||
loadTileset: LoadTilesetEvent;
|
||||
registerMenuCommand: MenuItemRegisterEvent;
|
||||
setTiles: SetTilesEvent;
|
||||
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
|
||||
};
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
@@ -73,6 +84,7 @@ export interface IframeResponseEventMap {
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
setVariable: SetVariableEvent;
|
||||
messageTriggered: MessageReferenceEvent;
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
@@ -105,6 +117,26 @@ export const iframeQueryMapTypeGuards = {
|
||||
query: isLoadTilesetEvent,
|
||||
answer: tg.isNumber,
|
||||
},
|
||||
triggerActionMessage: {
|
||||
query: isTriggerActionMessageEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
removeActionMessage: {
|
||||
query: isMessageReferenceEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
getEmbeddedWebsite: {
|
||||
query: tg.isString,
|
||||
answer: isCreateEmbeddedWebsiteEvent,
|
||||
},
|
||||
deleteEmbeddedWebsite: {
|
||||
query: tg.isString,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
createEmbeddedWebsite: {
|
||||
query: isCreateEmbeddedWebsiteEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
};
|
||||
|
||||
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
|
||||
@@ -141,7 +173,12 @@ export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQuer
|
||||
if (!isIframeQueryKey(type)) {
|
||||
return false;
|
||||
}
|
||||
return iframeQueryMapTypeGuards[type].query(event.data);
|
||||
|
||||
const result = iframeQueryMapTypeGuards[type].query(event.data);
|
||||
if (!result) {
|
||||
console.warn('Received a query with type "' + type + '" but the payload is invalid.');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const triggerActionMessage = "triggerActionMessage";
|
||||
export const removeActionMessage = "removeActionMessage";
|
||||
|
||||
export const isActionMessageType = tg.isSingletonStringUnion("message", "warning");
|
||||
|
||||
export type ActionMessageType = tg.GuardedType<typeof isActionMessageType>;
|
||||
|
||||
export const isTriggerActionMessageEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
message: tg.isString,
|
||||
uuid: tg.isString,
|
||||
type: isActionMessageType,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type TriggerActionMessageEvent = tg.GuardedType<typeof isTriggerActionMessageEvent>;
|
||||
|
||||
export const isMessageReferenceEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
uuid: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type MessageReferenceEvent = tg.GuardedType<typeof isMessageReferenceEvent>;
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
isMessageReferenceEvent,
|
||||
isTriggerActionMessageEvent,
|
||||
removeActionMessage,
|
||||
triggerActionMessage,
|
||||
} from './TriggerActionMessageEvent';
|
||||
|
||||
import * as tg from 'generic-type-guard';
|
||||
|
||||
const isTriggerMessageEventObject = new tg.IsInterface()
|
||||
.withProperties({
|
||||
type: tg.isSingletonString(triggerActionMessage),
|
||||
data: isTriggerActionMessageEvent,
|
||||
})
|
||||
.get();
|
||||
|
||||
const isTriggerMessageRemoveEventObject = new tg.IsInterface()
|
||||
.withProperties({
|
||||
type: tg.isSingletonString(removeActionMessage),
|
||||
data: isMessageReferenceEvent,
|
||||
})
|
||||
.get();
|
||||
|
||||
export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject);
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Subject } from "rxjs";
|
||||
import type * as tg from "generic-type-guard";
|
||||
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
|
||||
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
|
||||
@@ -31,6 +32,8 @@ import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
||||
import type { SetVariableEvent } from "./Events/SetVariableEvent";
|
||||
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
|
||||
import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"],
|
||||
@@ -111,6 +114,9 @@ class IframeListener {
|
||||
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
|
||||
public readonly setTilesStream = this._setTilesStream.asObservable();
|
||||
|
||||
private readonly _modifyEmbeddedWebsiteStream: Subject<ModifyEmbeddedWebsiteEvent> = new Subject();
|
||||
public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable();
|
||||
|
||||
private readonly iframes = new Set<HTMLIFrameElement>();
|
||||
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
@@ -124,7 +130,7 @@ class IframeListener {
|
||||
init() {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||
(message: MessageEvent<unknown>) => {
|
||||
// Do we trust the sender of this message?
|
||||
// Let's only accept messages from the iframe that are allowed.
|
||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||
@@ -266,6 +272,8 @@ class IframeListener {
|
||||
handleMenuItemRegistrationEvent(payload.data);
|
||||
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
||||
this._setTilesStream.next(payload.data);
|
||||
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
|
||||
this._modifyEmbeddedWebsiteStream.next(payload.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -423,6 +431,15 @@ class IframeListener {
|
||||
});
|
||||
}
|
||||
|
||||
sendActionMessageTriggered(uuid: string): void {
|
||||
this.postMessage({
|
||||
type: "messageTriggered",
|
||||
data: {
|
||||
uuid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
|
||||
@@ -1,51 +1,66 @@
|
||||
import type * as tg from "generic-type-guard";
|
||||
import type {
|
||||
IframeEvent,
|
||||
IframeEventMap, IframeQuery,
|
||||
IframeEventMap,
|
||||
IframeQuery,
|
||||
IframeQueryMap,
|
||||
IframeResponseEventMap
|
||||
} from '../Events/IframeEvent';
|
||||
import type {IframeQueryWrapper} from "../Events/IframeEvent";
|
||||
IframeResponseEventMap,
|
||||
} from "../Events/IframeEvent";
|
||||
import type { IframeQueryWrapper } from "../Events/IframeEvent";
|
||||
|
||||
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
|
||||
window.parent.postMessage(content, "*")
|
||||
window.parent.postMessage(content, "*");
|
||||
}
|
||||
|
||||
let queryNumber = 0;
|
||||
|
||||
export const answerPromises = new Map<number, {
|
||||
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
reject: (reason?: any) => void
|
||||
}>();
|
||||
export const answerPromises = new Map<
|
||||
number,
|
||||
{
|
||||
resolve: (
|
||||
value:
|
||||
| IframeQueryMap[keyof IframeQueryMap]["answer"]
|
||||
| PromiseLike<IframeQueryMap[keyof IframeQueryMap]["answer"]>
|
||||
) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
|
||||
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
|
||||
window.parent.postMessage({
|
||||
id: queryNumber,
|
||||
query: content
|
||||
} as IframeQueryWrapper<T>, "*");
|
||||
export function queryWorkadventure<T extends keyof IframeQueryMap>(
|
||||
content: IframeQuery<T>
|
||||
): Promise<IframeQueryMap[T]["answer"]> {
|
||||
return new Promise<IframeQueryMap[T]["answer"]>((resolve, reject) => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
id: queryNumber,
|
||||
query: content,
|
||||
} as IframeQueryWrapper<T>,
|
||||
"*"
|
||||
);
|
||||
|
||||
answerPromises.set(queryNumber, {
|
||||
resolve,
|
||||
reject
|
||||
reject,
|
||||
});
|
||||
|
||||
queryNumber++;
|
||||
});
|
||||
}
|
||||
|
||||
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
|
||||
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never;
|
||||
|
||||
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
|
||||
|
||||
typeChecker: Guard,
|
||||
callback: (payloadData: T) => void
|
||||
export interface IframeCallback<
|
||||
Key extends keyof IframeResponseEventMap,
|
||||
T = IframeResponseEventMap[Key],
|
||||
Guard = tg.TypeGuard<T>
|
||||
> {
|
||||
typeChecker: Guard;
|
||||
callback: (payloadData: T) => void;
|
||||
}
|
||||
|
||||
export interface IframeCallbackContribution<Key extends keyof IframeResponseEventMap> extends IframeCallback<Key> {
|
||||
|
||||
type: Key
|
||||
type: Key;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,9 +69,10 @@ export interface IframeCallbackContribution<Key extends keyof IframeResponseEven
|
||||
*
|
||||
*/
|
||||
|
||||
export abstract class IframeApiContribution<T extends {
|
||||
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>,
|
||||
}> {
|
||||
|
||||
abstract callbacks: T["callbacks"]
|
||||
export abstract class IframeApiContribution<
|
||||
T extends {
|
||||
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>;
|
||||
}
|
||||
> {
|
||||
abstract callbacks: T["callbacks"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { sendToWorkadventure } from "../IframeApiContribution";
|
||||
import type {
|
||||
CreateEmbeddedWebsiteEvent,
|
||||
ModifyEmbeddedWebsiteEvent,
|
||||
Rectangle,
|
||||
} from "../../Events/EmbeddedWebsiteEvent";
|
||||
|
||||
export class EmbeddedWebsite {
|
||||
public readonly name: string;
|
||||
private _url: string;
|
||||
private _visible: boolean;
|
||||
private _allow: string;
|
||||
private _allowApi: boolean;
|
||||
private _position: Rectangle;
|
||||
|
||||
constructor(private config: CreateEmbeddedWebsiteEvent) {
|
||||
this.name = config.name;
|
||||
this._url = config.url;
|
||||
this._visible = config.visible ?? true;
|
||||
this._allow = config.allow ?? "";
|
||||
this._allowApi = config.allowApi ?? false;
|
||||
this._position = config.position;
|
||||
}
|
||||
|
||||
public set url(url: string) {
|
||||
this._url = url;
|
||||
sendToWorkadventure({
|
||||
type: "modifyEmbeddedWebsite",
|
||||
data: {
|
||||
name: this.name,
|
||||
url: this._url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public set visible(visible: boolean) {
|
||||
this._visible = visible;
|
||||
sendToWorkadventure({
|
||||
type: "modifyEmbeddedWebsite",
|
||||
data: {
|
||||
name: this.name,
|
||||
visible: this._visible,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public set x(x: number) {
|
||||
this._position.x = x;
|
||||
sendToWorkadventure({
|
||||
type: "modifyEmbeddedWebsite",
|
||||
data: {
|
||||
name: this.name,
|
||||
x: this._position.x,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public set y(y: number) {
|
||||
this._position.y = y;
|
||||
sendToWorkadventure({
|
||||
type: "modifyEmbeddedWebsite",
|
||||
data: {
|
||||
name: this.name,
|
||||
y: this._position.y,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public set width(width: number) {
|
||||
this._position.width = width;
|
||||
sendToWorkadventure({
|
||||
type: "modifyEmbeddedWebsite",
|
||||
data: {
|
||||
name: this.name,
|
||||
width: this._position.width,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public set height(height: number) {
|
||||
this._position.height = height;
|
||||
sendToWorkadventure({
|
||||
type: "modifyEmbeddedWebsite",
|
||||
data: {
|
||||
name: this.name,
|
||||
height: this._position.height,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ActionMessageType,
|
||||
MessageReferenceEvent,
|
||||
removeActionMessage,
|
||||
triggerActionMessage,
|
||||
TriggerActionMessageEvent,
|
||||
} from "../../Events/ui/TriggerActionMessageEvent";
|
||||
import { queryWorkadventure } from "../IframeApiContribution";
|
||||
import type { ActionMessageOptions } from "../ui";
|
||||
function uuidv4() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0,
|
||||
v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export class ActionMessage {
|
||||
public readonly uuid: string;
|
||||
private readonly type: ActionMessageType;
|
||||
private readonly message: string;
|
||||
private readonly callback: () => void;
|
||||
|
||||
constructor(actionMessageOptions: ActionMessageOptions, private onRemove: () => void) {
|
||||
this.uuid = uuidv4();
|
||||
this.message = actionMessageOptions.message;
|
||||
this.type = actionMessageOptions.type ?? "message";
|
||||
this.callback = actionMessageOptions.callback;
|
||||
this.create();
|
||||
}
|
||||
|
||||
private async create() {
|
||||
await queryWorkadventure({
|
||||
type: triggerActionMessage,
|
||||
data: {
|
||||
message: this.message,
|
||||
type: this.type,
|
||||
uuid: this.uuid,
|
||||
} as TriggerActionMessageEvent,
|
||||
});
|
||||
}
|
||||
|
||||
async remove() {
|
||||
await queryWorkadventure({
|
||||
type: removeActionMessage,
|
||||
data: {
|
||||
uuid: this.uuid,
|
||||
} as MessageReferenceEvent,
|
||||
});
|
||||
this.onRemove();
|
||||
}
|
||||
|
||||
triggerCallback() {
|
||||
this.callback();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
import type { WorkadventureRoomWebsiteCommands } from "./website";
|
||||
import website from "./website";
|
||||
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
@@ -105,6 +107,7 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
}
|
||||
return mapURL;
|
||||
}
|
||||
|
||||
async loadTileset(url: string): Promise<number> {
|
||||
return await queryWorkadventure({
|
||||
type: "loadTileset",
|
||||
@@ -113,6 +116,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get website(): WorkadventureRoomWebsiteCommands {
|
||||
return website;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
|
||||
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
|
||||
import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
|
||||
import { Popup } from "./Ui/Popup";
|
||||
import { ActionMessage } from "./Ui/ActionMessage";
|
||||
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
|
||||
|
||||
let popupId = 0;
|
||||
const popups: Map<number, Popup> = new Map<number, Popup>();
|
||||
@@ -14,6 +15,7 @@ const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
|
||||
>();
|
||||
|
||||
const menuCallbacks: Map<string, (command: string) => void> = new Map();
|
||||
const actionMessages = new Map<string, ActionMessage>();
|
||||
|
||||
interface ZonedPopupOptions {
|
||||
zone: string;
|
||||
@@ -23,6 +25,12 @@ interface ZonedPopupOptions {
|
||||
popupOptions: Array<ButtonDescriptor>;
|
||||
}
|
||||
|
||||
export interface ActionMessageOptions {
|
||||
message: string;
|
||||
type?: "message" | "warning";
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
@@ -50,6 +58,16 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
|
||||
}
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "messageTriggered",
|
||||
typeChecker: isMessageReferenceEvent,
|
||||
callback: (event) => {
|
||||
const actionMessage = actionMessages.get(event.uuid);
|
||||
if (actionMessage) {
|
||||
actionMessage.triggerCallback();
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[], input: boolean = false): Popup {
|
||||
@@ -105,6 +123,14 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
|
||||
removeBubble(): void {
|
||||
sendToWorkadventure({ type: "removeBubble", data: null });
|
||||
}
|
||||
|
||||
displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {
|
||||
const actionMessage = new ActionMessage(actionMessageOptions, () => {
|
||||
actionMessages.delete(actionMessage.uuid);
|
||||
});
|
||||
actionMessages.set(actionMessage.uuid, actionMessage);
|
||||
return actionMessage;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkAdventureUiCommands();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
|
||||
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
|
||||
import type { StopSoundEvent } from "../Events/StopSoundEvent";
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { Sound } from "./Sound/Sound";
|
||||
import { EmbeddedWebsite } from "./Room/EmbeddedWebsite";
|
||||
import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent";
|
||||
|
||||
export class WorkadventureRoomWebsiteCommands extends IframeApiContribution<WorkadventureRoomWebsiteCommands> {
|
||||
callbacks = [];
|
||||
|
||||
async get(objectName: string): Promise<EmbeddedWebsite> {
|
||||
const websiteEvent = await queryWorkadventure({
|
||||
type: "getEmbeddedWebsite",
|
||||
data: objectName,
|
||||
});
|
||||
return new EmbeddedWebsite(websiteEvent);
|
||||
}
|
||||
|
||||
create(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent): EmbeddedWebsite {
|
||||
queryWorkadventure({
|
||||
type: "createEmbeddedWebsite",
|
||||
data: createEmbeddedWebsiteEvent,
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
return new EmbeddedWebsite(createEmbeddedWebsiteEvent);
|
||||
}
|
||||
|
||||
async delete(objectName: string): Promise<void> {
|
||||
return await queryWorkadventure({
|
||||
type: "deleteEmbeddedWebsite",
|
||||
data: objectName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureRoomWebsiteCommands();
|
||||
@@ -33,6 +33,8 @@
|
||||
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import {warningContainerStore} from "../Stores/MenuStore";
|
||||
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
|
||||
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore";
|
||||
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
@@ -79,6 +81,11 @@
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $layoutManagerVisibilityStore}
|
||||
<div>
|
||||
<LayoutManager></LayoutManager>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<VideoOverlay></VideoOverlay>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
|
||||
|
||||
function onClick(callback: () => void) {
|
||||
callback();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="layout-manager-list">
|
||||
{#each $layoutManagerActionStore as action}
|
||||
<div class="nes-container is-rounded {action.type}" on:click={() => onClick(action.callback)}>
|
||||
<p>{action.message}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.layout-manager-list {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 40px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: clamp(200px, 20vw, 20vw);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
animation: moveMessage .5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
div.nes-container.is-rounded {
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
|
||||
font-family: Lato;
|
||||
color: whitesmoke;
|
||||
background-color: rgb(0,0,0,0.5);
|
||||
|
||||
&.warning {
|
||||
background-color: #ff9800eb;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveMessage {
|
||||
0% {bottom: 40px;}
|
||||
50% {bottom: 30px;}
|
||||
100% {bottom: 40px;}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
import type { GameScene } from "./GameScene";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import type { Subscription } from "rxjs";
|
||||
import type { CreateEmbeddedWebsiteEvent, ModifyEmbeddedWebsiteEvent } from "../../Api/Events/EmbeddedWebsiteEvent";
|
||||
import DOMElement = Phaser.GameObjects.DOMElement;
|
||||
|
||||
type EmbeddedWebsite = CreateEmbeddedWebsiteEvent & { iframe: HTMLIFrameElement; phaserObject: DOMElement };
|
||||
|
||||
export class EmbeddedWebsiteManager {
|
||||
private readonly embeddedWebsites = new Map<string, EmbeddedWebsite>();
|
||||
private readonly subscription: Subscription;
|
||||
|
||||
constructor(private gameScene: GameScene) {
|
||||
iframeListener.registerAnswerer("getEmbeddedWebsite", (name: string) => {
|
||||
const website = this.embeddedWebsites.get(name);
|
||||
if (website === undefined) {
|
||||
throw new Error('Cannot find embedded website with name "' + name + '"');
|
||||
}
|
||||
const rect = website.iframe.getBoundingClientRect();
|
||||
return {
|
||||
url: website.url,
|
||||
name: website.name,
|
||||
visible: website.visible,
|
||||
allowApi: website.allowApi,
|
||||
allow: website.allow,
|
||||
position: {
|
||||
x: website.phaserObject.x,
|
||||
y: website.phaserObject.y,
|
||||
width: rect["width"],
|
||||
height: rect["height"],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
iframeListener.registerAnswerer("deleteEmbeddedWebsite", (name: string) => {
|
||||
const website = this.embeddedWebsites.get(name);
|
||||
if (!website) {
|
||||
throw new Error('Could not find website to delete with the name "' + name + '" in your map');
|
||||
}
|
||||
|
||||
website.iframe.remove();
|
||||
website.phaserObject.destroy();
|
||||
this.embeddedWebsites.delete(name);
|
||||
});
|
||||
|
||||
iframeListener.registerAnswerer(
|
||||
"createEmbeddedWebsite",
|
||||
(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent) => {
|
||||
if (this.embeddedWebsites.has(createEmbeddedWebsiteEvent.name)) {
|
||||
throw new Error('An embedded website with the name "' + name + '" already exists in your map');
|
||||
}
|
||||
|
||||
this.createEmbeddedWebsite(
|
||||
createEmbeddedWebsiteEvent.name,
|
||||
createEmbeddedWebsiteEvent.url,
|
||||
createEmbeddedWebsiteEvent.position.x,
|
||||
createEmbeddedWebsiteEvent.position.y,
|
||||
createEmbeddedWebsiteEvent.position.width,
|
||||
createEmbeddedWebsiteEvent.position.height,
|
||||
createEmbeddedWebsiteEvent.visible ?? true,
|
||||
createEmbeddedWebsiteEvent.allowApi ?? false,
|
||||
createEmbeddedWebsiteEvent.allow ?? ""
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
this.subscription = iframeListener.modifyEmbeddedWebsiteStream.subscribe(
|
||||
(embeddedWebsiteEvent: ModifyEmbeddedWebsiteEvent) => {
|
||||
const website = this.embeddedWebsites.get(embeddedWebsiteEvent.name);
|
||||
if (!website) {
|
||||
throw new Error(
|
||||
'Could not find website with the name "' + embeddedWebsiteEvent.name + '" in your map'
|
||||
);
|
||||
}
|
||||
|
||||
gameScene.markDirty();
|
||||
|
||||
if (embeddedWebsiteEvent.url !== undefined) {
|
||||
website.url = embeddedWebsiteEvent.url;
|
||||
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
|
||||
website.iframe.src = absoluteUrl;
|
||||
}
|
||||
|
||||
if (embeddedWebsiteEvent.visible !== undefined) {
|
||||
website.visible = embeddedWebsiteEvent.visible;
|
||||
website.phaserObject.visible = embeddedWebsiteEvent.visible;
|
||||
}
|
||||
|
||||
if (embeddedWebsiteEvent.allowApi !== undefined) {
|
||||
website.allowApi = embeddedWebsiteEvent.allowApi;
|
||||
if (embeddedWebsiteEvent.allowApi) {
|
||||
iframeListener.registerIframe(website.iframe);
|
||||
} else {
|
||||
iframeListener.unregisterIframe(website.iframe);
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddedWebsiteEvent.allow !== undefined) {
|
||||
website.allow = embeddedWebsiteEvent.allow;
|
||||
website.iframe.allow = embeddedWebsiteEvent.allow;
|
||||
}
|
||||
|
||||
if (embeddedWebsiteEvent?.x !== undefined) {
|
||||
website.phaserObject.x = embeddedWebsiteEvent.x;
|
||||
}
|
||||
if (embeddedWebsiteEvent?.y !== undefined) {
|
||||
website.phaserObject.y = embeddedWebsiteEvent.y;
|
||||
}
|
||||
if (embeddedWebsiteEvent?.width !== undefined) {
|
||||
website.iframe.style.width = embeddedWebsiteEvent.width + "px";
|
||||
}
|
||||
if (embeddedWebsiteEvent?.height !== undefined) {
|
||||
website.iframe.style.height = embeddedWebsiteEvent.height + "px";
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public createEmbeddedWebsite(
|
||||
name: string,
|
||||
url: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
visible: boolean,
|
||||
allowApi: boolean,
|
||||
allow: string
|
||||
): void {
|
||||
if (this.embeddedWebsites.has(name)) {
|
||||
throw new Error('An embedded website with the name "' + name + '" already exists in your map');
|
||||
}
|
||||
|
||||
const embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent = {
|
||||
name,
|
||||
url,
|
||||
/*x,
|
||||
y,
|
||||
width,
|
||||
height,*/
|
||||
allow,
|
||||
allowApi,
|
||||
visible,
|
||||
position: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
};
|
||||
|
||||
const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible);
|
||||
|
||||
this.embeddedWebsites.set(name, embeddedWebsite);
|
||||
}
|
||||
|
||||
private doCreateEmbeddedWebsite(
|
||||
embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent,
|
||||
visible: boolean
|
||||
): EmbeddedWebsite {
|
||||
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = absoluteUrl;
|
||||
iframe.style.width = embeddedWebsiteEvent.position.width + "px";
|
||||
iframe.style.height = embeddedWebsiteEvent.position.height + "px";
|
||||
iframe.style.margin = "0";
|
||||
iframe.style.padding = "0";
|
||||
iframe.style.border = "none";
|
||||
|
||||
const embeddedWebsite = {
|
||||
...embeddedWebsiteEvent,
|
||||
phaserObject: this.gameScene.add
|
||||
.dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe)
|
||||
.setVisible(visible)
|
||||
.setOrigin(0, 0),
|
||||
iframe: iframe,
|
||||
};
|
||||
if (embeddedWebsiteEvent.allowApi) {
|
||||
iframeListener.registerIframe(iframe);
|
||||
}
|
||||
|
||||
return embeddedWebsite;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
for (const [key, website] of this.embeddedWebsites) {
|
||||
if (website.allowApi) {
|
||||
iframeListener.unregisterIframe(website.iframe);
|
||||
}
|
||||
}
|
||||
|
||||
this.subscription.unsubscribe();
|
||||
iframeListener.unregisterAnswerer("getEmbeddedWebsite");
|
||||
iframeListener.unregisterAnswerer("deleteEmbeddedWebsite");
|
||||
iframeListener.unregisterAnswerer("createEmbeddedWebsite");
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
AUDIO_VOLUME_PROPERTY,
|
||||
Box,
|
||||
JITSI_MESSAGE_PROPERTIES,
|
||||
layoutManager,
|
||||
ON_ACTION_TRIGGER_BUTTON,
|
||||
TRIGGER_JITSI_PROPERTIES,
|
||||
TRIGGER_WEBSITE_PROPERTIES,
|
||||
@@ -85,8 +84,12 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor
|
||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { PropertyUtils } from "../Map/PropertyUtils";
|
||||
import Tileset = Phaser.Tilemaps.Tileset;
|
||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
|
||||
import { get } from "svelte/store";
|
||||
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
|
||||
|
||||
export interface GameSceneInitInterface {
|
||||
initPosition: PointInterface | null;
|
||||
@@ -197,6 +200,8 @@ export class GameScene extends DirtyScene {
|
||||
private preloading: boolean = true;
|
||||
private startPositionCalculator!: StartPositionCalculator;
|
||||
private sharedVariablesManager!: SharedVariablesManager;
|
||||
private objectsByType = new Map<string, ITiledMapObject[]>();
|
||||
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
|
||||
|
||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||
super({
|
||||
@@ -336,27 +341,27 @@ export class GameScene extends DirtyScene {
|
||||
});
|
||||
|
||||
// Scan the object layers for objects to load and load them.
|
||||
const objects = new Map<string, ITiledMapObject[]>();
|
||||
this.objectsByType = new Map<string, ITiledMapObject[]>();
|
||||
|
||||
for (const layer of this.mapFile.layers) {
|
||||
if (layer.type === "objectgroup") {
|
||||
for (const object of layer.objects) {
|
||||
let objectsOfType: ITiledMapObject[] | undefined;
|
||||
if (!objects.has(object.type)) {
|
||||
if (!this.objectsByType.has(object.type)) {
|
||||
objectsOfType = new Array<ITiledMapObject>();
|
||||
} else {
|
||||
objectsOfType = objects.get(object.type);
|
||||
objectsOfType = this.objectsByType.get(object.type);
|
||||
if (objectsOfType === undefined) {
|
||||
throw new Error("Unexpected object type not found");
|
||||
}
|
||||
}
|
||||
objectsOfType.push(object);
|
||||
objects.set(object.type, objectsOfType);
|
||||
this.objectsByType.set(object.type, objectsOfType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [itemType, objectsOfType] of objects) {
|
||||
for (const [itemType, objectsOfType] of this.objectsByType) {
|
||||
// FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin.
|
||||
|
||||
let itemFactory: ItemFactoryInterface;
|
||||
@@ -456,6 +461,8 @@ export class GameScene extends DirtyScene {
|
||||
//permit to set bound collision
|
||||
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
|
||||
|
||||
this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this);
|
||||
|
||||
//add layer on map
|
||||
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
|
||||
for (const layer of this.gameMap.flatLayers) {
|
||||
@@ -476,6 +483,28 @@ export class GameScene extends DirtyScene {
|
||||
if (object.text) {
|
||||
TextUtils.createTextFromITiledMapObject(this, object);
|
||||
}
|
||||
if (object.type === "website") {
|
||||
// Let's load iframes in the map
|
||||
const url = PropertyUtils.mustFindStringProperty(
|
||||
"url",
|
||||
object.properties,
|
||||
'in the "' + object.name + '" object of type "website"'
|
||||
);
|
||||
const allowApi = PropertyUtils.findBooleanProperty("allowApi", object.properties);
|
||||
|
||||
// TODO: add a "allow" property to iframe
|
||||
this.embeddedWebsiteManager.createEmbeddedWebsite(
|
||||
object.name,
|
||||
url,
|
||||
object.x,
|
||||
object.y,
|
||||
object.width,
|
||||
object.height,
|
||||
object.visible,
|
||||
allowApi ?? false,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -791,7 +820,7 @@ export class GameScene extends DirtyScene {
|
||||
});
|
||||
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
||||
if (newValue === undefined) {
|
||||
layoutManager.removeActionButton("openWebsite", this.userInputManager);
|
||||
layoutManagerActionStore.removeAction("openWebsite");
|
||||
coWebsiteManager.closeCoWebsite();
|
||||
} else {
|
||||
const openWebsiteFunction = () => {
|
||||
@@ -801,7 +830,7 @@ export class GameScene extends DirtyScene {
|
||||
allProps.get("openWebsiteAllowApi") as boolean | undefined,
|
||||
allProps.get("openWebsitePolicy") as string | undefined
|
||||
);
|
||||
layoutManager.removeActionButton("openWebsite", this.userInputManager);
|
||||
layoutManagerActionStore.removeAction("openWebsite");
|
||||
};
|
||||
|
||||
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
|
||||
@@ -810,14 +839,13 @@ export class GameScene extends DirtyScene {
|
||||
if (message === undefined) {
|
||||
message = "Press SPACE or touch here to open web site";
|
||||
}
|
||||
layoutManager.addActionButton(
|
||||
"openWebsite",
|
||||
message.toString(),
|
||||
() => {
|
||||
openWebsiteFunction();
|
||||
},
|
||||
this.userInputManager
|
||||
);
|
||||
layoutManagerActionStore.addAction({
|
||||
uuid: "openWebsite",
|
||||
type: "message",
|
||||
message: message,
|
||||
callback: () => openWebsiteFunction(),
|
||||
userInputManager: this.userInputManager,
|
||||
});
|
||||
} else {
|
||||
openWebsiteFunction();
|
||||
}
|
||||
@@ -825,7 +853,7 @@ export class GameScene extends DirtyScene {
|
||||
});
|
||||
this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => {
|
||||
if (newValue === undefined) {
|
||||
layoutManager.removeActionButton("jitsiRoom", this.userInputManager);
|
||||
layoutManagerActionStore.removeAction("jitsi");
|
||||
this.stopJitsi();
|
||||
} else {
|
||||
const openJitsiRoomFunction = () => {
|
||||
@@ -838,7 +866,7 @@ export class GameScene extends DirtyScene {
|
||||
} else {
|
||||
this.startJitsi(roomName, undefined);
|
||||
}
|
||||
layoutManager.removeActionButton("jitsiRoom", this.userInputManager);
|
||||
layoutManagerActionStore.removeAction("jitsi");
|
||||
};
|
||||
|
||||
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
|
||||
@@ -847,14 +875,13 @@ export class GameScene extends DirtyScene {
|
||||
if (message === undefined) {
|
||||
message = "Press SPACE or touch here to enter Jitsi Meet room";
|
||||
}
|
||||
layoutManager.addActionButton(
|
||||
"jitsiRoom",
|
||||
message.toString(),
|
||||
() => {
|
||||
openJitsiRoomFunction();
|
||||
},
|
||||
this.userInputManager
|
||||
);
|
||||
layoutManagerActionStore.addAction({
|
||||
uuid: "jitsi",
|
||||
type: "message",
|
||||
message: message,
|
||||
callback: () => openJitsiRoomFunction(),
|
||||
userInputManager: this.userInputManager,
|
||||
});
|
||||
} else {
|
||||
openJitsiRoomFunction();
|
||||
}
|
||||
@@ -1183,6 +1210,44 @@ export class GameScene extends DirtyScene {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
iframeListener.registerAnswerer("triggerActionMessage", (message) =>
|
||||
layoutManagerActionStore.addAction({
|
||||
uuid: message.uuid,
|
||||
type: "message",
|
||||
message: message.message,
|
||||
callback: () => {
|
||||
layoutManagerActionStore.removeAction(message.uuid);
|
||||
iframeListener.sendActionMessageTriggered(message.uuid);
|
||||
},
|
||||
userInputManager: this.userInputManager,
|
||||
})
|
||||
);
|
||||
|
||||
iframeListener.registerAnswerer("removeActionMessage", (message) => {
|
||||
layoutManagerActionStore.removeAction(message.uuid);
|
||||
});
|
||||
|
||||
this.iframeSubscriptionList.push(
|
||||
iframeListener.modifyEmbeddedWebsiteStream.subscribe((embeddedWebsite) => {
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
// TODO
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private setPropertyLayer(
|
||||
@@ -1250,7 +1315,7 @@ export class GameScene extends DirtyScene {
|
||||
let targetRoom: Room;
|
||||
try {
|
||||
targetRoom = await Room.createRoom(roomUrl);
|
||||
} catch (e) {
|
||||
} catch (e /*: unknown*/) {
|
||||
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
|
||||
this.mapTransitioning = false;
|
||||
return;
|
||||
@@ -1305,7 +1370,12 @@ export class GameScene extends DirtyScene {
|
||||
this.biggestAvailableAreaStoreUnsubscribe();
|
||||
iframeListener.unregisterAnswerer("getState");
|
||||
iframeListener.unregisterAnswerer("loadTileset");
|
||||
iframeListener.unregisterAnswerer("getMapData");
|
||||
iframeListener.unregisterAnswerer("getState");
|
||||
iframeListener.unregisterAnswerer("triggerActionMessage");
|
||||
iframeListener.unregisterAnswerer("removeActionMessage");
|
||||
this.sharedVariablesManager?.close();
|
||||
this.embeddedWebsiteManager?.close();
|
||||
|
||||
mediaManager.hideGameOverlay();
|
||||
|
||||
@@ -1377,7 +1447,7 @@ export class GameScene extends DirtyScene {
|
||||
try {
|
||||
const room = await Room.createRoom(exitRoomPath);
|
||||
return gameManager.loadMap(room, this.scene);
|
||||
} catch (e) {
|
||||
} catch (e /*: unknown*/) {
|
||||
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ITiledMapProperty } from "./ITiledMap";
|
||||
|
||||
export class PropertyUtils {
|
||||
public static findProperty(
|
||||
name: string,
|
||||
properties: ITiledMapProperty[] | undefined
|
||||
): string | boolean | number | undefined {
|
||||
return properties?.find((property) => property.name === name)?.value;
|
||||
}
|
||||
|
||||
public static findBooleanProperty(
|
||||
name: string,
|
||||
properties: ITiledMapProperty[] | undefined,
|
||||
context?: string
|
||||
): boolean | undefined {
|
||||
const property = PropertyUtils.findProperty(name, properties);
|
||||
if (property === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof property !== "boolean") {
|
||||
throw new Error(
|
||||
'Expected property "' + name + '" to be a boolean. ' + (context ? " (" + context + ")" : "")
|
||||
);
|
||||
}
|
||||
return property;
|
||||
}
|
||||
|
||||
public static mustFindProperty(
|
||||
name: string,
|
||||
properties: ITiledMapProperty[] | undefined,
|
||||
context?: string
|
||||
): string | boolean | number {
|
||||
const property = PropertyUtils.findProperty(name, properties);
|
||||
if (property === undefined) {
|
||||
throw new Error('Could not find property "' + name + '"' + (context ? " (" + context + ")" : ""));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
|
||||
public static mustFindStringProperty(
|
||||
name: string,
|
||||
properties: ITiledMapProperty[] | undefined,
|
||||
context?: string
|
||||
): string {
|
||||
const property = PropertyUtils.mustFindProperty(name, properties, context);
|
||||
if (typeof property !== "string") {
|
||||
throw new Error(
|
||||
'Expected property "' + name + '" to be a string. ' + (context ? " (" + context + ")" : "")
|
||||
);
|
||||
}
|
||||
return property;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { derived, writable } from "svelte/store";
|
||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||
|
||||
export interface LayoutManagerAction {
|
||||
uuid: string;
|
||||
type: "warning" | "message";
|
||||
message: string | number | boolean | undefined;
|
||||
callback: () => void;
|
||||
userInputManager: UserInputManager | undefined;
|
||||
}
|
||||
|
||||
function createLayoutManagerAction() {
|
||||
const { subscribe, set, update } = writable<LayoutManagerAction[]>([]);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
addAction: (newAction: LayoutManagerAction): void => {
|
||||
update((list: LayoutManagerAction[]) => {
|
||||
let found = false;
|
||||
for (const action of list) {
|
||||
if (action.uuid === newAction.uuid) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
list.push(newAction);
|
||||
newAction.userInputManager?.addSpaceEventListner(newAction.callback);
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
},
|
||||
removeAction: (uuid: string): void => {
|
||||
update((list: LayoutManagerAction[]) => {
|
||||
const index = list.findIndex((action) => action.uuid === uuid);
|
||||
|
||||
if (index !== -1) {
|
||||
list[index].userInputManager?.removeSpaceEventListner(list[index].callback);
|
||||
list.splice(index, 1);
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
},
|
||||
clearActions: (): void => {
|
||||
set([]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const layoutManagerActionStore = createLayoutManagerAction();
|
||||
|
||||
export const layoutManagerVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => {
|
||||
return !!$layoutManagerActionStore.length;
|
||||
});
|
||||
@@ -1,7 +1,3 @@
|
||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||
import { HtmlUtils } from "./HtmlUtils";
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
|
||||
export enum LayoutMode {
|
||||
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
|
||||
Presentation = "Presentation",
|
||||
@@ -28,87 +24,3 @@ export const AUDIO_VOLUME_PROPERTY = "audioVolume";
|
||||
export const AUDIO_LOOP_PROPERTY = "audioLoop";
|
||||
|
||||
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
|
||||
|
||||
class LayoutManager {
|
||||
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
||||
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
|
||||
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
|
||||
//delete previous element
|
||||
this.removeActionButton(id, userInputManager);
|
||||
|
||||
//create div and text html component
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('action-body');
|
||||
p.classList.add('nes-btn');
|
||||
p.classList.add('is-dark');
|
||||
p.innerHTML = sanitizeHtml(text);
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("action");
|
||||
div.id = id;
|
||||
div.appendChild(p);
|
||||
|
||||
this.actionButtonInformation.set(id, div);
|
||||
|
||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||
mainContainer.appendChild(div);
|
||||
|
||||
//add trigger action
|
||||
div.onpointerdown = () => callBack();
|
||||
this.actionButtonTrigger.set(id, callBack);
|
||||
userInputManager.addSpaceEventListner(callBack);
|
||||
}
|
||||
|
||||
public removeActionButton(id: string, userInputManager?: UserInputManager) {
|
||||
//delete previous element
|
||||
const previousDiv = this.actionButtonInformation.get(id);
|
||||
if (previousDiv) {
|
||||
previousDiv.remove();
|
||||
this.actionButtonInformation.delete(id);
|
||||
}
|
||||
const previousEventCallback = this.actionButtonTrigger.get(id);
|
||||
if (previousEventCallback && userInputManager) {
|
||||
userInputManager.removeSpaceEventListner(previousEventCallback);
|
||||
}
|
||||
}
|
||||
|
||||
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
|
||||
//delete previous element
|
||||
for (const [key, value] of this.actionButtonInformation) {
|
||||
this.removeActionButton(key, userInputManager);
|
||||
}
|
||||
|
||||
//create div and text html component
|
||||
const p = document.createElement("p");
|
||||
p.classList.add("action-body");
|
||||
p.innerText = text;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("action");
|
||||
div.classList.add(id);
|
||||
div.id = id;
|
||||
div.appendChild(p);
|
||||
|
||||
this.actionButtonInformation.set(id, div);
|
||||
|
||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||
mainContainer.appendChild(div);
|
||||
//add trigger action
|
||||
if (callBack) {
|
||||
div.onpointerdown = () => {
|
||||
callBack();
|
||||
this.removeActionButton(id, userInputManager);
|
||||
};
|
||||
}
|
||||
|
||||
//remove it after 10 sec
|
||||
setTimeout(() => {
|
||||
this.removeActionButton(id, userInputManager);
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
const layoutManager = new LayoutManager();
|
||||
|
||||
export { layoutManager };
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { layoutManager } from "./LayoutManager";
|
||||
import { HtmlUtils } from "./HtmlUtils";
|
||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||
import { localStreamStore } from "../Stores/MediaStore";
|
||||
@@ -10,6 +9,8 @@ export type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||
|
||||
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
|
||||
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
||||
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export class MediaManager {
|
||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||
@@ -23,14 +24,19 @@ export class MediaManager {
|
||||
localStreamStore.subscribe((result) => {
|
||||
if (result.type === "error") {
|
||||
console.error(result.error);
|
||||
layoutManager.addInformation(
|
||||
"warning",
|
||||
"Camera access denied. Click here and check your browser permissions.",
|
||||
() => {
|
||||
layoutManagerActionStore.addAction({
|
||||
uuid: "cameraAccessDenied",
|
||||
type: "warning",
|
||||
message: "Camera access denied. Click here and check your browser permissions.",
|
||||
callback: () => {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
},
|
||||
this.userInputManager
|
||||
);
|
||||
userInputManager: this.userInputManager,
|
||||
});
|
||||
//remove it after 10 sec
|
||||
setTimeout(() => {
|
||||
layoutManagerActionStore.removeAction("cameraAccessDenied");
|
||||
}, 10000);
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -38,14 +44,19 @@ export class MediaManager {
|
||||
screenSharingLocalStreamStore.subscribe((result) => {
|
||||
if (result.type === "error") {
|
||||
console.error(result.error);
|
||||
layoutManager.addInformation(
|
||||
"warning",
|
||||
"Screen sharing denied. Click here and check your browser permissions.",
|
||||
() => {
|
||||
layoutManagerActionStore.addAction({
|
||||
uuid: "screenSharingAccessDenied",
|
||||
type: "warning",
|
||||
message: "Screen sharing denied. Click here and check your browser permissions.",
|
||||
callback: () => {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
},
|
||||
this.userInputManager
|
||||
);
|
||||
userInputManager: this.userInputManager,
|
||||
});
|
||||
//remove it after 10 sec
|
||||
setTimeout(() => {
|
||||
layoutManagerActionStore.removeAction("screenSharingAccessDenied");
|
||||
}, 10000);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -380,6 +380,10 @@ body {
|
||||
|
||||
#game {
|
||||
position: relative; /* Position relative is needed for the game-overlay. */
|
||||
|
||||
iframe {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.audioplayer:first-child {
|
||||
|
||||
Reference in New Issue
Block a user