Merge pull request #1735 from thecodingmachine/walking-shortest-path

Walking shortest path on click
This commit is contained in:
David Négrier 2022-01-19 15:34:49 +01:00 committed by GitHub
commit 1e3c81617f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 556 additions and 244 deletions

1
front/.gitignore vendored
View file

@ -6,6 +6,7 @@
/dist/main.*.css.map
/dist/tests/
/yarn-error.log
/package-lock.json
/dist/webpack.config.js
/dist/webpack.config.js.map
/dist/src

View file

@ -48,6 +48,7 @@
"axios": "^0.21.2",
"cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0",
"easystarjs": "^0.4.4",
"generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0",
"phaser": "^3.54.0",

View file

@ -0,0 +1,12 @@
export interface UserInputHandlerInterface {
handleMouseWheelEvent: (
pointer: Phaser.Input.Pointer,
gameObjects: Phaser.GameObjects.GameObject[],
deltaX: number,
deltaY: number,
deltaZ: number
) => void;
handlePointerUpEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handlePointerDownEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handleSpaceKeyUpEvent: (event: Event) => Event;
}

View file

@ -40,30 +40,22 @@ export class MobileJoystick extends VirtualJoystick {
this.visible = false;
this.enable = false;
this.scene.input.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
if (!pointer.wasTouch) {
return;
}
// Let's only display the joystick if there is one finger on the screen
if ((pointer.event as TouchEvent).touches.length === 1) {
this.x = pointer.x;
this.y = pointer.y;
this.visible = true;
this.enable = true;
} else {
this.visible = false;
this.enable = false;
}
});
this.scene.input.on("pointerup", () => {
this.visible = false;
this.enable = false;
});
this.resizeCallback = this.resize.bind(this);
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
}
public showAt(x: number, y: number): void {
this.x = x;
this.y = y;
this.visible = true;
this.enable = true;
}
public hide(): void {
this.visible = false;
this.enable = false;
}
public resize() {
this.base.setDisplaySize(this.getDisplaySizeByElement(baseSize), this.getDisplaySizeByElement(baseSize));
this.thumb.setDisplaySize(this.getDisplaySizeByElement(thumbSize), this.getDisplaySizeByElement(thumbSize));

View file

@ -1,10 +1,4 @@
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapObjectLayer,
ITiledMapProperty,
} from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapObject, ITiledMapProperty } from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
@ -120,8 +114,24 @@ export class GameMap {
return [];
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
public getCollisionsGrid(): number[][] {
const grid: number[][] = [];
for (let y = 0; y < this.map.height; y += 1) {
const row: number[] = [];
for (let x = 0; x < this.map.width; x += 1) {
row.push(this.isCollidingAt(x, y) ? 1 : 0);
}
grid.push(row);
}
return grid;
}
public getTileDimensions(): { width: number; height: number } {
return { width: this.map.tilewidth, height: this.map.tileheight };
}
public getTileIndexAt(x: number, y: number): { x: number; y: number } {
return { x: Math.floor(x / this.map.tilewidth), y: Math.floor(y / this.map.tileheight) };
}
/**
@ -149,6 +159,151 @@ export class GameMap {
this.triggerLayersChange();
}
public getCurrentProperties(): Map<string, string | boolean | number> {
return this.lastProperties;
}
public getMap(): ITiledMap {
return this.map;
}
/**
* Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on.
*/
public onPropertyChange(propName: string, callback: PropertyChangeCallback) {
let callbacksArray = this.propertiesChangeCallbacks.get(propName);
if (callbacksArray === undefined) {
callbacksArray = new Array<PropertyChangeCallback>();
this.propertiesChangeCallbacks.set(propName, callbacksArray);
}
callbacksArray.push(callback);
}
/**
* Registers a callback called when the user moves inside another layer.
*/
public onEnterLayer(callback: layerChangeCallback) {
this.enterLayerCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves outside another layer.
*/
public onLeaveLayer(callback: layerChangeCallback) {
this.leaveLayerCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves inside another zone.
*/
public onEnterZone(callback: zoneChangeCallback) {
this.enterZoneCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves outside another zone.
*/
public onLeaveZone(callback: zoneChangeCallback) {
this.leaveZoneCallbacks.push(callback);
}
public findLayer(layerName: string): ITiledMapLayer | undefined {
return this.flatLayers.find((layer) => layer.name === layerName);
}
public findPhaserLayer(layerName: string): TilemapLayer | undefined {
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
}
public findPhaserLayers(groupName: string): TilemapLayer[] {
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
}
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain);
}
}
public putTile(tile: string | number | null, x: number, y: number, layer: string): void {
const phaserLayer = this.findPhaserLayer(layer);
if (phaserLayer) {
if (tile === null) {
phaserLayer.putTileAt(-1, x, y);
return;
}
const tileIndex = this.getIndexForTileType(tile);
if (tileIndex !== undefined) {
this.putTileInFlatLayer(tileIndex, x, y, layer);
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
for (const property of this.getTileProperty(tileIndex)) {
if (property.name === GameMapProperties.COLLIDES && property.value) {
phaserTile.setCollision(true);
}
}
} else {
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
}
} else {
console.error("The layer '" + layer + "' does not exist (or is not a tilelaye).");
}
}
public setLayerProperty(
layerName: string,
propertyName: string,
propertyValue: string | number | undefined | boolean
) {
const layer = this.findLayer(layerName);
if (layer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (layer.properties === undefined) {
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
this.triggerAllProperties();
this.triggerLayersChange();
}
/**
* Trigger all the callbacks (used when exiting a map)
*/
public triggerExitCallbacks(): void {
const emptyProps = new Map<string, string | boolean | number>();
for (const [oldPropName, oldPropValue] of this.lastProperties.entries()) {
// We found a property that disappeared
this.trigger(oldPropName, oldPropValue, undefined, emptyProps);
}
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
}
private isCollidingAt(x: number, y: number): boolean {
for (const layer of this.phaserLayers) {
if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) {
return true;
}
}
return false;
}
private triggerAllProperties(): void {
const newProps = this.getProperties(this.key ?? 0);
const oldProps = this.lastProperties;
@ -247,10 +402,6 @@ export class GameMap {
}
}
public getCurrentProperties(): Map<string, string | boolean | number> {
return this.lastProperties;
}
private getProperties(key: number): Map<string, string | boolean | number> {
const properties = new Map<string, string | boolean | number>();
@ -292,10 +443,6 @@ export class GameMap {
return properties;
}
public getMap(): ITiledMap {
return this.map;
}
private getTileProperty(index: number): Array<ITiledMapProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
@ -317,64 +464,6 @@ export class GameMap {
}
}
/**
* Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on.
*/
public onPropertyChange(propName: string, callback: PropertyChangeCallback) {
let callbacksArray = this.propertiesChangeCallbacks.get(propName);
if (callbacksArray === undefined) {
callbacksArray = new Array<PropertyChangeCallback>();
this.propertiesChangeCallbacks.set(propName, callbacksArray);
}
callbacksArray.push(callback);
}
/**
* Registers a callback called when the user moves inside another layer.
*/
public onEnterLayer(callback: layerChangeCallback) {
this.enterLayerCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves outside another layer.
*/
public onLeaveLayer(callback: layerChangeCallback) {
this.leaveLayerCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves inside another zone.
*/
public onEnterZone(callback: zoneChangeCallback) {
this.enterZoneCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves outside another zone.
*/
public onLeaveZone(callback: zoneChangeCallback) {
this.leaveZoneCallbacks.push(callback);
}
public findLayer(layerName: string): ITiledMapLayer | undefined {
return this.flatLayers.find((layer) => layer.name === layerName);
}
public findPhaserLayer(layerName: string): TilemapLayer | undefined {
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
}
public findPhaserLayers(groupName: string): TilemapLayer[] {
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
}
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain);
}
}
private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void {
const fLayer = this.findLayer(layer);
if (fLayer == undefined) {
@ -396,30 +485,6 @@ export class GameMap {
fLayer.data[x + y * fLayer.width] = index;
}
public putTile(tile: string | number | null, x: number, y: number, layer: string): void {
const phaserLayer = this.findPhaserLayer(layer);
if (phaserLayer) {
if (tile === null) {
phaserLayer.putTileAt(-1, x, y);
return;
}
const tileIndex = this.getIndexForTileType(tile);
if (tileIndex !== undefined) {
this.putTileInFlatLayer(tileIndex, x, y, layer);
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
for (const property of this.getTileProperty(tileIndex)) {
if (property.name === GameMapProperties.COLLIDES && property.value) {
phaserTile.setCollision(true);
}
}
} else {
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
}
} else {
console.error("The layer '" + layer + "' does not exist (or is not a tilelaye).");
}
}
private getIndexForTileType(tile: string | number): number | undefined {
if (typeof tile == "number") {
return tile;
@ -427,48 +492,6 @@ export class GameMap {
return this.tileNameMap.get(tile);
}
public setLayerProperty(
layerName: string,
propertyName: string,
propertyValue: string | number | undefined | boolean
) {
const layer = this.findLayer(layerName);
if (layer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (layer.properties === undefined) {
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
this.triggerAllProperties();
this.triggerLayersChange();
}
/**
* Trigger all the callbacks (used when exiting a map)
*/
public triggerExitCallbacks(): void {
const emptyProps = new Map<string, string | boolean | number>();
for (const [oldPropName, oldPropValue] of this.lastProperties.entries()) {
// We found a property that disappeared
this.trigger(oldPropName, oldPropValue, undefined, emptyProps);
}
}
private getObjectsFromLayers(layers: ITiledMapLayer[]): ITiledMapObject[] {
const objects: ITiledMapObject[] = [];

View file

@ -48,9 +48,9 @@ import { PropertyUtils } from "../Map/PropertyUtils";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { GameMapProperties } from "./GameMapProperties";
import { PathfindingManager } from "../../Utils/PathfindingManager";
import type {
GroupCreatedUpdatedMessageInterface,
MessageUserJoined,
MessageUserMovedInterface,
MessageUserPositionInterface,
OnConnectInterface,
@ -66,7 +66,6 @@ import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITi
import type { AddPlayerInterface } from "./AddPlayerInterface";
import { CameraManager, CameraManagerEvent, CameraManagerEventCameraUpdateData } from "./CameraManager";
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import type { Character } from "../Entity/Character";
import { peerStore } from "../../Stores/PeerStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
@ -89,9 +88,9 @@ import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
import { deepCopy } from "deep-copy-ts";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore";
import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator";
import { followUsersColorStore } from "../../Stores/FollowStore";
import Camera = Phaser.Cameras.Scene2D.Camera;
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -203,6 +202,7 @@ export class GameScene extends DirtyScene {
private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time.
private emoteManager!: EmoteManager;
private cameraManager!: CameraManager;
private pathfindingManager!: PathfindingManager;
private preloading: boolean = true;
private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager;
@ -549,7 +549,7 @@ export class GameScene extends DirtyScene {
this.MapPlayers = this.physics.add.group({ immovable: true });
//create input to move
this.userInputManager = new UserInputManager(this);
this.userInputManager = new UserInputManager(this, new GameSceneUserInputHandler(this));
mediaManager.setUserInputManager(this.userInputManager);
if (localUserStore.getFullscreen()) {
@ -568,6 +568,8 @@ export class GameScene extends DirtyScene {
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels },
waScaleManager
);
this.pathfindingManager = new PathfindingManager(this, this.gameMap.getCollisionsGrid());
biggestAvailableAreaStore.recompute();
this.cameraManager.startFollowPlayer(this.CurrentPlayer);
@ -605,10 +607,6 @@ export class GameScene extends DirtyScene {
scriptPromises.push(iframeListener.registerScript(script, !disableModuleMode));
}
this.userInputManager.spaceEvent(() => {
this.outlinedItem?.activate();
});
this.reposition();
// From now, this game scene will be notified of reposition events
@ -677,6 +675,10 @@ export class GameScene extends DirtyScene {
);
}
public activateOutlinedItem(): void {
this.outlinedItem?.activate();
}
/**
* Initializes the connection to Pusher.
*/
@ -1688,7 +1690,6 @@ ${escapedMessage}
texturesPromise,
PlayerAnimationDirections.Down,
false,
this.userInputManager,
this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
);
@ -1808,7 +1809,7 @@ ${escapedMessage}
update(time: number, delta: number): void {
this.dirty = false;
this.currentTick = time;
this.CurrentPlayer.moveUser(delta);
this.CurrentPlayer.moveUser(delta, this.userInputManager.getEventListForGameTick());
// Let's handle all events
while (this.pendingEvents.length !== 0) {
@ -2172,4 +2173,16 @@ ${escapedMessage}
this.scene.stop(this.scene.key);
this.scene.remove(this.scene.key);
}
public getGameMap(): GameMap {
return this.gameMap;
}
public getCameraManager(): CameraManager {
return this.cameraManager;
}
public getPathfindingManager(): PathfindingManager {
return this.pathfindingManager;
}
}

View file

@ -1,8 +1,7 @@
import { PlayerAnimationDirections } from "./Animation";
import type { GameScene } from "../Game/GameScene";
import { ActiveEventList, UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { ActiveEventList, UserInputEvent } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character";
import type { RemotePlayer } from "../Entity/RemotePlayer";
import { get } from "svelte/store";
import { userMovingStore } from "../../Stores/GameStore";
@ -12,6 +11,8 @@ export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote";
export class Player extends Character {
private pathToFollow?: { x: number; y: number }[];
constructor(
Scene: GameScene,
x: number,
@ -20,7 +21,6 @@ export class Player extends Character {
texturesPromise: Promise<string[]>,
direction: PlayerAnimationDirections,
moving: boolean,
private userInputManager: UserInputManager,
companion: string | null,
companionTexturePromise?: Promise<string>
) {
@ -30,6 +30,55 @@ export class Player extends Character {
this.getBody().setImmovable(false);
}
public moveUser(delta: number, activeUserInputEvents: ActiveEventList): void {
const state = get(followStateStore);
const role = get(followRoleStore);
if (activeUserInputEvents.get(UserInputEvent.Follow)) {
if (state === "off" && this.scene.groups.size > 0) {
this.sendFollowRequest();
} else if (state === "active") {
followStateStore.set("ending");
}
}
if (this.pathToFollow && activeUserInputEvents.anyExcept(UserInputEvent.SpeedUp)) {
this.pathToFollow = undefined;
}
let x = 0;
let y = 0;
if ((state === "active" || state === "ending") && role === "follower") {
[x, y] = this.computeFollowMovement();
}
if (this.pathToFollow) {
[x, y] = this.computeFollowPathMovement();
}
this.inputStep(activeUserInputEvents, x, y);
}
public sendFollowRequest() {
this.scene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
}
public startFollowing() {
followStateStore.set("active");
this.scene.connection?.emitFollowConfirmation();
}
public setPathToFollow(path: { x: number; y: number }[]): void {
// take collider offset into consideraton
this.pathToFollow = this.adjustPathToFollowToColliderBounds(path);
}
private adjustPathToFollowToColliderBounds(path: { x: number; y: number }[]): { x: number; y: number }[] {
return path.map((step) => {
return { x: step.x, y: step.y - this.getBody().offset.y };
});
}
private inputStep(activeEvents: ActiveEventList, x: number, y: number) {
// Process input events
if (activeEvents.get(UserInputEvent.MoveUp)) {
@ -95,40 +144,29 @@ export class Player extends Character {
if (distance < 2000) {
return [0, 0];
}
const xMovement = xDistance / Math.sqrt(distance);
const yMovement = yDistance / Math.sqrt(distance);
return [xMovement, yMovement];
return this.getMovementDirection(xDistance, yDistance, distance);
}
public moveUser(delta: number): void {
const activeEvents = this.userInputManager.getEventListForGameTick();
const state = get(followStateStore);
const role = get(followRoleStore);
if (activeEvents.get(UserInputEvent.Follow)) {
if (state === "off" && this.scene.groups.size > 0) {
this.sendFollowRequest();
} else if (state === "active") {
followStateStore.set("ending");
}
private computeFollowPathMovement(): number[] {
if (this.pathToFollow?.length === 0) {
this.pathToFollow = undefined;
}
let x = 0;
let y = 0;
if ((state === "active" || state === "ending") && role === "follower") {
[x, y] = this.computeFollowMovement();
if (!this.pathToFollow) {
return [0, 0];
}
this.inputStep(activeEvents, x, y);
const nextStep = this.pathToFollow[0];
// Compute movement direction
const xDistance = nextStep.x - this.x;
const yDistance = nextStep.y - this.y;
const distance = Math.pow(xDistance, 2) + Math.pow(yDistance, 2);
if (distance < 200) {
this.pathToFollow.shift();
}
return this.getMovementDirection(xDistance, yDistance, distance);
}
public sendFollowRequest() {
this.scene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
}
public startFollowing() {
followStateStore.set("active");
this.scene.connection?.emitFollowConfirmation();
private getMovementDirection(xDistance: number, yDistance: number, distance: number): [number, number] {
return [xDistance / Math.sqrt(distance), yDistance / Math.sqrt(distance)];
}
}

View file

@ -0,0 +1,58 @@
import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface";
import type { GameScene } from "../Game/GameScene";
export class GameSceneUserInputHandler implements UserInputHandlerInterface {
private gameScene: GameScene;
constructor(gameScene: GameScene) {
this.gameScene = gameScene;
}
public handleMouseWheelEvent(
pointer: Phaser.Input.Pointer,
gameObjects: Phaser.GameObjects.GameObject[],
deltaX: number,
deltaY: number,
deltaZ: number
): void {
this.gameScene.zoomByFactor(1 - (deltaY / 53) * 0.1);
}
public handlePointerUpEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {
if (pointer.rightButtonReleased() || pointer.getDuration() > 250) {
return;
}
const camera = this.gameScene.getCameraManager().getCamera();
const index = this.gameScene
.getGameMap()
.getTileIndexAt(pointer.x + camera.scrollX, pointer.y + camera.scrollY);
const startTile = this.gameScene
.getGameMap()
.getTileIndexAt(this.gameScene.CurrentPlayer.x, this.gameScene.CurrentPlayer.y);
this.gameScene
.getPathfindingManager()
.findPath(startTile, index, true)
.then((path) => {
const tileDimensions = this.gameScene.getGameMap().getTileDimensions();
const pixelPath = path.map((step) => {
return {
x: step.x * tileDimensions.width + tileDimensions.width * 0.5,
y: step.y * tileDimensions.height + tileDimensions.height * 0.5,
};
});
// Remove first step as it is for the tile we are currently standing on
pixelPath.shift();
this.gameScene.CurrentPlayer.setPathToFollow(pixelPath);
})
.catch((reason) => {
console.warn(reason);
});
}
public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {}
public handleSpaceKeyUpEvent(event: Event): Event {
this.gameScene.activateOutlinedItem();
return event;
}
}

View file

@ -1,8 +1,8 @@
import type { GameScene } from "../Game/GameScene";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { MobileJoystick } from "../Components/MobileJoystick";
import { enableUserInputsStore } from "../../Stores/UserInputStore";
import type { Direction } from "phaser3-rex-plugins/plugins/virtualjoystick.js";
import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface";
interface UserInputManagerDatum {
keyInstance: Phaser.Input.Keyboard.Key;
@ -37,12 +37,21 @@ export class ActiveEventList {
any(): boolean {
return Array.from(this.eventMap.values()).reduce((accu, curr) => accu || curr, false);
}
anyExcept(...exceptions: UserInputEvent[]): boolean {
const userInputEvents = Array.from(this.eventMap);
for (const event of userInputEvents) {
if (event[1] && !exceptions.includes(event[0])) {
return true;
}
}
return false;
}
}
//this class is responsible for catching user inputs and listing all active user actions at every game tick events.
export class UserInputManager {
private KeysCode!: UserInputManagerDatum[];
private Scene: GameScene;
private keysCode!: UserInputManagerDatum[];
private scene: Phaser.Scene;
private isInputDisabled: boolean;
private joystick!: MobileJoystick;
@ -51,11 +60,15 @@ export class UserInputManager {
private joystickForceAccuX = 0;
private joystickForceAccuY = 0;
constructor(Scene: GameScene) {
this.Scene = Scene;
private userInputHandler: UserInputHandlerInterface;
constructor(scene: Phaser.Scene, userInputHandler: UserInputHandlerInterface) {
this.scene = scene;
this.userInputHandler = userInputHandler;
this.isInputDisabled = false;
this.initKeyBoardEvent();
this.initMouseWheel();
this.bindInputEventHandlers();
if (touchScreenManager.supportTouchScreen) {
this.initVirtualJoystick();
}
@ -66,7 +79,7 @@ export class UserInputManager {
}
initVirtualJoystick() {
this.joystick = new MobileJoystick(this.Scene);
this.joystick = new MobileJoystick(this.scene);
this.joystick.on("update", () => {
this.joystickForceAccuX = this.joystick.forceX ? this.joystickForceAccuX : 0;
this.joystickForceAccuY = this.joystick.forceY ? this.joystickForceAccuY : 0;
@ -92,80 +105,80 @@ export class UserInputManager {
}
initKeyBoardEvent() {
this.KeysCode = [
this.keysCode = [
{
event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false),
},
{
event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false),
},
{
event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false),
},
{
event: UserInputEvent.SpeedUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false),
},
{
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false),
},
{
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
},
{
event: UserInputEvent.Follow,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
{
event: UserInputEvent.Shout,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
];
}
clearAllListeners() {
this.Scene.input.keyboard.removeAllListeners();
this.scene.input.keyboard.removeAllListeners();
}
//todo: should we also disable the joystick?
disableControls() {
this.Scene.input.keyboard.removeAllKeys();
this.scene.input.keyboard.removeAllKeys();
this.isInputDisabled = true;
}
@ -201,7 +214,7 @@ export class UserInputManager {
}
});
eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any());
this.KeysCode.forEach((d) => {
this.keysCode.forEach((d) => {
if (d.keyInstance.isDown) {
eventsMap.set(d.event, true);
}
@ -209,30 +222,60 @@ export class UserInputManager {
return eventsMap;
}
spaceEvent(callback: Function) {
this.Scene.input.keyboard.on("keyup-SPACE", (event: Event) => {
callback();
return event;
});
}
addSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.addListener("keyup-SPACE", callback);
this.scene.input.keyboard.addListener("keyup-SPACE", callback);
}
removeSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.removeListener("keyup-SPACE", callback);
this.scene.input.keyboard.removeListener("keyup-SPACE", callback);
}
destroy(): void {
this.joystick?.destroy();
}
private initMouseWheel() {
this.Scene.input.on(
"wheel",
(pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
this.Scene.zoomByFactor(1 - (deltaY / 53) * 0.1);
private bindInputEventHandlers() {
this.scene.input.on(
Phaser.Input.Events.POINTER_WHEEL,
(
pointer: Phaser.Input.Pointer,
gameObjects: Phaser.GameObjects.GameObject[],
deltaX: number,
deltaY: number,
deltaZ: number
) => {
if (this.isInputDisabled) {
return;
}
this.userInputHandler.handleMouseWheelEvent(pointer, gameObjects, deltaX, deltaY, deltaZ);
}
);
this.scene.input.on(
Phaser.Input.Events.POINTER_UP,
(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => {
this.joystick.hide();
this.userInputHandler.handlePointerUpEvent(pointer, gameObjects);
}
);
this.scene.input.on(
Phaser.Input.Events.POINTER_DOWN,
(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => {
if (!pointer.wasTouch) {
return;
}
this.userInputHandler.handlePointerDownEvent(pointer, gameObjects);
// Let's only display the joystick if there is one finger on the screen
if ((pointer.event as TouchEvent).touches.length === 1) {
this.joystick.showAt(pointer.x, pointer.y);
} else {
this.joystick.hide();
}
}
);
this.scene.input.keyboard.on("keyup-SPACE", (event: Event) => {
this.userInputHandler.handleSpaceKeyUpEvent(event);
});
}
}

View file

@ -22,4 +22,13 @@ export class MathUtils {
public static isBetween(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
public static distanceBetween(
p1: { x: number; y: number },
p2: { x: number; y: number },
squared: boolean = true
): number {
const distance = Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
return squared ? Math.sqrt(distance) : distance;
}
}

View file

@ -0,0 +1,109 @@
import * as EasyStar from "easystarjs";
import { MathUtils } from "./MathUtils";
export class PathfindingManager {
private scene: Phaser.Scene;
private easyStar;
private grid: number[][];
constructor(scene: Phaser.Scene, collisionsGrid: number[][]) {
this.scene = scene;
this.easyStar = new EasyStar.js();
this.easyStar.enableDiagonals();
this.grid = collisionsGrid;
this.setEasyStarGrid(collisionsGrid);
}
public async findPath(
start: { x: number; y: number },
end: { x: number; y: number },
tryFindingNearestAvailable: boolean = false
): Promise<{ x: number; y: number }[]> {
let endPoints: { x: number; y: number }[] = [end];
if (tryFindingNearestAvailable) {
endPoints = [
end,
...this.getNeighbouringTiles(end).sort((a, b) => {
const aDist = MathUtils.distanceBetween(a, start, false);
const bDist = MathUtils.distanceBetween(b, start, false);
if (aDist > bDist) {
return 1;
}
if (aDist < bDist) {
return -1;
}
return 0;
}),
];
}
let path: { x: number; y: number }[] = [];
while (endPoints.length > 0) {
const endPoint = endPoints.shift();
if (!endPoint) {
return [];
}
// rejected Promise will return undefined for path
path = await this.getPath(start, endPoint).catch();
if (path && path.length > 0) {
return path;
}
}
return [];
}
private getNeighbouringTiles(tile: { x: number; y: number }): { x: number; y: number }[] {
const xOffsets = [-1, 0, 1, 1, 1, 0, -1, -1];
const yOffsets = [-1, -1, -1, 0, 1, 1, 1, 0];
const neighbours: { x: number; y: number }[] = [];
for (let i = 0; i < 8; i += 1) {
const tileToCheck = { x: tile.x + xOffsets[i], y: tile.y + yOffsets[i] };
if (this.isTileWithinMap(tileToCheck)) {
neighbours.push(tileToCheck);
}
}
return neighbours;
}
private isTileWithinMap(tile: { x: number; y: number }): boolean {
const mapHeight = this.grid.length ?? 0;
const mapWidth = this.grid[0]?.length ?? 0;
return MathUtils.isBetween(tile.x, 0, mapWidth) && MathUtils.isBetween(tile.y, 0, mapHeight);
}
/**
* Returns empty array if path was not found
*/
private async getPath(
start: { x: number; y: number },
end: { x: number; y: number }
): Promise<{ x: number; y: number }[]> {
return new Promise((resolve, reject) => {
this.easyStar.findPath(start.x, start.y, end.x, end.y, (path) => {
if (path === null) {
resolve([]);
} else {
resolve(path);
}
});
this.easyStar.calculate();
});
}
private setEasyStarGrid(grid: number[][]): void {
this.easyStar.setGrid(grid);
this.easyStar.setAcceptableTiles([0]); // zeroes are walkable
}
private logGridToTheConsole(grid: number[][]): void {
let rowNumber = 0;
for (const row of grid) {
console.log(`${rowNumber}:\t${row}`);
rowNumber += 1;
}
}
}

View file

@ -102,6 +102,7 @@ const config: GameConfig = {
dom: {
createContainer: true,
},
disableContextMenu: true,
render: {
pixelArt: true,
roundPixels: true,

View file

@ -1990,6 +1990,13 @@ dot-case@^3.0.4:
no-case "^3.0.4"
tslib "^2.0.3"
easystarjs@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/easystarjs/-/easystarjs-0.4.4.tgz#8cec6d20d0d8660715da0301d1da440370a8f40a"
integrity sha512-ZSt0TkB8xuIXRIrKsM3jkmk1/cZUtyvf0DqOXf6wuKq9slx9UA5kkLtiaWhtmOQFJFKdabbvXwk6RO0znghArQ==
dependencies:
heap "0.2.6"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -2925,6 +2932,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
heap@0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac"
integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"