merged develop
16
docs/dev/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Developer documentation
|
||||
|
||||
This (work in progress) documentation provides a number of "how-to" guides explaining how to work on the WorkAdventure
|
||||
code.
|
||||
|
||||
This documentation is targeted at developers looking to open Pull Requests on WorkAdventure.
|
||||
|
||||
If you "only" want to design dynamic maps, please refer instead to the [scripting API documentation](https://workadventu.re/map-building/scripting.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out the [contributing guide](../../CONTRIBUTING.md)
|
||||
|
||||
## Front documentation
|
||||
|
||||
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)
|
276
docs/dev/contributing-to-scripting-api.md
Normal file
|
@ -0,0 +1,276 @@
|
|||
# How to add new functions in the scripting API
|
||||
|
||||
This documentation is intended at contributors who want to participate in the development of WorkAdventure itself.
|
||||
Before reading this, please be sure you are familiar with the [scripting API](https://workadventu.re/map-building/scripting.md).
|
||||
|
||||
The [scripting API](https://workadventu.re/map-building/scripting.md) allows map developers to add dynamic features in their maps.
|
||||
|
||||
## Why extend the scripting API?
|
||||
|
||||
The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to
|
||||
offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance,
|
||||
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
|
||||
|
||||
We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that
|
||||
a feature is missing in the API. This article is here to explain to you how to add this feature.
|
||||
|
||||
## How to extend the scripting API?
|
||||
|
||||
Extending the scripting API means modifying the core of WorkAdventure. You can of course run these
|
||||
modifications on your self-hosted instance.
|
||||
But if you want to share it with the wider community, I strongly encourage you to start by [opening an issue](https://github.com/thecodingmachine/workadventure/issues)
|
||||
on GitHub before starting the development. Check with the core maintainers that they are willing to merge your idea
|
||||
before starting developing it. Once a new function makes it into the scripting API, it is very difficult to make it
|
||||
evolve (or to deprecate), so the design of the function you add needs to be carefully considered.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Scripts are executed in the browser, inside an iframe.
|
||||
|
||||

|
||||
|
||||
The iframe allows WorkAdventure to isolate the script in a sandbox. Because the iframe is sandbox (or on a different
|
||||
domain than the WorkAdventure server), scripts cannot directly manipulate the DOM of WorkAdventure. They also cannot
|
||||
directly access Phaser objects (Phaser is the game engine used in WorkAdventure). This is by-design. Since anyone
|
||||
can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be
|
||||
a huge XSS security flaw).
|
||||
|
||||
Instead, the only way the script can interact with WorkAdventure is by sending messages using the
|
||||
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
||||
|
||||

|
||||
|
||||
We want to make life easy for map developers. So instead of asking them to directly send messages using the postMessage
|
||||
API, we provide a nice library that does this work for them. This library is what we call the "Scripting API" (we sometimes
|
||||
refer to it as the "Client API").
|
||||
|
||||
The scripting API provides the global `WA` object.
|
||||
|
||||
## A simple example
|
||||
|
||||
So let's take an example with a sample script:
|
||||
|
||||
```typescript
|
||||
WA.chat.sendChatMessage('Hello world!', 'John Doe');
|
||||
```
|
||||
|
||||
When this script is called, the scripting API is dispatching a JSON message to WorkAdventure.
|
||||
|
||||
In our case, the `sendChatMessage` function looks like this:
|
||||
|
||||
**src/Api/iframe/chat.ts**
|
||||
```typescript
|
||||
sendChatMessage(message: string, author: string) {
|
||||
sendToWorkadventure({
|
||||
type: "chat",
|
||||
data: {
|
||||
message: message,
|
||||
author: author,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `sendToWorkadventure` function is a utility function that dispatches the message to the main frame.
|
||||
|
||||
In WorkAdventure, the message is received in the [`IframeListener` listener class](http://github.com/thecodingmachine/workadventure/blob/1e6ce4dec8697340e2c91798864b94da9528b482/front/src/Api/IframeListener.ts#L200-L203).
|
||||
This class is in charge of analyzing the JSON messages received and dispatching them to the right place in the WorkAdventure application.
|
||||
|
||||
The message callback implemented in `IframeListener` is a giant (and disgusting) `if` statement branching to the correct
|
||||
part of the code depending on the `type` property.
|
||||
|
||||
**src/Api/IframeListener.ts**
|
||||
```typescript
|
||||
// ...
|
||||
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
|
||||
this._setPropertyStream.next(payload.data);
|
||||
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
|
||||
scriptUtils.sendAnonymousChat(payload.data);
|
||||
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
|
||||
// ...
|
||||
```
|
||||
|
||||
In this particular case, we call `scriptUtils.sendAnonymousChat` that is doing the work of displaying the chat message.
|
||||
|
||||
## Scripting API entry point
|
||||
|
||||
The `WA` object originates from the scripting API. This script is hosted on the front server, at `https://[front_WA_server]/iframe_api.js.`.
|
||||
|
||||
The entry point for this script is the file `front/src/iframe_api.ts`.
|
||||
All the other files dedicated to the iframe API are located in the `src/Api/iframe` directory.
|
||||
|
||||
## Utility functions to exchange messages
|
||||
|
||||
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
|
||||
[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function.
|
||||
|
||||
Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes.
|
||||
We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this.
|
||||
|
||||
Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function
|
||||
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
|
||||
[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function.
|
||||
|
||||
## Types
|
||||
|
||||
The JSON messages sent over the postMessage API are strictly defined using Typescript types.
|
||||
Those types are not defined using classical Typescript interfaces.
|
||||
|
||||
Indeed, Typescript interfaces only exist at compilation time but cannot be enforced on runtime. The postMessage API
|
||||
is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could
|
||||
send specially crafted JSON packages to try to hack WA).
|
||||
|
||||
In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package
|
||||
allows us to create interfaces AND custom type guards in one go.
|
||||
|
||||
Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API:
|
||||
|
||||
```typescript
|
||||
sendToWorkadventure({
|
||||
type: "chat",
|
||||
data: {
|
||||
message: message,
|
||||
author: author,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`:
|
||||
|
||||
```typescript
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isChatEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
message: tg.isString,
|
||||
author: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
|
||||
```
|
||||
|
||||
Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`).
|
||||
From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code.
|
||||
|
||||
The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received
|
||||
over the postMessage API is indeed correctly formatted.
|
||||
|
||||
If you are not familiar with Typescript type guards, you can read [an introduction to type guards in the Typescript documentation](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards).
|
||||
|
||||
### Typing one way messages
|
||||
|
||||
For "one-way" messages (from the iframe to WorkAdventure), the `sendToWorkadventure` method expects the passed
|
||||
object to be of type `IframeEvent<keyof IframeEventMap>`.
|
||||
|
||||
Note: I'd like here to thank @jonnytest1 for helping set up this type system. It rocks ;)
|
||||
|
||||
The `IFrameEvent` type is defined in `front/src/Api/Events/IframeEvent.ts`:
|
||||
|
||||
```typescript
|
||||
export type IframeEventMap = {
|
||||
loadPage: LoadPageEvent;
|
||||
chat: ChatEvent;
|
||||
openPopup: OpenPopupEvent;
|
||||
closePopup: ClosePopupEvent;
|
||||
openTab: OpenTabEvent;
|
||||
// ...
|
||||
// All the possible messages go here
|
||||
// The key goes into the "type" JSON property
|
||||
// ...
|
||||
};
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
data: IframeEventMap[T];
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, if you want to type messages from WorkAdventure to the iframe, there is a very similar `IframeResponseEvent`.
|
||||
|
||||
```typescript
|
||||
export interface IframeResponseEventMap {
|
||||
userInputChat: UserInputChatEvent;
|
||||
enterEvent: EnterLeaveEvent;
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
// ...
|
||||
// All the possible messages go here
|
||||
// The key goes into the "type" JSON property
|
||||
// ...
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
data: IframeResponseEventMap[T];
|
||||
}
|
||||
```
|
||||
|
||||
### Typing queries (messages with answers)
|
||||
|
||||
If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to
|
||||
define the type of the query and the type of the response.
|
||||
|
||||
The signature of `queryWorkadventure` is:
|
||||
|
||||
```typescript
|
||||
function queryWorkadventure<T extends keyof IframeQueryMap>(
|
||||
content: IframeQuery<T>
|
||||
): Promise<IframeQueryMap[T]["answer"]>
|
||||
```
|
||||
|
||||
Yes, that's a bit cryptic. Hopefully, all you need to know is that to add a new query, you need to edit the `iframeQueryMapTypeGuards`
|
||||
array in `front/src/Api/Events/IframeEvent.ts`:
|
||||
|
||||
```typescript
|
||||
export const iframeQueryMapTypeGuards = {
|
||||
openCoWebsite: {
|
||||
query: isOpenCoWebsiteEvent,
|
||||
answer: isCoWebsite,
|
||||
},
|
||||
getCoWebsites: {
|
||||
query: tg.isUndefined,
|
||||
answer: tg.isArray(isCoWebsite),
|
||||
},
|
||||
// ...
|
||||
// the `query` key points to the type guard of the query
|
||||
// the `answer` key points to the type guard of the response
|
||||
};
|
||||
```
|
||||
|
||||
### Responding to a query on the WorkAdventure side
|
||||
|
||||
In the WorkAdventure code, each possible query should be handled by what we call an "answerer".
|
||||
|
||||
Registering an answerer happens using the `iframeListener.registerAnswerer()` method.
|
||||
|
||||
Here is a sample:
|
||||
|
||||
```typescript
|
||||
iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => {
|
||||
// ...
|
||||
|
||||
return /*...*/;
|
||||
});
|
||||
```
|
||||
|
||||
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
|
||||
(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`).
|
||||
|
||||
Important:
|
||||
|
||||
- there can be only one answerer registered for a given query type.
|
||||
- if the answerer is not valid any more, you need to unregister the answerer using `iframeListener.unregisterAnswerer`.
|
||||
|
||||
|
||||
## sendToWorkadventure VS queryWorkadventure
|
||||
|
||||
- `sendToWorkadventure` is used to send messages one way from the iframe to WorkAdventure. No response is expected. In particular
|
||||
if an error happens in WorkAdventure, the iframe will not be notified.
|
||||
- `queryWorkadventure` is used to send queries that expect an answer. If an error happens in WorkAdventure (i.e. if an
|
||||
exception is raised), the exception will be propagated to the iframe.
|
||||
|
||||
Because `queryWorkadventure` handles exceptions properly, it can be interesting to use `queryWorkadventure` instead
|
||||
of `sendToWorkadventure`, even for "one-way" messages. The return message type is simply `undefined` in this case.
|
||||
|
1
docs/dev/images/scripting_1.svg
Normal file
After Width: | Height: | Size: 86 KiB |
1
docs/dev/images/scripting_2.svg
Normal file
After Width: | Height: | Size: 64 KiB |
|
@ -79,6 +79,34 @@ WA.onInit().then(() => {
|
|||
```
|
||||
|
||||
|
||||
### Get the user-room token of the player
|
||||
|
||||
```
|
||||
WA.player.userRoomToken: string;
|
||||
```
|
||||
|
||||
The user-room token is available from the `WA.player.userRoomToken` property.
|
||||
|
||||
This token can be used by third party services to authenticate a player and prove that the player is in a given room.
|
||||
The token is generated by the administration panel linked to WorkAdventure. The token is a string and is depending on your implementation of the administration panel.
|
||||
In WorkAdventure SAAS version, the token is a JWT token that contains information such as the player's room ID and its associated membership ID.
|
||||
|
||||
If you are using the self-hosted version of WorkAdventure and you developed your own administration panel, the token can be anything.
|
||||
By default, self-hosted versions of WorkAdventure don't come with an administration panel, so the token string will be empty.
|
||||
|
||||
{.alert.alert-info}
|
||||
A typical use-case for the user-room token is providing logo upload capabilities in a map.
|
||||
The token can be used as a way to authenticate a WorkAdventure player and ensure he is indeed in the map and authorized to upload a logo.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.player.userRoomToken`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Token: ', WA.player.userRoomToken);
|
||||
})
|
||||
```
|
||||
|
||||
### Listen to player movement
|
||||
```
|
||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||
|
|
86
docs/maps/camera.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
{.section-title.accent.text-primary}
|
||||
# Working with camera
|
||||
|
||||
## Focusable Zones
|
||||
|
||||
It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/0_focusable_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
### Adding new **Focusable Zone**:
|
||||
|
||||
1. Make sure you are editing an **Object Layer**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/1_object_layer.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Select **Insert Rectangle** tool
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/2_rectangle_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
3. Define new object wherever you want. For example, you can make your chilling room event cosier!
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/3_define_new_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
4. Edit this new object and click on **Add Property**, like this:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/4_click_add_property.png" alt="" />
|
||||
</div>
|
||||
|
||||
5. Add a **bool** property of name *focusable*:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/5_add_focusable_prop.png" alt="" />
|
||||
</div>
|
||||
|
||||
6. Make sure it's checked! :)
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/6_make_sure_checked.png" alt="" />
|
||||
</div>
|
||||
|
||||
All should be set up now and your new **Focusable Zone** should be working fine!
|
||||
|
||||
### Defining custom zoom margin:
|
||||
|
||||
If you want, you can add an additional property to control how much should the camera zoom onto focusable zone.
|
||||
|
||||
1. Like before, click on **Add Property**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/4_click_add_property.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Add a **float** property of name *zoom_margin*:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/7_add_zoom_margin.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Define how much (in percentage value) should the zoom be decreased:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/8_optional_zoom_margin_defined.png" alt="" />
|
||||
</div>
|
||||
|
||||
For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300.
|
||||
|
||||
- No margin defined
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/no_margin.png" alt="" />
|
||||
</div>
|
||||
|
||||
- Margin set to **0.35**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/with_margin.png" alt="" />
|
||||
</div>
|
BIN
docs/maps/images/camera/0_focusable_zone.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
docs/maps/images/camera/1_object_layer.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/maps/images/camera/2_rectangle_zone.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
docs/maps/images/camera/3_define_new_zone.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/maps/images/camera/4_click_add_property.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
docs/maps/images/camera/5_add_focusable_prop.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/maps/images/camera/6_make_sure_checked.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
docs/maps/images/camera/7_add_zoom_margin.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/maps/images/camera/8_optional_zoom_margin_defined.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
docs/maps/images/camera/no_margin.png
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
docs/maps/images/camera/with_margin.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
docs/maps/images/mapProperties.png
Normal file
After Width: | Height: | Size: 130 KiB |
|
@ -51,6 +51,12 @@ return [
|
|||
'markdown' => 'maps.website-in-map',
|
||||
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md',
|
||||
],
|
||||
[
|
||||
'title' => 'Camera',
|
||||
'url' => '/map-building/camera.md',
|
||||
'markdown' => 'maps.camera',
|
||||
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/camera.md',
|
||||
],
|
||||
[
|
||||
'title' => 'Variables',
|
||||
'url' => '/map-building/variables.md',
|
||||
|
|
|
@ -92,3 +92,19 @@ You can add properties either on individual tiles of a tileset OR on a complete
|
|||
If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer.
|
||||
|
||||
The exception is the "collides" property that can only be set on tiles, but not on a complete layer.
|
||||
|
||||
## Insert helpful information in your map
|
||||
|
||||
By setting properties on the map itself, you can help visitors know more about the creators of the map.
|
||||
|
||||
The following *map* properties are supported:
|
||||
* `mapName` (string)
|
||||
* `mapDescription` (string)
|
||||
* `mapCopyright` (string)
|
||||
|
||||
And *each tileset* can also have a property called `tilesetCopyright` (string).
|
||||
|
||||
Resulting in a "credit" page in the menu looking like this:
|
||||
|
||||
{.document-img}
|
||||
|
||||
|
|