Merge pull request #1525 from thecodingmachine/player-local-storage

Allows to read and write "Player properties" from LocalStorage & Adds a camera API
This commit is contained in:
David Négrier 2022-01-03 15:24:14 +01:00 committed by GitHub
commit a28d86c16d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 564 additions and 189 deletions

24
docs/maps/api-camera.md Normal file
View file

@ -0,0 +1,24 @@
{.section-title.accent.text-primary}
# API Camera functions Reference
### Listen to camera updates
```
WA.camera.onCameraUpdate(): Subscription
```
Listens to updates of the camera viewport. It will trigger for every update of the camera's properties (position or scale for instance). An event will be sent.
The event has the following attributes :
* **x (number):** coordinate X of the camera's world view (the area looked at by the camera).
* **y (number):** coordinate Y of the camera's world view.
* **width (number):** the width of the camera's world view.
* **height (number):** the height of the camera's world view.
**callback:** the function that will be called when the camera is updated.
Example :
```javascript
const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView));
//later...
subscription.unsubscribe();

View file

@ -86,6 +86,27 @@ WA.onInit().then(() => {
}) })
``` ```
### Get the position of the player
```
WA.player.getPosition(): Promise<Position>
```
The player's current position is available using the `WA.player.getPosition()` function.
`Position` has the following attributes :
* **x (number) :** The coordinate x of the current player's position.
* **y (number) :** The coordinate y of the current player's position.
{.alert.alert-info}
You need to wait for the end of the initialization before calling `WA.player.getPosition()`
```typescript
WA.onInit().then(async () => {
console.log('Position: ', await WA.player.getPosition());
})
```
### Listen to player movement ### Listen to player movement
``` ```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
@ -107,6 +128,30 @@ Example :
WA.player.onPlayerMove(console.log); WA.player.onPlayerMove(console.log);
``` ```
## Player specific variables
Similarly to maps (see [API state related functions](api-state.md)), it is possible to store data **related to a specific player** in a "state". Such data will be stored using the local storage from the user's browser. Any value that is serializable in JSON can be stored.
{.alert.alert-info}
In the future, player-related variables will be stored on the WorkAdventure server if the current player is logged.
Any value that is serializable in JSON can be stored.
### Setting a property
A player property can be set simply by assigning a value.
Example:
```javascript
WA.player.state.toto = "value" //will set the "toto" key to "value"
```
### Reading a variable
A player variable can be read by calling its key from the player's state.
Example:
```javascript
WA.player.state.toto //will retrieve the variable
```
### Set the outline color of the player ### Set the outline color of the player
``` ```
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>; WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;

View file

@ -10,5 +10,6 @@
- [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)
- [Camera functions](api-camera.md)
- [List of deprecated functions](api-deprecated.md) - [List of deprecated functions](api-deprecated.md)

View file

@ -1,8 +1,11 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Room functions Reference # API Room functions Reference
### Working with group layers ### 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.
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 : Example :
<div class="row"> <div class="row">
@ -12,6 +15,7 @@ Example :
</div> </div>
The name of the layers of this map are : The name of the layers of this map are :
* `entries/start` * `entries/start`
* `bottom/ground/under` * `bottom/ground/under`
* `bottom/build/carpet` * `bottom/build/carpet`
@ -26,29 +30,32 @@ WA.room.onLeaveLayer(name: string): Subscription
Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer. Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer.
* **name**: the name of the layer who as defined in Tiled. * **name**: the name of the layer who as defined in Tiled.
Example: Example:
```javascript ```javascript
WA.room.onEnterLayer('myLayer').subscribe(() => { WA.room.onEnterLayer('myLayer').subscribe(() => {
WA.chat.sendChatMessage("Hello!", 'Mr Robot'); WA.chat.sendChatMessage("Hello!", 'Mr Robot');
}); });
WA.room.onLeaveLayer('myLayer').subscribe(() => { WA.room.onLeaveLayer('myLayer').subscribe(() => {
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
}); });
``` ```
### Show / Hide a layer ### Show / Hide a layer
``` ```
WA.room.showLayer(layerName : string): void WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void WA.room.hideLayer(layerName : string) : void
``` ```
These 2 methods can be used to show and hide a layer.
if `layerName` is the name of a group layer, show/hide all the layer in that group layer. These 2 methods can be used to show and hide a layer. if `layerName` is the name of a group layer, show/hide all the
layer in that group layer.
Example : Example :
```javascript ```javascript
WA.room.showLayer('bottom'); WA.room.showLayer('bottom');
//... //...
@ -61,12 +68,14 @@ WA.room.hideLayer('bottom');
WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; 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`. 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`.
Note : Note :
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`. To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
Example : Example :
```javascript ```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
``` ```
@ -79,13 +88,12 @@ WA.room.id: string;
The ID of the current room is available from the `WA.room.id` property. The ID of the current room is available from the `WA.room.id` property.
{.alert.alert-info} {.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.id`
You need to wait for the end of the initialization before accessing `WA.room.id`
```typescript ```typescript
WA.onInit().then(() => { WA.onInit().then(() => {
console.log('Room id: ', WA.room.id); console.log('Room id: ', WA.room.id);
// Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
}) })
``` ```
@ -97,19 +105,17 @@ WA.room.mapURL: string;
The URL of the map is available from the `WA.room.mapURL` property. The URL of the map is available from the `WA.room.mapURL` property.
{.alert.alert-info} {.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.mapURL`
You need to wait for the end of the initialization before accessing `WA.room.mapURL`
```typescript ```typescript
WA.onInit().then(() => { WA.onInit().then(() => {
console.log('Map URL: ', WA.room.mapURL); console.log('Map URL: ', WA.room.mapURL);
// Will output something like: 'https://mymap.org/map.json" // Will output something like: 'https://mymap.org/map.json"
}) })
``` ```
### Getting map data ### Getting map data
``` ```
WA.room.getTiledMap(): Promise<ITiledMap> WA.room.getTiledMap(): Promise<ITiledMap>
``` ```
@ -121,12 +127,16 @@ const map = await WA.room.getTiledMap();
console.log("Map generated with Tiled version ", map.tiledversion); console.log("Map generated with Tiled version ", map.tiledversion);
``` ```
Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/). Check
the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/)
.
### Changing tiles ### Changing tiles
``` ```
WA.room.setTiles(tiles: TileDescriptor[]): void WA.room.setTiles(tiles: TileDescriptor[]): void
``` ```
Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`.
If `tile` is a string, it's not the id of the tile but the value of the property `name`. If `tile` is a string, it's not the id of the tile but the value of the property `name`.
@ -137,43 +147,48 @@ If `tile` is a string, it's not the id of the tile but the value of the property
</div> </div>
`TileDescriptor` has the following attributes : `TileDescriptor` has the following attributes :
* **x (number) :** The coordinate x of the tile that you want to replace. * **x (number) :** The coordinate x of the tile that you want to replace.
* **y (number) :** The coordinate y of the tile that you want to replace. * **y (number) :** The coordinate y of the tile that you want to replace.
* **tile (number | string) :** The id of the tile that will be placed in the map. * **tile (number | string) :** The id of the tile that will be placed in the map.
* **layer (string) :** The name of the layer where the tile will be placed. * **layer (string) :** The name of the layer where the tile will be placed.
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor. **Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want
to the id of the tile in Tiled Editor.
Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`. Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
Example : Example :
```javascript ```javascript
WA.room.setTiles([ WA.room.setTiles([
{x: 6, y: 4, tile: 'blue', layer: 'setTiles'}, { x: 6, y: 4, tile: 'blue', layer: 'setTiles' },
{x: 7, y: 4, tile: 109, layer: 'setTiles'}, { x: 7, y: 4, tile: 109, layer: 'setTiles' },
{x: 8, y: 4, tile: 109, layer: 'setTiles'}, { x: 8, y: 4, tile: 109, layer: 'setTiles' },
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'} { x: 9, y: 4, tile: 'blue', layer: 'setTiles' }
]); ]);
``` ```
### Loading a tileset ### Loading a tileset
``` ```
WA.room.loadTileset(url: string): Promise<number> WA.room.loadTileset(url: string): Promise<number>
``` ```
Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset. Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
You can create a tileset file in Tile Editor. You can create a tileset file in Tile Editor.
```javascript ```javascript
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]); WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: 'bottom' }]);
}) })
``` ```
## Embedding websites in a map ## Embedding websites in a map
You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)). You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using
the ["website" objects](website-in-map.md)).
### Getting an instance of a website already embedded in the map ### Getting an instance of a website already embedded in the map
@ -181,8 +196,8 @@ You can use the scripting API to embed websites in a map, or to edit websites th
WA.room.website.get(objectName: string): Promise<EmbeddedWebsite> WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
``` ```
You can get an instance of an embedded website by using the `WA.room.website.get()` method. You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of
It returns a promise of an `EmbeddedWebsite` instance. an `EmbeddedWebsite` instance.
```javascript ```javascript
// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) // Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
@ -191,7 +206,6 @@ website.url = 'https://example.com';
website.visible = true; website.visible = true;
``` ```
### Adding a new website in a map ### Adding a new website in a map
``` ```
@ -201,34 +215,38 @@ interface CreateEmbeddedWebsiteEvent {
name: string; // A unique name for this iframe name: string; // A unique name for this iframe
url: string; // The URL the iframe points to. url: string; // The URL the iframe points to.
position: { position: {
x: number, // In pixels, relative to the map coordinates x: number, // In "game" pixels, relative to the map or player coordinates, depending on origin
y: number, // In pixels, relative to the map coordinates y: number, // In "game" pixels, relative to the map or player coordinates, depending on origin
width: number, // In pixels, sensitive to zoom level width: number, // In "game" pixels
height: number, // In pixels, sensitive to zoom level height: number, // In "game" pixels
}, },
visible?: boolean, // Whether to display the iframe or not visible?: boolean, // Whether to display the iframe or not
allowApi?: boolean, // Whether the scripting API should be available to the iframe allowApi?: boolean, // Whether the scripting API should be available to the iframe
allow?: string, // The list of feature policies allowed allow?: string, // The list of feature policies allowed
origin: "player" | "map" // The origin used to place the x and y coordinates of the iframe's top-left corner, defaults to "map"
scale: number, // A ratio used to resize the iframe
} }
``` ```
You can create an instance of an embedded website by using the `WA.room.website.create()` method. You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns
It returns an `EmbeddedWebsite` instance. an `EmbeddedWebsite` instance.
```javascript ```javascript
// Create a new website object // Create a new website object
const website = WA.room.website.create({ const website = WA.room.website.create({
name: "my_website", name: "my_website",
url: "https://example.com", url: "https://example.com",
position: { position: {
x: 64, x: 64,
y: 128, y: 128,
width: 320, width: 320,
height: 240, height: 240,
}, },
visible: true, visible: true,
allowApi: true, allowApi: true,
allow: "fullscreen", allow: "fullscreen",
origin: "map",
scale: 1,
}); });
``` ```
@ -240,30 +258,28 @@ WA.room.website.delete(name: string): Promise<void>
Use `WA.room.website.delete` to completely remove an embedded website from your map. Use `WA.room.website.delete` to completely remove an embedded website from your map.
### The EmbeddedWebsite class ### The EmbeddedWebsite class
Instances of the `EmbeddedWebsite` class represent the website displayed on the map. Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
```typescript ```typescript
class EmbeddedWebsite { class EmbeddedWebsite {
readonly name: string; readonly name: string;
url: string; url: string;
visible: boolean; visible: boolean;
allow: string; allow: string;
allowApi: boolean; allowApi: boolean;
x: number; // In pixels, relative to the map coordinates x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
y: number; // In pixels, relative to the map coordinates y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
width: number; // In pixels, sensitive to zoom level width: number; // In "game" pixels
height: number; // In pixels, sensitive to zoom level height: number; // In "game" pixels
origin: "player" | "map";
scale: number;
} }
``` ```
When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map. When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map.
{.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them
{.alert.alert-warning} to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users.
The websites you add/edit/delete via the scripting API are only shown locally. If you want them
to be displayed for every player, you can use [variables](api-start.md) to share a common state
between all users.

View file

@ -22,6 +22,8 @@ export const isEmbeddedWebsiteEvent = new tg.IsInterface()
y: tg.isNumber, y: tg.isNumber,
width: tg.isNumber, width: tg.isNumber,
height: tg.isNumber, height: tg.isNumber,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
}) })
.get(); .get();
@ -35,6 +37,8 @@ export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
visible: tg.isBoolean, visible: tg.isBoolean,
allowApi: tg.isBoolean, allowApi: tg.isBoolean,
allow: tg.isString, allow: tg.isString,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
}) })
.get(); .get();

View file

@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface()
tags: tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
variables: tg.isObject, variables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
playerVariables: tg.isObject,
}) })
.get(); .get();
/** /**

View file

@ -30,6 +30,8 @@ import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEv
import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent"; import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import { isColorEvent } from "./ColorEvent"; import { isColorEvent } from "./ColorEvent";
import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -50,6 +52,7 @@ export type IframeEventMap = {
displayBubble: null; displayBubble: null;
removeBubble: null; removeBubble: null;
onPlayerMove: undefined; onPlayerMove: undefined;
onCameraUpdate: undefined;
showLayer: LayerEvent; showLayer: LayerEvent;
hideLayer: LayerEvent; hideLayer: LayerEvent;
setProperty: SetPropertyEvent; setProperty: SetPropertyEvent;
@ -82,6 +85,7 @@ export interface IframeResponseEventMap {
leaveZoneEvent: ChangeZoneEvent; leaveZoneEvent: ChangeZoneEvent;
buttonClickedEvent: ButtonClickedEvent; buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent; hasPlayerMoved: HasPlayerMovedEvent;
wasCameraUpdated: WasCameraUpdatedEvent;
menuItemClicked: MenuItemClickedEvent; menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent; setVariable: SetVariableEvent;
messageTriggered: MessageReferenceEvent; messageTriggered: MessageReferenceEvent;
@ -161,6 +165,10 @@ export const iframeQueryMapTypeGuards = {
query: tg.isUndefined, query: tg.isUndefined,
answer: tg.isUndefined, answer: tg.isUndefined,
}, },
getPlayerPosition: {
query: tg.isUndefined,
answer: isPlayerPosition,
},
}; };
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;

View file

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

View file

@ -4,6 +4,7 @@ export const isSetVariableEvent = new tg.IsInterface()
.withProperties({ .withProperties({
key: tg.isString, key: tg.isString,
value: tg.isUnknown, value: tg.isUnknown,
target: tg.isSingletonStringUnion("global", "player"),
}) })
.get(); .get();
/** /**

View file

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

View file

@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
@ -85,6 +86,9 @@ class IframeListener {
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject(); private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable(); public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly _trackCameraUpdateStream: Subject<LoadSoundEvent> = new Subject();
public readonly trackCameraUpdateStream = this._trackCameraUpdateStream.asObservable();
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject(); private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable(); public readonly setTilesStream = this._setTilesStream.asObservable();
@ -226,6 +230,8 @@ class IframeListener {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true; this.sendPlayerMove = true;
} else if (payload.type == "onCameraUpdate") {
this._trackCameraUpdateStream.next();
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data); this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
@ -442,6 +448,13 @@ class IframeListener {
} }
} }
sendCameraUpdated(event: WasCameraUpdatedEvent) {
this.postMessage({
type: "wasCameraUpdated",
data: event,
});
}
sendButtonClickedEvent(popupId: number, buttonId: number): void { sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({ this.postMessage({
type: "buttonClickedEvent", type: "buttonClickedEvent",

View file

@ -12,6 +12,8 @@ export class EmbeddedWebsite {
private _allow: string; private _allow: string;
private _allowApi: boolean; private _allowApi: boolean;
private _position: Rectangle; private _position: Rectangle;
private readonly origin: "map" | "player" | undefined;
private _scale: number;
constructor(private config: CreateEmbeddedWebsiteEvent) { constructor(private config: CreateEmbeddedWebsiteEvent) {
this.name = config.name; this.name = config.name;
@ -20,6 +22,12 @@ export class EmbeddedWebsite {
this._allow = config.allow ?? ""; this._allow = config.allow ?? "";
this._allowApi = config.allowApi ?? false; this._allowApi = config.allowApi ?? false;
this._position = config.position; this._position = config.position;
this.origin = config.origin;
this._scale = config.scale ?? 1;
}
public get url() {
return this._url;
} }
public set url(url: string) { public set url(url: string) {
@ -33,6 +41,10 @@ export class EmbeddedWebsite {
}); });
} }
public get visible() {
return this._visible;
}
public set visible(visible: boolean) { public set visible(visible: boolean) {
this._visible = visible; this._visible = visible;
sendToWorkadventure({ sendToWorkadventure({
@ -44,6 +56,10 @@ export class EmbeddedWebsite {
}); });
} }
public get x() {
return this._position.x;
}
public set x(x: number) { public set x(x: number) {
this._position.x = x; this._position.x = x;
sendToWorkadventure({ sendToWorkadventure({
@ -55,6 +71,10 @@ export class EmbeddedWebsite {
}); });
} }
public get y() {
return this._position.y;
}
public set y(y: number) { public set y(y: number) {
this._position.y = y; this._position.y = y;
sendToWorkadventure({ sendToWorkadventure({
@ -66,6 +86,10 @@ export class EmbeddedWebsite {
}); });
} }
public get width() {
return this._position.width;
}
public set width(width: number) { public set width(width: number) {
this._position.width = width; this._position.width = width;
sendToWorkadventure({ sendToWorkadventure({
@ -77,6 +101,10 @@ export class EmbeddedWebsite {
}); });
} }
public get height() {
return this._position.height;
}
public set height(height: number) { public set height(height: number) {
this._position.height = height; this._position.height = height;
sendToWorkadventure({ sendToWorkadventure({
@ -87,4 +115,19 @@ export class EmbeddedWebsite {
}, },
}); });
} }
public get scale(): number {
return this._scale;
}
public set scale(scale: number) {
this._scale = scale;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
scale: this._scale,
},
});
}
} }

View file

@ -0,0 +1,29 @@
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { Subject } from "rxjs";
import type { WasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
import { apiCallback } from "./registeredCallbacks";
import { isWasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
const moveStream = new Subject<WasCameraUpdatedEvent>();
export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdventureCameraCommands> {
callbacks = [
apiCallback({
type: "wasCameraUpdated",
typeChecker: isWasCameraUpdatedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
},
}),
];
onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
sendToWorkadventure({
type: "onCameraUpdate",
data: null,
});
return moveStream;
}
}
export default new WorkAdventureCameraCommands();

View file

@ -3,6 +3,7 @@ import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
import { createState } from "./state";
const moveStream = new Subject<HasPlayerMovedEvent>(); const moveStream = new Subject<HasPlayerMovedEvent>();
@ -31,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => {
}; };
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> { export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
readonly state = createState("player");
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: "hasPlayerMoved", type: "hasPlayerMoved",
@ -74,6 +77,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
return uuid; return uuid;
} }
async getPosition(): Promise<Position> {
return await queryWorkadventure({
type: "getPlayerPosition",
data: undefined,
});
}
get userRoomToken(): string | undefined { get userRoomToken(): string | undefined {
if (userRoomToken === undefined) { if (userRoomToken === undefined) {
throw new Error( throw new Error(
@ -102,4 +112,9 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
} }
} }
export type Position = {
x: number;
y: number;
};
export default new WorkadventurePlayerCommands(); export default new WorkadventurePlayerCommands();

View file

@ -8,93 +8,101 @@ import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
const setVariableResolvers = new Subject<SetVariableEvent>();
const variables = new Map<string, unknown>();
const variableSubscribers = new Map<string, Subject<unknown>>();
export const initVariables = (_variables: Map<string, unknown>): void => {
for (const [name, value] of _variables.entries()) {
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
if (!variables.has(name)) {
variables.set(name, value);
}
}
};
setVariableResolvers.subscribe((event) => {
const oldValue = variables.get(event.key);
// If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return;
}*/
variables.set(event.key, event.value);
const subject = variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> { export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
private setVariableResolvers = new Subject<SetVariableEvent>();
private variables = new Map<string, unknown>();
private variableSubscribers = new Map<string, Subject<unknown>>();
constructor(private target: "global" | "player") {
super();
this.setVariableResolvers.subscribe((event) => {
const oldValue = this.variables.get(event.key);
// If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return;
}*/
this.variables.set(event.key, event.value);
const subject = this.variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
}
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: "setVariable", type: "setVariable",
typeChecker: isSetVariableEvent, typeChecker: isSetVariableEvent,
callback: (payloadData) => { callback: (payloadData) => {
setVariableResolvers.next(payloadData); if (payloadData.target === this.target) {
this.setVariableResolvers.next(payloadData);
}
}, },
}), }),
]; ];
// TODO: see how we can remove this method from types exposed to WA.state object
initVariables(_variables: Map<string, unknown>): void {
for (const [name, value] of _variables.entries()) {
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
if (!this.variables.has(name)) {
this.variables.set(name, value);
}
}
}
saveVariable(key: string, value: unknown): Promise<void> { saveVariable(key: string, value: unknown): Promise<void> {
variables.set(key, value); this.variables.set(key, value);
return queryWorkadventure({ return queryWorkadventure({
type: "setVariable", type: "setVariable",
data: { data: {
key, key,
value, value,
target: this.target,
}, },
}); });
} }
loadVariable(key: string): unknown { loadVariable(key: string): unknown {
return variables.get(key); return this.variables.get(key);
} }
hasVariable(key: string): boolean { hasVariable(key: string): boolean {
return variables.has(key); return this.variables.has(key);
} }
onVariableChange(key: string): Observable<unknown> { onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key); let subject = this.variableSubscribers.get(key);
if (subject === undefined) { if (subject === undefined) {
subject = new Subject<unknown>(); subject = new Subject<unknown>();
variableSubscribers.set(key, subject); this.variableSubscribers.set(key, subject);
} }
return subject.asObservable(); return subject.asObservable();
} }
} }
const proxyCommand = new Proxy(new WorkadventureStateCommands(), { export function createState(target: "global" | "player"): WorkadventureStateCommands & { [key: string]: unknown } {
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { return new Proxy(new WorkadventureStateCommands(target), {
if (p in target) { get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
return Reflect.get(target, p, receiver); if (p in target) {
} return Reflect.get(target, p, receiver);
return target.loadVariable(p.toString()); }
}, return target.loadVariable(p.toString());
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { },
// Note: when using "set", there is no way to wait, so we ignore the return of the promise. set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
// User must use WA.state.saveVariable to have error message. // Note: when using "set", there is no way to wait, so we ignore the return of the promise.
target.saveVariable(p.toString(), value); // User must use WA.state.saveVariable to have error message.
return true; target.saveVariable(p.toString(), value);
},
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
return true; return true;
} },
return target.hasVariable(p.toString()); has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
}, if (p in target) {
}) as WorkadventureStateCommands & { [key: string]: unknown }; return true;
}
export default proxyCommand; return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
}

View file

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

View file

@ -22,8 +22,8 @@ const nonce = "nonce";
const notification = "notificationPermission"; const notification = "notificationPermission";
const code = "code"; const code = "code";
const cameraSetup = "cameraSetup"; const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache"; const cacheAPIIndex = "workavdenture-cache";
const userProperties = "user-properties";
class LocalUserStore { class LocalUserStore {
saveUser(localUser: LocalUser) { saveUser(localUser: LocalUser) {
@ -220,6 +220,27 @@ class LocalUserStore {
const cameraSetupValues = localStorage.getItem(cameraSetup); const cameraSetupValues = localStorage.getItem(cameraSetup);
return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined;
} }
getAllUserProperties(): Map<string, unknown> {
const result = new Map<string, string>();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
if (key.startsWith(userProperties + "_")) {
const value = localStorage.getItem(key);
if (value) {
const userKey = key.substr((userProperties + "_").length);
result.set(userKey, JSON.parse(value));
}
}
}
}
return result;
}
setUserProperty(name: string, value: unknown): void {
localStorage.setItem(userProperties + "_" + name, JSON.stringify(value));
}
} }
export const localUserStore = new LocalUserStore(); export const localUserStore = new LocalUserStore();

View file

@ -16,7 +16,8 @@ export class EmbeddedWebsiteManager {
if (website === undefined) { if (website === undefined) {
throw new Error('Cannot find embedded website with name "' + name + '"'); throw new Error('Cannot find embedded website with name "' + name + '"');
} }
const rect = website.iframe.getBoundingClientRect();
const scale = website.scale ?? 1;
return { return {
url: website.url, url: website.url,
name: website.name, name: website.name,
@ -26,9 +27,11 @@ export class EmbeddedWebsiteManager {
position: { position: {
x: website.phaserObject.x, x: website.phaserObject.x,
y: website.phaserObject.y, y: website.phaserObject.y,
width: rect["width"], width: website.phaserObject.width * scale,
height: rect["height"], height: website.phaserObject.height * scale,
}, },
origin: website.origin,
scale: website.scale,
}; };
}); });
@ -59,7 +62,9 @@ export class EmbeddedWebsiteManager {
createEmbeddedWebsiteEvent.position.height, createEmbeddedWebsiteEvent.position.height,
createEmbeddedWebsiteEvent.visible ?? true, createEmbeddedWebsiteEvent.visible ?? true,
createEmbeddedWebsiteEvent.allowApi ?? false, createEmbeddedWebsiteEvent.allowApi ?? false,
createEmbeddedWebsiteEvent.allow ?? "" createEmbeddedWebsiteEvent.allow ?? "",
createEmbeddedWebsiteEvent.origin ?? "map",
createEmbeddedWebsiteEvent.scale ?? 1
); );
} }
); );
@ -107,10 +112,18 @@ export class EmbeddedWebsiteManager {
website.phaserObject.y = embeddedWebsiteEvent.y; website.phaserObject.y = embeddedWebsiteEvent.y;
} }
if (embeddedWebsiteEvent?.width !== undefined) { if (embeddedWebsiteEvent?.width !== undefined) {
website.iframe.style.width = embeddedWebsiteEvent.width + "px"; website.position.width = embeddedWebsiteEvent.width;
website.iframe.style.width = embeddedWebsiteEvent.width / website.phaserObject.scale + "px";
} }
if (embeddedWebsiteEvent?.height !== undefined) { if (embeddedWebsiteEvent?.height !== undefined) {
website.iframe.style.height = embeddedWebsiteEvent.height + "px"; website.position.height = embeddedWebsiteEvent.height;
website.iframe.style.height = embeddedWebsiteEvent.height / website.phaserObject.scale + "px";
}
if (embeddedWebsiteEvent?.scale !== undefined) {
website.phaserObject.scale = embeddedWebsiteEvent.scale;
website.iframe.style.width = website.position.width / embeddedWebsiteEvent.scale + "px";
website.iframe.style.height = website.position.height / embeddedWebsiteEvent.scale + "px";
} }
} }
); );
@ -125,7 +138,9 @@ export class EmbeddedWebsiteManager {
height: number, height: number,
visible: boolean, visible: boolean,
allowApi: boolean, allowApi: boolean,
allow: string allow: string,
origin: "map" | "player" | undefined,
scale: number | undefined
): void { ): void {
if (this.embeddedWebsites.has(name)) { if (this.embeddedWebsites.has(name)) {
throw new Error('An embedded website with the name "' + name + '" already exists in your map'); throw new Error('An embedded website with the name "' + name + '" already exists in your map');
@ -135,9 +150,9 @@ export class EmbeddedWebsiteManager {
name, name,
url, url,
/*x, /*x,
y, y,
width, width,
height,*/ height,*/
allow, allow,
allowApi, allowApi,
visible, visible,
@ -147,6 +162,8 @@ export class EmbeddedWebsiteManager {
width, width,
height, height,
}, },
origin,
scale,
}; };
const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible); const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible);
@ -161,22 +178,43 @@ export class EmbeddedWebsiteManager {
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString(); const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
const scale = embeddedWebsiteEvent.scale ?? 1;
iframe.src = absoluteUrl; iframe.src = absoluteUrl;
iframe.tabIndex = -1; iframe.tabIndex = -1;
iframe.style.width = embeddedWebsiteEvent.position.width + "px"; iframe.style.width = embeddedWebsiteEvent.position.width / scale + "px";
iframe.style.height = embeddedWebsiteEvent.position.height + "px"; iframe.style.height = embeddedWebsiteEvent.position.height / scale + "px";
iframe.style.margin = "0"; iframe.style.margin = "0";
iframe.style.padding = "0"; iframe.style.padding = "0";
iframe.style.border = "none"; iframe.style.border = "none";
const domElement = new DOMElement(
this.gameScene,
embeddedWebsiteEvent.position.x,
embeddedWebsiteEvent.position.y,
iframe
);
domElement.setOrigin(0, 0);
if (embeddedWebsiteEvent.scale) {
domElement.scale = embeddedWebsiteEvent.scale;
}
domElement.setVisible(visible);
switch (embeddedWebsiteEvent.origin) {
case "player":
this.gameScene.CurrentPlayer.add(domElement);
break;
case "map":
default:
this.gameScene.add.existing(domElement);
}
const embeddedWebsite = { const embeddedWebsite = {
...embeddedWebsiteEvent, ...embeddedWebsiteEvent,
phaserObject: this.gameScene.add phaserObject: domElement,
.dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe)
.setVisible(visible)
.setOrigin(0, 0),
iframe: iframe, iframe: iframe,
}; };
if (embeddedWebsiteEvent.allowApi) { if (embeddedWebsiteEvent.allowApi) {
iframeListener.registerIframe(iframe); iframeListener.registerIframe(iframe);
} }

View file

@ -93,6 +93,8 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb"; import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb";
import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore"; import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore";
import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator"; import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator";
import Camera = Phaser.Cameras.Scene2D.Camera;
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -210,6 +212,8 @@ export class GameScene extends DirtyScene {
private objectsByType = new Map<string, ITiledMapObject[]>(); private objectsByType = new Map<string, ITiledMapObject[]>();
private embeddedWebsiteManager!: EmbeddedWebsiteManager; private embeddedWebsiteManager!: EmbeddedWebsiteManager;
private loader: Loader; private loader: Loader;
private lastCameraEvent: WasCameraUpdatedEvent | undefined;
private firstCameraUpdateSent: boolean = false;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({ super({
@ -523,7 +527,9 @@ export class GameScene extends DirtyScene {
object.height, object.height,
object.visible, object.visible,
allowApi ?? false, allowApi ?? false,
"" "",
"map",
1
); );
} }
} }
@ -1100,9 +1106,33 @@ ${escapedMessage}
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.stopSoundStream.subscribe((stopSoundEvent) => { iframeListener.trackCameraUpdateStream.subscribe(() => {
const url = new URL(stopSoundEvent.url, this.MapUrlFile); if (!this.firstCameraUpdateSent) {
soundManager.stopSound(this.sound, url.toString()); this.cameras.main.on("followupdate", (camera: Camera) => {
const cameraEvent: WasCameraUpdatedEvent = {
x: camera.worldView.x,
y: camera.worldView.y,
width: camera.worldView.width,
height: camera.worldView.height,
zoom: camera.scaleManager.zoom,
};
if (
this.lastCameraEvent?.x == cameraEvent.x &&
this.lastCameraEvent?.y == cameraEvent.y &&
this.lastCameraEvent?.width == cameraEvent.width &&
this.lastCameraEvent?.height == cameraEvent.height &&
this.lastCameraEvent?.zoom == cameraEvent.zoom
) {
return;
}
this.lastCameraEvent = cameraEvent;
iframeListener.sendCameraUpdated(cameraEvent);
this.firstCameraUpdateSent = true;
});
iframeListener.sendCameraUpdated(this.cameras.main);
}
}) })
); );
@ -1165,6 +1195,12 @@ ${escapedMessage}
}) })
); );
this.iframeSubscriptionList.push(
iframeListener.setPropertyStream.subscribe((setProperty) => {
this.setPropertyLayer(setProperty.layerName, setProperty.propertyName, setProperty.propertyValue);
})
);
iframeListener.registerAnswerer("openCoWebsite", async (openCoWebsite, source) => { iframeListener.registerAnswerer("openCoWebsite", async (openCoWebsite, source) => {
if (!source) { if (!source) {
throw new Error("Unknown query source"); throw new Error("Unknown query source");
@ -1235,6 +1271,7 @@ ${escapedMessage}
roomId: this.roomUrl, roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [], tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables, variables: this.sharedVariablesManager.variables,
playerVariables: localUserStore.getAllUserProperties(),
userRoomToken: this.connection ? this.connection.userRoomToken : "", userRoomToken: this.connection ? this.connection.userRoomToken : "",
}; };
}); });
@ -1325,6 +1362,22 @@ ${escapedMessage}
}) })
); );
iframeListener.registerAnswerer("setVariable", (event, source) => {
switch (event.target) {
case "global": {
this.sharedVariablesManager.setVariable(event, source);
break;
}
case "player": {
localUserStore.setUserProperty(event.key, event.value);
break;
}
default: {
const _exhaustiveCheck: never = event.target;
}
}
});
iframeListener.registerAnswerer("removeActionMessage", (message) => { iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid); layoutManagerActionStore.removeAction(message.uuid);
}); });
@ -1343,6 +1396,13 @@ ${escapedMessage}
this.CurrentPlayer.removeOutlineColor(); this.CurrentPlayer.removeOutlineColor();
this.connection?.emitPlayerOutlineColor(null); this.connection?.emitPlayerOutlineColor(null);
}); });
iframeListener.registerAnswerer("getPlayerPosition", () => {
return {
x: this.CurrentPlayer.x,
y: this.CurrentPlayer.y,
};
});
} }
private setPropertyLayer( private setPropertyLayer(
@ -1467,6 +1527,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("openCoWebsite");
iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("getCoWebsites");
iframeListener.unregisterAnswerer("setPlayerOutline"); iframeListener.unregisterAnswerer("setPlayerOutline");
iframeListener.unregisterAnswerer("setVariable");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close(); this.embeddedWebsiteManager?.close();
@ -1945,6 +2006,7 @@ ${escapedMessage}
this.loader.resize(); this.loader.resize();
} }
private getObjectLayerData(objectName: string): ITiledMapObject | undefined { private getObjectLayerData(objectName: string): ITiledMapObject | undefined {
for (const layer of this.mapFile.layers) { for (const layer of this.mapFile.layers) {
if (layer.type === "objectgroup" && layer.name === "floorLayer") { if (layer.type === "objectgroup" && layer.name === "floorLayer") {
@ -1957,6 +2019,7 @@ ${escapedMessage}
} }
return undefined; return undefined;
} }
private reposition(): void { private reposition(): void {
// Recompute camera offset if needed // Recompute camera offset if needed
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();

View file

@ -3,6 +3,7 @@ import { iframeListener } from "../../Api/IframeListener";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent";
interface Variable { interface Variable {
defaultValue: unknown; defaultValue: unknown;
@ -48,51 +49,51 @@ export class SharedVariablesManager {
iframeListener.setVariable({ iframeListener.setVariable({
key: name, key: name,
value: value, value: value,
target: "global",
}); });
}); });
}
// When a variable is modified from an iFrame public setVariable(event: SetVariableEvent, source: MessageEventSource | null): void {
iframeListener.registerAnswerer("setVariable", (event, source) => { const key = event.key;
const key = event.key;
const object = this.variableObjects.get(key); const object = this.variableObjects.get(key);
if (object === undefined) { if (object === undefined) {
const errMsg = const errMsg =
'A script is trying to modify variable "' + 'A script is trying to modify variable "' +
key + key +
'" but this variable is not defined in the map.' + '" but this variable is not defined in the map.' +
'There should be an object in the map whose name is "' + 'There should be an object in the map whose name is "' +
key + key +
'" and whose type is "variable"'; '" and whose type is "variable"';
console.error(errMsg); console.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) { if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
const errMsg = const errMsg =
'A script is trying to modify variable "' + 'A script is trying to modify variable "' +
key + key +
'" but this variable is only writable for users with tag "' + '" but this variable is only writable for users with tag "' +
object.writableBy + object.writableBy +
'".'; '".';
console.error(errMsg); console.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
// Let's stop any propagation of the value we set is the same as the existing value. // Let's stop any propagation of the value we set is the same as the existing value.
if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) { if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
return; return;
} }
this._variables.set(key, event.value); this._variables.set(key, event.value);
// Dispatch to the room connection. // Dispatch to the room connection.
this.roomConnection.emitSetVariableEvent(key, event.value); this.roomConnection.emitSetVariableEvent(key, event.value);
// Dispatch to other iframes // Dispatch to other iframes
iframeListener.dispatchVariableToOtherIframes(key, event.value, source); iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
});
} }
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> { private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {

View file

@ -14,25 +14,29 @@ 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, { setMapURL, setRoomId } from "./Api/iframe/room"; import room, { setMapURL, setRoomId } from "./Api/iframe/room";
import state, { initVariables } from "./Api/iframe/state"; import { createState } from "./Api/iframe/state";
import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player"; import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound"; import type { Sound } from "./Api/iframe/Sound/Sound";
import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution"; import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution";
import camera from "./Api/iframe/camera";
const globalState = createState("global");
// Notify WorkAdventure that we are ready to receive data // Notify WorkAdventure that we are ready to receive data
const initPromise = queryWorkadventure({ const initPromise = queryWorkadventure({
type: "getState", type: "getState",
data: undefined, data: undefined,
}).then((state) => { }).then((gameState) => {
setPlayerName(state.nickname); setPlayerName(gameState.nickname);
setRoomId(state.roomId); setRoomId(gameState.roomId);
setMapURL(state.mapUrl); setMapURL(gameState.mapUrl);
setTags(state.tags); setTags(gameState.tags);
setUuid(state.uuid); setUuid(gameState.uuid);
initVariables(state.variables as Map<string, unknown>); globalState.initVariables(gameState.variables as Map<string, unknown>);
setUserRoomToken(state.userRoomToken); player.state.initVariables(gameState.playerVariables as Map<string, unknown>);
setUserRoomToken(gameState.userRoomToken);
}); });
const wa = { const wa = {
@ -43,7 +47,8 @@ const wa = {
sound, sound,
room, room,
player, player,
state, camera,
state: globalState,
onInit(): Promise<void> { onInit(): Promise<void> {
return initPromise; return initPromise;
@ -225,7 +230,5 @@ window.addEventListener(
callback?.callback(payloadData); callback?.callback(payloadData);
} }
} }
// ...
} }
); );

View file

@ -15,6 +15,8 @@
const heightField = document.getElementById('height'); const heightField = document.getElementById('height');
const urlField = document.getElementById('url'); const urlField = document.getElementById('url');
const visibleField = document.getElementById('visible'); const visibleField = document.getElementById('visible');
const originField = document.getElementById('origin');
const scaleField = document.getElementById('scale');
createButton.addEventListener('click', () => { createButton.addEventListener('click', () => {
console.log('CREATING NEW EMBEDDED IFRAME'); console.log('CREATING NEW EMBEDDED IFRAME');
@ -28,6 +30,8 @@
height: parseInt(heightField.value), height: parseInt(heightField.value),
}, },
visible: !!visibleField.value, visible: !!visibleField.value,
origin: originField.value,
scale: parseFloat(scaleField.value),
}); });
}); });
@ -61,6 +65,16 @@
const website = await WA.room.website.get('test'); const website = await WA.room.website.get('test');
website.visible = this.checked; website.visible = this.checked;
}); });
originField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.origin = this.value;
});
scaleField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.scale = parseFloat(this.value);
});
}); });
}) })
</script> </script>
@ -72,6 +86,8 @@ width: <input type="text" id="width" value="600" /><br/>
height: <input type="text" id="height" value="400" /><br/> height: <input type="text" id="height" value="400" /><br/>
URL: <input type="text" id="url" value="https://mensuel.framapad.org/p/rt6c904745-9oxm?lang=en" /><br/> URL: <input type="text" id="url" value="https://mensuel.framapad.org/p/rt6c904745-9oxm?lang=en" /><br/>
Visible: <input type="checkbox" id="visible" value=1 /><br/> Visible: <input type="checkbox" id="visible" value=1 /><br/>
Origin: <input type="text" id="origin" value="map" /><br/>
Scale: <input type="text" id="scale" value=1 /><br/>
<button id="createEmbeddedWebsite">Create embedded website</button> <button id="createEmbeddedWebsite">Create embedded website</button>