Actions menu api (#1862)

* wip

* wip

* random action on click

* removing actions

* register single key per command use

* change removeActionsMenu action name

* fixed actions menu not hiding content properly:

* actions menu fix

* added mock Block Player action

* ActionsMenu buttons styling

* added displaying priority for menu actions

* moved utils actionMenu features to the UI

* import as a type:

* more object oriented style for API

* removed registered actions from RemotePlayer instance

* readme update

* Fixing typos / Improving wording

* added instructions on AlterActionsMenu test map

Co-authored-by: Hanusiak Piotr <piotr@ltmp.co>
Co-authored-by: David Négrier <d.negrier@thecodingmachine.com>
This commit is contained in:
Piotr Hanusiak
2022-03-14 10:15:10 +01:00
committed by GitHub
parent 55db6a9b12
commit d4dcd0d5ce
16 changed files with 565 additions and 41 deletions
@@ -0,0 +1,12 @@
import * as tg from "generic-type-guard";
export const isActionsMenuActionClickedEvent = new tg.IsInterface()
.withProperties({
id: tg.isNumber,
actionName: tg.isString,
})
.get();
export type ActionsMenuActionClickedEvent = tg.GuardedType<typeof isActionsMenuActionClickedEvent>;
export type ActionsMenuActionClickedEventCallback = (event: ActionsMenuActionClickedEvent) => void;
@@ -0,0 +1,12 @@
import * as tg from "generic-type-guard";
export const isAddActionsMenuKeyToRemotePlayerEvent = new tg.IsInterface()
.withProperties({
id: tg.isNumber,
actionKey: tg.isString,
})
.get();
export type AddActionsMenuKeyToRemotePlayerEvent = tg.GuardedType<typeof isAddActionsMenuKeyToRemotePlayerEvent>;
export type AddActionsMenuKeyToRemotePlayerEventCallback = (event: AddActionsMenuKeyToRemotePlayerEvent) => void;
+9
View File
@@ -36,6 +36,10 @@ import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
import { isColorEvent } from "./ColorEvent";
import { isMovePlayerToEventConfig } from "./MovePlayerToEvent";
import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer";
import type { RemotePlayerClickedEvent } from "./RemotePlayerClickedEvent";
import type { AddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent";
import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent";
import type { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@@ -45,6 +49,8 @@ export interface TypedMessageEvent<T> extends MessageEvent {
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = {
addActionsMenuKeyToRemotePlayer: AddActionsMenuKeyToRemotePlayerEvent;
removeActionsMenuKeyFromRemotePlayer: RemoveActionsMenuKeyFromRemotePlayerEvent;
loadPage: LoadPageEvent;
chat: ChatEvent;
cameraFollowPlayer: CameraFollowPlayerEvent;
@@ -58,6 +64,7 @@ export type IframeEventMap = {
displayBubble: null;
removeBubble: null;
onPlayerMove: undefined;
onOpenActionMenu: undefined;
onCameraUpdate: undefined;
showLayer: LayerEvent;
hideLayer: LayerEvent;
@@ -90,6 +97,8 @@ export interface IframeResponseEventMap {
enterZoneEvent: ChangeZoneEvent;
leaveZoneEvent: ChangeZoneEvent;
buttonClickedEvent: ButtonClickedEvent;
remotePlayerClickedEvent: RemotePlayerClickedEvent;
actionsMenuActionClickedEvent: ActionsMenuActionClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent;
wasCameraUpdated: WasCameraUpdatedEvent;
menuItemClicked: MenuItemClickedEvent;
@@ -0,0 +1,15 @@
import * as tg from "generic-type-guard";
// TODO: Change for player Clicked, add all neccessary data
export const isRemotePlayerClickedEvent = new tg.IsInterface()
.withProperties({
id: tg.isNumber,
})
.get();
/**
* A message sent from the game to the iFrame when RemotePlayer is clicked.
*/
export type RemotePlayerClickedEvent = tg.GuardedType<typeof isRemotePlayerClickedEvent>;
export type RemotePlayerClickedEventCallback = (event: RemotePlayerClickedEvent) => void;
@@ -0,0 +1,16 @@
import * as tg from "generic-type-guard";
export const isRemoveActionsMenuKeyFromRemotePlayerEvent = new tg.IsInterface()
.withProperties({
id: tg.isNumber,
actionKey: tg.isString,
})
.get();
export type RemoveActionsMenuKeyFromRemotePlayerEvent = tg.GuardedType<
typeof isRemoveActionsMenuKeyFromRemotePlayerEvent
>;
export type RemoveActionsMenuKeyFromRemotePlayerEventCallback = (
event: RemoveActionsMenuKeyFromRemotePlayerEvent
) => void;
+43
View File
@@ -34,6 +34,16 @@ import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
import { CameraSetEvent, isCameraSetEvent } from "./Events/CameraSetEvent";
import { CameraFollowPlayerEvent, isCameraFollowPlayerEvent } from "./Events/CameraFollowPlayerEvent";
import type { RemotePlayerClickedEvent } from "./Events/RemotePlayerClickedEvent";
import {
AddActionsMenuKeyToRemotePlayerEvent,
isAddActionsMenuKeyToRemotePlayerEvent,
} from "./Events/AddActionsMenuKeyToRemotePlayerEvent";
import type { ActionsMenuActionClickedEvent } from "./Events/ActionsMenuActionClickedEvent";
import {
isRemoveActionsMenuKeyFromRemotePlayerEvent,
RemoveActionsMenuKeyFromRemotePlayerEvent,
} from "./Events/RemoveActionsMenuKeyFromRemotePlayerEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"],
@@ -63,6 +73,15 @@ class IframeListener {
private readonly _cameraFollowPlayerStream: Subject<CameraFollowPlayerEvent> = new Subject();
public readonly cameraFollowPlayerStream = this._cameraFollowPlayerStream.asObservable();
private readonly _addActionsMenuKeyToRemotePlayerStream: Subject<AddActionsMenuKeyToRemotePlayerEvent> =
new Subject();
public readonly addActionsMenuKeyToRemotePlayerStream = this._addActionsMenuKeyToRemotePlayerStream.asObservable();
private readonly _removeActionsMenuKeyFromRemotePlayerEvent: Subject<RemoveActionsMenuKeyFromRemotePlayerEvent> =
new Subject();
public readonly removeActionsMenuKeyFromRemotePlayerEvent =
this._removeActionsMenuKeyFromRemotePlayerEvent.asObservable();
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
@@ -241,6 +260,16 @@ class IframeListener {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (
payload.type == "addActionsMenuKeyToRemotePlayer" &&
isAddActionsMenuKeyToRemotePlayerEvent(payload.data)
) {
this._addActionsMenuKeyToRemotePlayerStream.next(payload.data);
} else if (
payload.type == "removeActionsMenuKeyFromRemotePlayer" &&
isRemoveActionsMenuKeyFromRemotePlayerEvent(payload.data)
) {
this._removeActionsMenuKeyFromRemotePlayerEvent.next(payload.data);
} else if (payload.type == "onCameraUpdate") {
this._trackCameraUpdateStream.next();
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
@@ -439,6 +468,20 @@ class IframeListener {
}
}
sendRemotePlayerClickedEvent(event: RemotePlayerClickedEvent) {
this.postMessage({
type: "remotePlayerClickedEvent",
data: event,
});
}
sendActionsMenuActionClickedEvent(event: ActionsMenuActionClickedEvent) {
this.postMessage({
type: "actionsMenuActionClickedEvent",
data: event,
});
}
sendCameraUpdated(event: WasCameraUpdatedEvent) {
this.postMessage({
type: "wasCameraUpdated",
+113 -5
View File
@@ -8,6 +8,12 @@ import { ActionMessage } from "./Ui/ActionMessage";
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
import { Menu } from "./Ui/Menu";
import type { RequireOnlyOne } from "../types";
import { isRemotePlayerClickedEvent, RemotePlayerClickedEvent } from "../Events/RemotePlayerClickedEvent";
import {
ActionsMenuActionClickedEvent,
isActionsMenuActionClickedEvent,
} from "../Events/ActionsMenuActionClickedEvent";
import { Observable, Subject } from "rxjs";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
@@ -42,7 +48,77 @@ export interface ActionMessageOptions {
callback: () => void;
}
export interface RemotePlayerInterface {
addAction(key: string, callback: Function): void;
}
export class RemotePlayer implements RemotePlayerInterface {
private id: number;
private actions: Map<string, ActionsMenuAction> = new Map<string, ActionsMenuAction>();
constructor(id: number) {
this.id = id;
}
public addAction(key: string, callback: Function): ActionsMenuAction {
const newAction = new ActionsMenuAction(this, key, callback);
this.actions.set(key, newAction);
sendToWorkadventure({
type: "addActionsMenuKeyToRemotePlayer",
data: { id: this.id, actionKey: key },
});
return newAction;
}
public callAction(key: string): void {
const action = this.actions.get(key);
if (action) {
action.call();
}
}
public removeAction(key: string): void {
this.actions.delete(key);
sendToWorkadventure({
type: "removeActionsMenuKeyFromRemotePlayer",
data: { id: this.id, actionKey: key },
});
}
}
export class ActionsMenuAction {
private remotePlayer: RemotePlayer;
private key: string;
private callback: Function;
constructor(remotePlayer: RemotePlayer, key: string, callback: Function) {
this.remotePlayer = remotePlayer;
this.key = key;
this.callback = callback;
}
public call(): void {
this.callback();
}
public remove(): void {
this.remotePlayer.removeAction(this.key);
}
}
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
public readonly _onRemotePlayerClicked: Subject<RemotePlayerInterface>;
public readonly onRemotePlayerClicked: Observable<RemotePlayerInterface>;
private currentlyClickedRemotePlayer?: RemotePlayer;
constructor() {
super();
this._onRemotePlayerClicked = new Subject<RemotePlayerInterface>();
this.onRemotePlayerClicked = this._onRemotePlayerClicked.asObservable();
}
callbacks = [
apiCallback({
type: "buttonClickedEvent",
@@ -82,9 +158,38 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
}
},
}),
apiCallback({
type: "remotePlayerClickedEvent",
typeChecker: isRemotePlayerClickedEvent,
callback: (payloadData: RemotePlayerClickedEvent) => {
this.currentlyClickedRemotePlayer = new RemotePlayer(payloadData.id);
this._onRemotePlayerClicked.next(this.currentlyClickedRemotePlayer);
},
}),
apiCallback({
type: "actionsMenuActionClickedEvent",
typeChecker: isActionsMenuActionClickedEvent,
callback: (payloadData: ActionsMenuActionClickedEvent) => {
this.currentlyClickedRemotePlayer?.callAction(payloadData.actionName);
},
}),
];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
public addActionsMenuKeyToRemotePlayer(id: number, actionKey: string): void {
sendToWorkadventure({
type: "addActionsMenuKeyToRemotePlayer",
data: { id, actionKey },
});
}
public removeActionsMenuKeyFromRemotePlayer(id: number, actionKey: string): void {
sendToWorkadventure({
type: "removeActionsMenuKeyFromRemotePlayer",
data: { id, actionKey },
});
}
public openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++;
const popup = new Popup(popupId);
const btnMap = new Map<number, () => void>();
@@ -119,7 +224,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
return popup;
}
registerMenuCommand(commandDescriptor: string, options: MenuOptions | ((commandDescriptor: string) => void)): Menu {
public registerMenuCommand(
commandDescriptor: string,
options: MenuOptions | ((commandDescriptor: string) => void)
): Menu {
const menu = new Menu(commandDescriptor);
if (typeof options === "function") {
@@ -168,15 +276,15 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
return menu;
}
displayBubble(): void {
public displayBubble(): void {
sendToWorkadventure({ type: "displayBubble", data: null });
}
removeBubble(): void {
public removeBubble(): void {
sendToWorkadventure({ type: "removeBubble", data: null });
}
displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {
public displayActionMessage(actionMessageOptions: ActionMessageOptions): ActionMessage {
const actionMessage = new ActionMessage(actionMessageOptions, () => {
actionMessages.delete(actionMessage.uuid);
});