commit
dbe7dc4ce0
@ -177,3 +177,101 @@ WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
|
|||||||
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
|
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Embedding websites in a map
|
||||||
|
|
||||||
|
You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)).
|
||||||
|
|
||||||
|
### Getting an instance of a website already embedded in the map
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get an instance of an embedded website by using the `WA.room.website.get()` method.
|
||||||
|
It returns a promise of an `EmbeddedWebsite` instance.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
|
||||||
|
const website = await WA.room.website.get('my_website');
|
||||||
|
website.url = 'https://example.com';
|
||||||
|
website.visible = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Adding a new website in a map
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.website.create(website: CreateEmbeddedWebsiteEvent): EmbeddedWebsite
|
||||||
|
|
||||||
|
interface CreateEmbeddedWebsiteEvent {
|
||||||
|
name: string; // A unique name for this iframe
|
||||||
|
url: string; // The URL the iframe points to.
|
||||||
|
position: {
|
||||||
|
x: number, // In pixels, relative to the map coordinates
|
||||||
|
y: number, // In pixels, relative to the map coordinates
|
||||||
|
width: number, // In pixels, sensitive to zoom level
|
||||||
|
height: number, // In pixels, sensitive to zoom level
|
||||||
|
},
|
||||||
|
visible?: boolean, // Whether to display the iframe or not
|
||||||
|
allowApi?: boolean, // Whether the scripting API should be available to the iframe
|
||||||
|
allow?: string, // The list of feature policies allowed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can create an instance of an embedded website by using the `WA.room.website.create()` method.
|
||||||
|
It returns an `EmbeddedWebsite` instance.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create a new website object
|
||||||
|
const website = WA.room.website.create({
|
||||||
|
name: "my_website",
|
||||||
|
url: "https://example.com",
|
||||||
|
position: {
|
||||||
|
x: 64,
|
||||||
|
y: 128,
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
},
|
||||||
|
visible: true,
|
||||||
|
allowApi: true,
|
||||||
|
allow: "fullscreen",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting a website from a map
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.website.delete(name: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `WA.room.website.delete` to completely remove an embedded website from your map.
|
||||||
|
|
||||||
|
|
||||||
|
### The EmbeddedWebsite class
|
||||||
|
|
||||||
|
Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class EmbeddedWebsite {
|
||||||
|
readonly name: string;
|
||||||
|
url: string;
|
||||||
|
visible: boolean;
|
||||||
|
allow: string;
|
||||||
|
allowApi: boolean;
|
||||||
|
x: number; // In pixels, relative to the map coordinates
|
||||||
|
y: number; // In pixels, relative to the map coordinates
|
||||||
|
width: number; // In pixels, sensitive to zoom level
|
||||||
|
height: number; // In pixels, sensitive to zoom level
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map.
|
||||||
|
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
The websites you add/edit/delete via the scripting API are only shown locally. If you want them
|
||||||
|
to be displayed for every player, you can use [variables](api-start.md) to share a common state
|
||||||
|
between all users.
|
||||||
|
|
||||||
|
@ -87,3 +87,50 @@ WA.ui.registerMenuCommand("test", () => {
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<img src="https://workadventu.re/img/docs/menu-command.png" class="figure-img img-fluid rounded" alt="" />
|
<img src="https://workadventu.re/img/docs/menu-command.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Awaiting User Confirmation (with space bar)
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.ui.displayActionMessage({
|
||||||
|
message: string,
|
||||||
|
callback: () => void,
|
||||||
|
type?: "message"|"warning",
|
||||||
|
}): ActionMessage
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays a message at the bottom of the screen (that will disappear when space bar is pressed).
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<img src="https://workadventu.re/img/docs/trigger_message.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const triggerMessage = WA.ui.displayActionMessage({
|
||||||
|
message: "press 'space' to confirm",
|
||||||
|
callback: () => {
|
||||||
|
WA.chat.sendChatMessage("confirmed", "trigger message logic")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// later
|
||||||
|
triggerMessage.remove();
|
||||||
|
}, 1000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that `displayActionMessage` returns an object of the `ActionMessage` class.
|
||||||
|
|
||||||
|
The `ActionMessage` class contains a single method: `remove(): Promise<void>`. This will obviously remove the message when called.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ActionMessage {
|
||||||
|
/**
|
||||||
|
* Hides the message
|
||||||
|
*/
|
||||||
|
remove() {};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
40
docs/maps/website-in-map.md
Normal file
40
docs/maps/website-in-map.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# Putting a website inside a map
|
||||||
|
|
||||||
|
You can inject a website directly into your map, at a given position.
|
||||||
|
|
||||||
|
To do this in Tiled:
|
||||||
|
|
||||||
|
- Select an object layer
|
||||||
|
- Create a rectangular object, at the position where you want your website to appear
|
||||||
|
- Add a `url` property to your object pointing to the URL you want to open
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<figure class="figure">
|
||||||
|
<img src="https://workadventu.re/img/docs/website_url_property.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
|
||||||
|
<figcaption class="figure-caption">A "website" object</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The `url` can be absolute, or relative to your map.
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
Internally, WorkAdventure will create an "iFrame" to load the website.
|
||||||
|
Some websites forbid being opened by iframes using the [`X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options)
|
||||||
|
HTTP header.
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
Please note that the website always appears **on top** of the tiles (even if you put the object layer that
|
||||||
|
contains the "website" object under the tiles).
|
||||||
|
|
||||||
|
## Allowing the scripting API in your iframe
|
||||||
|
|
||||||
|
If you are planning to use the WorkAdventure scripting API inside your iframe, you need
|
||||||
|
to explicitly allow it, by setting an additional `allowApi` property to `true`.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<figure class="figure">
|
||||||
|
<img src="https://workadventu.re/img/docs/website_allowapi_property.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
|
||||||
|
<figcaption class="figure-caption">A "website" object that can communicate using the Iframe API</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
@ -26,7 +26,6 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
|
||||||
// TODO: remove those ignored rules and write a stronger code!
|
// TODO: remove those ignored rules and write a stronger code!
|
||||||
"@typescript-eslint/no-floating-promises": "off",
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
48
front/src/Api/Events/EmbeddedWebsiteEvent.ts
Normal file
48
front/src/Api/Events/EmbeddedWebsiteEvent.ts
Normal file
@ -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 { OpenPopupEvent } from "./OpenPopupEvent";
|
||||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||||
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
||||||
|
import type { MapDataEvent } from "./MapDataEvent";
|
||||||
import type { LayerEvent } from "./LayerEvent";
|
import type { LayerEvent } from "./LayerEvent";
|
||||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||||
@ -21,8 +22,17 @@ 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 } from "./EmbeddedWebsiteEvent";
|
||||||
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
|
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
|
||||||
import { isLoadTilesetEvent } 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 {
|
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||||
data: T;
|
data: T;
|
||||||
@ -55,6 +65,7 @@ export type IframeEventMap = {
|
|||||||
loadTileset: LoadTilesetEvent;
|
loadTileset: LoadTilesetEvent;
|
||||||
registerMenuCommand: MenuItemRegisterEvent;
|
registerMenuCommand: MenuItemRegisterEvent;
|
||||||
setTiles: SetTilesEvent;
|
setTiles: SetTilesEvent;
|
||||||
|
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
|
||||||
};
|
};
|
||||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||||
type: T;
|
type: T;
|
||||||
@ -73,6 +84,7 @@ export interface IframeResponseEventMap {
|
|||||||
hasPlayerMoved: HasPlayerMovedEvent;
|
hasPlayerMoved: HasPlayerMovedEvent;
|
||||||
menuItemClicked: MenuItemClickedEvent;
|
menuItemClicked: MenuItemClickedEvent;
|
||||||
setVariable: SetVariableEvent;
|
setVariable: SetVariableEvent;
|
||||||
|
messageTriggered: MessageReferenceEvent;
|
||||||
}
|
}
|
||||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||||
type: T;
|
type: T;
|
||||||
@ -105,6 +117,26 @@ export const iframeQueryMapTypeGuards = {
|
|||||||
query: isLoadTilesetEvent,
|
query: isLoadTilesetEvent,
|
||||||
answer: tg.isNumber,
|
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;
|
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)) {
|
if (!isIframeQueryKey(type)) {
|
||||||
return false;
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
26
front/src/Api/Events/ui/TriggerActionMessageEvent.ts
Normal file
26
front/src/Api/Events/ui/TriggerActionMessageEvent.ts
Normal file
@ -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>;
|
24
front/src/Api/Events/ui/TriggerMessageEventHandler.ts
Normal file
24
front/src/Api/Events/ui/TriggerMessageEventHandler.ts
Normal file
@ -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 { Subject } from "rxjs";
|
||||||
|
import type * as tg from "generic-type-guard";
|
||||||
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
|
import { ChatEvent, 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";
|
||||||
@ -31,6 +32,8 @@ import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
|||||||
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||||
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
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 { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
|
||||||
|
|
||||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||||
query: IframeQueryMap[T]["query"],
|
query: IframeQueryMap[T]["query"],
|
||||||
@ -108,6 +111,9 @@ class IframeListener {
|
|||||||
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
|
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
|
||||||
public readonly setTilesStream = this._setTilesStream.asObservable();
|
public readonly setTilesStream = this._setTilesStream.asObservable();
|
||||||
|
|
||||||
|
private readonly _modifyEmbeddedWebsiteStream: Subject<ModifyEmbeddedWebsiteEvent> = new Subject();
|
||||||
|
public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable();
|
||||||
|
|
||||||
private readonly iframes = new Set<HTMLIFrameElement>();
|
private readonly iframes = new Set<HTMLIFrameElement>();
|
||||||
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
|
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
|
||||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||||
@ -121,7 +127,7 @@ class IframeListener {
|
|||||||
init() {
|
init() {
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"message",
|
"message",
|
||||||
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
(message: MessageEvent<unknown>) => {
|
||||||
// Do we trust the sender of this message?
|
// Do we trust the sender of this message?
|
||||||
// Let's only accept messages from the iframe that are allowed.
|
// 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).
|
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||||
@ -263,6 +269,8 @@ class IframeListener {
|
|||||||
handleMenuItemRegistrationEvent(payload.data);
|
handleMenuItemRegistrationEvent(payload.data);
|
||||||
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
|
||||||
this._setTilesStream.next(payload.data);
|
this._setTilesStream.next(payload.data);
|
||||||
|
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
|
||||||
|
this._modifyEmbeddedWebsiteStream.next(payload.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -416,6 +424,15 @@ class IframeListener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendActionMessageTriggered(uuid: string): void {
|
||||||
|
this.postMessage({
|
||||||
|
type: "messageTriggered",
|
||||||
|
data: {
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the message... to all allowed iframes.
|
* Sends the message... to all allowed iframes.
|
||||||
*/
|
*/
|
||||||
|
@ -1,51 +1,66 @@
|
|||||||
import type * as tg from "generic-type-guard";
|
import type * as tg from "generic-type-guard";
|
||||||
import type {
|
import type {
|
||||||
IframeEvent,
|
IframeEvent,
|
||||||
IframeEventMap, IframeQuery,
|
IframeEventMap,
|
||||||
|
IframeQuery,
|
||||||
IframeQueryMap,
|
IframeQueryMap,
|
||||||
IframeResponseEventMap
|
IframeResponseEventMap,
|
||||||
} from '../Events/IframeEvent';
|
} 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<keyof IframeEventMap>) {
|
||||||
window.parent.postMessage(content, "*")
|
window.parent.postMessage(content, "*");
|
||||||
}
|
}
|
||||||
|
|
||||||
let queryNumber = 0;
|
let queryNumber = 0;
|
||||||
|
|
||||||
export const answerPromises = new Map<number, {
|
export const answerPromises = new Map<
|
||||||
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
|
number,
|
||||||
|
{
|
||||||
|
resolve: (
|
||||||
|
value:
|
||||||
|
| IframeQueryMap[keyof IframeQueryMap]["answer"]
|
||||||
|
| PromiseLike<IframeQueryMap[keyof IframeQueryMap]["answer"]>
|
||||||
|
) => void;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
reject: (reason?: any) => void
|
reject: (reason?: any) => void;
|
||||||
}>();
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
|
export function queryWorkadventure<T extends keyof IframeQueryMap>(
|
||||||
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
|
content: IframeQuery<T>
|
||||||
window.parent.postMessage({
|
): Promise<IframeQueryMap[T]["answer"]> {
|
||||||
|
return new Promise<IframeQueryMap[T]["answer"]>((resolve, reject) => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
id: queryNumber,
|
id: queryNumber,
|
||||||
query: content
|
query: content,
|
||||||
} as IframeQueryWrapper<T>, "*");
|
} as IframeQueryWrapper<T>,
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
|
||||||
answerPromises.set(queryNumber, {
|
answerPromises.set(queryNumber, {
|
||||||
resolve,
|
resolve,
|
||||||
reject
|
reject,
|
||||||
});
|
});
|
||||||
|
|
||||||
queryNumber++;
|
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>> {
|
export interface IframeCallback<
|
||||||
|
Key extends keyof IframeResponseEventMap,
|
||||||
typeChecker: Guard,
|
T = IframeResponseEventMap[Key],
|
||||||
callback: (payloadData: T) => void
|
Guard = tg.TypeGuard<T>
|
||||||
|
> {
|
||||||
|
typeChecker: Guard;
|
||||||
|
callback: (payloadData: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IframeCallbackContribution<Key extends keyof IframeResponseEventMap> extends IframeCallback<Key> {
|
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 {
|
export abstract class IframeApiContribution<
|
||||||
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>,
|
T extends {
|
||||||
}> {
|
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>;
|
||||||
|
}
|
||||||
abstract callbacks: T["callbacks"]
|
> {
|
||||||
|
abstract callbacks: T["callbacks"];
|
||||||
}
|
}
|
||||||
|
90
front/src/Api/iframe/Room/EmbeddedWebsite.ts
Normal file
90
front/src/Api/iframe/Room/EmbeddedWebsite.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
56
front/src/Api/iframe/Ui/ActionMessage.ts
Normal file
56
front/src/Api/iframe/Ui/ActionMessage.ts
Normal file
@ -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 { apiCallback } from "./registeredCallbacks";
|
||||||
|
|
||||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
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 enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||||
const leaveStreams: 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;
|
return mapURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTileset(url: string): Promise<number> {
|
async loadTileset(url: string): Promise<number> {
|
||||||
return await queryWorkadventure({
|
return await queryWorkadventure({
|
||||||
type: "loadTileset",
|
type: "loadTileset",
|
||||||
@ -113,6 +116,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get website(): WorkadventureRoomWebsiteCommands {
|
||||||
|
return website;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WorkadventureRoomCommands();
|
export default new WorkadventureRoomCommands();
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
|
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
|
||||||
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
|
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
|
||||||
import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
|
|
||||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||||
import { apiCallback } from "./registeredCallbacks";
|
import { apiCallback } from "./registeredCallbacks";
|
||||||
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
|
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
|
||||||
import { Popup } from "./Ui/Popup";
|
import { Popup } from "./Ui/Popup";
|
||||||
|
import { ActionMessage } from "./Ui/ActionMessage";
|
||||||
|
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
|
||||||
|
|
||||||
let popupId = 0;
|
let popupId = 0;
|
||||||
const popups: Map<number, Popup> = new Map<number, Popup>();
|
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 menuCallbacks: Map<string, (command: string) => void> = new Map();
|
||||||
|
const actionMessages = new Map<string, ActionMessage>();
|
||||||
|
|
||||||
interface ZonedPopupOptions {
|
interface ZonedPopupOptions {
|
||||||
zone: string;
|
zone: string;
|
||||||
@ -23,6 +25,12 @@ interface ZonedPopupOptions {
|
|||||||
popupOptions: Array<ButtonDescriptor>;
|
popupOptions: Array<ButtonDescriptor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActionMessageOptions {
|
||||||
|
message: string;
|
||||||
|
type?: "message" | "warning";
|
||||||
|
callback: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||||
callbacks = [
|
callbacks = [
|
||||||
apiCallback({
|
apiCallback({
|
||||||
@ -49,6 +57,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[]): Popup {
|
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||||
@ -103,6 +121,14 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
|
|||||||
removeBubble(): void {
|
removeBubble(): void {
|
||||||
sendToWorkadventure({ type: "removeBubble", data: null });
|
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();
|
export default new WorkAdventureUiCommands();
|
||||||
|
38
front/src/Api/iframe/website.ts
Normal file
38
front/src/Api/iframe/website.ts
Normal file
@ -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,10 @@
|
|||||||
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
|
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
|
||||||
import {warningContainerStore} from "../Stores/MenuStore";
|
import {warningContainerStore} from "../Stores/MenuStore";
|
||||||
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
|
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
|
||||||
|
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore";
|
||||||
|
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
|
||||||
|
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore";
|
||||||
|
import AudioManager from "./AudioManager/AudioManager.svelte"
|
||||||
|
|
||||||
export let game: Game;
|
export let game: Game;
|
||||||
|
|
||||||
@ -79,6 +83,16 @@
|
|||||||
<AudioPlaying url={$soundPlayingStore} />
|
<AudioPlaying url={$soundPlayingStore} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $audioManagerVisibilityStore}
|
||||||
|
<div>
|
||||||
|
<AudioManager></AudioManager>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if $layoutManagerVisibilityStore}
|
||||||
|
<div>
|
||||||
|
<LayoutManager></LayoutManager>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if $gameOverlayVisibilityStore}
|
{#if $gameOverlayVisibilityStore}
|
||||||
<div>
|
<div>
|
||||||
<VideoOverlay></VideoOverlay>
|
<VideoOverlay></VideoOverlay>
|
||||||
|
119
front/src/Components/AudioManager/AudioManager.svelte
Normal file
119
front/src/Components/AudioManager/AudioManager.svelte
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import audioImg from "../images/audio.svg";
|
||||||
|
import audioMuteImg from "../images/audio-mute.svg";
|
||||||
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
|
import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
|
||||||
|
import {
|
||||||
|
audioManagerFileStore,
|
||||||
|
audioManagerVolumeStore,
|
||||||
|
} from "../../Stores/AudioManagerStore";
|
||||||
|
import {get} from "svelte/store";
|
||||||
|
import type { Unsubscriber } from "svelte/store";
|
||||||
|
import {onDestroy, onMount} from "svelte";
|
||||||
|
|
||||||
|
let HTMLAudioPlayer: HTMLAudioElement;
|
||||||
|
let unsubscriberFileStore: Unsubscriber | null = null;
|
||||||
|
let unsubscriberVolumeStore: Unsubscriber | null = null;
|
||||||
|
|
||||||
|
let volume: number = 1;
|
||||||
|
let decreaseWhileTalking: boolean = true;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{
|
||||||
|
HTMLAudioPlayer.pause();
|
||||||
|
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
|
||||||
|
HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
|
||||||
|
HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted;
|
||||||
|
HTMLAudioPlayer.play();
|
||||||
|
});
|
||||||
|
unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => {
|
||||||
|
const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking;
|
||||||
|
if (reduceVolume && !audioManager.volumeReduced) {
|
||||||
|
audioManager.volume *= 0.5;
|
||||||
|
} else if (!reduceVolume && audioManager.volumeReduced) {
|
||||||
|
audioManager.volume *= 2.0;
|
||||||
|
}
|
||||||
|
audioManager.volumeReduced = reduceVolume;
|
||||||
|
HTMLAudioPlayer.volume = audioManager.volume;
|
||||||
|
HTMLAudioPlayer.muted = audioManager.muted;
|
||||||
|
HTMLAudioPlayer.loop = audioManager.loop;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (unsubscriberFileStore) {
|
||||||
|
unsubscriberFileStore();
|
||||||
|
}
|
||||||
|
if (unsubscriberVolumeStore) {
|
||||||
|
unsubscriberVolumeStore();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMute() {
|
||||||
|
audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted);
|
||||||
|
localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVolume() {
|
||||||
|
audioManagerVolumeStore.setVolume(volume)
|
||||||
|
localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDecrease() {
|
||||||
|
audioManagerVolumeStore.setDecreaseWhileTalking(decreaseWhileTalking);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="main-audio-manager nes-container is-rounded">
|
||||||
|
<div class="audio-manager-player-volume">
|
||||||
|
<img src={$audioManagerVolumeStore.muted ? audioMuteImg : audioImg} alt="player volume" on:click={onMute}>
|
||||||
|
<input type="range" min="0" max="1" step="0.025" bind:value={volume} on:change={setVolume}>
|
||||||
|
</div>
|
||||||
|
<div class="audio-manager-reduce-conversation">
|
||||||
|
<label>
|
||||||
|
reduce in conversations
|
||||||
|
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease}>
|
||||||
|
</label>
|
||||||
|
<section class="audio-manager-file">
|
||||||
|
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
|
||||||
|
<source src={$audioManagerFileStore}>
|
||||||
|
</audio>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
div.main-audio-manager.nes-container.is-rounded {
|
||||||
|
position: relative;
|
||||||
|
top: 0.5rem;
|
||||||
|
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
|
||||||
|
width: clamp(200px, 15vw, 15vw);
|
||||||
|
padding: 3px 3px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
background-color: rgb(0,0,0,0.5);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 50% 50%;
|
||||||
|
color: whitesmoke;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
div.audio-manager-player-volume {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.audio-manager-file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
57
front/src/Components/LayoutManager/LayoutManager.svelte
Normal file
57
front/src/Components/LayoutManager/LayoutManager.svelte
Normal file
@ -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>
|
3
front/src/Components/images/audio-mute.svg
Normal file
3
front/src/Components/images/audio-mute.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 376 B |
8
front/src/Components/images/audio.svg
Normal file
8
front/src/Components/images/audio.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
|
||||||
|
<g>
|
||||||
|
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
|
||||||
|
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
|
||||||
|
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 885 B |
@ -1,90 +0,0 @@
|
|||||||
|
|
||||||
const IGNORED_KEYS = new Set([
|
|
||||||
'Esc',
|
|
||||||
'Escape',
|
|
||||||
'Alt',
|
|
||||||
'Meta',
|
|
||||||
'Control',
|
|
||||||
'Ctrl',
|
|
||||||
'Space',
|
|
||||||
'Backspace'
|
|
||||||
])
|
|
||||||
|
|
||||||
export class TextInput extends Phaser.GameObjects.BitmapText {
|
|
||||||
private minUnderLineLength = 4;
|
|
||||||
private underLine: Phaser.GameObjects.Text;
|
|
||||||
private domInput = document.createElement('input');
|
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string,
|
|
||||||
onChange: (text: string) => void) {
|
|
||||||
super(scene, x, y, 'main_font', text, 32);
|
|
||||||
this.setOrigin(0.5).setCenterAlign();
|
|
||||||
this.scene.add.existing(this);
|
|
||||||
|
|
||||||
const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'};
|
|
||||||
this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style);
|
|
||||||
this.underLine.setOrigin(0.5);
|
|
||||||
|
|
||||||
this.domInput.maxLength = maxLength;
|
|
||||||
this.domInput.style.opacity = "0";
|
|
||||||
if (text) {
|
|
||||||
this.domInput.value = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.domInput.addEventListener('keydown', event => {
|
|
||||||
if (IGNORED_KEYS.has(event.key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.domInput.addEventListener('input', (event) => {
|
|
||||||
if (event.defaultPrevented) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.text = this.domInput.value;
|
|
||||||
this.underLine.text = this.getUnderLineBody(this.text.length);
|
|
||||||
onChange(this.text);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.append(this.domInput);
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUnderLineBody(textLength:number): string {
|
|
||||||
if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength;
|
|
||||||
let text = '_______';
|
|
||||||
for (let i = this.minUnderLineLength; i < textLength; i++) {
|
|
||||||
text += '__';
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
getText(): string {
|
|
||||||
return this.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
setX(x: number): this {
|
|
||||||
super.setX(x);
|
|
||||||
this.underLine.x = x;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setY(y: number): this {
|
|
||||||
super.setY(y);
|
|
||||||
this.underLine.y = y+1;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.domInput.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
super.destroy();
|
|
||||||
this.domInput.remove();
|
|
||||||
}
|
|
||||||
}
|
|
198
front/src/Phaser/Game/EmbeddedWebsiteManager.ts
Normal file
198
front/src/Phaser/Game/EmbeddedWebsiteManager.ts
Normal file
@ -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,
|
AUDIO_VOLUME_PROPERTY,
|
||||||
Box,
|
Box,
|
||||||
JITSI_MESSAGE_PROPERTIES,
|
JITSI_MESSAGE_PROPERTIES,
|
||||||
layoutManager,
|
|
||||||
ON_ACTION_TRIGGER_BUTTON,
|
ON_ACTION_TRIGGER_BUTTON,
|
||||||
TRIGGER_JITSI_PROPERTIES,
|
TRIGGER_JITSI_PROPERTIES,
|
||||||
TRIGGER_WEBSITE_PROPERTIES,
|
TRIGGER_WEBSITE_PROPERTIES,
|
||||||
@ -33,7 +32,6 @@ import type { RoomConnection } from "../../Connexion/RoomConnection";
|
|||||||
import { Room } from "../../Connexion/Room";
|
import { Room } from "../../Connexion/Room";
|
||||||
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
||||||
import { urlManager } from "../../Url/UrlManager";
|
import { urlManager } from "../../Url/UrlManager";
|
||||||
import { audioManager } from "../../WebRtc/AudioManager";
|
|
||||||
import { TextureError } from "../../Exception/TextureError";
|
import { TextureError } from "../../Exception/TextureError";
|
||||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||||
@ -85,8 +83,17 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor
|
|||||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||||
import { playersStore } from "../../Stores/PlayersStore";
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
import {
|
||||||
|
audioManagerFileStore,
|
||||||
|
audioManagerVisibilityStore,
|
||||||
|
audioManagerVolumeStore,
|
||||||
|
} from "../../Stores/AudioManagerStore";
|
||||||
|
import { PropertyUtils } from "../Map/PropertyUtils";
|
||||||
import Tileset = Phaser.Tilemaps.Tileset;
|
import Tileset = Phaser.Tilemaps.Tileset;
|
||||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||||
|
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
|
||||||
|
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
@ -197,6 +204,8 @@ export class GameScene extends DirtyScene {
|
|||||||
private preloading: boolean = true;
|
private preloading: boolean = true;
|
||||||
private startPositionCalculator!: StartPositionCalculator;
|
private startPositionCalculator!: StartPositionCalculator;
|
||||||
private sharedVariablesManager!: SharedVariablesManager;
|
private sharedVariablesManager!: SharedVariablesManager;
|
||||||
|
private objectsByType = new Map<string, ITiledMapObject[]>();
|
||||||
|
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||||
super({
|
super({
|
||||||
@ -336,27 +345,27 @@ export class GameScene extends DirtyScene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Scan the object layers for objects to load and load them.
|
// 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) {
|
for (const layer of this.mapFile.layers) {
|
||||||
if (layer.type === "objectgroup") {
|
if (layer.type === "objectgroup") {
|
||||||
for (const object of layer.objects) {
|
for (const object of layer.objects) {
|
||||||
let objectsOfType: ITiledMapObject[] | undefined;
|
let objectsOfType: ITiledMapObject[] | undefined;
|
||||||
if (!objects.has(object.type)) {
|
if (!this.objectsByType.has(object.type)) {
|
||||||
objectsOfType = new Array<ITiledMapObject>();
|
objectsOfType = new Array<ITiledMapObject>();
|
||||||
} else {
|
} else {
|
||||||
objectsOfType = objects.get(object.type);
|
objectsOfType = this.objectsByType.get(object.type);
|
||||||
if (objectsOfType === undefined) {
|
if (objectsOfType === undefined) {
|
||||||
throw new Error("Unexpected object type not found");
|
throw new Error("Unexpected object type not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objectsOfType.push(object);
|
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.
|
// 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;
|
let itemFactory: ItemFactoryInterface;
|
||||||
@ -456,6 +465,8 @@ export class GameScene extends DirtyScene {
|
|||||||
//permit to set bound collision
|
//permit to set bound collision
|
||||||
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
|
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
|
||||||
|
|
||||||
|
this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this);
|
||||||
|
|
||||||
//add layer on map
|
//add layer on map
|
||||||
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
|
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
|
||||||
for (const layer of this.gameMap.flatLayers) {
|
for (const layer of this.gameMap.flatLayers) {
|
||||||
@ -476,6 +487,28 @@ export class GameScene extends DirtyScene {
|
|||||||
if (object.text) {
|
if (object.text) {
|
||||||
TextUtils.createTextFromITiledMapObject(this, object);
|
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,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -698,12 +731,12 @@ export class GameScene extends DirtyScene {
|
|||||||
this.simplePeer.registerPeerConnectionListener({
|
this.simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(peer) {
|
onConnect(peer) {
|
||||||
//self.openChatIcon.setVisible(true);
|
//self.openChatIcon.setVisible(true);
|
||||||
audioManager.decreaseVolume();
|
audioManagerVolumeStore.setTalking(true);
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
if (self.simplePeer.getNbConnections() === 0) {
|
if (self.simplePeer.getNbConnections() === 0) {
|
||||||
//self.openChatIcon.setVisible(false);
|
//self.openChatIcon.setVisible(false);
|
||||||
audioManager.restoreVolume();
|
audioManagerVolumeStore.setTalking(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -791,7 +824,7 @@ export class GameScene extends DirtyScene {
|
|||||||
});
|
});
|
||||||
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
||||||
if (newValue === undefined) {
|
if (newValue === undefined) {
|
||||||
layoutManager.removeActionButton("openWebsite", this.userInputManager);
|
layoutManagerActionStore.removeAction("openWebsite");
|
||||||
coWebsiteManager.closeCoWebsite();
|
coWebsiteManager.closeCoWebsite();
|
||||||
} else {
|
} else {
|
||||||
const openWebsiteFunction = () => {
|
const openWebsiteFunction = () => {
|
||||||
@ -801,7 +834,7 @@ export class GameScene extends DirtyScene {
|
|||||||
allProps.get("openWebsiteAllowApi") as boolean | undefined,
|
allProps.get("openWebsiteAllowApi") as boolean | undefined,
|
||||||
allProps.get("openWebsitePolicy") as string | undefined
|
allProps.get("openWebsitePolicy") as string | undefined
|
||||||
);
|
);
|
||||||
layoutManager.removeActionButton("openWebsite", this.userInputManager);
|
layoutManagerActionStore.removeAction("openWebsite");
|
||||||
};
|
};
|
||||||
|
|
||||||
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
|
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
|
||||||
@ -810,14 +843,13 @@ export class GameScene extends DirtyScene {
|
|||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
message = "Press SPACE or touch here to open web site";
|
message = "Press SPACE or touch here to open web site";
|
||||||
}
|
}
|
||||||
layoutManager.addActionButton(
|
layoutManagerActionStore.addAction({
|
||||||
"openWebsite",
|
uuid: "openWebsite",
|
||||||
message.toString(),
|
type: "message",
|
||||||
() => {
|
message: message,
|
||||||
openWebsiteFunction();
|
callback: () => openWebsiteFunction(),
|
||||||
},
|
userInputManager: this.userInputManager,
|
||||||
this.userInputManager
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
openWebsiteFunction();
|
openWebsiteFunction();
|
||||||
}
|
}
|
||||||
@ -825,7 +857,7 @@ export class GameScene extends DirtyScene {
|
|||||||
});
|
});
|
||||||
this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => {
|
this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => {
|
||||||
if (newValue === undefined) {
|
if (newValue === undefined) {
|
||||||
layoutManager.removeActionButton("jitsiRoom", this.userInputManager);
|
layoutManagerActionStore.removeAction("jitsi");
|
||||||
this.stopJitsi();
|
this.stopJitsi();
|
||||||
} else {
|
} else {
|
||||||
const openJitsiRoomFunction = () => {
|
const openJitsiRoomFunction = () => {
|
||||||
@ -838,7 +870,7 @@ export class GameScene extends DirtyScene {
|
|||||||
} else {
|
} else {
|
||||||
this.startJitsi(roomName, undefined);
|
this.startJitsi(roomName, undefined);
|
||||||
}
|
}
|
||||||
layoutManager.removeActionButton("jitsiRoom", this.userInputManager);
|
layoutManagerActionStore.removeAction("jitsi");
|
||||||
};
|
};
|
||||||
|
|
||||||
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
|
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
|
||||||
@ -847,14 +879,13 @@ export class GameScene extends DirtyScene {
|
|||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
message = "Press SPACE or touch here to enter Jitsi Meet room";
|
message = "Press SPACE or touch here to enter Jitsi Meet room";
|
||||||
}
|
}
|
||||||
layoutManager.addActionButton(
|
layoutManagerActionStore.addAction({
|
||||||
"jitsiRoom",
|
uuid: "jitsi",
|
||||||
message.toString(),
|
type: "message",
|
||||||
() => {
|
message: message,
|
||||||
openJitsiRoomFunction();
|
callback: () => openJitsiRoomFunction(),
|
||||||
},
|
userInputManager: this.userInputManager,
|
||||||
this.userInputManager
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
openJitsiRoomFunction();
|
openJitsiRoomFunction();
|
||||||
}
|
}
|
||||||
@ -871,14 +902,16 @@ export class GameScene extends DirtyScene {
|
|||||||
const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined;
|
const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined;
|
||||||
const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined;
|
const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined;
|
||||||
newValue === undefined
|
newValue === undefined
|
||||||
? audioManager.unloadAudio()
|
? audioManagerFileStore.unloadAudio()
|
||||||
: audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop);
|
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop);
|
||||||
|
audioManagerVisibilityStore.set(!(newValue === undefined));
|
||||||
});
|
});
|
||||||
// TODO: This legacy property should be removed at some point
|
// TODO: This legacy property should be removed at some point
|
||||||
this.gameMap.onPropertyChange("playAudioLoop", (newValue, oldValue) => {
|
this.gameMap.onPropertyChange("playAudioLoop", (newValue, oldValue) => {
|
||||||
newValue === undefined
|
newValue === undefined
|
||||||
? audioManager.unloadAudio()
|
? audioManagerFileStore.unloadAudio()
|
||||||
: audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
|
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true);
|
||||||
|
audioManagerVisibilityStore.set(!(newValue === undefined));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.gameMap.onPropertyChange("zone", (newValue, oldValue) => {
|
this.gameMap.onPropertyChange("zone", (newValue, oldValue) => {
|
||||||
@ -910,7 +943,7 @@ export class GameScene extends DirtyScene {
|
|||||||
let html = `<div id="container" hidden><div class="nes-container with-title is-centered">
|
let html = `<div id="container" hidden><div class="nes-container with-title is-centered">
|
||||||
${escapedMessage}
|
${escapedMessage}
|
||||||
</div> `;
|
</div> `;
|
||||||
const buttonContainer = `<div class="buttonContainer"</div>`;
|
const buttonContainer = '<div class="buttonContainer"</div>';
|
||||||
html += buttonContainer;
|
html += buttonContainer;
|
||||||
let id = 0;
|
let id = 0;
|
||||||
for (const button of openPopupEvent.buttons) {
|
for (const button of openPopupEvent.buttons) {
|
||||||
@ -1150,6 +1183,44 @@ ${escapedMessage}
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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(
|
private setPropertyLayer(
|
||||||
@ -1217,7 +1288,7 @@ ${escapedMessage}
|
|||||||
let targetRoom: Room;
|
let targetRoom: Room;
|
||||||
try {
|
try {
|
||||||
targetRoom = await Room.createRoom(roomUrl);
|
targetRoom = await Room.createRoom(roomUrl);
|
||||||
} catch (e) {
|
} catch (e /*: unknown*/) {
|
||||||
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
|
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
|
||||||
this.mapTransitioning = false;
|
this.mapTransitioning = false;
|
||||||
return;
|
return;
|
||||||
@ -1258,7 +1329,7 @@ ${escapedMessage}
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.stopJitsi();
|
this.stopJitsi();
|
||||||
audioManager.unloadAudio();
|
audioManagerFileStore.unloadAudio();
|
||||||
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
|
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
|
||||||
this.connection?.closeConnection();
|
this.connection?.closeConnection();
|
||||||
this.simplePeer?.closeAllConnections();
|
this.simplePeer?.closeAllConnections();
|
||||||
@ -1272,7 +1343,12 @@ ${escapedMessage}
|
|||||||
this.biggestAvailableAreaStoreUnsubscribe();
|
this.biggestAvailableAreaStoreUnsubscribe();
|
||||||
iframeListener.unregisterAnswerer("getState");
|
iframeListener.unregisterAnswerer("getState");
|
||||||
iframeListener.unregisterAnswerer("loadTileset");
|
iframeListener.unregisterAnswerer("loadTileset");
|
||||||
|
iframeListener.unregisterAnswerer("getMapData");
|
||||||
|
iframeListener.unregisterAnswerer("getState");
|
||||||
|
iframeListener.unregisterAnswerer("triggerActionMessage");
|
||||||
|
iframeListener.unregisterAnswerer("removeActionMessage");
|
||||||
this.sharedVariablesManager?.close();
|
this.sharedVariablesManager?.close();
|
||||||
|
this.embeddedWebsiteManager?.close();
|
||||||
|
|
||||||
mediaManager.hideGameOverlay();
|
mediaManager.hideGameOverlay();
|
||||||
|
|
||||||
@ -1344,7 +1420,7 @@ ${escapedMessage}
|
|||||||
try {
|
try {
|
||||||
const room = await Room.createRoom(exitRoomPath);
|
const room = await Room.createRoom(exitRoomPath);
|
||||||
return gameManager.loadMap(room, this.scene);
|
return gameManager.loadMap(room, this.scene);
|
||||||
} catch (e) {
|
} catch (e /*: unknown*/) {
|
||||||
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
front/src/Phaser/Map/PropertyUtils.ts
Normal file
53
front/src/Phaser/Map/PropertyUtils.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
105
front/src/Stores/AudioManagerStore.ts
Normal file
105
front/src/Stores/AudioManagerStore.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { get, writable } from "svelte/store";
|
||||||
|
|
||||||
|
export interface audioManagerVolume {
|
||||||
|
muted: boolean;
|
||||||
|
volume: number;
|
||||||
|
decreaseWhileTalking: boolean;
|
||||||
|
volumeReduced: boolean;
|
||||||
|
loop: boolean;
|
||||||
|
talking: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAudioManagerVolumeStore() {
|
||||||
|
const { subscribe, update } = writable<audioManagerVolume>({
|
||||||
|
muted: false,
|
||||||
|
volume: 1,
|
||||||
|
decreaseWhileTalking: true,
|
||||||
|
volumeReduced: false,
|
||||||
|
loop: false,
|
||||||
|
talking: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
setMuted: (newMute: boolean): void => {
|
||||||
|
update((audioPlayerVolume: audioManagerVolume) => {
|
||||||
|
audioPlayerVolume.muted = newMute;
|
||||||
|
return audioPlayerVolume;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setVolume: (newVolume: number): void => {
|
||||||
|
update((audioPlayerVolume: audioManagerVolume) => {
|
||||||
|
audioPlayerVolume.volume = newVolume;
|
||||||
|
return audioPlayerVolume;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDecreaseWhileTalking: (newDecrease: boolean): void => {
|
||||||
|
update((audioManagerVolume: audioManagerVolume) => {
|
||||||
|
audioManagerVolume.decreaseWhileTalking = newDecrease;
|
||||||
|
return audioManagerVolume;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setVolumeReduced: (newVolumeReduced: boolean): void => {
|
||||||
|
update((audioManagerVolume: audioManagerVolume) => {
|
||||||
|
audioManagerVolume.volumeReduced = newVolumeReduced;
|
||||||
|
return audioManagerVolume;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setLoop: (newLoop: boolean): void => {
|
||||||
|
update((audioManagerVolume: audioManagerVolume) => {
|
||||||
|
audioManagerVolume.loop = newLoop;
|
||||||
|
return audioManagerVolume;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setTalking: (newTalk: boolean): void => {
|
||||||
|
update((audioManagerVolume: audioManagerVolume) => {
|
||||||
|
audioManagerVolume.talking = newTalk;
|
||||||
|
return audioManagerVolume;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAudioManagerFileStore() {
|
||||||
|
const { subscribe, update } = writable<string>("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
playAudio: (
|
||||||
|
url: string | number | boolean,
|
||||||
|
mapDirUrl: string,
|
||||||
|
volume: number | undefined,
|
||||||
|
loop = false
|
||||||
|
): void => {
|
||||||
|
update((file: string) => {
|
||||||
|
const audioPath = url as string;
|
||||||
|
|
||||||
|
if (audioPath.indexOf("://") > 0) {
|
||||||
|
// remote file or stream
|
||||||
|
file = audioPath;
|
||||||
|
} else {
|
||||||
|
// local file, include it relative to map directory
|
||||||
|
file = mapDirUrl + "/" + url;
|
||||||
|
}
|
||||||
|
audioManagerVolumeStore.setVolume(
|
||||||
|
volume ? Math.min(volume, get(audioManagerVolumeStore).volume) : get(audioManagerVolumeStore).volume
|
||||||
|
);
|
||||||
|
audioManagerVolumeStore.setLoop(loop);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
unloadAudio: () => {
|
||||||
|
update((file: string) => {
|
||||||
|
audioManagerVolumeStore.setLoop(false);
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audioManagerVisibilityStore = writable(false);
|
||||||
|
|
||||||
|
export const audioManagerVolumeStore = createAudioManagerVolumeStore();
|
||||||
|
|
||||||
|
export const audioManagerFileStore = createAudioManagerFileStore();
|
56
front/src/Stores/LayoutManagerStore.ts
Normal file
56
front/src/Stores/LayoutManagerStore.ts
Normal file
@ -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,188 +0,0 @@
|
|||||||
import {HtmlUtils} from "./HtmlUtils";
|
|
||||||
import {isUndefined} from "generic-type-guard";
|
|
||||||
import {localUserStore} from "../Connexion/LocalUserStore";
|
|
||||||
|
|
||||||
enum audioStates {
|
|
||||||
closed = 0,
|
|
||||||
loading = 1,
|
|
||||||
playing = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioPlayerDivId = "audioplayer";
|
|
||||||
const audioPlayerCtrlId = "audioplayerctrl";
|
|
||||||
const audioPlayerVolId = "audioplayer_volume";
|
|
||||||
const audioPlayerMuteId = "audioplayer_volume_icon_playing";
|
|
||||||
const animationTime = 500;
|
|
||||||
|
|
||||||
class AudioManager {
|
|
||||||
private opened = audioStates.closed;
|
|
||||||
|
|
||||||
private audioPlayerDiv: HTMLDivElement;
|
|
||||||
private audioPlayerCtrl: HTMLDivElement;
|
|
||||||
private audioPlayerElem: HTMLAudioElement | undefined;
|
|
||||||
private audioPlayerVol: HTMLInputElement;
|
|
||||||
private audioPlayerMute: HTMLInputElement;
|
|
||||||
|
|
||||||
private volume = 1;
|
|
||||||
private muted = false;
|
|
||||||
private decreaseWhileTalking = true;
|
|
||||||
private volumeReduced = false;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.audioPlayerDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(audioPlayerDivId);
|
|
||||||
this.audioPlayerCtrl = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(audioPlayerCtrlId);
|
|
||||||
this.audioPlayerVol = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(audioPlayerVolId);
|
|
||||||
this.audioPlayerMute = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(audioPlayerMuteId);
|
|
||||||
|
|
||||||
this.volume = localUserStore.getAudioPlayerVolume();
|
|
||||||
this.audioPlayerVol.value = '' + this.volume;
|
|
||||||
|
|
||||||
this.muted = localUserStore.getAudioPlayerMuted();
|
|
||||||
if (this.muted) {
|
|
||||||
this.audioPlayerMute.classList.add('muted');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public playAudio(url: string|number|boolean, mapDirUrl: string, volume: number|undefined, loop=false): void {
|
|
||||||
const audioPath = url as string;
|
|
||||||
let realAudioPath = '';
|
|
||||||
|
|
||||||
if (audioPath.indexOf('://') > 0) {
|
|
||||||
// remote file or stream
|
|
||||||
realAudioPath = audioPath;
|
|
||||||
} else {
|
|
||||||
// local file, include it relative to map directory
|
|
||||||
realAudioPath = mapDirUrl + '/' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadAudio(realAudioPath, volume);
|
|
||||||
|
|
||||||
if (loop) {
|
|
||||||
this.loop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private close(): void {
|
|
||||||
this.audioPlayerCtrl.classList.remove('loading');
|
|
||||||
this.audioPlayerCtrl.classList.add('hidden');
|
|
||||||
this.opened = audioStates.closed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private load(): void {
|
|
||||||
this.audioPlayerCtrl.classList.remove('hidden');
|
|
||||||
this.audioPlayerCtrl.classList.add('loading');
|
|
||||||
this.opened = audioStates.loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
private open(): void {
|
|
||||||
this.audioPlayerCtrl.classList.remove('hidden', 'loading');
|
|
||||||
this.opened = audioStates.playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private changeVolume(talking = false): void {
|
|
||||||
if (isUndefined(this.audioPlayerElem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reduceVolume = talking && this.decreaseWhileTalking;
|
|
||||||
if (reduceVolume && !this.volumeReduced) {
|
|
||||||
this.volume *= 0.5;
|
|
||||||
} else if (!reduceVolume && this.volumeReduced) {
|
|
||||||
this.volume *= 2.0;
|
|
||||||
}
|
|
||||||
this.volumeReduced = reduceVolume;
|
|
||||||
|
|
||||||
this.audioPlayerElem.volume = this.volume;
|
|
||||||
this.audioPlayerVol.value = '' + this.volume;
|
|
||||||
this.audioPlayerElem.muted = this.muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setVolume(volume: number): void {
|
|
||||||
this.volume = volume;
|
|
||||||
localUserStore.setAudioPlayerVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadAudio(url: string, volume: number|undefined): void {
|
|
||||||
this.load();
|
|
||||||
|
|
||||||
/* Solution 1, remove whole audio player */
|
|
||||||
this.audioPlayerDiv.innerHTML = ''; // necessary, if switching from one audio context to another! (else both streams would play simultaneously)
|
|
||||||
|
|
||||||
this.audioPlayerElem = document.createElement('audio');
|
|
||||||
this.audioPlayerElem.id = 'audioplayerelem';
|
|
||||||
this.audioPlayerElem.controls = false;
|
|
||||||
this.audioPlayerElem.preload = 'none';
|
|
||||||
|
|
||||||
const srcElem = document.createElement('source');
|
|
||||||
srcElem.type = "audio/mp3";
|
|
||||||
srcElem.src = url;
|
|
||||||
|
|
||||||
this.audioPlayerElem.append(srcElem);
|
|
||||||
|
|
||||||
this.audioPlayerDiv.append(this.audioPlayerElem);
|
|
||||||
this.volume = volume ? Math.min(volume, this.volume) : this.volume;
|
|
||||||
this.changeVolume();
|
|
||||||
this.audioPlayerElem.play();
|
|
||||||
|
|
||||||
const muteElem = HtmlUtils.getElementByIdOrFail<HTMLInputElement>('audioplayer_mute');
|
|
||||||
muteElem.onclick = (ev: Event) => {
|
|
||||||
this.muted = !this.muted;
|
|
||||||
this.changeVolume();
|
|
||||||
localUserStore.setAudioPlayerMuted(this.muted);
|
|
||||||
|
|
||||||
if (this.muted) {
|
|
||||||
this.audioPlayerMute.classList.add('muted');
|
|
||||||
} else {
|
|
||||||
this.audioPlayerMute.classList.remove('muted');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.audioPlayerVol.oninput = (ev: Event)=> {
|
|
||||||
this.setVolume(parseFloat((<HTMLInputElement>ev.currentTarget).value));
|
|
||||||
this.changeVolume();
|
|
||||||
|
|
||||||
(<HTMLInputElement>ev.currentTarget).blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
const decreaseElem = HtmlUtils.getElementByIdOrFail<HTMLInputElement>('audioplayer_decrease_while_talking');
|
|
||||||
decreaseElem.oninput = (ev: Event)=> {
|
|
||||||
this.decreaseWhileTalking = (<HTMLInputElement>ev.currentTarget).checked;
|
|
||||||
this.changeVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
private loop(): void {
|
|
||||||
if (this.audioPlayerElem !== undefined) {
|
|
||||||
this.audioPlayerElem.loop = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public unloadAudio(): void {
|
|
||||||
try {
|
|
||||||
const audioElem = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('audioplayerelem');
|
|
||||||
this.volume = audioElem.volume;
|
|
||||||
this.muted = audioElem.muted;
|
|
||||||
audioElem.pause();
|
|
||||||
audioElem.loop = false;
|
|
||||||
audioElem.src = "";
|
|
||||||
audioElem.innerHTML = "";
|
|
||||||
audioElem.load();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('No audio element loaded to unload');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public decreaseVolume(): void {
|
|
||||||
this.changeVolume(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public restoreVolume(): void {
|
|
||||||
this.changeVolume(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const audioManager = new AudioManager();
|
|
@ -1,6 +1,3 @@
|
|||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
|
||||||
import { HtmlUtils } from "./HtmlUtils";
|
|
||||||
|
|
||||||
export enum LayoutMode {
|
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.
|
// 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",
|
Presentation = "Presentation",
|
||||||
@ -27,85 +24,3 @@ export const AUDIO_VOLUME_PROPERTY = "audioVolume";
|
|||||||
export const AUDIO_LOOP_PROPERTY = "audioLoop";
|
export const AUDIO_LOOP_PROPERTY = "audioLoop";
|
||||||
|
|
||||||
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
|
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.innerText = 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 { HtmlUtils } from "./HtmlUtils";
|
||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||||
import { localStreamStore } from "../Stores/MediaStore";
|
import { localStreamStore } from "../Stores/MediaStore";
|
||||||
@ -10,6 +9,8 @@ export type StopScreenSharingCallback = (media: MediaStream) => void;
|
|||||||
|
|
||||||
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
|
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
|
||||||
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
||||||
|
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
export class MediaManager {
|
export class MediaManager {
|
||||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||||
@ -23,14 +24,19 @@ export class MediaManager {
|
|||||||
localStreamStore.subscribe((result) => {
|
localStreamStore.subscribe((result) => {
|
||||||
if (result.type === "error") {
|
if (result.type === "error") {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
layoutManager.addInformation(
|
layoutManagerActionStore.addAction({
|
||||||
"warning",
|
uuid: "cameraAccessDenied",
|
||||||
"Camera access denied. Click here and check your browser permissions.",
|
type: "warning",
|
||||||
() => {
|
message: "Camera access denied. Click here and check your browser permissions.",
|
||||||
|
callback: () => {
|
||||||
helpCameraSettingsVisibleStore.set(true);
|
helpCameraSettingsVisibleStore.set(true);
|
||||||
},
|
},
|
||||||
this.userInputManager
|
userInputManager: this.userInputManager,
|
||||||
);
|
});
|
||||||
|
//remove it after 10 sec
|
||||||
|
setTimeout(() => {
|
||||||
|
layoutManagerActionStore.removeAction("cameraAccessDenied");
|
||||||
|
}, 10000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -38,14 +44,19 @@ export class MediaManager {
|
|||||||
screenSharingLocalStreamStore.subscribe((result) => {
|
screenSharingLocalStreamStore.subscribe((result) => {
|
||||||
if (result.type === "error") {
|
if (result.type === "error") {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
layoutManager.addInformation(
|
layoutManagerActionStore.addAction({
|
||||||
"warning",
|
uuid: "screenSharingAccessDenied",
|
||||||
"Screen sharing denied. Click here and check your browser permissions.",
|
type: "warning",
|
||||||
() => {
|
message: "Screen sharing denied. Click here and check your browser permissions.",
|
||||||
|
callback: () => {
|
||||||
helpCameraSettingsVisibleStore.set(true);
|
helpCameraSettingsVisibleStore.set(true);
|
||||||
},
|
},
|
||||||
this.userInputManager
|
userInputManager: this.userInputManager,
|
||||||
);
|
});
|
||||||
|
//remove it after 10 sec
|
||||||
|
setTimeout(() => {
|
||||||
|
layoutManagerActionStore.removeAction("screenSharingAccessDenied");
|
||||||
|
}, 10000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -385,6 +385,10 @@ body {
|
|||||||
|
|
||||||
#game {
|
#game {
|
||||||
position: relative; /* Position relative is needed for the game-overlay. */
|
position: relative; /* Position relative is needed for the game-overlay. */
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.audioplayer:first-child {
|
.audioplayer:first-child {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
10
maps/tests/EmbeddedWebsite/integrated_website_1.html
Normal file
10
maps/tests/EmbeddedWebsite/integrated_website_1.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world</h1>
|
||||||
|
<p>This is a webpage integrated in your map</p>
|
||||||
|
|
||||||
|
<p>Here is a form, right in your map! <input type="text" value="because I can!"> <button>Submit!</button></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
99
maps/tests/EmbeddedWebsite/website_in_map.json
Normal file
99
maps/tests/EmbeddedWebsite/website_in_map.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":30,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
"height":30,
|
||||||
|
"id":1,
|
||||||
|
"name":"floor",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":30,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":30,
|
||||||
|
"id":2,
|
||||||
|
"name":"start",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":30,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"floorLayer",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":83.6666666666666,
|
||||||
|
"id":1,
|
||||||
|
"name":"",
|
||||||
|
"rotation":0,
|
||||||
|
"text":
|
||||||
|
{
|
||||||
|
"fontfamily":"Sans Serif",
|
||||||
|
"pixelsize":13,
|
||||||
|
"text":"Test:\nWalk around the map\nResult:\nYou should see a transparent website at the center of the map, that is \"fixed\" on the map floor.",
|
||||||
|
"wrap":true
|
||||||
|
},
|
||||||
|
"type":"",
|
||||||
|
"visible":true,
|
||||||
|
"width":315.4375,
|
||||||
|
"x":68.4021076998051,
|
||||||
|
"y":8.73391812865529
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":419.805068226121,
|
||||||
|
"id":2,
|
||||||
|
"name":"demo website",
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"url",
|
||||||
|
"type":"string",
|
||||||
|
"value":"integrated_website_1.html"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"website",
|
||||||
|
"visible":true,
|
||||||
|
"width":637.504873294347,
|
||||||
|
"x":189.005847953216,
|
||||||
|
"y":156.569200779727
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":6,
|
||||||
|
"nextobjectid":3,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"2021.03.23",
|
||||||
|
"tileheight":32,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":11,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tileset1.png",
|
||||||
|
"imageheight":352,
|
||||||
|
"imagewidth":352,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tileset1",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":121,
|
||||||
|
"tileheight":32,
|
||||||
|
"tilewidth":32
|
||||||
|
}],
|
||||||
|
"tilewidth":32,
|
||||||
|
"type":"map",
|
||||||
|
"version":1.5,
|
||||||
|
"width":30
|
||||||
|
}
|
85
maps/tests/EmbeddedWebsite/website_in_map_script.html
Normal file
85
maps/tests/EmbeddedWebsite/website_in_map_script.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
var script = document.createElement('script');
|
||||||
|
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
|
||||||
|
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
|
||||||
|
script.setAttribute('src', document.referrer + 'iframe_api.js');
|
||||||
|
document.head.appendChild(script);
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
console.log('On load');
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('After WA init');
|
||||||
|
const createButton = document.getElementById('createEmbeddedWebsite');
|
||||||
|
const deleteButton = document.getElementById('deleteEmbeddedWebsite');
|
||||||
|
const xField = document.getElementById('x');
|
||||||
|
const yField = document.getElementById('y');
|
||||||
|
const widthField = document.getElementById('width');
|
||||||
|
const heightField = document.getElementById('height');
|
||||||
|
const urlField = document.getElementById('url');
|
||||||
|
const visibleField = document.getElementById('visible');
|
||||||
|
|
||||||
|
createButton.addEventListener('click', () => {
|
||||||
|
console.log('CREATING NEW EMBEDDED IFRAME');
|
||||||
|
WA.room.website.create({
|
||||||
|
name: "test",
|
||||||
|
url: urlField.value,
|
||||||
|
position: {
|
||||||
|
x: parseInt(xField.value),
|
||||||
|
y: parseInt(yField.value),
|
||||||
|
width: parseInt(widthField.value),
|
||||||
|
height: parseInt(heightField.value),
|
||||||
|
},
|
||||||
|
visible: !!visibleField.value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteButton.addEventListener('click', () => {
|
||||||
|
WA.room.website.delete("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
xField.addEventListener('change', async function() {
|
||||||
|
const website = await WA.room.website.get('test');
|
||||||
|
website.x = parseInt(this.value);
|
||||||
|
});
|
||||||
|
yField.addEventListener('change', async function() {
|
||||||
|
const website = await WA.room.website.get('test');
|
||||||
|
website.y = parseInt(this.value);
|
||||||
|
});
|
||||||
|
widthField.addEventListener('change', async function() {
|
||||||
|
const website = await WA.room.website.get('test');
|
||||||
|
website.width = parseInt(this.value);
|
||||||
|
});
|
||||||
|
heightField.addEventListener('change', async function() {
|
||||||
|
const website = await WA.room.website.get('test');
|
||||||
|
website.height = parseInt(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
urlField.addEventListener('change', async function() {
|
||||||
|
const website = await WA.room.website.get('test');
|
||||||
|
website.url = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
visibleField.addEventListener('change', async function() {
|
||||||
|
const website = await WA.room.website.get('test');
|
||||||
|
website.visible = this.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
X: <input type="text" id="x" value="64" /><br/>
|
||||||
|
Y: <input type="text" id="y" value="64" /><br/>
|
||||||
|
width: <input type="text" id="width" value="600" /><br/>
|
||||||
|
height: <input type="text" id="height" value="400" /><br/>
|
||||||
|
URL: <input type="text" id="url" value="https://mensuel.framapad.org/p/rt6c904745-9oxm?lang=en" /><br/>
|
||||||
|
Visible: <input type="checkbox" id="visible" value=1 /><br/>
|
||||||
|
|
||||||
|
<button id="createEmbeddedWebsite">Create embedded website</button>
|
||||||
|
|
||||||
|
<button id="deleteEmbeddedWebsite">Delete embedded website</button>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
93
maps/tests/EmbeddedWebsite/website_in_map_script.json
Normal file
93
maps/tests/EmbeddedWebsite/website_in_map_script.json
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":30,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
"height":30,
|
||||||
|
"id":1,
|
||||||
|
"name":"floor",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"openWebsite",
|
||||||
|
"type":"string",
|
||||||
|
"value":"website_in_map_script.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"openWebsiteAllowApi",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":30,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":30,
|
||||||
|
"id":2,
|
||||||
|
"name":"start",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":30,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"floorLayer",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":393,
|
||||||
|
"id":1,
|
||||||
|
"name":"",
|
||||||
|
"rotation":0,
|
||||||
|
"text":
|
||||||
|
{
|
||||||
|
"fontfamily":"Sans Serif",
|
||||||
|
"pixelsize":13,
|
||||||
|
"text":"Test:\nClick the \"create\" button.\n\nResult:\nA website should appear.\n\nTest:\nUse the fields to modify settings.\n\nResult:\nThe iframe is modified accordingly.\n\nTest:\nClick the \"delete\" button.\n\nResult:\nThe iframe is deleted\n\nTest:\nClick the \"create\" button.\n\nResult:\nA website should appear.\n",
|
||||||
|
"wrap":true
|
||||||
|
},
|
||||||
|
"type":"",
|
||||||
|
"visible":true,
|
||||||
|
"width":315.4375,
|
||||||
|
"x":68.4021076998051,
|
||||||
|
"y":8.73391812865529
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":6,
|
||||||
|
"nextobjectid":3,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"2021.03.23",
|
||||||
|
"tileheight":32,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":11,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tileset1.png",
|
||||||
|
"imageheight":352,
|
||||||
|
"imagewidth":352,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tileset1",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":121,
|
||||||
|
"tileheight":32,
|
||||||
|
"tilewidth":32
|
||||||
|
}],
|
||||||
|
"tilewidth":32,
|
||||||
|
"type":"map",
|
||||||
|
"version":1.5,
|
||||||
|
"width":30
|
||||||
|
}
|
16
maps/tests/TriggerMessageApi/script.js
Normal file
16
maps/tests/TriggerMessageApi/script.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
WA.onInit().then(() => {
|
||||||
|
let message;
|
||||||
|
|
||||||
|
WA.room.onEnterZone("carpet", () => {
|
||||||
|
message = WA.ui.displayActionMessage({
|
||||||
|
message: "This is a test message. Press space to display a chat message. Walk out to hide the message.",
|
||||||
|
callback: () => {
|
||||||
|
WA.chat.sendChatMessage("Hello world!", "The bot");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
WA.room.onLeaveZone("carpet", () => {
|
||||||
|
message && message.remove();
|
||||||
|
});
|
||||||
|
});
|
106
maps/tests/TriggerMessageApi/triggerMessage.json
Normal file
106
maps/tests/TriggerMessageApi/triggerMessage.json
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":10,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
"height":10,
|
||||||
|
"id":1,
|
||||||
|
"name":"floor",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":10,
|
||||||
|
"id":2,
|
||||||
|
"name":"start",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":10,
|
||||||
|
"id":8,
|
||||||
|
"name":"carpet",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"zone",
|
||||||
|
"type":"string",
|
||||||
|
"value":"carpet"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"floorLayer",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":304.037037037037,
|
||||||
|
"id":3,
|
||||||
|
"name":"",
|
||||||
|
"rotation":0,
|
||||||
|
"text":
|
||||||
|
{
|
||||||
|
"fontfamily":"Sans Serif",
|
||||||
|
"pixelsize":11,
|
||||||
|
"text":"Test:\nWalk on the carpet\n\nResult:\nA message is displayed at the bottom of the screen\n\nTest:\nPress space\n\nResult:\nA chat message is displayed\n\n\nTest:\nWalk out of the carpet\n\nResult:\nThe message is hidden\n",
|
||||||
|
"wrap":true
|
||||||
|
},
|
||||||
|
"type":"",
|
||||||
|
"visible":true,
|
||||||
|
"width":252.4375,
|
||||||
|
"x":2.78125,
|
||||||
|
"y":2.5
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":9,
|
||||||
|
"nextobjectid":11,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"script",
|
||||||
|
"type":"string",
|
||||||
|
"value":"script.js"
|
||||||
|
}],
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"2021.03.23",
|
||||||
|
"tileheight":32,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":11,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tileset1.png",
|
||||||
|
"imageheight":352,
|
||||||
|
"imagewidth":352,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tileset1",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":121,
|
||||||
|
"tileheight":32,
|
||||||
|
"tilewidth":32
|
||||||
|
}],
|
||||||
|
"tilewidth":32,
|
||||||
|
"type":"map",
|
||||||
|
"version":1.5,
|
||||||
|
"width":10
|
||||||
|
}
|
@ -202,6 +202,30 @@
|
|||||||
<a href="#" class="testLink" data-testmap="Variables/shared_variables.json" target="_blank">Testing shared scripting variables</a>
|
<a href="#" class="testLink" data-testmap="Variables/shared_variables.json" target="_blank">Testing shared scripting variables</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="test-trigger-message-api"> Success <input type="radio" name="test-trigger-message-api"> Failure <input type="radio" name="test-trigger-message-api" checked> Pending
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="testLink" data-testmap="TriggerMessageApi/triggerMessage.json" target="_blank">Testing trigger message API</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="test-website-objects"> Success <input type="radio" name="test-website-objects"> Failure <input type="radio" name="test-website-objects" checked> Pending
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="testLink" data-testmap="EmbeddedWebsite/website_in_map.json" target="_blank">Testing websites inside a map</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="test-website-objects-script"> Success <input type="radio" name="test-website-objects-script"> Failure <input type="radio" name="test-website-objects-script" checked> Pending
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="testLink" data-testmap="EmbeddedWebsite/website_in_map_script.json" target="_blank">Testing scripting API for websites inside a map</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
///<reference path="../../front/src/iframe_api.ts" />
|
||||||
console.log('SCRIPT LAUNCHED');
|
console.log('SCRIPT LAUNCHED');
|
||||||
//WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot');
|
//WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot');
|
||||||
var isFirstTimeTuto = false;
|
var isFirstTimeTuto = false;
|
||||||
@ -77,3 +78,10 @@ WA.room.onLeaveZone('popupZone', () => {
|
|||||||
if(popUpExplanation !== undefined) popUpExplanation.close();
|
if(popUpExplanation !== undefined) popUpExplanation.close();
|
||||||
WA.ui.removeBubble();
|
WA.ui.removeBubble();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const message = WA.ui.displayActionMessage("testMessage", () => {
|
||||||
|
WA.chat.sendChatMessage("triggered", "triggerbot");
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
message.remove();
|
||||||
|
}, 5000)
|
||||||
|
12
package-lock.json
generated
Normal file
12
package-lock.json
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"requires": true,
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"husky": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^6.0.0"
|
"husky": "^7.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
|
Loading…
Reference in New Issue
Block a user