diff --git a/front/src/Components/ActionsMenu/ActionsMenu.svelte b/front/src/Components/ActionsMenu/ActionsMenu.svelte
new file mode 100644
index 00000000..d660a570
--- /dev/null
+++ b/front/src/Components/ActionsMenu/ActionsMenu.svelte
@@ -0,0 +1,99 @@
+
+
+
+
+{#if actionsMenuData}
+
+{/if}
+
+
diff --git a/front/src/Components/FollowMenu/FollowMenu.svelte b/front/src/Components/FollowMenu/FollowMenu.svelte
index 0a0f5b68..1dee7bb5 100644
--- a/front/src/Components/FollowMenu/FollowMenu.svelte
+++ b/front/src/Components/FollowMenu/FollowMenu.svelte
@@ -7,7 +7,7 @@
function name(userId: number): string {
const user = gameScene.MapPlayersByKey.get(userId);
- return user ? user.PlayerValue : "";
+ return user ? user.playerName : "";
}
function acceptFollowRequest() {
diff --git a/front/src/Components/MainLayout.svelte b/front/src/Components/MainLayout.svelte
index cf273e50..54fa9d3b 100644
--- a/front/src/Components/MainLayout.svelte
+++ b/front/src/Components/MainLayout.svelte
@@ -36,6 +36,8 @@
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import { LayoutMode } from "../WebRtc/LayoutManager";
+ import { actionsMenuStore } from "../Stores/ActionsMenuStore";
+ import ActionsMenu from "./ActionsMenu/ActionsMenu.svelte";
let mainLayout: HTMLDivElement;
@@ -106,6 +108,10 @@
{/if}
+ {#if $actionsMenuStore}
+
+ {/if}
+
{#if $requestVisitCardsStore}
{/if}
diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts
index dc984c6d..4465a6b6 100644
--- a/front/src/Phaser/Entity/Character.ts
+++ b/front/src/Phaser/Entity/Character.ts
@@ -15,6 +15,7 @@ import { TexturesHelper } from "../Helpers/TexturesHelper";
import type { PictureStore } from "../../Stores/PictureStore";
import { Unsubscriber, Writable, writable } from "svelte/store";
import { createColorStore } from "../../Stores/OutlineColorStore";
+import type { OutlineableInterface } from "../Game/OutlineableInterface";
import type CancelablePromise from "cancelable-promise";
const playerNameY = -25;
@@ -29,14 +30,15 @@ interface AnimationData {
const interactiveRadius = 35;
-export abstract class Character extends Container {
+export abstract class Character extends Container implements OutlineableInterface {
private bubble: SpeechBubble | null = null;
- private readonly playerName: Text;
- public PlayerValue: string;
+ private readonly playerNameText: Text;
+ public playerName: string;
public sprites: Map;
protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
//private teleportation: Sprite;
private invisible: boolean;
+ private clickable: boolean;
public companion?: Companion;
private emote: Phaser.GameObjects.DOMElement | null = null;
private emoteTween: Phaser.Tweens.Tween | null = null;
@@ -61,8 +63,9 @@ export abstract class Character extends Container {
) {
super(scene, x, y /*, texture, frame*/);
this.scene = scene;
- this.PlayerValue = name;
+ this.playerName = name;
this.invisible = true;
+ this.clickable = false;
this.sprites = new Map();
this._pictureStore = writable(undefined);
@@ -88,7 +91,7 @@ export abstract class Character extends Container {
this.texturePromise = undefined;
});
- this.playerName = new Text(scene, 0, playerNameY, name, {
+ this.playerNameText = new Text(scene, 0, playerNameY, name, {
fontFamily: '"Press Start 2P"',
fontSize: "8px",
strokeThickness: 2,
@@ -99,30 +102,17 @@ export abstract class Character extends Container {
fontSize: 35,
},
});
- this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
- this.add(this.playerName);
+ this.playerNameText.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
+ this.add(this.playerNameText);
- if (isClickable) {
- this.setInteractive({
- hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius),
- hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
- useHandCursor: true,
- });
-
- this.on("pointerover", () => {
- this.outlineColorStore.pointerOver();
- });
- this.on("pointerout", () => {
- this.outlineColorStore.pointerOut();
- });
- }
+ this.setClickable(isClickable);
this.outlineColorStoreUnsubscribe = this.outlineColorStore.subscribe((color) => {
if (color === undefined) {
- this.getOutlinePlugin()?.remove(this.playerName);
+ this.getOutlinePlugin()?.remove(this.playerNameText);
} else {
- this.getOutlinePlugin()?.remove(this.playerName);
- this.getOutlinePlugin()?.add(this.playerName, {
+ this.getOutlinePlugin()?.remove(this.playerNameText);
+ this.getOutlinePlugin()?.add(this.playerNameText, {
thickness: 2,
outlineColor: color,
});
@@ -145,6 +135,34 @@ export abstract class Character extends Container {
}
}
+ public setClickable(clickable: boolean = true): void {
+ if (this.clickable === clickable) {
+ return;
+ }
+ this.clickable = clickable;
+ if (clickable) {
+ this.setInteractive({
+ hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius),
+ hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
+ useHandCursor: true,
+ });
+ return;
+ }
+ this.disableInteractive();
+ }
+
+ public isClickable() {
+ return this.clickable;
+ }
+
+ public getPosition(): { x: number; y: number } {
+ return { x: this.x, y: this.y };
+ }
+
+ public getObjectToOutline(): Phaser.GameObjects.GameObject {
+ return this.playerNameText;
+ }
+
private async getSnapshot(): Promise {
const sprites = Array.from(this.sprites.values()).map((sprite) => {
return { sprite, frame: 1 };
@@ -414,18 +432,42 @@ export abstract class Character extends Container {
private destroyEmote() {
this.emote?.destroy();
this.emote = null;
- this.playerName.setVisible(true);
+ this.playerNameText.setVisible(true);
}
public get pictureStore(): PictureStore {
return this._pictureStore;
}
- public setOutlineColor(color: number): void {
- this.outlineColorStore.setColor(color);
+ public setFollowOutlineColor(color: number): void {
+ this.outlineColorStore.setFollowColor(color);
}
- public removeOutlineColor(): void {
- this.outlineColorStore.removeColor();
+ public removeFollowOutlineColor(): void {
+ this.outlineColorStore.removeFollowColor();
+ }
+
+ public setApiOutlineColor(color: number): void {
+ this.outlineColorStore.setApiColor(color);
+ }
+
+ public removeApiOutlineColor(): void {
+ this.outlineColorStore.removeApiColor();
+ }
+
+ public pointerOverOutline(): void {
+ this.outlineColorStore.pointerOver();
+ }
+
+ public pointerOutOutline(): void {
+ this.outlineColorStore.pointerOut();
+ }
+
+ public characterCloseByOutline(): void {
+ this.outlineColorStore.characterCloseBy();
+ }
+
+ public characterFarAwayOutline(): void {
+ this.outlineColorStore.characterFarAway();
}
}
diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts
index d8bb5388..df496896 100644
--- a/front/src/Phaser/Entity/RemotePlayer.ts
+++ b/front/src/Phaser/Entity/RemotePlayer.ts
@@ -1,16 +1,24 @@
+import { requestVisitCardsStore } from "../../Stores/GameStore";
+import { ActionsMenuData, actionsMenuStore } from "../../Stores/ActionsMenuStore";
+import { Character } from "../Entity/Character";
import type { GameScene } from "../Game/GameScene";
import type { PointInterface } from "../../Connexion/ConnexionModels";
-import { Character } from "../Entity/Character";
import type { PlayerAnimationDirections } from "../Player/Animation";
-import { requestVisitCardsStore } from "../../Stores/GameStore";
+import type { Unsubscriber } from "svelte/store";
+import type { ActivatableInterface } from "../Game/ActivatableInterface";
import type CancelablePromise from "cancelable-promise";
/**
* Class representing the sprite of a remote player (a player that plays on another computer)
*/
-export class RemotePlayer extends Character {
- userId: number;
+export class RemotePlayer extends Character implements ActivatableInterface {
+ public userId: number;
+ public readonly activationRadius: number;
+
+ private registeredActions: { actionName: string; callback: Function }[];
private visitCardUrl: string | null;
+ private isActionsMenuInitialized: boolean = false;
+ private actionsMenuStoreUnsubscriber: Unsubscriber;
constructor(
userId: number,
@@ -23,34 +31,26 @@ export class RemotePlayer extends Character {
moving: boolean,
visitCardUrl: string | null,
companion: string | null,
- companionTexturePromise?: Promise
+ companionTexturePromise?: Promise,
+ activationRadius?: number
) {
- super(
- Scene,
- x,
- y,
- texturesPromise,
- name,
- direction,
- moving,
- 1,
- !!visitCardUrl,
- companion,
- companionTexturePromise
- );
+ super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
//set data
this.userId = userId;
+ this.registeredActions = [];
+ this.registerDefaultActionsMenuActions();
+ this.setClickable(this.registeredActions.length > 0);
+ this.activationRadius = activationRadius ?? 96;
this.visitCardUrl = visitCardUrl;
-
- this.on("pointerdown", (event: Phaser.Input.Pointer) => {
- if (event.downElement.nodeName === "CANVAS") {
- requestVisitCardsStore.set(this.visitCardUrl);
- }
+ this.actionsMenuStoreUnsubscriber = actionsMenuStore.subscribe((value: ActionsMenuData | undefined) => {
+ this.isActionsMenuInitialized = value ? true : false;
});
+
+ this.bindEventHandlers();
}
- updatePosition(position: PointInterface): void {
+ public updatePosition(position: PointInterface): void {
this.playAnimation(position.direction as PlayerAnimationDirections, position.moving);
this.setX(position.x);
this.setY(position.y);
@@ -61,4 +61,66 @@ export class RemotePlayer extends Character {
this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections);
}
}
+
+ public registerActionsMenuAction(action: { actionName: string; callback: Function }): void {
+ this.registeredActions.push(action);
+ this.updateIsClickable();
+ }
+
+ public unregisterActionsMenuAction(actionName: string) {
+ const index = this.registeredActions.findIndex((action) => action.actionName === actionName);
+ if (index !== -1) {
+ this.registeredActions.splice(index, 1);
+ }
+ this.updateIsClickable();
+ }
+
+ public activate(): void {
+ this.toggleActionsMenu();
+ }
+
+ public destroy(): void {
+ this.actionsMenuStoreUnsubscriber();
+ actionsMenuStore.clear();
+ super.destroy();
+ }
+
+ public isActivatable(): boolean {
+ return this.isClickable();
+ }
+
+ private updateIsClickable(): void {
+ this.setClickable(this.registeredActions.length > 0);
+ }
+
+ private toggleActionsMenu(): void {
+ if (this.isActionsMenuInitialized) {
+ actionsMenuStore.clear();
+ return;
+ }
+ actionsMenuStore.initialize(this.playerName);
+ for (const action of this.registeredActions) {
+ actionsMenuStore.addAction(action.actionName, action.callback);
+ }
+ }
+
+ private registerDefaultActionsMenuActions(): void {
+ if (this.visitCardUrl) {
+ this.registeredActions.push({
+ actionName: "Visiting Card",
+ callback: () => {
+ requestVisitCardsStore.set(this.visitCardUrl);
+ actionsMenuStore.clear();
+ },
+ });
+ }
+ }
+
+ private bindEventHandlers(): void {
+ this.on(Phaser.Input.Events.POINTER_DOWN, (event: Phaser.Input.Pointer) => {
+ if (event.downElement.nodeName === "CANVAS") {
+ this.toggleActionsMenu();
+ }
+ });
+ }
}
diff --git a/front/src/Phaser/Game/ActivatableInterface.ts b/front/src/Phaser/Game/ActivatableInterface.ts
new file mode 100644
index 00000000..809a09b6
--- /dev/null
+++ b/front/src/Phaser/Game/ActivatableInterface.ts
@@ -0,0 +1,6 @@
+export interface ActivatableInterface {
+ readonly activationRadius: number;
+ isActivatable: () => boolean;
+ activate: () => void;
+ getPosition: () => { x: number; y: number };
+}
diff --git a/front/src/Phaser/Game/ActivatablesManager.ts b/front/src/Phaser/Game/ActivatablesManager.ts
new file mode 100644
index 00000000..60e967d9
--- /dev/null
+++ b/front/src/Phaser/Game/ActivatablesManager.ts
@@ -0,0 +1,93 @@
+import { isOutlineable } from "../../Utils/CustomTypeGuards";
+import { MathUtils } from "../../Utils/MathUtils";
+import type { Player } from "../Player/Player";
+import type { ActivatableInterface } from "./ActivatableInterface";
+
+export class ActivatablesManager {
+ // The item that can be selected by pressing the space key.
+ private selectedActivatableObjectByDistance?: ActivatableInterface;
+ private selectedActivatableObjectByPointer?: ActivatableInterface;
+ private activatableObjectsDistances: Map = new Map();
+
+ private currentPlayer: Player;
+
+ constructor(currentPlayer: Player) {
+ this.currentPlayer = currentPlayer;
+ }
+
+ public handlePointerOverActivatableObject(object: ActivatableInterface): void {
+ if (this.selectedActivatableObjectByPointer === object) {
+ return;
+ }
+ if (isOutlineable(this.selectedActivatableObjectByDistance)) {
+ this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
+ }
+ if (isOutlineable(this.selectedActivatableObjectByPointer)) {
+ this.selectedActivatableObjectByPointer?.pointerOutOutline();
+ }
+ this.selectedActivatableObjectByPointer = object;
+ if (isOutlineable(this.selectedActivatableObjectByPointer)) {
+ this.selectedActivatableObjectByPointer?.pointerOverOutline();
+ }
+ }
+
+ public handlePointerOutActivatableObject(): void {
+ if (isOutlineable(this.selectedActivatableObjectByPointer)) {
+ this.selectedActivatableObjectByPointer?.pointerOutOutline();
+ }
+ this.selectedActivatableObjectByPointer = undefined;
+ if (isOutlineable(this.selectedActivatableObjectByDistance)) {
+ this.selectedActivatableObjectByDistance?.characterCloseByOutline();
+ }
+ }
+
+ public getSelectedActivatableObject(): ActivatableInterface | undefined {
+ return this.selectedActivatableObjectByPointer ?? this.selectedActivatableObjectByDistance;
+ }
+
+ public deduceSelectedActivatableObjectByDistance(): void {
+ const newNearestObject = this.findNearestActivatableObject();
+ if (this.selectedActivatableObjectByDistance === newNearestObject) {
+ return;
+ }
+ // update value but do not change the outline
+ if (this.selectedActivatableObjectByPointer) {
+ this.selectedActivatableObjectByDistance = newNearestObject;
+ return;
+ }
+ if (isOutlineable(this.selectedActivatableObjectByDistance)) {
+ this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
+ }
+ this.selectedActivatableObjectByDistance = newNearestObject;
+ if (isOutlineable(this.selectedActivatableObjectByDistance)) {
+ this.selectedActivatableObjectByDistance?.characterCloseByOutline();
+ }
+ }
+
+ private findNearestActivatableObject(): ActivatableInterface | undefined {
+ let shortestDistance: number = Infinity;
+ let closestObject: ActivatableInterface | undefined = undefined;
+
+ for (const [object, distance] of this.activatableObjectsDistances.entries()) {
+ if (object.isActivatable() && object.activationRadius > distance && shortestDistance > distance) {
+ shortestDistance = distance;
+ closestObject = object;
+ }
+ }
+ return closestObject;
+ }
+ public updateActivatableObjectsDistances(objects: ActivatableInterface[]): void {
+ const currentPlayerPos = this.currentPlayer.getPosition();
+ for (const object of objects) {
+ const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
+ this.activatableObjectsDistances.set(object, distance);
+ }
+ }
+
+ public updateDistanceForSingleActivatableObject(object: ActivatableInterface): void {
+ this.activatableObjectsDistances.set(
+ object,
+ MathUtils.distanceBetween(this.currentPlayer.getPosition(), object.getPosition())
+ );
+ }
+}
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index 19b9791d..242d97c8 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -12,7 +12,7 @@ import { UserInputManager } from "../UserInput/UserInputManager";
import { gameManager } from "./GameManager";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
-import { waScaleManager, WaScaleManagerEvent } from "../Services/WaScaleManager";
+import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import { soundManager } from "./SoundManager";
import { SharedVariablesManager } from "./SharedVariablesManager";
@@ -49,6 +49,7 @@ import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { GameMapProperties } from "./GameMapProperties";
import { PathfindingManager } from "../../Utils/PathfindingManager";
+import { ActivatablesManager } from "./ActivatablesManager";
import type {
GroupCreatedUpdatedMessageInterface,
MessageUserMovedInterface,
@@ -89,10 +90,8 @@ import { deepCopy } from "deep-copy-ts";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore";
-import Camera = Phaser.Cameras.Scene2D.Camera;
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte";
-
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@@ -189,8 +188,6 @@ export class GameScene extends DirtyScene {
private gameMap!: GameMap;
private actionableItems: Map = new Map();
- // The item that can be selected by pressing the space key.
- private outlinedItem: ActionableItem | null = null;
public userInputManager!: UserInputManager;
private isReconnecting: boolean | undefined = undefined;
private playerName!: string;
@@ -204,6 +201,7 @@ export class GameScene extends DirtyScene {
private emoteManager!: EmoteManager;
private cameraManager!: CameraManager;
private pathfindingManager!: PathfindingManager;
+ private activatablesManager!: ActivatablesManager;
private preloading: boolean = true;
private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager;
@@ -576,6 +574,14 @@ export class GameScene extends DirtyScene {
waScaleManager
);
+ this.pathfindingManager = new PathfindingManager(
+ this,
+ this.gameMap.getCollisionsGrid(),
+ this.gameMap.getTileDimensions()
+ );
+
+ this.activatablesManager = new ActivatablesManager(this.CurrentPlayer);
+
biggestAvailableAreaStore.recompute();
this.cameraManager.startFollowPlayer(this.CurrentPlayer);
@@ -657,10 +663,10 @@ export class GameScene extends DirtyScene {
this.followUsersColorStoreUnsubscribe = followUsersColorStore.subscribe((color) => {
if (color !== undefined) {
- this.CurrentPlayer.setOutlineColor(color);
+ this.CurrentPlayer.setFollowOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color);
} else {
- this.CurrentPlayer.removeOutlineColor();
+ this.CurrentPlayer.removeFollowOutlineColor();
this.connection?.emitPlayerOutlineColor(null);
}
});
@@ -677,10 +683,6 @@ export class GameScene extends DirtyScene {
);
}
- public activateOutlinedItem(): void {
- this.outlinedItem?.activate();
- }
-
/**
* Initializes the connection to Pusher.
*/
@@ -803,11 +805,8 @@ export class GameScene extends DirtyScene {
this.simplePeer = new SimplePeer(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
- //listen event to share position of user
- this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this));
- this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this));
this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => {
- this.gameMap.setPosition(event.x, event.y);
+ this.handleCurrentPlayerHasMovedEvent(event);
});
// Set up variables manager
@@ -1443,12 +1442,12 @@ ${escapedMessage}
const green = normalizeColor(message.green);
const blue = normalizeColor(message.blue);
const color = (red << 16) | (green << 8) | blue;
- this.CurrentPlayer.setOutlineColor(color);
+ this.CurrentPlayer.setApiOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color);
});
iframeListener.registerAnswerer("removePlayerOutline", (message) => {
- this.CurrentPlayer.removeOutlineColor();
+ this.CurrentPlayer.removeApiOutlineColor();
this.connection?.emitPlayerOutlineColor(null);
});
@@ -1681,7 +1680,18 @@ ${escapedMessage}
}
}
- createCollisionWithPlayer() {
+ private handleCurrentPlayerHasMovedEvent(event: HasPlayerMovedEvent): void {
+ //listen event to share position of user
+ this.pushPlayerPosition(event);
+ this.gameMap.setPosition(event.x, event.y);
+ this.activatablesManager.updateActivatableObjectsDistances([
+ ...Array.from(this.MapPlayersByKey.values()),
+ ...this.actionableItems.values(),
+ ]);
+ this.activatablesManager.deduceSelectedActivatableObjectByDistance();
+ }
+
+ private createCollisionWithPlayer() {
//add collision layer
for (const phaserLayer of this.gameMap.phaserLayers) {
this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => {
@@ -1700,7 +1710,7 @@ ${escapedMessage}
}
}
- createCurrentPlayer() {
+ private createCurrentPlayer() {
//TODO create animation moving between exit and start
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers);
try {
@@ -1715,7 +1725,7 @@ ${escapedMessage}
this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
);
- this.CurrentPlayer.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
+ this.CurrentPlayer.on(Phaser.Input.Events.POINTER_DOWN, (pointer: Phaser.Input.Pointer) => {
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
return; //we don't want the menu to open when pinching on a touch screen.
}
@@ -1758,7 +1768,7 @@ ${escapedMessage}
this.createCollisionWithPlayer();
}
- pushPlayerPosition(event: HasPlayerMovedEvent) {
+ private pushPlayerPosition(event: HasPlayerMovedEvent) {
if (this.lastMoveEventSent === event) {
return;
}
@@ -1784,49 +1794,6 @@ ${escapedMessage}
// Otherwise, do nothing.
}
- /**
- * Finds the correct item to outline and outline it (if there is an item to be outlined)
- * @param event
- */
- private outlineItem(event: HasPlayerMovedEvent): void {
- let x = event.x;
- let y = event.y;
- switch (event.direction) {
- case PlayerAnimationDirections.Up:
- y -= 32;
- break;
- case PlayerAnimationDirections.Down:
- y += 32;
- break;
- case PlayerAnimationDirections.Left:
- x -= 32;
- break;
- case PlayerAnimationDirections.Right:
- x += 32;
- break;
- default:
- throw new Error('Unexpected direction "' + event.direction + '"');
- }
-
- let shortestDistance: number = Infinity;
- let selectedItem: ActionableItem | null = null;
- for (const item of this.actionableItems.values()) {
- const distance = item.actionableDistance(x, y);
- if (distance !== null && distance < shortestDistance) {
- shortestDistance = distance;
- selectedItem = item;
- }
- }
-
- if (this.outlinedItem === selectedItem) {
- return;
- }
-
- this.outlinedItem?.notSelectable();
- this.outlinedItem = selectedItem;
- this.outlinedItem?.selectable();
- }
-
private doPushPlayerPosition(event: HasPlayerMovedEvent): void {
this.lastMoveEventSent = event;
this.lastSentTick = this.currentTick;
@@ -1844,7 +1811,7 @@ ${escapedMessage}
* @param time
* @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
*/
- update(time: number, delta: number): void {
+ public update(time: number, delta: number): void {
this.dirty = false;
this.currentTick = time;
this.CurrentPlayer.moveUser(delta, this.userInputManager.getEventListForGameTick());
@@ -1863,9 +1830,15 @@ ${escapedMessage}
case "RemovePlayerEvent":
this.doRemovePlayer(event.userId);
break;
- case "UserMovedEvent":
+ case "UserMovedEvent": {
this.doUpdatePlayerPosition(event.event);
+ const remotePlayer = this.MapPlayersByKey.get(event.event.userId);
+ if (remotePlayer) {
+ this.activatablesManager.updateDistanceForSingleActivatableObject(remotePlayer);
+ this.activatablesManager.deduceSelectedActivatableObjectByDistance();
+ }
break;
+ }
case "GroupCreatedUpdatedEvent":
this.doShareGroupPosition(event.event);
break;
@@ -1952,11 +1925,21 @@ ${escapedMessage}
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
);
if (addPlayerData.outlineColor !== undefined) {
- player.setOutlineColor(addPlayerData.outlineColor);
+ player.setApiOutlineColor(addPlayerData.outlineColor);
}
this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player);
player.updatePosition(addPlayerData.position);
+
+ player.on(Phaser.Input.Events.POINTER_OVER, () => {
+ this.activatablesManager.handlePointerOverActivatableObject(player);
+ this.markDirty();
+ });
+
+ player.on(Phaser.Input.Events.POINTER_OUT, () => {
+ this.activatablesManager.handlePointerOutActivatableObject();
+ this.markDirty();
+ });
}
/**
@@ -1986,7 +1969,7 @@ ${escapedMessage}
this.playersPositionInterpolator.removePlayer(userId);
}
- public updatePlayerPosition(message: MessageUserMovedInterface): void {
+ private updatePlayerPosition(message: MessageUserMovedInterface): void {
this.pendingEvents.enqueue({
type: "UserMovedEvent",
event: message,
@@ -2016,7 +1999,7 @@ ${escapedMessage}
this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement);
}
- public shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) {
+ private shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) {
this.pendingEvents.enqueue({
type: "GroupCreatedUpdatedEvent",
event: groupPositionMessage,
@@ -2068,9 +2051,9 @@ ${escapedMessage}
return;
}
if (message.removeOutlineColor) {
- character.removeOutlineColor();
+ character.removeApiOutlineColor();
} else {
- character.setOutlineColor(message.outlineColor);
+ character.setApiOutlineColor(message.outlineColor);
}
}
@@ -2227,4 +2210,8 @@ ${escapedMessage}
public getPathfindingManager(): PathfindingManager {
return this.pathfindingManager;
}
+
+ public getActivatablesManager(): ActivatablesManager {
+ return this.activatablesManager;
+ }
}
diff --git a/front/src/Phaser/Game/OutlineableInterface.ts b/front/src/Phaser/Game/OutlineableInterface.ts
new file mode 100644
index 00000000..bee560cc
--- /dev/null
+++ b/front/src/Phaser/Game/OutlineableInterface.ts
@@ -0,0 +1,10 @@
+export interface OutlineableInterface {
+ setFollowOutlineColor(color: number): void;
+ removeFollowOutlineColor(): void;
+ setApiOutlineColor(color: number): void;
+ removeApiOutlineColor(): void;
+ pointerOverOutline(): void;
+ pointerOutOutline(): void;
+ characterCloseByOutline(): void;
+ characterFarAwayOutline(): void;
+}
diff --git a/front/src/Phaser/Items/ActionableItem.ts b/front/src/Phaser/Items/ActionableItem.ts
index 44b633ed..ff85e232 100644
--- a/front/src/Phaser/Items/ActionableItem.ts
+++ b/front/src/Phaser/Items/ActionableItem.ts
@@ -5,10 +5,11 @@
import Sprite = Phaser.GameObjects.Sprite;
import type { GameScene } from "../Game/GameScene";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
+import type { ActivatableInterface } from "../Game/ActivatableInterface";
type EventCallback = (state: unknown, parameters: unknown) => void;
-export class ActionableItem {
+export class ActionableItem implements ActivatableInterface {
private readonly activationRadiusSquared: number;
private isSelectable: boolean = false;
private callbacks: Map> = new Map>();
@@ -17,7 +18,7 @@ export class ActionableItem {
private id: number,
private sprite: Sprite,
private eventHandler: GameScene,
- private activationRadius: number,
+ public readonly activationRadius: number,
private onActivateCallback: (item: ActionableItem) => void
) {
this.activationRadiusSquared = activationRadius * activationRadius;
@@ -40,6 +41,10 @@ export class ActionableItem {
}
}
+ public getPosition(): { x: number; y: number } {
+ return { x: this.sprite.x, y: this.sprite.y };
+ }
+
/**
* Show the outline of the sprite.
*/
@@ -70,9 +75,10 @@ export class ActionableItem {
return this.sprite.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
}
- /**
- * Triggered when the "space" key is pressed and the object is in range of being activated.
- */
+ public isActivatable(): boolean {
+ return this.isSelectable;
+ }
+
public activate(): void {
this.onActivateCallback(this);
}
diff --git a/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts b/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts
index 19bb02bb..4d9ac8a9 100644
--- a/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts
+++ b/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts
@@ -1,3 +1,6 @@
+import { Player } from "../Player/Player";
+import { RemotePlayer } from "../Entity/RemotePlayer";
+
import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface";
import type { GameScene } from "../Game/GameScene";
@@ -19,9 +22,14 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
}
public handlePointerUpEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {
- if (pointer.rightButtonReleased() || pointer.getDuration() > 250) {
+ if ((!pointer.wasTouch && pointer.leftButtonReleased()) || pointer.getDuration() > 250) {
return;
}
+ for (const object of gameObjects) {
+ if (object instanceof Player || object instanceof RemotePlayer) {
+ return;
+ }
+ }
const camera = this.gameScene.getCameraManager().getCamera();
const index = this.gameScene
.getGameMap()
@@ -45,7 +53,10 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {}
public handleSpaceKeyUpEvent(event: Event): Event {
- this.gameScene.activateOutlinedItem();
+ const activatable = this.gameScene.getActivatablesManager().getSelectedActivatableObject();
+ if (activatable && activatable.isActivatable()) {
+ activatable.activate();
+ }
return event;
}
}
diff --git a/front/src/Stores/ActionsMenuStore.ts b/front/src/Stores/ActionsMenuStore.ts
new file mode 100644
index 00000000..f891dad8
--- /dev/null
+++ b/front/src/Stores/ActionsMenuStore.ts
@@ -0,0 +1,43 @@
+import { writable } from "svelte/store";
+
+export interface ActionsMenuData {
+ playerName: string;
+ actions: { actionName: string; callback: Function }[];
+}
+
+function createActionsMenuStore() {
+ const { subscribe, update, set } = writable(undefined);
+
+ return {
+ subscribe,
+ initialize: (playerName: string) => {
+ set({
+ playerName,
+ actions: [],
+ });
+ },
+ addAction: (actionName: string, callback: Function) => {
+ update((data) => {
+ data?.actions.push({ actionName, callback });
+ return data;
+ });
+ },
+ removeAction: (actionName: string) => {
+ update((data) => {
+ const actionIndex = data?.actions.findIndex((action) => action.actionName === actionName);
+ if (actionIndex !== undefined && actionIndex != -1) {
+ data?.actions.splice(actionIndex, 1);
+ }
+ return data;
+ });
+ },
+ /**
+ * Hides menu
+ */
+ clear: () => {
+ set(undefined);
+ },
+ };
+}
+
+export const actionsMenuStore = createActionsMenuStore();
diff --git a/front/src/Stores/OutlineColorStore.ts b/front/src/Stores/OutlineColorStore.ts
index 1618eebc..a35cc9c9 100644
--- a/front/src/Stores/OutlineColorStore.ts
+++ b/front/src/Stores/OutlineColorStore.ts
@@ -3,14 +3,17 @@ import { writable } from "svelte/store";
export function createColorStore() {
const { subscribe, set } = writable(undefined);
- let color: number | undefined = undefined;
- let focused: boolean = false;
+ let followColor: number | undefined = undefined;
+ let apiColor: number | undefined = undefined;
+
+ let pointedByPointer: boolean = false;
+ let pointedByCharacter: boolean = false;
const updateColor = () => {
- if (focused) {
+ if (pointedByPointer || pointedByCharacter) {
set(0xffff00);
} else {
- set(color);
+ set(followColor ?? apiColor);
}
};
@@ -18,22 +21,42 @@ export function createColorStore() {
subscribe,
pointerOver() {
- focused = true;
+ pointedByPointer = true;
updateColor();
},
pointerOut() {
- focused = false;
+ pointedByPointer = false;
updateColor();
},
- setColor(newColor: number) {
- color = newColor;
+ characterCloseBy() {
+ pointedByCharacter = true;
updateColor();
},
- removeColor() {
- color = undefined;
+ characterFarAway() {
+ pointedByCharacter = false;
+ updateColor();
+ },
+
+ setFollowColor(newColor: number) {
+ followColor = newColor;
+ updateColor();
+ },
+
+ removeFollowColor() {
+ followColor = undefined;
+ updateColor();
+ },
+
+ setApiColor(newColor: number) {
+ apiColor = newColor;
+ updateColor();
+ },
+
+ removeApiColor() {
+ apiColor = undefined;
updateColor();
},
};
diff --git a/front/src/Utils/CustomTypeGuards.ts b/front/src/Utils/CustomTypeGuards.ts
new file mode 100644
index 00000000..f2bdb0f9
--- /dev/null
+++ b/front/src/Utils/CustomTypeGuards.ts
@@ -0,0 +1,5 @@
+import type { OutlineableInterface } from "../Phaser/Game/OutlineableInterface";
+
+export function isOutlineable(object: unknown): object is OutlineableInterface {
+ return (object as OutlineableInterface)?.pointerOverOutline !== undefined;
+}