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

This commit is contained in:
GRL 2021-08-09 16:59:12 +02:00
commit 9c1926f636
61 changed files with 1911 additions and 552 deletions

View File

@ -473,7 +473,22 @@ export class GameRoom {
const variablesManager = new VariablesManager(this.roomUrl, null); const variablesManager = new VariablesManager(this.roomUrl, null);
return variablesManager.init(); return variablesManager.init();
} else { } else {
throw e; // An error occurred while loading the map
// Right now, let's bypass the error. In the future, we should make sure the user is aware of that
// and that he/she will act on it to fix the problem.
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
setTimeout(() => {
for (const roomListener of this.roomListeners) {
emitErrorOnRoomSocket(
roomListener,
"Your map does not seem accessible from the WorkAdventure servers. Is it behind a firewall or a proxy? Your map should be accessible from the WorkAdventure servers. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
);
}
}, 1000);
const variablesManager = new VariablesManager(this.roomUrl, null);
return variablesManager.init();
} }
}); });
} }

View File

@ -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.

View File

@ -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() {};
}
```

View 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>

View File

@ -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",

View File

@ -8,7 +8,6 @@ self.addEventListener('install', function(event) {
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) caches.open(CACHE_NAME)
.then(function(cache) { .then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache); return cache.addAll(urlsToCache);
}) })
); );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -48,41 +48,10 @@
"type": "image\/png" "type": "image\/png"
}, },
{ {
"src": "/static/images/favicons/android-icon-36x36.png", "src": "/static/images/favicons/apple-icon.png",
"sizes": "36x36", "sizes": "192x192",
"type": "image\/png", "type": "image\/png",
"density": "0.75" "purpose": "any"
},
{
"src": "/static/images/favicons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "/static/images/favicons/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-16x16.png",
"sizes": "16x16",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/favicon-32x32.png",
"sizes": "32x32",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
}, },
{ {
@ -122,6 +91,25 @@
"density": "4.0", "density": "4.0",
"purpose": "any maskable" "purpose": "any maskable"
}, },
{
"src": "/static/images/favicons/favicon-16x16.png",
"sizes": "16x16",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/favicon-32x32.png",
"sizes": "32x32",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{ {
"src": "/static/images/favicons/icon-512x512.png", "src": "/static/images/favicons/icon-512x512.png",
"sizes": "512x512", "sizes": "512x512",
@ -132,6 +120,7 @@
"background_color": "#000000", "background_color": "#000000",
"display_override": ["window-control-overlay", "minimal-ui"], "display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone", "display": "standalone",
"orientation": "portrait-primary",
"scope": "/", "scope": "/",
"lang": "en", "lang": "en",
"theme_color": "#000000", "theme_color": "#000000",

View 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>;

View File

@ -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

View 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>;

View 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);

View File

@ -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.
*/ */

View File

@ -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"];
} }

View 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,
},
});
}
}

View 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();
}
}

View File

@ -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();

View File

@ -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();

View 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();

View File

@ -36,6 +36,8 @@
import WarningContainer from "./WarningContainer/WarningContainer.svelte"; import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore"; import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore";
import LayoutManager from "./LayoutManager/LayoutManager.svelte"; 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;
@ -82,6 +84,11 @@
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
</div> </div>
{/if} {/if}
{#if $audioManagerVisibilityStore}
<div>
<AudioManager></AudioManager>
</div>
{/if}
{#if $layoutManagerVisibilityStore} {#if $layoutManagerVisibilityStore}
<div> <div>
<LayoutManager></LayoutManager> <LayoutManager></LayoutManager>

View 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>

View File

@ -1,26 +1,5 @@
<script lang="ts"> <script lang="ts">
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
onMount(() => {
for (const action of get(layoutManagerActionStore)) {
action.userInputManager?.addSpaceEventListner(action.callback);
if ( action.type === 'warning') {
//remove it after 10 sec
setTimeout(() => {
layoutManagerActionStore.removeAction(action);
}, 10000)
}
}
})
onDestroy(() => {
for (const action of get(layoutManagerActionStore)) {
action.userInputManager?.removeSpaceEventListner(action.callback);
}
layoutManagerActionStore.clearActions();
})
function onClick(callback: () => void) { function onClick(callback: () => void) {
callback(); callback();

View 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

View 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

View File

@ -105,6 +105,15 @@ class ConnectionManager {
let roomPath: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL; roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
//get last room path from cache api
try {
const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi();
if (lastRoomUrl != undefined) {
roomPath = lastRoomUrl;
}
} catch (err) {
console.error(err);
}
} else { } else {
roomPath = roomPath =
window.location.protocol + window.location.protocol +

View File

@ -18,6 +18,8 @@ const state = "state";
const nonce = "nonce"; const nonce = "nonce";
const notification = "notificationPermission"; const notification = "notificationPermission";
const cacheAPIIndex = "workavdenture-cache-v1";
class LocalUserStore { class LocalUserStore {
saveUser(localUser: LocalUser) { saveUser(localUser: LocalUser) {
localStorage.setItem("localUser", JSON.stringify(localUser)); localStorage.setItem("localUser", JSON.stringify(localUser));
@ -117,10 +119,23 @@ class LocalUserStore {
setLastRoomUrl(roomUrl: string): void { setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString()); localStorage.setItem(lastRoomUrl, roomUrl.toString());
caches.open(cacheAPIIndex).then((cache) => {
const stringResponse = new Response(JSON.stringify({ roomUrl }));
cache.put(`/${lastRoomUrl}`, stringResponse);
});
} }
getLastRoomUrl(): string { getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? ""; return localStorage.getItem(lastRoomUrl) ?? "";
} }
getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => {
return cache.match(`/${lastRoomUrl}`).then((res) => {
return res?.json().then((data) => {
return data.roomUrl;
});
});
});
}
setAuthToken(value: string | null) { setAuthToken(value: string | null) {
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken); value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);

View File

@ -6,15 +6,13 @@ export class _ServiceWorker {
} }
init() { init() {
window.addEventListener("load", () => {
navigator.serviceWorker navigator.serviceWorker
.register("/resources/service-worker.js") .register("/service-worker.js")
.then((serviceWorker) => { .then((serviceWorker) => {
console.info("Service Worker registered: ", serviceWorker); console.info("Service Worker registered: ", serviceWorker);
}) })
.catch((error) => { .catch((error) => {
console.error("Error registering the Service Worker: ", error); console.error("Error registering the Service Worker: ", error);
}); });
});
} }
} }

View File

@ -1,12 +1,12 @@
import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js'; import VirtualJoystick from "phaser3-rex-plugins/plugins/virtualjoystick.js";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free //the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free
export const joystickBaseKey = 'joystickBase'; export const joystickBaseKey = "joystickBase";
export const joystickBaseImg = 'resources/objects/joystickSplitted.png'; export const joystickBaseImg = "resources/objects/joystickSplitted.png";
export const joystickThumbKey = 'joystickThumb'; export const joystickThumbKey = "joystickThumb";
export const joystickThumbImg = 'resources/objects/smallHandleFilledGrey.png'; export const joystickThumbImg = "resources/objects/smallHandleFilledGrey.png";
const baseSize = 50; const baseSize = 50;
const thumbSize = 25; const thumbSize = 25;
@ -20,15 +20,27 @@ export class MobileJoystick extends VirtualJoystick {
x: -1000, x: -1000,
y: -1000, y: -1000,
radius: radius * window.devicePixelRatio, radius: radius * window.devicePixelRatio,
base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX), base: scene.add
thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX), .image(0, 0, joystickBaseKey)
.setDisplaySize(
(baseSize / waScaleManager.zoomModifier) * window.devicePixelRatio,
(baseSize / waScaleManager.zoomModifier) * window.devicePixelRatio
)
.setDepth(DEPTH_INGAME_TEXT_INDEX),
thumb: scene.add
.image(0, 0, joystickThumbKey)
.setDisplaySize(
(thumbSize / waScaleManager.zoomModifier) * window.devicePixelRatio,
(thumbSize / waScaleManager.zoomModifier) * window.devicePixelRatio
)
.setDepth(DEPTH_INGAME_TEXT_INDEX),
enable: true, enable: true,
dir: "8dir", dir: "8dir",
}); });
this.visible = false; this.visible = false;
this.enable = false; this.enable = false;
this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { this.scene.input.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
if (!pointer.wasTouch) { if (!pointer.wasTouch) {
return; return;
} }
@ -44,7 +56,7 @@ export class MobileJoystick extends VirtualJoystick {
this.enable = false; this.enable = false;
} }
}); });
this.scene.input.on('pointerup', () => { this.scene.input.on("pointerup", () => {
this.visible = false; this.visible = false;
this.enable = false; this.enable = false;
}); });
@ -52,10 +64,16 @@ export class MobileJoystick extends VirtualJoystick {
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback); this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
} }
private resize() { public resize() {
this.base.setDisplaySize(baseSize / waScaleManager.zoomModifier * window.devicePixelRatio, baseSize / waScaleManager.zoomModifier * window.devicePixelRatio); this.base.setDisplaySize(this.getDisplaySizeByElement(baseSize), this.getDisplaySizeByElement(baseSize));
this.thumb.setDisplaySize(thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio, thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio); this.thumb.setDisplaySize(this.getDisplaySizeByElement(thumbSize), this.getDisplaySizeByElement(thumbSize));
this.setRadius(radius / waScaleManager.zoomModifier * window.devicePixelRatio); this.setRadius(
(radius / (waScaleManager.zoomModifier * waScaleManager.uiScalingFactor)) * window.devicePixelRatio
);
}
private getDisplaySizeByElement(element: integer): integer {
return (element / (waScaleManager.zoomModifier * waScaleManager.uiScalingFactor)) * window.devicePixelRatio;
} }
public destroy() { public destroy() {

View File

@ -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();
}
}

View 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");
}
}

View File

@ -32,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";
@ -84,10 +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, layoutManagerVisibilityStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -198,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({
@ -337,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;
@ -457,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) {
@ -477,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,
""
);
}
} }
} }
} }
@ -699,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);
} }
}, },
}); });
@ -792,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) {
layoutManagerVisibilityStore.set(false); layoutManagerActionStore.removeAction("openWebsite");
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
} else { } else {
const openWebsiteFunction = () => { const openWebsiteFunction = () => {
@ -802,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
); );
layoutManagerVisibilityStore.set(false); layoutManagerActionStore.removeAction("openWebsite");
}; };
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
@ -812,12 +844,12 @@ export class GameScene extends DirtyScene {
message = "Press SPACE or touch here to open web site"; message = "Press SPACE or touch here to open web site";
} }
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
type: "openWebsite", uuid: "openWebsite",
type: "message",
message: message, message: message,
callback: () => openWebsiteFunction(), callback: () => openWebsiteFunction(),
userInputManager: this.userInputManager, userInputManager: this.userInputManager,
}); });
layoutManagerVisibilityStore.set(true);
} 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) {
layoutManagerVisibilityStore.set(false); 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);
} }
layoutManagerVisibilityStore.set(false); layoutManagerActionStore.removeAction("jitsi");
}; };
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
@ -848,12 +880,12 @@ export class GameScene extends DirtyScene {
message = "Press SPACE or touch here to enter Jitsi Meet room"; message = "Press SPACE or touch here to enter Jitsi Meet room";
} }
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
type: "jitsiRoom", uuid: "jitsi",
type: "message",
message: message, message: message,
callback: () => openJitsiRoomFunction(), callback: () => openJitsiRoomFunction(),
userInputManager: this.userInputManager, userInputManager: this.userInputManager,
}); });
layoutManagerVisibilityStore.set(true);
} else { } else {
openJitsiRoomFunction(); openJitsiRoomFunction();
} }
@ -870,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) => {
@ -909,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) {
@ -1149,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(
@ -1216,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;
@ -1257,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();
@ -1271,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();
@ -1343,7 +1420,7 @@ ${escapedMessage}
try { try {
const room = await Room.createRoom(exitRoomPath); const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room); return gameManager.loadMap(room);
} 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);
} }
} }

View 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;
}
}

View 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();

View File

@ -1,15 +1,14 @@
import { writable } from "svelte/store"; import { derived, writable } from "svelte/store";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
export interface LayoutManagerAction { export interface LayoutManagerAction {
type: string; uuid: string;
type: "warning" | "message";
message: string | number | boolean | undefined; message: string | number | boolean | undefined;
callback: () => void; callback: () => void;
userInputManager: UserInputManager | undefined; userInputManager: UserInputManager | undefined;
} }
export const layoutManagerVisibilityStore = writable(false);
function createLayoutManagerAction() { function createLayoutManagerAction() {
const { subscribe, set, update } = writable<LayoutManagerAction[]>([]); const { subscribe, set, update } = writable<LayoutManagerAction[]>([]);
@ -18,26 +17,26 @@ function createLayoutManagerAction() {
addAction: (newAction: LayoutManagerAction): void => { addAction: (newAction: LayoutManagerAction): void => {
update((list: LayoutManagerAction[]) => { update((list: LayoutManagerAction[]) => {
let found = false; let found = false;
for (const actions of list) { for (const action of list) {
if (actions.type === newAction.type && actions.message === newAction.message) { if (action.uuid === newAction.uuid) {
found = true; found = true;
} }
} }
if (!found) { if (!found) {
list.push(newAction); list.push(newAction);
newAction.userInputManager?.addSpaceEventListner(newAction.callback);
} }
return list; return list;
}); });
}, },
removeAction: (oldAction: LayoutManagerAction): void => { removeAction: (uuid: string): void => {
update((list: LayoutManagerAction[]) => { update((list: LayoutManagerAction[]) => {
const index = list.findIndex( const index = list.findIndex((action) => action.uuid === uuid);
(actions) => actions.type === oldAction.type && actions.message === oldAction.message
);
if (index !== -1) { if (index !== -1) {
list[index].userInputManager?.removeSpaceEventListner(list[index].callback);
list.splice(index, 1); list.splice(index, 1);
} }
@ -51,3 +50,7 @@ function createLayoutManagerAction() {
} }
export const layoutManagerActionStore = createLayoutManagerAction(); export const layoutManagerActionStore = createLayoutManagerAction();
export const layoutManagerVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => {
return !!$layoutManagerActionStore.length;
});

View File

@ -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();

View File

@ -2,6 +2,7 @@ import {HtmlUtils} from "./HtmlUtils";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { iframeListener } from "../Api/IframeListener"; import { iframeListener } from "../Api/IframeListener";
import { touchScreenManager } from "../Touch/TouchScreenManager"; import { touchScreenManager } from "../Touch/TouchScreenManager";
import { waScaleManager } from "../Phaser/Services/WaScaleManager";
enum iframeStates { enum iframeStates {
closed = 1, closed = 1,
@ -9,13 +10,13 @@ enum iframeStates {
opened, opened,
} }
const cowebsiteDivId = 'cowebsite'; // the id of the whole container. const cowebsiteDivId = "cowebsite"; // the id of the whole container.
const cowebsiteMainDomId = 'cowebsite-main'; // the id of the parent div of the iframe. const cowebsiteMainDomId = "cowebsite-main"; // the id of the parent div of the iframe.
const cowebsiteAsideDomId = 'cowebsite-aside'; // the id of the parent div of the iframe. const cowebsiteAsideDomId = "cowebsite-aside"; // the id of the parent div of the iframe.
export const cowebsiteCloseButtonId = 'cowebsite-close'; export const cowebsiteCloseButtonId = "cowebsite-close";
const cowebsiteFullScreenButtonId = 'cowebsite-fullscreen'; const cowebsiteFullScreenButtonId = "cowebsite-fullscreen";
const cowebsiteOpenFullScreenImageId = 'cowebsite-fullscreen-open'; const cowebsiteOpenFullScreenImageId = "cowebsite-fullscreen-open";
const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close'; const cowebsiteCloseFullScreenImageId = "cowebsite-fullscreen-close";
const animationTime = 500; //time used by the css transitions, in ms. const animationTime = 500; //time used by the css transitions, in ms.
interface TouchMoveCoordinates { interface TouchMoveCoordinates {
@ -24,7 +25,6 @@ interface TouchMoveCoordinates {
} }
class CoWebsiteManager { class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed; private opened: iframeStates = iframeStates.closed;
private _onResize: Subject<void> = new Subject(); private _onResize: Subject<void> = new Subject();
@ -45,7 +45,7 @@ class CoWebsiteManager {
} }
set width(width: number) { set width(width: number) {
this.cowebsiteDiv.style.width = width+'px'; this.cowebsiteDiv.style.width = width + "px";
} }
get height(): number { get height(): number {
@ -53,7 +53,7 @@ class CoWebsiteManager {
} }
set height(height: number) { set height(height: number) {
this.cowebsiteDiv.style.height = height+'px'; this.cowebsiteDiv.style.height = height + "px";
} }
get verticalMode(): boolean { get verticalMode(): boolean {
@ -75,13 +75,13 @@ class CoWebsiteManager {
this.initResizeListeners(false); this.initResizeListeners(false);
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
buttonCloseFrame.addEventListener('click', () => { buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur(); buttonCloseFrame.blur();
this.closeCoWebsite(); this.closeCoWebsite();
}); });
const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId); const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
buttonFullScreenFrame.addEventListener('click', () => { buttonFullScreenFrame.addEventListener("click", () => {
buttonFullScreenFrame.blur(); buttonFullScreenFrame.blur();
this.fullscreen(); this.fullscreen();
}); });
@ -90,7 +90,7 @@ class CoWebsiteManager {
private initResizeListeners(touchMode: boolean) { private initResizeListeners(touchMode: boolean) {
const movecallback = (event: MouseEvent | TouchEvent) => { const movecallback = (event: MouseEvent | TouchEvent) => {
let x, y; let x, y;
if (event.type === 'mousemove') { if (event.type === "mousemove") {
x = (event as MouseEvent).movementX / this.getDevicePixelRatio(); x = (event as MouseEvent).movementX / this.getDevicePixelRatio();
y = (event as MouseEvent).movementY / this.getDevicePixelRatio(); y = (event as MouseEvent).movementY / this.getDevicePixelRatio();
} else { } else {
@ -102,29 +102,28 @@ class CoWebsiteManager {
y = last.y - previous.y; y = last.y - previous.y;
} }
this.verticalMode ? (this.height += y) : (this.width -= x);
this.verticalMode ? this.height += y : this.width -= x;
this.fire(); this.fire();
} };
this.cowebsiteAsideDom.addEventListener( touchMode ? 'touchstart' : 'mousedown', (event) => { this.cowebsiteAsideDom.addEventListener(touchMode ? "touchstart" : "mousedown", (event) => {
this.resizing = true; this.resizing = true;
this.getIframeDom().style.display = 'none'; this.getIframeDom().style.display = "none";
if (touchMode) { if (touchMode) {
const touchEvent = (event as TouchEvent).touches[0]; const touchEvent = (event as TouchEvent).touches[0];
this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY }; this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY };
} }
document.addEventListener(touchMode ? 'touchmove' : 'mousemove', movecallback); document.addEventListener(touchMode ? "touchmove" : "mousemove", movecallback);
}); });
document.addEventListener(touchMode ? 'touchend' : 'mouseup', (event) => { document.addEventListener(touchMode ? "touchend" : "mouseup", (event) => {
if (!this.resizing) return; if (!this.resizing) return;
if (touchMode) { if (touchMode) {
this.previousTouchMoveCoordinates = null; this.previousTouchMoveCoordinates = null;
} }
document.removeEventListener(touchMode ? 'touchmove' : 'mousemove', movecallback); document.removeEventListener(touchMode ? "touchmove" : "mousemove", movecallback);
this.getIframeDom().style.display = 'block'; this.getIframeDom().style.display = "block";
this.resizing = false; this.resizing = false;
}); });
} }
@ -132,34 +131,34 @@ class CoWebsiteManager {
private getDevicePixelRatio(): number { private getDevicePixelRatio(): number {
//on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels //on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels
//so on chrome-based browser we need to adjust using 'devicePixelRatio' //so on chrome-based browser we need to adjust using 'devicePixelRatio'
return window.navigator.userAgent.includes('Firefox') ? 1 : window.devicePixelRatio; return window.navigator.userAgent.includes("Firefox") ? 1 : window.devicePixelRatio;
} }
private close(): void { private close(): void {
this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.remove("loaded"); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('hidden'); this.cowebsiteDiv.classList.add("hidden");
this.opened = iframeStates.closed; this.opened = iframeStates.closed;
this.resetStyle(); this.resetStyle();
} }
private load(): void { private load(): void {
this.cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.remove("hidden"); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('loading'); this.cowebsiteDiv.classList.add("loading");
this.opened = iframeStates.loading; this.opened = iframeStates.loading;
} }
private open(): void { private open(): void {
this.cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.remove("loading", "hidden"); //edit the css class to trigger the transition
this.opened = iframeStates.opened; this.opened = iframeStates.opened;
this.resetStyle(); this.resetStyle();
} }
public resetStyle() { public resetStyle() {
this.cowebsiteDiv.style.width = ''; this.cowebsiteDiv.style.width = "";
this.cowebsiteDiv.style.height = ''; this.cowebsiteDiv.style.height = "";
} }
private getIframeDom(): HTMLIFrameElement { private getIframeDom(): HTMLIFrameElement {
const iframe = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId).querySelector('iframe'); const iframe = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId).querySelector("iframe");
if (!iframe) throw new Error('Could not find iframe!'); if (!iframe) throw new Error("Could not find iframe!");
return iframe; return iframe;
} }
@ -167,9 +166,9 @@ class CoWebsiteManager {
this.load(); this.load();
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = 'cowebsite-iframe'; iframe.id = "cowebsite-iframe";
iframe.src = (new URL(url, base)).toString(); iframe.src = new URL(url, base).toString();
if (allowPolicy) { if (allowPolicy) {
iframe.allow = allowPolicy; iframe.allow = allowPolicy;
} }
@ -183,14 +182,17 @@ class CoWebsiteManager {
const onTimeoutPromise = new Promise<void>((resolve) => { const onTimeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 2000); setTimeout(() => resolve(), 2000);
}); });
this.currentOperationPromise = this.currentOperationPromise.then(() =>Promise.race([onloadPromise, onTimeoutPromise])).then(() => { this.currentOperationPromise = this.currentOperationPromise
.then(() => Promise.race([onloadPromise, onTimeoutPromise]))
.then(() => {
this.open(); this.open();
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime) }, animationTime);
}).catch((err) => { })
console.error('Error loadCoWebsite => ', err); .catch((err) => {
this.closeCoWebsite() console.error("Error loadCoWebsite => ", err);
this.closeCoWebsite();
}); });
} }
@ -200,56 +202,63 @@ class CoWebsiteManager {
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void { public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
this.load(); this.load();
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteMainDom)).then(() => { this.currentOperationPromise = this.currentOperationPromise
.then(() => callback(this.cowebsiteMainDom))
.then(() => {
this.open(); this.open();
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime); }, animationTime);
}).catch((err) => { })
console.error('Error insertCoWebsite => ', err); .catch((err) => {
console.error("Error insertCoWebsite => ", err);
this.closeCoWebsite(); this.closeCoWebsite();
}); });
} }
public closeCoWebsite(): Promise<void> { public closeCoWebsite(): Promise<void> {
this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => { this.currentOperationPromise = this.currentOperationPromise.then(
() =>
new Promise((resolve, reject) => {
if (this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example if (this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close(); this.close();
this.fire(); this.fire();
const iframe = this.cowebsiteDiv.querySelector('iframe'); const iframe = this.cowebsiteDiv.querySelector("iframe");
if (iframe) { if (iframe) {
iframeListener.unregisterIframe(iframe); iframeListener.unregisterIframe(iframe);
} }
setTimeout(() => { setTimeout(() => {
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
resolve(); resolve();
}, animationTime) }, animationTime);
})); })
);
return this.currentOperationPromise; return this.currentOperationPromise;
} }
public getGameSize(): {width: number, height: number} { public getGameSize(): { width: number; height: number } {
if (this.opened !== iframeStates.opened) { if (this.opened !== iframeStates.opened) {
return { return {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight height: window.innerHeight,
} };
} }
if (!this.verticalMode) { if (!this.verticalMode) {
return { return {
width: window.innerWidth - this.width, width: window.innerWidth - this.width,
height: window.innerHeight height: window.innerHeight,
} };
} else { } else {
return { return {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight - this.height, height: window.innerHeight - this.height,
} };
} }
} }
private fire(): void { private fire(): void {
this._onResize.next(); this._onResize.next();
waScaleManager.applyNewSize();
} }
private fullscreen(): void { private fullscreen(): void {
@ -257,13 +266,13 @@ class CoWebsiteManager {
this.resetStyle(); this.resetStyle();
this.fire(); this.fire();
//we don't trigger a resize of the phaser game since it won't be visible anyway. //we don't trigger a resize of the phaser game since it won't be visible anyway.
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = 'inline'; HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = "inline";
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = 'none'; HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = "none";
} else { } else {
this.verticalMode ? this.height = window.innerHeight : this.width = window.innerWidth; this.verticalMode ? (this.height = window.innerHeight) : (this.width = window.innerWidth);
//we don't trigger a resize of the phaser game since it won't be visible anyway. //we don't trigger a resize of the phaser game since it won't be visible anyway.
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = 'none'; HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = "none";
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = 'inline'; HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = "inline";
} }
} }
} }

View File

@ -26,15 +26,18 @@ export class MediaManager {
if (result.type === "error") { if (result.type === "error") {
console.error(result.error); console.error(result.error);
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: "cameraAccessDenied",
type: "warning", type: "warning",
message: "Camera access denied. Click here and check your browser permissions.", message: "Camera access denied. Click here and check your browser permissions.",
callback: () => { callback: () => {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
layoutManagerVisibilityStore.set(false);
}, },
userInputManager: this.userInputManager, userInputManager: this.userInputManager,
}); });
layoutManagerVisibilityStore.set(true); //remove it after 10 sec
setTimeout(() => {
layoutManagerActionStore.removeAction("cameraAccessDenied");
}, 10000);
return; return;
} }
}); });
@ -43,15 +46,18 @@ export class MediaManager {
if (result.type === "error") { if (result.type === "error") {
console.error(result.error); console.error(result.error);
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: "screenSharingAccessDenied",
type: "warning", type: "warning",
message: "Screen sharing denied. Click here and check your browser permissions.", message: "Screen sharing denied. Click here and check your browser permissions.",
callback: () => { callback: () => {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
layoutManagerVisibilityStore.set(false);
}, },
userInputManager: this.userInputManager, userInputManager: this.userInputManager,
}); });
layoutManagerVisibilityStore.set(true); //remove it after 10 sec
setTimeout(() => {
layoutManagerActionStore.removeAction("screenSharingAccessDenied");
}, 10000);
return; return;
} }
}); });

View File

@ -384,6 +384,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

View 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>

View 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
}

View 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>

View 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
}

View 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();
});
});

View 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
}

View File

@ -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>

View File

@ -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
View 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
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"devDependencies": { "devDependencies": {
"husky": "^6.0.0" "husky": "^7.0.1"
}, },
"scripts": { "scripts": {
"prepare": "husky install" "prepare": "husky install"

View File

@ -11,7 +11,8 @@ export const tokenInvalidException = "tokenInvalid";
class JWTTokenManager { class JWTTokenManager {
public createAuthToken(identifier: string) { public createAuthToken(identifier: string) {
return Jwt.sign({ identifier }, SECRET_KEY, { expiresIn: "3d" }); //TODO fix me 200d when ory authentication will be available
return Jwt.sign({ identifier }, SECRET_KEY, { expiresIn: "200d" });
} }
public decodeJWTToken(token: string): AuthTokenData { public decodeJWTToken(token: string): AuthTokenData {

View File

@ -307,7 +307,7 @@ export class SocketManager implements ZoneEventListener {
reportPlayerMessage.getReporteduseruuid(), reportPlayerMessage.getReporteduseruuid(),
reportPlayerMessage.getReportcomment(), reportPlayerMessage.getReportcomment(),
client.userUuid, client.userUuid,
client.roomId.split("/")[2] client.roomId
); );
} catch (e) { } catch (e) {
console.error('An error occurred on "handleReportMessage"'); console.error('An error occurred on "handleReportMessage"');