Merge pull request #1080 from thecodingmachine/metadataScriptingApi

Customize metadata with scripting api
This commit is contained in:
David Négrier 2021-06-23 15:57:14 +02:00 committed by GitHub
commit f4209515a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 11371 additions and 605 deletions

View file

@ -7,6 +7,14 @@
- Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 - Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219
- Migrated the admin console to Svelte, and redesigned the console #1211 - Migrated the admin console to Svelte, and redesigned the console #1211
- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1) - Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1)
- New scripting API features :
- Use `WA.room.showLayer(): void` to show a layer
- Use `WA.room.hideLayer(): void` to hide a layer
- Use `WA.room.setProperty() : void` to add or change existing property of a layer
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
- Use `WA.room.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
## Version 1.4.1 ## Version 1.4.1
@ -50,6 +58,7 @@
- New scripting API features: - New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound - Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes ### Bug Fixes
- Pinch gesture does no longer move the character - Pinch gesture does no longer move the character

21
docs/maps/api-player.md Normal file
View file

@ -0,0 +1,21 @@
{.section-title.accent.text-primary}
# API Player functions Reference
### Listen to player movement
```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
```
Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction.
The event has the following attributes :
* **moving (boolean):** **true** when the current player is moving, **false** otherwise.
* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving.
* **x (number):** coordinate X of the current player.
* **y (number):** coordinate Y of the current player.
**callback:** the function that will be called when the current player is moving. It contains the event.
Example :
```javascript
WA.player.onPlayerMove(console.log);
```

View file

@ -4,8 +4,9 @@
- [Navigation functions](api-nav.md) - [Navigation functions](api-nav.md)
- [Chat functions](api-chat.md) - [Chat functions](api-chat.md)
- [Room functions](api-room.md) - [Room functions](api-room.md)
- [Player functions](api-player.md)
- [UI functions](api-ui.md) - [UI functions](api-ui.md)
- [Sound functions](api-sound.md) - [Sound functions](api-sound.md)
- [Controls functions](api-controls.md) - [Controls functions](api-controls.md)
- [List of deprecated functions](api-deprecated.md) - [List of deprecated functions](api-deprecated.md)

View file

@ -1,6 +1,22 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Room functions Reference # API Room functions Reference
### Working with group layers
If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together.
Example :
<div class="row">
<div class="col">
<img src="https://workadventu.re/img/docs/groupLayer.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
The name of the layers of this map are :
* `entries/start`
* `bottom/ground/under`
* `bottom/build/carpet`
* `wall`
### Detecting when the user enters/leaves a zone ### Detecting when the user enters/leaves a zone
``` ```
@ -31,3 +47,68 @@ WA.room.onLeaveZone('myZone', () => {
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
}) })
``` ```
### Show / Hide a layer
```
WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void
```
These 2 methods can be used to show and hide a layer.
Example :
```javascript
WA.room.showLayer('bottom');
//...
WA.room.hideLayer('bottom');
```
### Set/Create properties in a layer
```
WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void;
```
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
Example :
```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
```
### Getting information on the current room
```
WA.room.getCurrentRoom(): Promise<Room>
```
Return a promise that resolves to a `Room` object with the following attributes :
* **id (string) :** ID of the current room
* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called.
* **mapUrl (string) :** Url of the JSON map file
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer
Example :
```javascript
WA.room.getCurrentRoom((room) => {
if (room.id === '42') {
console.log(room.map);
window.open(room.mapUrl, '_blank');
}
})
```
### Getting information on the current user
```
WA.player.getCurrentUser(): Promise<User>
```
Return a promise that resolves to a `User` object with the following attributes :
* **id (string) :** ID of the current user
* **nickName (string) :** name displayed above the current user
* **tags (string[]) :** list of all the tags of the current user
Example :
```javascript
WA.room.getCurrentUser().then((user) => {
if (user.nickName === 'ABC') {
console.log(user.tags);
}
})
```

View file

@ -65,3 +65,25 @@ WA.room.onLeaveZone('myZone', () => {
helloWorldPopup.close(); helloWorldPopup.close();
}); });
``` ```
### Add custom menu
```typescript
WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void
```
Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`.
Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script.
Example:
```javascript
WA.ui.registerMenuCommand("test", () => {
WA.chat.sendChatMessage("test clicked", "menu cmd")
})
```
<div class="col">
<img src="https://workadventu.re/img/docs/menu-command.png" class="figure-img img-fluid rounded" alt="" />
</div>

View file

@ -101,7 +101,7 @@ You can now start by testing this with a simple message sent to the chat.
```html ```html
... ...
<script> <script>
WA.sendChatMessage('Hello world', 'Mr Robot'); WA.chat.sendChatMessage('Hello world', 'Mr Robot');
</script> </script>
... ...
``` ```

8393
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isDataLayerEvent =
new tg.IsInterface().withProperties({
data: tg.isObject
}).get();
/**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
*/
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;

View file

@ -0,0 +1,15 @@
import * as tg from "generic-type-guard";
export const isGameStateEvent =
new tg.IsInterface().withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags : tg.isArray(tg.isString),
}).get();
/**
* A message sent from the game to the iFrame when the gameState is received by the script
*/
export type GameStateEvent = tg.GuardedType<typeof isGameStateEvent>;

View file

@ -0,0 +1,19 @@
import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent =
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
moving: tg.isBoolean,
x: tg.isNumber,
y: tg.isNumber
}).get();
/**
* A message sent from the game to the iFrame to notify a movement from the current player.
*/
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void

View file

@ -1,5 +1,5 @@
import type { GameStateEvent } from './GameStateEvent';
import type { ButtonClickedEvent } from './ButtonClickedEvent'; import type { ButtonClickedEvent } from './ButtonClickedEvent';
import type { ChatEvent } from './ChatEvent'; import type { ChatEvent } from './ChatEvent';
import type { ClosePopupEvent } from './ClosePopupEvent'; import type { ClosePopupEvent } from './ClosePopupEvent';
@ -10,8 +10,14 @@ 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 { LoadSoundEvent} from "./LoadSoundEvent"; import type { DataLayerEvent } from "./DataLayerEvent";
import type {PlaySoundEvent} from "./PlaySoundEvent"; import type { LayerEvent } from './LayerEvent';
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent';
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
@ -33,9 +39,16 @@ export type IframeEventMap = {
restorePlayerControls: null restorePlayerControls: null
displayBubble: null displayBubble: null
removeBubble: null removeBubble: null
onPlayerMove: undefined
showLayer: LayerEvent
hideLayer: LayerEvent
setProperty: SetPropertyEvent
getDataLayer: undefined
loadSound: LoadSoundEvent loadSound: LoadSoundEvent
playSound: PlaySoundEvent playSound: PlaySoundEvent
stopSound: null, stopSound: null,
getState: undefined,
registerMenuCommand: MenuItemRegisterEvent
} }
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
@ -51,7 +64,10 @@ export interface IframeResponseEventMap {
enterEvent: EnterLeaveEvent enterEvent: EnterLeaveEvent
leaveEvent: EnterLeaveEvent leaveEvent: EnterLeaveEvent
buttonClickedEvent: ButtonClickedEvent buttonClickedEvent: ButtonClickedEvent
// gameState: GameStateEvent gameState: GameStateEvent
hasPlayerMoved: HasPlayerMovedEvent
dataLayer: DataLayerEvent
menuItemClicked: MenuItemClickedEvent
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;

View file

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

View file

@ -10,4 +10,4 @@ export const isLoadPageEvent =
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.
*/ */
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>; export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;

View file

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

View file

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

View file

@ -0,0 +1,25 @@
import * as tg from "generic-type-guard";
import { Subject } from 'rxjs';
export const isMenuItemRegisterEvent =
new tg.IsInterface().withProperties({
menutItem: tg.isString
}).get();
/**
* A message sent from the iFrame to the game to add a new menu item.
*/
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent =
new tg.IsInterface().withProperties({
type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent
}).get();
const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem)
}

View file

@ -1,26 +1,42 @@
import {Subject} from "rxjs";
import { Subject } from "rxjs"; 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"; import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils";
import { scriptUtils } from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import {
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; IframeEvent,
import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; IframeEventMap,
import { isLoadPageEvent } from './Events/LoadPageEvent'; IframeResponseEvent,
IframeResponseEventMap,
isIframeEventWrapper,
TypedMessageEvent
} from "./Events/IframeEvent";
import type {UserInputChatEvent} from "./Events/UserInputChatEvent";
//import { isLoadPageEvent } from './Events/LoadPageEvent';
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent";
import {isLayerEvent, LayerEvent} from "./Events/LayerEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent";
import type {GameStateEvent} from "./Events/GameStateEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent";
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
*/ */
class IframeListener { class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject(); private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable(); public readonly chatStream = this._chatStream.asObservable();
@ -33,7 +49,6 @@ class IframeListener {
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject(); private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable(); public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _loadPageStream: Subject<string> = new Subject(); private readonly _loadPageStream: Subject<string> = new Subject();
public readonly loadPageStream = this._loadPageStream.asObservable(); public readonly loadPageStream = this._loadPageStream.asObservable();
@ -58,6 +73,27 @@ class IframeListener {
private readonly _removeBubbleStream: Subject<void> = new Subject(); private readonly _removeBubbleStream: Subject<void> = new Subject();
public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
private readonly _showLayerStream: Subject<LayerEvent> = new Subject();
public readonly showLayerStream = this._showLayerStream.asObservable();
private readonly _hideLayerStream: Subject<LayerEvent> = new Subject();
public readonly hideLayerStream = this._hideLayerStream.asObservable();
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
private readonly _unregisterMenuCommandStream: Subject<string> = new Subject();
public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable();
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject(); private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable(); public readonly playSoundStream = this._playSoundStream.asObservable();
@ -68,7 +104,9 @@ class IframeListener {
public readonly loadSoundStream = this._loadSoundStream.asObservable(); public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
init() { init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => { window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
@ -77,26 +115,27 @@ class IframeListener {
// 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).
let foundSrc: string | undefined; let foundSrc: string | undefined;
foundSrc = [...this.scripts.keys()].find(key => { let iframe: HTMLIFrameElement;
return this.scripts.get(key)?.contentWindow == message.source for (iframe of this.iframes) {
}); if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
if (foundSrc === undefined) { if (foundSrc === undefined) {
for (const iframe of this.iframes) { return;
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
if (foundSrc === undefined) {
return;
}
} }
const payload = message.data; const payload = message.data;
if (isIframeEventWrapper(payload)) { if (isIframeEventWrapper(payload)) {
if (payload.type === 'chat' && isChatEvent(payload.data)) { if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data); this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data); this._openPopupStream.next(payload.data);
@ -109,6 +148,9 @@ class IframeListener {
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url); scriptUtils.goToPage(payload.data.url);
} }
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
}
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) { else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data); this._playSoundStream.next(payload.data);
} }
@ -131,30 +173,56 @@ class IframeListener {
} }
else if (payload.type === 'restorePlayerControls') { else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next(); this._enablePlayerControlStream.next();
} } else if (payload.type === 'displayBubble') {
else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next(); this._displayBubbleStream.next();
} } else if (payload.type === 'removeBubble') {
else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
}else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ } else if (payload.type == "getState") {
this._loadPageStream.next(payload.data.url); this._gameStateStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
})
handleMenuItemRegistrationEvent(payload.data)
} }
} }
}, false); }, false);
} }
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
'type' : 'dataLayer',
'data' : dataLayerEvent
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
});
}
/** /**
* Allows the passed iFrame to send/receive messages via the API. * Allows the passed iFrame to send/receive messages via the API.
*/ */
registerIframe(iframe: HTMLIFrameElement): void { registerIframe(iframe: HTMLIFrameElement): void {
this.iframes.add(iframe); this.iframes.add(iframe);
this.iframeCloseCallbacks.set(iframe, []);
} }
unregisterIframe(iframe: HTMLIFrameElement): void { unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => {
callback();
});
this.iframes.delete(iframe); this.iframes.delete(iframe);
} }
@ -164,7 +232,7 @@ class IframeListener {
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
// Using external iframe mode ( // Using external iframe mode (
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = 'none';
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
@ -179,25 +247,24 @@ class IframeListener {
} else { } else {
// production code // production code
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = 'none';
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation'); iframe.sandbox.add('allow-top-navigation-by-user-activation');
const html = '<!doctype html>\n' + //iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' +
'\n' + '\n' +
'<html lang="en">\n' + '<html lang="en">\n' +
'<head>\n' + '<head>\n' +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' + '<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
'<script src="' + scriptUrl + '" ></script>\n' + '<script src="' + scriptUrl + '" ></script>\n' +
'<title></title>\n' +
'</head>\n' + '</head>\n' +
'</html>\n'; '</html>\n';
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = html;
document.body.prepend(iframe); document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe); this.scripts.set(scriptUrl, iframe);
@ -207,12 +274,12 @@ class IframeListener {
} }
private getIFrameId(scriptUrl: string): string { private static getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl); return 'script' + btoa(scriptUrl);
} }
unregisterScript(scriptUrl: string): void { unregisterScript(scriptUrl: string): void {
const iFrameId = this.getIFrameId(scriptUrl); const iFrameId = IframeListener.getIFrameId(scriptUrl);
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId); const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
if (!iframe) { if (!iframe) {
throw new Error('Unknown iframe for script "' + scriptUrl + '"'); throw new Error('Unknown iframe for script "' + scriptUrl + '"');
@ -250,6 +317,15 @@ class IframeListener {
}); });
} }
hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) {
this.postMessage({
'type': 'hasPlayerMoved',
'data': event
});
}
}
sendButtonClickedEvent(popupId: number, buttonId: number): void { sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({ this.postMessage({
'type': 'buttonClickedEvent', 'type': 'buttonClickedEvent',
@ -263,7 +339,7 @@ class IframeListener {
/** /**
* Sends the message... to all allowed iframes. * Sends the message... to all allowed iframes.
*/ */
private postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) { public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*'); iframe.contentWindow?.postMessage(message, '*');
} }

View file

@ -0,0 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent';
import { iframeListener } from '../../IframeListener';
export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({
'type': 'menuItemClicked',
'data': {
menuItem: menuItem,
} as MenuItemClickedEvent
});
}

View file

@ -23,7 +23,7 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
data: { data: {
'message': message, 'message': message,
'author': author 'author': author
} as ChatEvent }
}) })
} }

View file

@ -2,6 +2,7 @@ import type { GoToPageEvent } from '../Events/GoToPageEvent';
import type { OpenTabEvent } from '../Events/OpenTabEvent'; import type { OpenTabEvent } from '../Events/OpenTabEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent"; import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
import type {LoadPageEvent} from "../Events/LoadPageEvent";
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> { class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
@ -13,7 +14,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution<Workadventur
"type": 'openTab', "type": 'openTab',
"data": { "data": {
url url
} as OpenTabEvent }
}); });
} }
@ -22,7 +23,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution<Workadventur
"type": 'goToPage', "type": 'goToPage',
"data": { "data": {
url url
} as GoToPageEvent }
}); });
} }
@ -40,7 +41,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution<Workadventur
"type": 'openCoWebSite', "type": 'openCoWebSite',
"data": { "data": {
url url
} as OpenCoWebSiteEvent }
}); });
} }

View file

@ -0,0 +1,29 @@
import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution";
import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent";
import {Subject} from "rxjs";
import {apiCallback} from "./registeredCallbacks";
import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent";
const moveStream = new Subject<HasPlayerMovedEvent>();
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [
apiCallback({
type: 'hasPlayerMoved',
typeChecker: isHasPlayerMovedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
}
}),
]
onPlayerMove(callback: HasPlayerMovedEventCallback): void {
moveStream.subscribe(callback);
sendToWorkadventure({
type: 'onPlayerMove',
data: null
})
}
}
export default new WorkadventurePlayerCommands();

View file

@ -1,10 +1,54 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent'; import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
import { IframeApiContribution } from './IframeApiContribution'; import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution';
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type {LayerEvent} from "../Events/LayerEvent";
import type {SetPropertyEvent} from "../Events/setPropertyEvent";
import type {GameStateEvent} from "../Events/GameStateEvent";
import type {ITiledMap} from "../../Phaser/Map/ITiledMap";
import type {DataLayerEvent} from "../Events/DataLayerEvent";
import {isGameStateEvent} from "../Events/GameStateEvent";
import {isDataLayerEvent} from "../Events/DataLayerEvent";
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>>();
const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>();
let immutableData: GameStateEvent;
interface Room {
id: string,
mapUrl: string,
map: ITiledMap,
startLayer: string | null
}
interface User {
id: string | undefined,
nickName: string | null,
tags: string[]
}
function getGameState(): Promise<GameStateEvent> {
if (immutableData) {
return Promise.resolve(immutableData);
}
else {
return new Promise<GameStateEvent>((resolver, thrower) => {
stateResolvers.subscribe(resolver);
sendToWorkadventure({type: "getState", data: null});
})
}
}
function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver);
sendToWorkadventure({type: "getDataLayer", data: null})
})
}
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> { class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [ callbacks = [
@ -21,8 +65,21 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
callback: (payloadData) => { callback: (payloadData) => {
leaveStreams.get(payloadData.name)?.next(); leaveStreams.get(payloadData.name)?.next();
} }
}) }),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
}
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
}
}),
] ]
@ -43,6 +100,34 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
} }
subject.subscribe(callback); subject.subscribe(callback);
} }
showLayer(layerName: string): void {
sendToWorkadventure({type: 'showLayer', data: {'name': layerName}});
}
hideLayer(layerName: string): void {
sendToWorkadventure({type: 'hideLayer', data: {'name': layerName}});
}
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
sendToWorkadventure({
type: 'setProperty',
data: {
'layerName': layerName,
'propertyName': propertyName,
'propertyValue': propertyValue,
}
})
}
getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => {
return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName};
})
})
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags};
})
}
} }

View file

@ -1,14 +1,17 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
import type { ClosePopupEvent } from '../Events/ClosePopupEvent'; 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 {Popup} from "./Ui/Popup"; import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor"; import { Popup } from "./Ui/Popup";
let popupId = 0; let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>(); const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>(); const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
const menuCallbacks: Map<string, (command: string) => void> = new Map()
interface ZonedPopupOptions { interface ZonedPopupOptions {
zone: string zone: string
objectLayerName?: string, objectLayerName?: string,
@ -33,6 +36,16 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
callback(popup); callback(popup);
} }
} }
}),
apiCallback({
type: "menuItemClicked",
typeChecker: isMenuItemClickedEvent,
callback: event => {
const callback = menuCallbacks.get(event.menuItem);
if (callback) {
callback(event.menuItem)
}
}
})]; })];
@ -71,6 +84,16 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
return popup; return popup;
} }
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
'type': 'registerMenuCommand',
'data': {
menutItem: commandDescriptor
}
});
}
displayBubble(): void { displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null }); sendToWorkadventure({ 'type': 'displayBubble', data: null });
} }

View file

@ -1,4 +1,4 @@
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import { PUSHER_URL, UPLOADER_URL } from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import { import {
BatchMessage, BatchMessage,
@ -33,9 +33,9 @@ import {
BanUserMessage, BanUserMessage,
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb"
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {ProtobufClientUtils} from "../Network/ProtobufClientUtils"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
import { import {
EventMessage, EventMessage,
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface,
@ -44,24 +44,24 @@ import {
ViewportInterface, WebRtcDisconnectMessageInterface, ViewportInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels"; } from "./ConnexionModels";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import {adminMessagesService} from "./AdminMessagesService"; import { adminMessagesService } from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream"; import { worldFullMessageStream } from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream"; import { worldFullWarningStream } from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager"; import { connectionManager } from "./ConnectionManager";
import {emoteEventStream} from "./EmoteEventStream"; import { emoteEventStream } from "./EmoteEventStream";
const manualPingDelay = 20000; const manualPingDelay = 20000;
export class RoomConnection implements RoomConnection { export class RoomConnection implements RoomConnection {
private readonly socket: WebSocket; private readonly socket: WebSocket;
private userId: number|null = null; private userId: number | null = null;
private listeners: Map<string, Function[]> = new Map<string, Function[]>(); private listeners: Map<string, Function[]> = new Map<string, Function[]>();
private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
private closed: boolean = false; private closed: boolean = false;
private tags: string[] = []; private tags: string[] = [];
public static setWebsocketFactory(websocketFactory: (url: string)=>any): void { // eslint-disable-line @typescript-eslint/no-explicit-any public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
} }
@ -70,28 +70,27 @@ export class RoomConnection implements RoomConnection {
* @param token A JWT token containing the UUID of the user * @param token A JWT token containing the UUID of the user
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
*/ */
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null) { public constructor(token: string | null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string | null) {
let url = new URL(PUSHER_URL, window.location.toString()).toString(); let url = new URL(PUSHER_URL, window.location.toString()).toString();
url = url.replace('http://', 'ws://').replace('https://', 'wss://'); url = url.replace('http://', 'ws://').replace('https://', 'wss://');
if (!url.endsWith('/')) { if (!url.endsWith('/')) {
url += '/'; url += '/';
} }
url += 'room'; url += 'room';
url += '?roomId='+(roomId ?encodeURIComponent(roomId):''); url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : '');
url += '&token='+(token ?encodeURIComponent(token):''); url += '&token=' + (token ? encodeURIComponent(token) : '');
url += '&name='+encodeURIComponent(name); url += '&name=' + encodeURIComponent(name);
for (const layer of characterLayers) { for (const layer of characterLayers) {
url += '&characterLayers='+encodeURIComponent(layer); url += '&characterLayers=' + encodeURIComponent(layer);
} }
url += '&x='+Math.floor(position.x); url += '&x=' + Math.floor(position.x);
url += '&y='+Math.floor(position.y); url += '&y=' + Math.floor(position.y);
url += '&top='+Math.floor(viewport.top); url += '&top=' + Math.floor(viewport.top);
url += '&bottom='+Math.floor(viewport.bottom); url += '&bottom=' + Math.floor(viewport.bottom);
url += '&left='+Math.floor(viewport.left); url += '&left=' + Math.floor(viewport.left);
url += '&right='+Math.floor(viewport.right); url += '&right=' + Math.floor(viewport.right);
if (typeof companion === 'string') { if (typeof companion === 'string') {
url += '&companion='+encodeURIComponent(companion); url += '&companion=' + encodeURIComponent(companion);
} }
if (RoomConnection.websocketFactory) { if (RoomConnection.websocketFactory) {
@ -102,7 +101,7 @@ export class RoomConnection implements RoomConnection {
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = 'arraybuffer';
let interval: ReturnType<typeof setInterval>|undefined = undefined; let interval: ReturnType<typeof setInterval> | undefined = undefined;
this.socket.onopen = (ev) => { this.socket.onopen = (ev) => {
//we manually ping every 20s to not be logged out by the server, even when the game is in background. //we manually ping every 20s to not be logged out by the server, even when the game is in background.
@ -161,7 +160,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRoomjoinedmessage()) { } else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
const items: { [itemId: number] : unknown } = {}; const items: { [itemId: number]: unknown } = {};
for (const item of roomJoinedMessage.getItemList()) { for (const item of roomJoinedMessage.getItemList()) {
items[item.getItemid()] = JSON.parse(item.getStatejson()); items[item.getItemid()] = JSON.parse(item.getStatejson());
} }
@ -181,7 +180,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWorldconnexionmessage()) { } else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true; this.closed = true;
}else if (message.hasWebrtcsignaltoclientmessage()) { } else if (message.hasWebrtcsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage()); this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage());
@ -238,7 +237,7 @@ export class RoomConnection implements RoomConnection {
this.closed = true; this.closed = true;
} }
private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage { private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessage {
const positionMessage = new PositionMessage(); const positionMessage = new PositionMessage();
positionMessage.setX(Math.floor(x)); positionMessage.setX(Math.floor(x));
positionMessage.setY(Math.floor(y)); positionMessage.setY(Math.floor(y));
@ -275,8 +274,8 @@ export class RoomConnection implements RoomConnection {
return viewportMessage; return viewportMessage;
} }
public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{ public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void {
if(!this.socket){ if (!this.socket) {
return; return;
} }
@ -481,7 +480,7 @@ export class RoomConnection implements RoomConnection {
if (this.closed === true || connectionManager.unloading) { if (this.closed === true || connectionManager.unloading) {
return; return;
} }
console.log('Socket closed with code '+event.code+". Reason: "+event.reason); console.log('Socket closed with code ' + event.code + ". Reason: " + event.reason);
if (event.code === 1000) { if (event.code === 1000) {
// Normal closure case // Normal closure case
return; return;
@ -527,8 +526,8 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public uploadAudio(file : FormData){ public uploadAudio(file: FormData) {
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: {data:{}}) => { return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: { data: {} }) => {
return res.data; return res.data;
}).catch((err) => { }).catch((err) => {
console.error(err); console.error(err);
@ -559,7 +558,7 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public emitGlobalMessage(message: PlayGlobalMessageInterface){ public emitGlobalMessage(message: PlayGlobalMessageInterface) {
const playGlobalMessage = new PlayGlobalMessage(); const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id); playGlobalMessage.setId(message.id);
playGlobalMessage.setType(message.type); playGlobalMessage.setType(message.type);
@ -571,7 +570,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public emitReportPlayerMessage(reportedUserId: number, reportComment: string ): void { public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void {
const reportPlayerMessage = new ReportPlayerMessage(); const reportPlayerMessage = new ReportPlayerMessage();
reportPlayerMessage.setReporteduserid(reportedUserId); reportPlayerMessage.setReporteduserid(reportedUserId);
reportPlayerMessage.setReportcomment(reportComment); reportPlayerMessage.setReportcomment(reportComment);
@ -582,7 +581,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string|undefined ): void { public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string | undefined): void {
const queryJitsiJwtMessage = new QueryJitsiJwtMessage(); const queryJitsiJwtMessage = new QueryJitsiJwtMessage();
queryJitsiJwtMessage.setJitsiroom(jitsiRoom); queryJitsiJwtMessage.setJitsiroom(jitsiRoom);
if (tag !== undefined) { if (tag !== undefined) {
@ -618,4 +617,8 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public getAllTags() : string[] {
return this.tags;
}
} }

View file

@ -37,6 +37,7 @@ export abstract class DirtyScene extends ResizableScene {
this.events.on(Events.RENDER, () => { this.events.on(Events.RENDER, () => {
this.objectListChanged = false; this.objectListChanged = false;
this.dirty = false;
}); });
this.physics.disableUpdate(); this.physics.disableUpdate();

View file

@ -10,12 +10,7 @@ import {get} from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore"; import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore"; import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
export interface HasMovedEvent {
direction: string;
moving: boolean;
x: number;
y: number;
}
/** /**
* This class should be responsible for any scene starting/stopping * This class should be responsible for any scene starting/stopping

View file

@ -1,5 +1,7 @@
import type { ITiledMap, ITiledMapLayerProperty } from "../Map/ITiledMap"; import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap";
import { LayersIterator } from "../Map/LayersIterator"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void;
@ -13,13 +15,22 @@ export class GameMap {
private callbacks = new Map<string, Array<PropertyChangeCallback>>(); private callbacks = new Map<string, Array<PropertyChangeCallback>>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {} private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {}
public readonly layersIterator: LayersIterator; public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = [];
public exitUrls: Array<string> = [] public exitUrls: Array<string> = []
public constructor(private map: ITiledMap) { public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array<Phaser.Tilemaps.Tileset>) {
this.layersIterator = new LayersIterator(map); this.flatLayers = flattenGroupLayersMap(map);
let depth = -2;
for (const layer of this.flatLayers) {
if(layer.type === 'tilelayer'){
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
}
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = DEPTH_OVERLAY_INDEX;
}
}
for (const tileset of map.tilesets) { for (const tileset of map.tilesets) {
tileset?.tiles?.forEach(tile => { tileset?.tiles?.forEach(tile => {
if (tile.properties) { if (tile.properties) {
@ -77,7 +88,7 @@ export class GameMap {
private getProperties(key: number): Map<string, string | boolean | number> { private getProperties(key: number): Map<string, string | boolean | number> {
const properties = new Map<string, string | boolean | number>(); const properties = new Map<string, string | boolean | number>();
for (const layer of this.layersIterator) { for (const layer of this.flatLayers) {
if (layer.type !== 'tilelayer') { if (layer.type !== 'tilelayer') {
continue; continue;
} }
@ -115,6 +126,10 @@ export class GameMap {
return properties; return properties;
} }
public getMap(): ITiledMap{
return this.map;
}
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) { private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) {
const callbacksArray = this.callbacks.get(propName); const callbacksArray = this.callbacks.get(propName);
if (callbacksArray !== undefined) { if (callbacksArray !== undefined) {
@ -135,4 +150,19 @@ export class GameMap {
} }
callbacksArray.push(callback); callbacksArray.push(callback);
} }
public findLayer(layerName: string): ITiledMapLayer | undefined {
return this.flatLayers.find((layer) => layer.name === layerName);
}
public findPhaserLayer(layerName: string): TilemapLayer | undefined {
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
}
public addTerrain(terrain : Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain);
}
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
import type {HasMovedEvent} from "./GameManager"; import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable"; import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type {PositionInterface} from "../../Connexion/ConnexionModels"; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
export class PlayerMovement { export class PlayerMovement {
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) {
} }
public isOutdated(tick: number): boolean { public isOutdated(tick: number): boolean {
@ -17,7 +17,7 @@ export class PlayerMovement {
return tick > this.endTick + MAX_EXTRAPOLATION_TIME; return tick > this.endTick + MAX_EXTRAPOLATION_TIME;
} }
public getPosition(tick: number): HasMovedEvent { public getPosition(tick: number): HasPlayerMovedEvent {
// Special case: end position reached and end position is not moving // Special case: end position reached and end position is not moving
if (tick >= this.endTick && this.endPosition.moving === false) { if (tick >= this.endTick && this.endPosition.moving === false) {
//console.log('Movement finished ', this.endPosition) //console.log('Movement finished ', this.endPosition)

View file

@ -2,13 +2,13 @@
* This class is in charge of computing the position of all players. * This class is in charge of computing the position of all players.
* Player movement is delayed by 200ms so position depends on ticks. * Player movement is delayed by 200ms so position depends on ticks.
*/ */
import type {PlayerMovement} from "./PlayerMovement"; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
import type {HasMovedEvent} from "./GameManager"; import type { PlayerMovement } from "./PlayerMovement";
export class PlayersPositionInterpolator { export class PlayersPositionInterpolator {
playerMovements: Map<number, PlayerMovement> = new Map<number, PlayerMovement>(); playerMovements: Map<number, PlayerMovement> = new Map<number, PlayerMovement>();
updatePlayerPosition(userId: number, playerMovement: PlayerMovement) : void { updatePlayerPosition(userId: number, playerMovement: PlayerMovement): void {
this.playerMovements.set(userId, playerMovement); this.playerMovements.set(userId, playerMovement);
} }
@ -16,8 +16,8 @@ export class PlayersPositionInterpolator {
this.playerMovements.delete(userId); this.playerMovements.delete(userId);
} }
getUpdatedPositions(tick: number) : Map<number, HasMovedEvent> { getUpdatedPositions(tick: number): Map<number, HasPlayerMovedEvent> {
const positions = new Map<number, HasMovedEvent>(); const positions = new Map<number, HasPlayerMovedEvent>();
this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => { this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => {
if (playerMovement.isOutdated(tick)) { if (playerMovement.isOutdated(tick)) {
//console.log("outdated") //console.log("outdated")

View file

@ -4,6 +4,8 @@
* Represents the interface for the Tiled exported data structure (JSON). Used * Represents the interface for the Tiled exported data structure (JSON). Used
* when loading resources via Resource loader. * when loading resources via Resource loader.
*/ */
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
export interface ITiledMap { export interface ITiledMap {
width: number; width: number;
height: number; height: number;
@ -81,6 +83,7 @@ export interface ITiledMapTileLayer {
* Draw order (topdown (default), index) * Draw order (topdown (default), index)
*/ */
draworder?: string; draworder?: string;
phaserLayer?: TilemapLayer;
} }
export interface ITiledMapObjectLayer { export interface ITiledMapObjectLayer {

View file

@ -0,0 +1,21 @@
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
/**
* Flatten the grouped layers
*/
export function flattenGroupLayersMap(map: ITiledMap) {
const flatLayers: ITiledMapLayer[] = [];
flattenGroupLayers(map.layers, '', flatLayers);
return flatLayers;
}
function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) {
for (const layer of layers) {
if (layer.type === 'group') {
flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers);
} else {
layer.name = prefix+layer.name
flatLayers.push(layer);
}
}
}

View file

@ -1,44 +0,0 @@
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
/**
* Iterates over the layers of a map, flattening the grouped layers
*/
export class LayersIterator implements IterableIterator<ITiledMapLayer> {
private layers: ITiledMapLayer[] = [];
private pointer: number = 0;
constructor(private map: ITiledMap) {
this.initLayersList(map.layers, '');
}
private initLayersList(layers : ITiledMapLayer[], prefix : string) {
for (const layer of layers) {
if (layer.type === 'group') {
this.initLayersList(layer.layers, prefix + layer.name + '/');
} else {
const layerWithNewName = { ...layer };
layerWithNewName.name = prefix+layerWithNewName.name;
this.layers.push(layerWithNewName);
}
}
}
public next(): IteratorResult<ITiledMapLayer> {
if (this.pointer < this.layers.length) {
return {
done: false,
value: this.layers[this.pointer++]
}
} else {
return {
done: true,
value: null
}
}
}
[Symbol.iterator](): IterableIterator<ITiledMapLayer> {
return new LayersIterator(this.map);
}
}

View file

@ -10,7 +10,12 @@ import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore"; import {menuIconVisible} from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore"; import { HtmlUtils } from '../../WebRtc/HtmlUtils';
import { iframeListener } from '../../Api/IframeListener';
import { Subscription } from 'rxjs';
import { videoConstraintStore } from "../../Stores/MediaStore";
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
import {get} from "svelte/store"; import {get} from "svelte/store";
@ -39,15 +44,41 @@ export class MenuScene extends Phaser.Scene {
private menuButton!: Phaser.GameObjects.DOMElement; private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null; private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription()
constructor() { constructor() {
super({key: MenuSceneName}); super({ key: MenuSceneName });
this.gameQualityValue = localUserStore.getGameQualityValue(); this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => {
this.addMenuOption(menuCommand);
}))
this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => {
this.destroyMenu(menuCommand);
}))
} }
preload () { reset() {
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
addedMenuItems[index].remove()
}
}
public addMenuOption(menuText: string) {
const wrappingSection = document.createElement("section")
const escapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
if (menuItemContainer) {
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove()
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"))
}
}
preload() {
this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); this.load.html(gameMenuKey, 'resources/html/gameMenu.html');
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
@ -70,11 +101,11 @@ export class MenuScene extends Phaser.Scene {
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare); this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare); MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener('click'); this.gameShareElement.addListener('click');
this.gameShareElement.on('click', (event:MouseEvent) => { this.gameShareElement.on('click', (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { if ((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') {
this.copyLink(); this.copyLink();
}else if((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { } else if ((event?.target as HTMLInputElement).id === 'gameShareFormCancel') {
this.closeGameShare(); this.closeGameShare();
} }
}); });
@ -130,8 +161,8 @@ export class MenuScene extends Phaser.Scene {
} }
//TODO bind with future metadata of card //TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){ //if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement;
adminSection.hidden = false; adminSection.hidden = false;
//} //}
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
@ -181,28 +212,28 @@ export class MenuScene extends Phaser.Scene {
this.settingsMenuOpened = true; this.settingsMenuOpened = true;
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
gameQualitySelect.value = ''+this.gameQualityValue; gameQualitySelect.value = '' + this.gameQualityValue;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
videoQualitySelect.value = ''+this.videoQualityValue; videoQualitySelect.value = '' + this.videoQualityValue;
this.gameQualityMenuElement.addListener('click'); this.gameQualityMenuElement.addListener('click');
this.gameQualityMenuElement.on('click', (event:MouseEvent) => { this.gameQualityMenuElement.on('click', (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') { if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') {
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value)); this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
} else if((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { } else if ((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') {
this.closeGameQualityMenu(); this.closeGameQualityMenu();
} }
}); });
let middleY = this.scale.height / 2 - 392/2; let middleY = this.scale.height / 2 - 392 / 2;
if(middleY < 0){ if (middleY < 0) {
middleY = 0; middleY = 0;
} }
let middleX = this.scale.width / 2 - 457/2; let middleX = this.scale.width / 2 - 457 / 2;
if(middleX < 0){ if (middleX < 0) {
middleX = 0; middleX = 0;
} }
this.tweens.add({ this.tweens.add({
@ -228,7 +259,7 @@ export class MenuScene extends Phaser.Scene {
} }
private openGameShare(): void{ private openGameShare(): void {
if (this.gameShareOpened) { if (this.gameShareOpened) {
this.closeGameShare(); this.closeGameShare();
return; return;
@ -242,11 +273,11 @@ export class MenuScene extends Phaser.Scene {
this.gameShareOpened = true; this.gameShareOpened = true;
let middleY = this.scale.height / 2 - 85; let middleY = this.scale.height / 2 - 85;
if(middleY < 0){ if (middleY < 0) {
middleY = 0; middleY = 0;
} }
let middleX = this.scale.width / 2 - 200; let middleX = this.scale.width / 2 - 200;
if(middleX < 0){ if (middleX < 0) {
middleX = 0; middleX = 0;
} }
this.tweens.add({ this.tweens.add({
@ -258,7 +289,7 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
private closeGameShare(): void{ private closeGameShare(): void {
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
gameShareInfo.innerText = ''; gameShareInfo.innerText = '';
gameShareInfo.style.display = 'none'; gameShareInfo.style.display = 'none';
@ -271,12 +302,18 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
private onMenuClick(event:MouseEvent) { private onMenuClick(event: MouseEvent) {
if((event?.target as HTMLInputElement).classList.contains('not-button')){ const htmlMenuItem = (event?.target as HTMLInputElement);
if (htmlMenuItem.classList.contains('not-button')) {
return; return;
} }
event.preventDefault(); event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) {
sendMenuClickedEvent(htmlMenuItem.id)
return
}
switch ((event?.target as HTMLInputElement).id) { switch ((event?.target as HTMLInputElement).id) {
case 'changeNameButton': case 'changeNameButton':
this.closeSideMenu(); this.closeSideMenu();
@ -322,7 +359,7 @@ export class MenuScene extends Phaser.Scene {
gameShareInfo.style.display = 'block'; gameShareInfo.style.display = 'block';
} }
private saveSetting(valueGame: number, valueVideo: number){ private saveSetting(valueGame: number, valueVideo: number) {
if (valueGame !== this.gameQualityValue) { if (valueGame !== this.gameQualityValue) {
this.gameQualityValue = valueGame; this.gameQualityValue = valueGame;
localUserStore.setGameQualityValue(valueGame); localUserStore.setGameQualityValue(valueGame);
@ -343,7 +380,7 @@ export class MenuScene extends Phaser.Scene {
window.open(sparkHost, '_blank'); window.open(sparkHost, '_blank');
} }
private closeAll(){ private closeAll() {
this.closeGameQualityMenu(); this.closeGameQualityMenu();
this.closeGameShare(); this.closeGameShare();
this.gameReportElement.close(); this.gameReportElement.close();
@ -360,6 +397,10 @@ export class MenuScene extends Phaser.Scene {
} }
} }
public destroyMenu(menu: string) {
this.menuElement.getChildByID(menu).remove();
}
public isDirty(): boolean { public isDirty(): boolean {
return false; return false;
} }

View file

@ -1,11 +1,11 @@
import {DivImportance, layoutManager} from "./LayoutManager"; import { DivImportance, layoutManager } from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
import {discussionManager, SendMessageCallback} from "./DiscussionManager"; import { discussionManager, SendMessageCallback } from "./DiscussionManager";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {localUserStore} from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import type {UserSimplePeerInterface} from "./SimplePeer"; import type { UserSimplePeerInterface } from "./SimplePeer";
import {SoundMeter} from "../Phaser/Components/SoundMeter"; import { SoundMeter } from "../Phaser/Components/SoundMeter";
import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import { import {
gameOverlayVisibilityStore, localStreamStore, gameOverlayVisibilityStore, localStreamStore,
} from "../Stores/MediaStore"; } from "../Stores/MediaStore";
@ -14,11 +14,11 @@ import {
} from "../Stores/ScreenSharingStore"; } from "../Stores/ScreenSharingStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"; import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void; export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void; export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void;
export type ReportCallback = (message: string) => void; export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void; export type ShowReportCallBack = (userId: string, userName: string | undefined) => void;
export type HelpCameraSettingsCallBack = () => void; export type HelpCameraSettingsCallBack = () => void;
import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
@ -27,13 +27,16 @@ export class MediaManager {
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>(); private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
//FIX ME SOUNDMETER: check stalability of sound meter calculation //FIX ME SOUNDMETER: check stalability of sound meter calculation
//mySoundMeterElement: HTMLDivElement; //mySoundMeterElement: HTMLDivElement;
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks : Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
private focused : boolean = true; startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks: Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
private focused: boolean = true;
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
@ -134,13 +137,16 @@ export class MediaManager {
gameOverlayVisibilityStore.hideGameOverlay(); gameOverlayVisibilityStore.hideGameOverlay();
} }
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
const userId = ''+user.userId
addActiveVideo(user: UserSimplePeerInterface, userName: string = "") {
const userId = '' + user.userId
userName = userName.toUpperCase(); userName = userName.toUpperCase();
const color = this.getColorByString(userName); const color = this.getColorByString(userName);
const html = ` const html = `
<div id="div-${userId}" class="video-container"> <div id="div-${userId}" class="video-container">
<div class="connecting-spinner"></div> <div class="connecting-spinner"></div>
<div class="rtc-error" style="display: none"></div> <div class="rtc-error" style="display: none"></div>
@ -168,7 +174,7 @@ export class MediaManager {
//permit to create participant in discussion part //permit to create participant in discussion part
const showReportUser = () => { const showReportUser = () => {
for(const callBack of this.showReportModalCallBacks){ for (const callBack of this.showReportModalCallBacks) {
callBack(userId, userName); callBack(userId, userName);
} }
}; };
@ -200,17 +206,17 @@ export class MediaManager {
return `screen-sharing-${userId}`; return `screen-sharing-${userId}`;
} }
disabledMicrophoneByUserId(userId: number){ disabledMicrophoneByUserId(userId: number) {
const element = document.getElementById(`microphone-${userId}`); const element = document.getElementById(`microphone-${userId}`);
if(!element){ if (!element) {
return; return;
} }
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'? element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
} }
enabledMicrophoneByUserId(userId: number){ enabledMicrophoneByUserId(userId: number) {
const element = document.getElementById(`microphone-${userId}`); const element = document.getElementById(`microphone-${userId}`);
if(!element){ if (!element) {
return; return;
} }
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'? element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
@ -227,22 +233,22 @@ export class MediaManager {
} }
} }
enabledVideoByUserId(userId: number){ enabledVideoByUserId(userId: number) {
let element = document.getElementById(`${userId}`); let element = document.getElementById(`${userId}`);
if(element){ if (element) {
element.style.opacity = "1"; element.style.opacity = "1";
} }
element = document.getElementById(`name-${userId}`); element = document.getElementById(`name-${userId}`);
if(element){ if (element) {
element.style.display = "none"; element.style.display = "none";
} }
} }
toggleBlockLogo(userId: number, show: boolean): void { toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-'+userId); const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
} }
addStreamRemoteVideo(userId: string, stream : MediaStream): void { addStreamRemoteVideo(userId: string, stream: MediaStream): void {
const remoteVideo = this.remoteVideo.get(userId); const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) { if (remoteVideo === undefined) {
throw `Unable to find video for ${userId}`; throw `Unable to find video for ${userId}`;
@ -256,7 +262,7 @@ export class MediaManager {
this.soundMeters.set(userId, soundMeter); this.soundMeters.set(userId, soundMeter);
this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));*/ this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));*/
} }
addStreamRemoteScreenSharing(userId: string, stream : MediaStream){ addStreamRemoteScreenSharing(userId: string, stream: MediaStream) {
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet // In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId)); const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId));
if (remoteVideo === undefined) { if (remoteVideo === undefined) {
@ -266,7 +272,7 @@ export class MediaManager {
this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream); this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
} }
removeActiveVideo(userId: string){ removeActiveVideo(userId: string) {
layoutManager.remove(userId); layoutManager.remove(userId);
this.remoteVideo.delete(userId); this.remoteVideo.delete(userId);
@ -301,10 +307,10 @@ export class MediaManager {
isError(userId: string): void { isError(userId: string): void {
console.info("isError", `div-${userId}`); console.info("isError", `div-${userId}`);
const element = document.getElementById(`div-${userId}`); const element = document.getElementById(`div-${userId}`);
if(!element){ if (!element) {
return; return;
} }
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null; const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement | null;
if (errorDiv === null) { if (errorDiv === null) {
return; return;
} }
@ -315,16 +321,16 @@ export class MediaManager {
} }
private getSpinner(userId: string): HTMLDivElement|null { private getSpinner(userId: string): HTMLDivElement | null {
const element = document.getElementById(`div-${userId}`); const element = document.getElementById(`div-${userId}`);
if(!element){ if (!element) {
return null; return null;
} }
const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement | null;
return connnectingSpinnerDiv; return connnectingSpinnerDiv;
} }
private getColorByString(str: String) : String|null { private getColorByString(str: String): String | null {
let hash = 0; let hash = 0;
if (str.length === 0) return null; if (str.length === 0) return null;
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
@ -339,18 +345,18 @@ export class MediaManager {
return color; return color;
} }
public addNewParticipant(userId: number|string, name: string|undefined, img?: string, showReportUserCallBack?: ShowReportCallBack){ public addNewParticipant(userId: number | string, name: string | undefined, img?: string, showReportUserCallBack?: ShowReportCallBack) {
discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack); discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack);
} }
public removeParticipant(userId: number|string){ public removeParticipant(userId: number | string) {
discussionManager.removeParticipant(userId); discussionManager.removeParticipant(userId);
} }
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
this.triggerCloseJistiFrame.set(id, Function); this.triggerCloseJistiFrame.set(id, Function);
} }
public removeTriggerCloseJitsiFrameButton(id: String){ public removeTriggerCloseJitsiFrameButton(id: String) {
this.triggerCloseJistiFrame.delete(id); this.triggerCloseJistiFrame.delete(id);
} }
@ -363,36 +369,36 @@ export class MediaManager {
* For some reasons, the microphone muted icon or the stream is not always up to date. * For some reasons, the microphone muted icon or the stream is not always up to date.
* Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser) * Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser)
**/ **/
private pingCameraStatus(){ private pingCameraStatus() {
/*setInterval(() => { /*setInterval(() => {
console.log('ping camera status'); console.log('ping camera status');
this.triggerUpdatedLocalStreamCallbacks(this.localStream); this.triggerUpdatedLocalStreamCallbacks(this.localStream);
}, 30000);*/ }, 30000);*/
} }
public addNewMessage(name: string, message: string, isMe: boolean = false){ public addNewMessage(name: string, message: string, isMe: boolean = false) {
discussionManager.addMessage(name, message, isMe); discussionManager.addMessage(name, message, isMe);
//when there are new message, show discussion //when there are new message, show discussion
if(!discussionManager.activatedDiscussion) { if (!discussionManager.activatedDiscussion) {
discussionManager.showDiscussionPart(); discussionManager.showDiscussionPart();
} }
} }
public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){ public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
discussionManager.onSendMessageCallback(userId, callback); discussionManager.onSendMessageCallback(userId, callback);
} }
get activatedDiscussion(){ get activatedDiscussion() {
return discussionManager.activatedDiscussion; return discussionManager.activatedDiscussion;
} }
public setUserInputManager(userInputManager : UserInputManager){ public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager; this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager); discussionManager.setUserInputManager(userInputManager);
} }
public setShowReportModalCallBacks(callback: ShowReportCallBack){ public setShowReportModalCallBacks(callback: ShowReportCallBack) {
this.showReportModalCallBacks.add(callback); this.showReportModalCallBacks.add(callback);
} }
@ -405,36 +411,36 @@ export class MediaManager {
for(const indexUserId of this.soundMeters.keys()){ for(const indexUserId of this.soundMeters.keys()){
const soundMeter = this.soundMeters.get(indexUserId); const soundMeter = this.soundMeters.get(indexUserId);
const soundMeterElement = this.soundMeterElements.get(indexUserId); const soundMeterElement = this.soundMeterElements.get(indexUserId);
if(!soundMeter || !soundMeterElement){ if (!soundMeter || !soundMeterElement) {
return; return;
} }
const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0)); const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0));
this.setVolumeSoundMeter(volumeByUser, soundMeterElement); this.setVolumeSoundMeter(volumeByUser, soundMeterElement);
} }
}catch(err){ } catch (err) {
//console.error(err); //console.error(err);
} }
}*/ }*/
private setVolumeSoundMeter(volume: number, element: HTMLDivElement){ private setVolumeSoundMeter(volume: number, element: HTMLDivElement) {
if(volume <= 0 && !element.classList.contains('active')){ if (volume <= 0 && !element.classList.contains('active')) {
return; return;
} }
element.classList.remove('active'); element.classList.remove('active');
if(volume <= 0){ if (volume <= 0) {
return; return;
} }
element.classList.add('active'); element.classList.add('active');
element.childNodes.forEach((value: ChildNode, index) => { element.childNodes.forEach((value: ChildNode, index) => {
const elementChildre = element.children.item(index); const elementChildren = element.children.item(index);
if(!elementChildre){ if (!elementChildren) {
return; return;
} }
elementChildre.classList.remove('active'); elementChildren.classList.remove('active');
if((index +1) > volume){ if ((index + 1) > volume) {
return; return;
} }
elementChildre.classList.add('active'); elementChildren.classList.add('active');
}); });
} }

View file

@ -1,4 +1,4 @@
import {registeredCallbacks} from "./Api/iframe/registeredCallbacks"; import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
import { import {
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
@ -6,15 +6,16 @@ import {
TypedMessageEvent TypedMessageEvent
} from "./Api/Events/IframeEvent"; } from "./Api/Events/IframeEvent";
import chat from "./Api/iframe/chat"; import chat from "./Api/iframe/chat";
import type {IframeCallback} from './Api/iframe/IframeApiContribution'; import type { IframeCallback } from './Api/iframe/IframeApiContribution';
import nav from "./Api/iframe/nav"; import nav from "./Api/iframe/nav";
import controls from "./Api/iframe/controls"; import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui"; import ui from "./Api/iframe/ui";
import sound from "./Api/iframe/sound"; import sound from "./Api/iframe/sound";
import room from "./Api/iframe/room"; import room from "./Api/iframe/room";
import type {ButtonDescriptor} from "./Api/iframe/Ui/ButtonDescriptor"; import player from "./Api/iframe/player";
import type {Popup} from "./Api/iframe/Ui/Popup"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type {Sound} from "./Api/iframe/Sound/Sound"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
const wa = { const wa = {
ui, ui,
@ -23,6 +24,7 @@ const wa = {
chat, chat,
sound, sound,
room, room,
player,
// All methods below are deprecated and should not be used anymore. // All methods below are deprecated and should not be used anymore.
// They are kept here for backward compatibility. // They are kept here for backward compatibility.
@ -77,7 +79,7 @@ const wa = {
/** /**
* @deprecated Use WA.sound.loadSound instead * @deprecated Use WA.sound.loadSound instead
*/ */
loadSound(url: string) : Sound { loadSound(url: string): Sound {
console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead'); console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead');
return sound.loadSound(url); return sound.loadSound(url);
}, },
@ -85,7 +87,7 @@ const wa = {
/** /**
* @deprecated Use WA.nav.goToPage instead * @deprecated Use WA.nav.goToPage instead
*/ */
goToPage(url : string) : void { goToPage(url: string): void {
console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead'); console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead');
nav.goToPage(url); nav.goToPage(url);
}, },
@ -101,7 +103,7 @@ const wa = {
/** /**
* @deprecated Use WA.nav.openCoWebSite instead * @deprecated Use WA.nav.openCoWebSite instead
*/ */
openCoWebSite(url : string) : void{ openCoWebSite(url: string): void {
console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead');
nav.openCoWebSite(url); nav.openCoWebSite(url);
}, },

View file

@ -1,145 +1,148 @@
import "jasmine"; import "jasmine";
import {Room} from "../../../src/Connexion/Room"; import {Room} from "../../../src/Connexion/Room";
import {LayersIterator} from "../../../src/Phaser/Map/LayersIterator"; import {flattenGroupLayersMap} from "../../../src/Phaser/Map/LayersFlattener";
import type {ITiledMapLayer} from "../../../src/Phaser/Map/ITiledMap";
describe("Layers iterator", () => { describe("Layers flattener", () => {
it("should iterate maps with no group", () => { it("should iterate maps with no group", () => {
const layersIterator = new LayersIterator({ let flatLayers:ITiledMapLayer[] = [];
"compressionlevel":-1, flatLayers = flattenGroupLayersMap({
"height":2, "compressionlevel": -1,
"infinite":false, "height": 2,
"layers":[ "infinite": false,
"layers": [
{ {
"data":[0, 0, 0, 0], "data": [0, 0, 0, 0],
"height":2, "height": 2,
"id":1, "id": 1,
"name":"Tile Layer 1", "name": "Tile Layer 1",
"opacity":1, "opacity": 1,
"type":"tilelayer", "type": "tilelayer",
"visible":true, "visible": true,
"width":2, "width": 2,
"x":0, "x": 0,
"y":0 "y": 0
}, },
{ {
"data":[0, 0, 0, 0], "data": [0, 0, 0, 0],
"height":2, "height": 2,
"id":1, "id": 1,
"name":"Tile Layer 2", "name": "Tile Layer 2",
"opacity":1, "opacity": 1,
"type":"tilelayer", "type": "tilelayer",
"visible":true, "visible": true,
"width":2, "width": 2,
"x":0, "x": 0,
"y":0 "y": 0
}], }],
"nextlayerid":2, "nextlayerid": 2,
"nextobjectid":1, "nextobjectid": 1,
"orientation":"orthogonal", "orientation": "orthogonal",
"renderorder":"right-down", "renderorder": "right-down",
"tiledversion":"2021.03.23", "tiledversion": "2021.03.23",
"tileheight":32, "tileheight": 32,
"tilesets":[], "tilesets": [],
"tilewidth":32, "tilewidth": 32,
"type":"map", "type": "map",
"version":1.5, "version": 1.5,
"width":2 "width": 2
}) })
const layers = []; const layers = [];
for (const layer of layersIterator) { for (const layer of flatLayers) {
layers.push(layer.name); layers.push(layer.name);
} }
expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']); expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']);
}); });
it("should iterate maps with recursive groups", () => { it("should iterate maps with recursive groups", () => {
const layersIterator = new LayersIterator({ let flatLayers:ITiledMapLayer[] = [];
"compressionlevel":-1, flatLayers = flattenGroupLayersMap({
"height":2, "compressionlevel": -1,
"infinite":false, "height": 2,
"layers":[ "infinite": false,
"layers": [
{ {
"id":6, "id": 6,
"layers":[ "layers": [
{ {
"id":5, "id": 5,
"layers":[ "layers": [
{ {
"data":[0, 0, 0, 0], "data": [0, 0, 0, 0],
"height":2, "height": 2,
"id":10, "id": 10,
"name":"Tile3", "name": "Tile3",
"opacity":1, "opacity": 1,
"type":"tilelayer", "type": "tilelayer",
"visible":true, "visible": true,
"width":2, "width": 2,
"x":0, "x": 0,
"y":0 "y": 0
}, },
{ {
"data":[0, 0, 0, 0], "data": [0, 0, 0, 0],
"height":2, "height": 2,
"id":9, "id": 9,
"name":"Tile2", "name": "Tile2",
"opacity":1, "opacity": 1,
"type":"tilelayer", "type": "tilelayer",
"visible":true, "visible": true,
"width":2, "width": 2,
"x":0, "x": 0,
"y":0 "y": 0
}], }],
"name":"Group 3", "name": "Group 3",
"opacity":1, "opacity": 1,
"type":"group", "type": "group",
"visible":true, "visible": true,
"x":0, "x": 0,
"y":0 "y": 0
}, },
{ {
"id":7, "id": 7,
"layers":[ "layers": [
{ {
"data":[0, 0, 0, 0], "data": [0, 0, 0, 0],
"height":2, "height": 2,
"id":8, "id": 8,
"name":"Tile1", "name": "Tile1",
"opacity":1, "opacity": 1,
"type":"tilelayer", "type": "tilelayer",
"visible":true, "visible": true,
"width":2, "width": 2,
"x":0, "x": 0,
"y":0 "y": 0
}], }],
"name":"Group 2", "name": "Group 2",
"opacity":1, "opacity": 1,
"type":"group", "type": "group",
"visible":true, "visible": true,
"x":0, "x": 0,
"y":0 "y": 0
}], }],
"name":"Group 1", "name": "Group 1",
"opacity":1, "opacity": 1,
"type":"group", "type": "group",
"visible":true, "visible": true,
"x":0, "x": 0,
"y":0 "y": 0
}], }],
"nextlayerid":11, "nextlayerid": 11,
"nextobjectid":1, "nextobjectid": 1,
"orientation":"orthogonal", "orientation": "orthogonal",
"renderorder":"right-down", "renderorder": "right-down",
"tiledversion":"2021.03.23", "tiledversion": "2021.03.23",
"tileheight":32, "tileheight": 32,
"tilesets":[], "tilesets": [],
"tilewidth":32, "tilewidth": 32,
"type":"map", "type": "map",
"version":1.5, "version": 1.5,
"width":2 "width": 2
}) })
const layers = []; const layers = [];
for (const layer of layersIterator) { for (const layer of flatLayers) {
layers.push(layer.name); layers.push(layer.name);
} }
expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']); expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']);

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
</head>
<body>
<script>
WA.ui.registerMenuCommand("test", () => {
WA.chat.sendChatMessage("test clicked", "menu cmd")
})
</script>
</body>
</html>

View file

@ -0,0 +1,279 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"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, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":6,
"name":"exit",
"opacity":1,
"properties":[
{
"name":"exitUrl",
"type":"string",
"value":"showHideLayer.json"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"customMenu.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":217.142414860681,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the grass, an iframe open.\nResult : \nOpen the menu, a new sub-menu is displayed.\n\nTest : \nExit the grass\nResult : \nOpen the menu, the submenu has disappeared.\n\nTest : \nClick on the 'HELP' menu.\nResult : \nChat open and a 'HELP' message is displayed.\n\nTest : \nWalk on the red tile then open the menu.\nResult : \nYou have exit the room to another room, the submenu has disappeared.\n",
"wrap":true
},
"type":"",
"visible":true,
"width":305.097705765524,
"x":15.1244925229277,
"y":103.029937496349
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":7,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{
"columns":8,
"firstgid":65,
"image":"floortileset.png",
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
</head>
<body>
<script>
WA.room.getCurrentRoom().then((room) => {
console.log('id : ', room.id);
console.log('map : ', room.map);
console.log('mapUrl : ', room.mapUrl);
console.log('startLayer : ', room.startLayer);
})
</script>
</body>
</html>

View file

@ -0,0 +1,279 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"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, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":10,
"name":"HereYouAppered",
"opacity":1,
"properties":[
{
"name":"startLayer",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"getCurrentRoom.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":191.607568521364,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the grass and open the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- map : data of the JSON file of the map\n\t- mapUrl : url of the JSON file of the map\n\t- startLayer : Name of the layer where the current user started (HereYouAppered)\n\n\n",
"wrap":true
},
"type":"",
"visible":true,
"width":305.097705765524,
"x":14.750638909983,
"y":128.564783835666
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":11,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{
"columns":8,
"firstgid":65,
"image":"floortileset.png",
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
</head>
<body>
<script>
WA.room.getCurrentUser().then((user) => {
console.log('id : ', user.id);
console.log('nickName : ', user.nickName);
console.log('tags : ', user.tags);
})
</script>
</body>
</html>

View file

@ -0,0 +1,296 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":9,
"name":"exit",
"opacity":1,
"properties":[
{
"name":"exitUrl",
"type":"string",
"value":"getCurrentRoom.json#HereYouAppered"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"getCurrentUser.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":151.839293303871,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the grass, open the console.\n\nResut : \nYou should see a console.log() of the following attributes :\n\t- id : ID of the current user\n\t- nickName : Name of the current user\n\t- tags : List of tags of the current user\n\nFinally : \nWalk on the red tile and continue the test in an another room.",
"wrap":true
},
"type":"",
"visible":true,
"width":305.097705765524,
"x":14.750638909983,
"y":159.621625296353
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":10,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{
"columns":8,
"firstgid":65,
"image":"floortileset.png",
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tiles":[
{
"animation":[
{
"duration":100,
"tileid":9
},
{
"duration":100,
"tileid":64
},
{
"duration":100,
"tileid":55
}],
"id":0
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
</head>
<body>
<div id="playerMovement"></div>
<script>
WA.player.onPlayerMove(console.log);
</script>
</body>
</html>

View file

@ -0,0 +1,254 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"playerMove.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":159.195104854255,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the grass, an iframe open.\nResult : \nIf you move on the grass, your movement will be displayed in the console.log(). \nYour movement appears according to the following rules : \n - When you stop (the moving attribute will be false)\n - When you change direction (the direction attribute will change value)\n - Every 200ms if you keep moving in the same direction.\n\nMovement are represented by the following attributes : \n - moving : if you are moving or not.\n - direction : the direction where you are moving into\n - X and Y coordinates : Place of your character in the room.\n\n\n",
"wrap":true
},
"type":"",
"visible":true,
"width":305.097705765524,
"x":14.750638909983,
"y":160.977247502775
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":10,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{
"columns":8,
"firstgid":65,
"image":"floortileset.png",
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
</head>
<body>
<script>
WA.room.setProperty('iframeTest', 'openWebsite', 'https://www.wikipedia.org/');
WA.room.setProperty('metadata', 'openWebsite', 'https://www.wikipedia.org/');
</script>
</body>
</html>

View file

@ -0,0 +1,266 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"setProperty.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"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, 101, 101, 101, 101, 101, 0, 0, 0, 0, 0, 101, 101, 101, 101, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":7,
"name":"iframeTest",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":157.325836789532,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the red tiles.\nResult :\nNothing happens.\n\nTest : \nWalk on the grass, an iframe open. Then walk on the red tiles.\nResult : \nAn iframe of Wikipedia open.\n\nTest : \nWalk on the grass again.\nResult : \nAn iframe of Wikipedia open.\n",
"wrap":true
},
"type":"",
"visible":true,
"width":305.097705765524,
"x":15.1244925229277,
"y":162.846515567498
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{
"columns":8,
"firstgid":65,
"image":"floortileset.png",
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

View file

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
</head>
<body>
<div>
<label for="show/hideLayer">Crysal Layer : </label><input type="checkbox" id="show/hideLayer" name="visible" value="show" checked>
</div>
<script>
document.getElementById('show/hideLayer').onclick = () => {
if (document.getElementById('show/hideLayer').checked) {
WA.room.showLayer('crystal');
}
else {
WA.room.hideLayer('crystal');
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,266 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[22, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 22, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":6,
"name":"crystal",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":4,
"name":"metadata",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"showHideLayer.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":191.346515567498,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":9,
"text":"Test : \nWalk on the grass, an iframe open, uncheck the checkbox.\nResult : \nCrystals disappeared.\n\nTest : \nCheck the checkbox\nResult : \nCrystals appear.",
"wrap":true
},
"type":"",
"visible":true,
"width":306.219266604358,
"x":14.0029316840937,
"y":128.078129563643
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":7,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"TDungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":3,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":4,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":9,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":19,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":20,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
},
{
"columns":8,
"firstgid":65,
"image":"floortileset.png",
"imageheight":288,
"imagewidth":256,
"margin":0,
"name":"Floor",
"spacing":0,
"tilecount":72,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

@ -2,15 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script> <script src="http://play.workadventure.localhost/iframe_api.js"></script>
<script>
</script>
</head> </head>
<body> <body>
<button id="sendchat">Send chat message</button> <button id="sendchat">Send chat message</button>
<script> <script>
document.getElementById('sendchat').onclick = () => { document.getElementById('sendchat').onclick = () => {
WA.sendChatMessage('Hello world!', 'Mr Robot'); WA.sendChatMessage('Hello world!', 'Mr ROBOT');
} }
</script> </script>
<div id="chatSent"></div> <div id="chatSent"></div>

View file

@ -1,4 +1,11 @@
{ "compressionlevel":-1, { "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10, "height":10,
"infinite":false, "infinite":false,
"layers":[ "layers":[
@ -49,6 +56,18 @@
"x":0, "x":0,
"y":0 "y":0
}, },
{
"data":[0, 0, 93, 0, 104, 0, 0, 0, 0, 0, 0, 0, 104, 0, 115, 0, 0, 0, 93, 0, 0, 0, 115, 0, 0, 0, 93, 0, 104, 0, 0, 0, 0, 0, 0, 0, 104, 0, 115, 93, 0, 0, 0, 0, 0, 0, 115, 0, 0, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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":6,
"name":"Metadata",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{ {
"draworder":"topdown", "draworder":"topdown",
"id":3, "id":3,
@ -78,11 +97,11 @@
"x":0, "x":0,
"y":0 "y":0
}], }],
"nextlayerid":6, "nextlayerid":7,
"nextobjectid":3, "nextobjectid":3,
"orientation":"orthogonal", "orientation":"orthogonal",
"renderorder":"right-down", "renderorder":"right-down",
"tiledversion":"2021.03.23", "tiledversion":"1.4.3",
"tileheight":32, "tileheight":32,
"tilesets":[ "tilesets":[
{ {
@ -100,6 +119,6 @@
}], }],
"tilewidth":32, "tilewidth":32,
"type":"map", "type":"map",
"version":1.5, "version":1.4,
"width":10 "width":10
} }

View file

@ -106,6 +106,54 @@
<a href="#" class="testLink" data-testmap="help_camera_setting.json" target="_blank">Test the HelpCameraSettingScene</a> <a href="#" class="testLink" data-testmap="help_camera_setting.json" target="_blank">Test the HelpCameraSettingScene</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-custom-menu"> Success <input type="radio" name="test-custom-menu"> Failure <input type="radio" name="test-custom-menu" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Metadata/customMenu.json" target="_blank">Testing add a custom menu by scripting API</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-getCurrentRoom"> Success <input type="radio" name="test-getCurrentRoom"> Failure <input type="radio" name="test-getCurrentRoom" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Metadata/getCurrentRoom.json" target="_blank">Testing return current room attributes by Scripting API (Need to test from current user)</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-getCurrentUser"> Success <input type="radio" name="test-getCurrentUser"> Failure <input type="radio" name="test-getCurrentUser" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Metadata/getCurrentUser.json" target="_blank">Testing return current user attributes by Scripting API</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-player-move"> Success <input type="radio" name="test-player-move"> Failure <input type="radio" name="test-player-move" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Metadata/playerMove.json" target="_blank">Test listening player movement by Scripting API</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-setProperty"> Success <input type="radio" name="test-setProperty"> Failure <input type="radio" name="test-setProperty" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Metadata/setProperty.json" target="_blank">Testing set a property on a layer by Scripting API</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-show-hide"> Success <input type="radio" name="test-show-hide"> Failure <input type="radio" name="test-show-hide" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Metadata/showHideLayer.json" target="_blank">Testing show or hide a layer by Scripting API</a>
</td>
</tr>
<tr> <tr>
<td> <td>
<input type="radio" name="test-animated-tiles"> Success <input type="radio" name="test-animated-tiles"> Failure <input type="radio" name="test-animated-tiles" checked> Pending <input type="radio" name="test-animated-tiles"> Success <input type="radio" name="test-animated-tiles"> Failure <input type="radio" name="test-animated-tiles" checked> Pending