diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index 9af0b1c2..35d5f464 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -106,3 +106,25 @@ Example : ```javascript WA.player.onPlayerMove(console.log); ``` + +### Set the outline color of the player +``` +WA.player.setOutlineColor(red: number, green: number, blue: number): Promise; +WA.player.removeOutlineColor(): Promise; +``` + +You can display a thin line around your player's name (the "outline"). + +Use `setOutlineColor` to set the outline and `removeOutlineColor` to remove it. + +Colors are expressed in RGB. Each parameter is an integer between 0 and 255. + +```typescript +// Let's add a red outline to our player +WA.player.setOutlineColor(255, 0, 0); +``` + +When you set the outline on your player, other players will see the outline too (the outline color is shared across +browsers automatically). + +![](images/outlines.png) diff --git a/front/src/Api/Events/ColorEvent.ts b/front/src/Api/Events/ColorEvent.ts new file mode 100644 index 00000000..c8e6d349 --- /dev/null +++ b/front/src/Api/Events/ColorEvent.ts @@ -0,0 +1,13 @@ +import * as tg from "generic-type-guard"; + +export const isColorEvent = new tg.IsInterface() + .withProperties({ + red: tg.isNumber, + green: tg.isNumber, + blue: tg.isNumber, + }) + .get(); +/** + * A message sent from the iFrame to the game to dynamically set the outline of the player. + */ +export type ColorEvent = tg.GuardedType; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index c338ddbe..2871b93c 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -29,6 +29,7 @@ import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/Trigg import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeZoneEvent } from "./ChangeZoneEvent"; +import { isColorEvent } from "./ColorEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -152,6 +153,14 @@ export const iframeQueryMapTypeGuards = { query: isCreateEmbeddedWebsiteEvent, answer: tg.isUndefined, }, + setPlayerOutline: { + query: isColorEvent, + answer: tg.isUndefined, + }, + removePlayerOutline: { + query: tg.isUndefined, + answer: tg.isUndefined, + }, }; type GuardedType = T extends (x: unknown) => x is infer T ? T : never; diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index c46f3fbc..2d187bf5 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -1,4 +1,4 @@ -import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent"; import { Subject } from "rxjs"; import { apiCallback } from "./registeredCallbacks"; @@ -82,6 +82,24 @@ export class WorkadventurePlayerCommands extends IframeApiContribution { + return queryWorkadventure({ + type: "setPlayerOutline", + data: { + red, + green, + blue, + }, + }); + } + + public removeOutlineColor(): Promise { + return queryWorkadventure({ + type: "removePlayerOutline", + data: undefined, + }); + } } export default new WorkadventurePlayerCommands(); diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 2e0bd363..3281afe3 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -13,7 +13,8 @@ import { isSilentStore } from "../../Stores/MediaStore"; import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager"; import { TexturesHelper } from "../Helpers/TexturesHelper"; import type { PictureStore } from "../../Stores/PictureStore"; -import { Writable, writable } from "svelte/store"; +import { Unsubscriber, Writable, writable } from "svelte/store"; +import { createColorStore } from "../../Stores/OutlineColorStore"; const playerNameY = -25; @@ -40,6 +41,8 @@ export abstract class Character extends Container { private emoteTween: Phaser.Tweens.Tween | null = null; scene: GameScene; private readonly _pictureStore: Writable; + private readonly outlineColorStore = createColorStore(); + private readonly outlineColorStoreUnsubscribe: Unsubscriber; constructor( scene: GameScene, @@ -97,18 +100,26 @@ export abstract class Character extends Container { }); this.on("pointerover", () => { - this.getOutlinePlugin()?.add(this.playerName, { - thickness: 2, - outlineColor: 0xffff00, - }); - this.scene.markDirty(); + this.outlineColorStore.pointerOver(); }); this.on("pointerout", () => { - this.getOutlinePlugin()?.remove(this.playerName); - this.scene.markDirty(); + this.outlineColorStore.pointerOut(); }); } + this.outlineColorStoreUnsubscribe = this.outlineColorStore.subscribe((color) => { + if (color === undefined) { + this.getOutlinePlugin()?.remove(this.playerName); + } else { + this.getOutlinePlugin()?.remove(this.playerName); + this.getOutlinePlugin()?.add(this.playerName, { + thickness: 2, + outlineColor: color, + }); + } + this.scene.markDirty(); + }); + scene.add.existing(this); this.scene.physics.world.enableBody(this); @@ -315,6 +326,7 @@ export abstract class Character extends Container { } } this.list.forEach((objectContaining) => objectContaining.destroy()); + this.outlineColorStoreUnsubscribe(); super.destroy(); } @@ -401,4 +413,12 @@ export abstract class Character extends Container { public get pictureStore(): PictureStore { return this._pictureStore; } + + public setOutlineColor(red: number, green: number, blue: number): void { + this.outlineColorStore.setColor((red << 16) | (green << 8) | blue); + } + + public removeOutlineColor(): void { + this.outlineColorStore.removeColor(); + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ae89e2c3..abe9137b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1300,6 +1300,18 @@ ${escapedMessage} iframeListener.registerAnswerer("removeActionMessage", (message) => { layoutManagerActionStore.removeAction(message.uuid); }); + + iframeListener.registerAnswerer("setPlayerOutline", (message) => { + const normalizeColor = (color: number) => Math.min(Math.max(0, Math.round(color)), 255); + const red = normalizeColor(message.red); + const green = normalizeColor(message.green); + const blue = normalizeColor(message.blue); + this.CurrentPlayer.setOutlineColor(red, green, blue); + }); + + iframeListener.registerAnswerer("removePlayerOutline", (message) => { + this.CurrentPlayer.removeOutlineColor(); + }); } private setPropertyLayer( @@ -1422,6 +1434,7 @@ ${escapedMessage} iframeListener.unregisterAnswerer("removeActionMessage"); iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("getCoWebsites"); + iframeListener.unregisterAnswerer("setPlayerOutline"); this.sharedVariablesManager?.close(); this.embeddedWebsiteManager?.close(); diff --git a/front/src/Stores/OutlineColorStore.ts b/front/src/Stores/OutlineColorStore.ts new file mode 100644 index 00000000..1618eebc --- /dev/null +++ b/front/src/Stores/OutlineColorStore.ts @@ -0,0 +1,40 @@ +import { writable } from "svelte/store"; + +export function createColorStore() { + const { subscribe, set } = writable(undefined); + + let color: number | undefined = undefined; + let focused: boolean = false; + + const updateColor = () => { + if (focused) { + set(0xffff00); + } else { + set(color); + } + }; + + return { + subscribe, + + pointerOver() { + focused = true; + updateColor(); + }, + + pointerOut() { + focused = false; + updateColor(); + }, + + setColor(newColor: number) { + color = newColor; + updateColor(); + }, + + removeColor() { + color = undefined; + updateColor(); + }, + }; +} diff --git a/maps/tests/Outline/outline.json b/maps/tests/Outline/outline.json new file mode 100644 index 00000000..476fe25c --- /dev/null +++ b/maps/tests/Outline/outline.json @@ -0,0 +1,93 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"outline.php" + }, + { + "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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":342.082007343941, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":13, + "text":"Test:\nPlay with the colors and the limits in the form\n\nResult:\nThe outline should be displayed. A mouse over displays the yellow outline but the normal outline comes back on mouse out.\n\nTest:\nClick the remove outline\n\nResult:\nThe outline is removed\n\nTest:\nClick with many players\n\nResult:\nThe outline is correctly shared", + "wrap":true + }, + "type":"", + "visible":true, + "width":274.96422378621, + "x":35.7623688177162, + "y":8.73391812865529 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"2021.03.23", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.5, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Outline/outline.php b/maps/tests/Outline/outline.php new file mode 100644 index 00000000..244ca4bc --- /dev/null +++ b/maps/tests/Outline/outline.php @@ -0,0 +1,36 @@ + + + + + + + +red:
+green:
+blue:
+ + + + + + + diff --git a/maps/tests/index.html b/maps/tests/index.html index c920c876..ffe389b3 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -251,6 +251,14 @@ Testing scripting API for enters/leaves layer + + + Success Failure Pending + + + Testing scripting API for outline on players + +

CoWebsite

diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 117ab582..d210c42e 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -13,6 +13,7 @@ message PositionMessage { } Direction direction = 3; bool moving = 4; + uint32 outlineColor = 5; } message PointMessage {