diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a591294..fc0bbe7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] - 2021-02-17 + +### Added + +- **Avatars** + + - Added ability to display effects for an avatar. To use, set the `avatar.effect` property + - Improved avatar loading by only loading the required libraries for a specific avatar + +- **Event handling** + - Added `onPointerOver` and `onPointerOut` event handling for Avatars and Furniture + - Added ability to skip event handling for certain kinds of room objects while propagating. + Use this in an event handler: `event.skip(FURNITURE, AVATAR)` or `event.skipExcept(TILE_CURSOR)` + +### Changed + +- Change furniture visualization handling for better testability +- Reorganize stories in storybook +- Deprecate `BasicFurnitureVisualization` in favor of `StaticFurnitureVisualization` + +### Removed + +- Remove use of `offsets.json` for avatars +- Remove `resumePropagation` for events +- Disable animation queueing for avatars and furnitures `move`/`walk` methods + ## [0.6.5] - 2021-01-25 ### Fixed diff --git a/package-lock.json b/package-lock.json index fdb0e8f4..e6a30af2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1268,6 +1268,11 @@ "@sinonjs/commons": "^1.7.0" } }, + "@timohausmann/quadtree-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.3.tgz", + "integrity": "sha512-uXGWcikTrN7fjriXwZa93+MorzrdHOUEMBgiwaemA4Kix9Gu2KW0zw6tvDPa+s2EKeKusf64vj29pUmT0eObFA==" + }, "@tweenjs/tween.js": { "version": "18.6.4", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", @@ -1481,6 +1486,12 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "dev": true }, + "@types/rbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.0.tgz", + "integrity": "sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg==", + "dev": true + }, "@types/react": { "version": "16.14.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.3.tgz", @@ -6776,6 +6787,19 @@ "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==", "dev": true }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, + "rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "requires": { + "quickselect": "^2.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", diff --git a/package.json b/package.json index c3b5033f..507a0af0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@types/jsdom": "^16.2.6", "@types/node": "^14.14.13", "@types/node-fetch": "^2.5.7", + "@types/rbush": "^3.0.0", "@types/react": "^16.9.56", "@types/tween.js": "^18.6.1", "@types/ws": "^7.4.0", @@ -41,6 +42,7 @@ }, "dependencies": { "@gizeta/swf-reader": "^1.0.0", + "@timohausmann/quadtree-js": "^1.2.3", "axios": "^0.21.1", "bin-pack": "^1.0.2", "bluebird": "^3.7.2", @@ -56,6 +58,7 @@ "jszip": "^3.5.0", "node-fetch": "^2.6.1", "quadtree-lib": "^1.0.9", + "rbush": "^3.0.1", "react": "^16.14.0", "rxjs": "^6.6.3", "stream": "0.0.2", @@ -75,7 +78,8 @@ "dump": "yarn ts-node-dev src/downloading/cli/index.tsx dump", "test": "jest", "build": "rm -rf dist && tsc", - "prepublishOnly": "yarn build" + "prepublishOnly": "yarn build", + "storybook": "cd storybook && yarn storybook" }, "bin": { "shroom": "dist/cli/index.js" diff --git a/src/index.ts b/src/index.ts index c60f9c55..eb5ca25a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +import { IEventManagerEvent } from "./objects/events/interfaces/IEventManagerEvent"; + export { RoomObject } from "./objects/RoomObject"; export { Avatar } from "./objects/avatar/Avatar"; export { FloorFurniture } from "./objects/furniture/FloorFurniture"; @@ -7,12 +9,10 @@ export { RoomCamera } from "./objects/room/RoomCamera"; export { loadRoomTexture } from "./util/loadRoomTexture"; export { parseTileMapString } from "./util/parseTileMapString"; export { IFurniture, IFurnitureBehavior } from "./objects/furniture/IFurniture"; -export { HitEvent } from "./interfaces/IHitDetection"; export { IFurnitureData } from "./interfaces/IFurnitureData"; export { FurnitureData } from "./objects/furniture/FurnitureData"; export { Shroom } from "./objects/Shroom"; export { Landscape } from "./objects/room/Landscape"; -export { HitDetection } from "./objects/hitdetection/HitDetection"; export { AnimationTicker } from "./objects/animation/AnimationTicker"; export { FurnitureLoader } from "./objects/furniture/FurnitureLoader"; export { AvatarLoader } from "./objects/avatar/AvatarLoader"; @@ -34,3 +34,11 @@ export { IFurnitureVisualization } from "./objects/furniture/IFurnitureVisualiza export { WallLeft } from "./objects/room/parts/WallLeft"; export { WallRight } from "./objects/room/parts/WallRight"; export { RoomModelVisualization } from "./objects/room/RoomModelVisualization"; + +export { + AVATAR, + TILE_CURSOR, + FURNITURE, +} from "./objects/events/interfaces/IEventGroup"; + +export type HitEvent = IEventManagerEvent; diff --git a/src/interfaces/IHitDetection.ts b/src/interfaces/IHitDetection.ts index e8a2d7a1..e69de29b 100644 --- a/src/interfaces/IHitDetection.ts +++ b/src/interfaces/IHitDetection.ts @@ -1,35 +0,0 @@ -export interface Rect { - x: number; - y: number; - width: number; - height: number; - zIndex: number; -} - -export type HitEventType = "click" | "pointerdown" | "pointerup"; - -export interface HitEvent { - mouseEvent: MouseEvent; - tag?: string; - target: HitDetectionElement; - - stopPropagation(): void; - resumePropagation(): void; -} - -export interface HitDetectionElement { - group?: unknown; - - trigger(type: HitEventType, event: HitEvent): void; - hits(x: number, y: number): boolean; - getHitDetectionZIndex(): number; - createDebugSprite?(): PIXI.Sprite | undefined; -} - -export interface HitDetectionNode { - remove(): void; -} - -export interface IHitDetection { - register(rectangle: HitDetectionElement): HitDetectionNode; -} diff --git a/src/interfaces/IRoomContext.ts b/src/interfaces/IRoomContext.ts index 22f746a0..893e6379 100644 --- a/src/interfaces/IRoomContext.ts +++ b/src/interfaces/IRoomContext.ts @@ -1,10 +1,10 @@ +import { IEventManager } from "../objects/events/interfaces/IEventManager"; import { ILandscapeContainer } from "../objects/room/ILandscapeContainer"; import { Room } from "../objects/room/Room"; import { IAnimationTicker } from "./IAnimationTicker"; import { IAvatarLoader } from "./IAvatarLoader"; import { IConfiguration } from "./IConfiguration"; import { IFurnitureLoader } from "./IFurnitureLoader"; -import { IHitDetection } from "./IHitDetection"; import { IRoomGeometry } from "./IRoomGeometry"; import { IRoomObjectContainer } from "./IRoomObjectContainer"; import { IRoomVisualization } from "./IRoomVisualization"; @@ -17,10 +17,10 @@ export interface IRoomContext { animationTicker: IAnimationTicker; visualization: IRoomVisualization; roomObjectContainer: IRoomObjectContainer; - hitDetection: IHitDetection; configuration: IConfiguration; tilemap: ITileMap; landscapeContainer: ILandscapeContainer; application: PIXI.Application; room: Room; + eventManager: IEventManager; } diff --git a/src/objects/RoomObject.ts b/src/objects/RoomObject.ts index 7c19d2a5..666f7578 100644 --- a/src/objects/RoomObject.ts +++ b/src/objects/RoomObject.ts @@ -72,8 +72,8 @@ export abstract class RoomObject implements IRoomObject { return this.getRoomContext().avatarLoader; } - protected get hitDetection() { - return this.getRoomContext().hitDetection; + protected get eventManager() { + return this.getRoomContext().eventManager; } protected get tilemap() { diff --git a/src/objects/Shroom.ts b/src/objects/Shroom.ts index 48f05b7e..ea5cc3e6 100644 --- a/src/objects/Shroom.ts +++ b/src/objects/Shroom.ts @@ -2,7 +2,6 @@ import { AnimationTicker } from "./animation/AnimationTicker"; import { AvatarLoader } from "./avatar/AvatarLoader"; import { FurnitureLoader } from "./furniture/FurnitureLoader"; import { FurnitureData } from "./furniture/FurnitureData"; -import { HitDetection } from "./hitdetection/HitDetection"; import { Dependencies } from "./room/Room"; export class Shroom { @@ -31,7 +30,6 @@ export class Shroom { avatarLoader, furnitureData, furnitureLoader, - hitDetection, }: { resourcePath?: string; } & Partial) { @@ -45,7 +43,6 @@ export class Shroom { return { for: (application: PIXI.Application) => { - const _hitDetection = hitDetection ?? HitDetection.create(application); const _animationTicker = animationTicker ?? AnimationTicker.create(application); @@ -53,7 +50,6 @@ export class Shroom { animationTicker: _animationTicker, avatarLoader: _avatarLoader, furnitureLoader: _furnitureLoader, - hitDetection: _hitDetection, configuration: _configuration, furnitureData: _furnitureData, application, diff --git a/src/objects/avatar/Avatar.ts b/src/objects/avatar/Avatar.ts index ea6b0c9f..862fedbe 100644 --- a/src/objects/avatar/Avatar.ts +++ b/src/objects/avatar/Avatar.ts @@ -44,6 +44,8 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { private _onDoubleClick: HitEventHandler | undefined = undefined; private _onPointerDown: HitEventHandler | undefined = undefined; private _onPointerUp: HitEventHandler | undefined = undefined; + private _onPointerOver: HitEventHandler | undefined = undefined; + private _onPointerOut: HitEventHandler | undefined = undefined; constructor({ look, @@ -135,6 +137,24 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { this._updateEventHandlers(); } + public get onPointerOver() { + return this._onPointerOver; + } + + public set onPointerOver(value) { + this._onPointerOver = value; + this._updateEventHandlers(); + } + + public get onPointerOut() { + return this._onPointerOut; + } + + public set onPointerOut(value) { + this._onPointerOut = value; + this._updateEventHandlers(); + } + /** * The x position of the avatar in the room. * The y-Axis is marked in the following graphic: @@ -288,12 +308,12 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { * for placing UI relative to the user. */ get screenPosition() { - const worldTransform = this._avatarSprites.worldTransform; + const worldTransform = this._avatarSprites.getGlobalPosition(); if (worldTransform == null) return; return { - x: worldTransform.tx, - y: worldTransform.ty, + x: worldTransform.x, + y: worldTransform.y, }; } @@ -376,14 +396,14 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { this._placeholderSprites.dependencies = { animationTicker: this.animationTicker, avatarLoader: this.avatarLoader, - hitDetection: this.hitDetection, + eventManager: this.eventManager, }; } this._loadingAvatarSprites.dependencies = { animationTicker: this.animationTicker, avatarLoader: this.avatarLoader, - hitDetection: this.hitDetection, + eventManager: this.eventManager, }; this._updateAvatarSprites(); @@ -454,12 +474,16 @@ export class Avatar extends RoomObject implements IMoveable, IScreenPositioned { this._placeholderSprites.onDoubleClick = this._onDoubleClick; this._placeholderSprites.onPointerDown = this._onPointerDown; this._placeholderSprites.onPointerUp = this._onPointerUp; + this._placeholderSprites.onPointerOut = this._onPointerOut; + this._placeholderSprites.onPointerOver = this._onPointerOver; } this._loadingAvatarSprites.onClick = this._onClick; this._loadingAvatarSprites.onDoubleClick = this._onDoubleClick; this._loadingAvatarSprites.onPointerDown = this._onPointerDown; this._loadingAvatarSprites.onPointerUp = this._onPointerUp; + this._loadingAvatarSprites.onPointerOut = this._onPointerOut; + this._loadingAvatarSprites.onPointerOver = this._onPointerOver; } private _getPlaceholderLookOptions(): LookOptions { diff --git a/src/objects/avatar/BaseAvatar.ts b/src/objects/avatar/BaseAvatar.ts index a8d7c015..2f5ad564 100644 --- a/src/objects/avatar/BaseAvatar.ts +++ b/src/objects/avatar/BaseAvatar.ts @@ -7,7 +7,6 @@ import { import { ClickHandler } from "../hitdetection/ClickHandler"; import { HitSprite } from "../hitdetection/HitSprite"; import { isSetEqual } from "../../util/isSetEqual"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IAnimationTicker } from "../../interfaces/IAnimationTicker"; import { Shroom } from "../Shroom"; import { AvatarFigurePartType } from "./enum/AvatarFigurePartType"; @@ -17,6 +16,14 @@ import { DefaultAvatarDrawPart, } from "./types"; import { AvatarDrawDefinition } from "./structure/AvatarDrawDefinition"; +import { IEventManager } from "../events/interfaces/IEventManager"; +import { NOOP_EVENT_MANAGER } from "../events/EventManager"; +import { + AVATAR, + EventGroupIdentifier, + IEventGroup, +} from "../events/interfaces/IEventGroup"; +import { EventOverOutHandler } from "../events/EventOverOutHandler"; const bodyPartTypes: Set = new Set([ AvatarFigurePartType.Head, @@ -47,15 +54,16 @@ export interface BaseAvatarOptions { } export interface BaseAvatarDependencies { - hitDetection: IHitDetection; + eventManager: IEventManager; animationTicker: IAnimationTicker; avatarLoader: IAvatarLoader; } -export class BaseAvatar extends PIXI.Container { +export class BaseAvatar extends PIXI.Container implements IEventGroup { private _container: PIXI.Container | undefined; private _avatarLoaderResult: AvatarLoaderResult | undefined; private _avatarDrawDefinition: AvatarDrawDefinition | undefined; + private _avatarDestroyed = false; private _lookOptions: LookOptions | undefined; private _nextLookOptions: LookOptions | undefined; @@ -66,6 +74,7 @@ export class BaseAvatar extends PIXI.Container { private _currentFrame = 0; private _clickHandler: ClickHandler = new ClickHandler(); + private _overOutHandler: EventOverOutHandler = new EventOverOutHandler(); private _refreshFrame = false; private _refreshLook = false; @@ -140,6 +149,22 @@ export class BaseAvatar extends PIXI.Container { this._clickHandler.onPointerUp = value; } + get onPointerOut() { + return this._overOutHandler.onOut; + } + + set onPointerOut(value) { + this._overOutHandler.onOut = value; + } + + get onPointerOver() { + return this._overOutHandler.onOver; + } + + set onPointerOver(value) { + this._overOutHandler.onOver = value; + } + get lookOptions() { if (this._nextLookOptions != null) { return this._nextLookOptions; @@ -182,10 +207,17 @@ export class BaseAvatar extends PIXI.Container { static fromShroom(shroom: Shroom, options: BaseAvatarOptions) { const avatar = new BaseAvatar({ ...options }); - avatar.dependencies = shroom.dependencies; + avatar.dependencies = { + ...shroom.dependencies, + eventManager: NOOP_EVENT_MANAGER, + }; return avatar; } + getEventGroupIdentifier(): EventGroupIdentifier { + return AVATAR; + } + destroy(): void { super.destroy(); this._destroyAssets(); @@ -196,7 +228,10 @@ export class BaseAvatar extends PIXI.Container { } private _destroyAssets() { - this._sprites.forEach((sprite) => sprite.destroy()); + this._sprites.forEach((sprite) => { + this._overOutHandler.remove(sprite.events); + sprite.destroy(); + }); this._sprites = new Map(); this._container?.destroy(); } @@ -250,6 +285,7 @@ export class BaseAvatar extends PIXI.Container { drawDefinition: AvatarDrawDefinition, currentFrame: number ) { + if (this._destroyed) throw new Error("BaseAvatar was destroyed already"); if (!this.mounted) return; this._sprites.forEach((value) => { @@ -279,6 +315,10 @@ export class BaseAvatar extends PIXI.Container { if (sprite == null) { sprite = this._createAsset(part, asset); + + if (sprite != null) { + this._overOutHandler.register(sprite.events); + } } if (sprite == null) return; @@ -300,6 +340,10 @@ export class BaseAvatar extends PIXI.Container { if (sprite == null) { sprite = this._createAsset(part, asset); + + if (sprite != null) { + this._overOutHandler.register(sprite.events); + } } if (sprite == null) return; @@ -342,24 +386,24 @@ export class BaseAvatar extends PIXI.Container { if (texture == null) return; const sprite = new HitSprite({ - hitDetection: this.dependencies.hitDetection, + eventManager: this.dependencies.eventManager, mirrored: asset.mirror, group: this, }); sprite.hitTexture = texture; - sprite.x = asset.x; sprite.y = asset.y; - sprite.addEventListener("click", (event) => { + + sprite.events.addEventListener("click", (event) => { this._clickHandler.handleClick(event); }); - sprite.addEventListener("pointerdown", (event) => { + sprite.events.addEventListener("pointerdown", (event) => { this._clickHandler.handlePointerDown(event); }); - sprite.addEventListener("pointerup", (event) => { + sprite.events.addEventListener("pointerup", (event) => { this._clickHandler.handlePointerUp(event); }); @@ -391,6 +435,7 @@ export class BaseAvatar extends PIXI.Container { skipCaching: this._skipCaching, }) .then((result) => { + if (this._destroyed) return; if (requestId !== this._updateId) return; this._avatarLoaderResult = result; @@ -424,6 +469,9 @@ export class BaseAvatar extends PIXI.Container { this._cancelTicker(); } + if (this._cancelTicker != null) { + this._cancelTicker(); + } this._cancelTicker = this.dependencies.animationTicker.subscribe(() => { if (this._refreshLook) { this._refreshLook = false; diff --git a/src/objects/events/EventEmitter.ts b/src/objects/events/EventEmitter.ts new file mode 100644 index 00000000..042f19fe --- /dev/null +++ b/src/objects/events/EventEmitter.ts @@ -0,0 +1,44 @@ +export class EventEmitter> { + private _map = new Map>>(); + + addEventListener( + name: K, + callback: EventCallback + ) { + const key = name.toString(); + const currentEventCallbackSet = + this._map.get(key) ?? new Set>(); + currentEventCallbackSet.add(callback); + + this._map.set(key, currentEventCallbackSet); + } + + removeEventListener( + name: K, + callback: EventCallback + ) { + const key = name.toString(); + const currentEventCallbackSet = this._map.get(key); + + if (currentEventCallbackSet != null) { + currentEventCallbackSet.delete(callback); + } + } + + trigger(name: K, value: TMap[K]) { + const key = name.toString(); + const currentEventCallbackSet = this._map.get(key); + + currentEventCallbackSet?.forEach((callback) => callback(value)); + } +} + +type EventCallback> = ( + event: TMap[K] +) => void; + +window.addEventListener; + +type BaseTypeMap = { + [k in keyof T]: unknown; +}; diff --git a/src/objects/events/EventManager.test.ts b/src/objects/events/EventManager.test.ts new file mode 100644 index 00000000..ddeb7889 --- /dev/null +++ b/src/objects/events/EventManager.test.ts @@ -0,0 +1,429 @@ +import { InteractionEvent } from "pixi.js"; +import { BehaviorSubject } from "rxjs"; +import { Rectangle } from "../room/IRoomRectangle"; +import { EventManager } from "./EventManager"; +import { + AVATAR, + FURNITURE, + IEventGroup, + TILE_CURSOR, +} from "./interfaces/IEventGroup"; +import { IEventTarget } from "./interfaces/IEventTarget"; + +const interactionEvent: InteractionEvent = { + data: {}, +} as any; + +test("handles click when mounted", () => { + const manager = new EventManager(); + + const target: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const node = manager.register(target); + manager.click(interactionEvent, 10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(1); + + node.destroy(); + manager.click(interactionEvent, 10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("handles click on hit elements", () => { + const manager = new EventManager(); + + const target: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const node = manager.register(target); + manager.click(interactionEvent, 10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("doesn't handle click on not hit elements", () => { + const manager = new EventManager(); + + const target: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => false, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const node = manager.register(target); + manager.click(interactionEvent, 10, 10); + expect(target.triggerClick).toHaveBeenCalledTimes(0); +}); + +test("handles click on multiple elements", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target3: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target4: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), + hits: () => false, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + manager.register(target3); + manager.register(target4); + + manager.click(interactionEvent, 80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(1); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); + expect(target3.triggerClick).toHaveBeenCalledTimes(1); + expect(target4.triggerClick).toHaveBeenCalledTimes(0); +}); + +test("handles click on multiple elements", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + + manager.click(interactionEvent, 80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(1); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("only handles first element when elements from same group", () => { + const manager = new EventManager(); + + const group: IEventGroup = { getEventGroupIdentifier: () => FURNITURE }; + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => group, + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => group, + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + + manager.click(interactionEvent, 80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(0); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("event.skip() skips elements", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skip([AVATAR])), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target3: IEventTarget = { + getEventZOrder: () => 9, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target4: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skip([FURNITURE])), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + manager.register(target3); + manager.register(target4); + + manager.click(interactionEvent, 80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(0); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); + expect(target3.triggerClick).toHaveBeenCalledTimes(0); + expect(target4.triggerClick).toHaveBeenCalledTimes(1); +}); + +test("move triggers correct events", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: (x) => x <= 100, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.move(interactionEvent, 80, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(interactionEvent, 85, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(interactionEvent, 90, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(interactionEvent, 95, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(interactionEvent, 100, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(0); + + manager.move(interactionEvent, 105, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(1); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(1); + + manager.move(interactionEvent, 100, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(1); + + manager.move(interactionEvent, 105, 80); + expect(target1.triggerPointerOver).toHaveBeenCalledTimes(2); + expect(target1.triggerPointerOut).toHaveBeenCalledTimes(2); +}); + +test("event.skipExcept() skips elements except the specified", () => { + const manager = new EventManager(); + + const target1: IEventTarget = { + getEventZOrder: () => 2, + getGroup: () => ({ getEventGroupIdentifier: () => TILE_CURSOR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 0, y: 0, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target2: IEventTarget = { + getEventZOrder: () => 5, + getGroup: () => ({ getEventGroupIdentifier: () => AVATAR }), + getRectangleObservable: () => + new BehaviorSubject({ x: 50, y: 50, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skipExcept(TILE_CURSOR)), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target3: IEventTarget = { + getEventZOrder: () => 9, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 75, y: 75, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn(), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + const target4: IEventTarget = { + getEventZOrder: () => 10, + getGroup: () => ({ getEventGroupIdentifier: () => FURNITURE }), + getRectangleObservable: () => + new BehaviorSubject({ x: 79, y: 79, width: 100, height: 100 }), + hits: () => true, + triggerClick: jest.fn((event) => event.skipExcept(AVATAR, TILE_CURSOR)), + triggerPointerDown: jest.fn(), + triggerPointerOut: jest.fn(), + triggerPointerOver: jest.fn(), + triggerPointerUp: jest.fn(), + triggerPointerTargetChanged: jest.fn(), + }; + + manager.register(target1); + manager.register(target2); + manager.register(target3); + manager.register(target4); + + manager.click(interactionEvent, 80, 80); + expect(target1.triggerClick).toHaveBeenCalledTimes(1); + expect(target2.triggerClick).toHaveBeenCalledTimes(1); + expect(target3.triggerClick).toHaveBeenCalledTimes(0); + expect(target4.triggerClick).toHaveBeenCalledTimes(1); +}); diff --git a/src/objects/events/EventManager.ts b/src/objects/events/EventManager.ts new file mode 100644 index 00000000..a255e806 --- /dev/null +++ b/src/objects/events/EventManager.ts @@ -0,0 +1,252 @@ +import { EventManagerNode } from "./EventManagerNode"; +import { IEventManagerNode } from "./interfaces/IEventManagerNode"; +import { IEventTarget } from "./interfaces/IEventTarget"; +import RBush from "rbush"; +import { + EventGroupIdentifierParam, + IEventManagerEvent, +} from "./interfaces/IEventManagerEvent"; +import { + EventGroupIdentifier, + IEventGroup, + TILE_CURSOR, +} from "./interfaces/IEventGroup"; +import { IEventManager } from "./interfaces/IEventManager"; +import { InteractionEvent } from "pixi.js"; + +export class EventManager { + private _nodes = new Map(); + private _bush = new RBush(); + private _currentOverElements: Set = new Set(); + private _pointerDownElements: Set = new Set(); + + click(event: InteractionEvent, x: number, y: number) { + const elements = this._performHitTest(x, y); + new Propagation(event, elements.activeNodes, (target, event) => + target.triggerClick(event) + ); + } + + pointerDown(event: InteractionEvent, x: number, y: number) { + const elements = this._performHitTest(x, y); + + this._pointerDownElements = new Set(elements.activeNodes); + + new Propagation(event, elements.activeNodes, (target, event) => + target.triggerPointerDown(event) + ); + } + + pointerUp(event: InteractionEvent, x: number, y: number) { + const elements = this._performHitTest(x, y); + + const elementsSet = new Set(elements.activeNodes); + const clickedNodes = new Set(); + this._pointerDownElements.forEach((node) => { + if (elementsSet.has(node)) { + clickedNodes.add(node); + } + }); + + new Propagation(event, elements.activeNodes, (target, event) => + target.triggerPointerUp(event) + ); + + new Propagation(event, Array.from(clickedNodes), (target, event) => { + target.triggerClick(event); + }); + } + + move(event: InteractionEvent, x: number, y: number) { + const elements = this._performHitTest(x, y); + const current = new Set( + elements.activeNodes.filter( + (node, index) => + // Only interested in the top most element + index === 0 || + // or the tile cursor + node.target.getGroup().getEventGroupIdentifier() === TILE_CURSOR + ) + ); + const previous = this._currentOverElements; + + const added = new Set(); + current.forEach((node) => { + if (!previous.has(node)) { + added.add(node); + } + }); + + const removed = new Set(); + previous.forEach((node) => { + if (!current.has(node)) { + removed.add(node); + } + }); + + const addedGroups = new Set(); + added.forEach((node) => { + addedGroups.add(node.target.getGroup()); + }); + + const removedButGroupPresent = new Set(); + const actualRemoved = new Set(); + + removed.forEach((node) => { + if (addedGroups.has(node.target.getGroup())) { + removedButGroupPresent.add(node); + } + + actualRemoved.add(node); + }); + + this._currentOverElements = current; + + new Propagation( + event, + Array.from(removedButGroupPresent), + (target, event) => { + target.triggerPointerTargetChanged(event); + } + ); + + new Propagation(event, Array.from(actualRemoved), (target, event) => + target.triggerPointerOut(event) + ); + + new Propagation(event, Array.from(added), (target, event) => + target.triggerPointerOver(event) + ); + } + + register(target: IEventTarget): IEventManagerNode { + if (this._nodes.has(target)) throw new Error("Target already registered"); + + const node = new EventManagerNode(target, this._bush); + this._nodes.set(target, node); + + return node; + } + + remove(target: IEventTarget) { + const current = this._nodes.get(target); + if (current == null) throw new Error("Target isn't in the event manager"); + + current.destroy(); + } + + private _performHitTest(x: number, y: number) { + const qualifyingElements = this._bush.search({ + minX: x, + minY: y, + maxX: x, + maxY: y, + }); + + const sortedElements = qualifyingElements + .sort((a, b) => b.target.getEventZOrder() - a.target.getEventZOrder()) + .filter((node) => node.target.hits(x, y)); + + const groups = new Map(); + const groupElements: EventManagerNode[] = []; + + sortedElements.forEach((node) => { + const group = node.target.getGroup(); + if (group != null && groups.has(group)) return; + + groupElements.push(node); + + if (group != null) { + groups.set(group, node); + } + }); + + return { + activeNodes: groupElements, + activeGroups: groups, + }; + } +} + +class Propagation { + private _skip = new Set(); + private _allow = new Set(); + private _stopped = false; + + constructor( + private event: InteractionEvent, + private path: EventManagerNode[], + private _trigger: (target: IEventTarget, event: IEventManagerEvent) => void + ) { + this._propagate(); + } + + private _propagate() { + const event = this._createEvent(); + for (let i = 0; i < this.path.length; i++) { + if (this._stopped) return; + const node = this.path[i]; + + if ( + this._skip.has(node.target.getGroup().getEventGroupIdentifier()) && + !this._allow.has(node.target.getGroup().getEventGroupIdentifier()) + ) { + continue; + } + + if ( + this._allow.size > 0 && + !this._allow.has(node.target.getGroup().getEventGroupIdentifier()) + ) { + continue; + } + + this._trigger(this.path[i].target, event); + } + } + + private _createEvent(): IEventManagerEvent { + return { + interactionEvent: this.event, + mouseEvent: this.event.data.originalEvent, + stopPropagation: () => { + this._stopped = true; + }, + skip: (...identifiers) => { + const add = (identifier: EventGroupIdentifierParam) => { + if (Array.isArray(identifier)) { + identifier.forEach((value) => add(value)); + } else { + this._skip.add(identifier); + } + }; + + add(identifiers); + }, + skipExcept: (...identifiers) => { + const add = (identifier: EventGroupIdentifierParam) => { + if (Array.isArray(identifier)) { + identifier.forEach((value) => add(value)); + } else { + this._allow.add(identifier); + } + }; + + add(identifiers); + }, + }; + } +} + +export const NOOP_EVENT_MANAGER: IEventManager = { + register: (): IEventManagerNode => { + return { + destroy: () => { + // Do nothing + }, + }; + }, + remove: () => { + // Do nothing + }, +}; diff --git a/src/objects/events/EventManagerContainer.ts b/src/objects/events/EventManagerContainer.ts new file mode 100644 index 00000000..8817c0bc --- /dev/null +++ b/src/objects/events/EventManagerContainer.ts @@ -0,0 +1,65 @@ +import * as PIXI from "pixi.js"; +import { EventManager } from "./EventManager"; + +export class EventManagerContainer { + private _box: PIXI.TilingSprite | undefined; + + constructor( + private _application: PIXI.Application, + private _eventManager: EventManager + ) { + this._updateRectangle(); + + _application.ticker.add(this._updateRectangle); + + const interactionManager: PIXI.InteractionManager = this._application + .renderer.plugins.interaction; + + interactionManager.addListener( + "pointermove", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.move(event, position.x, position.y); + }, + true + ); + + interactionManager.addListener( + "pointerup", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.pointerUp(event, position.x, position.y); + }, + true + ); + + interactionManager.addListener( + "pointerdown", + (event: PIXI.InteractionEvent) => { + const position = event.data.getLocalPosition(this._application.stage); + + this._eventManager.pointerDown(event, position.x, position.y); + }, + true + ); + } + + destroy() { + this._application.ticker.remove(this._updateRectangle); + } + + private _updateRectangle = () => { + //this._box?.destroy(); + + const renderer = this._application.renderer; + const width = renderer.width / renderer.resolution; + const height = renderer.height / renderer.resolution; + + this._box = new PIXI.TilingSprite(PIXI.Texture.WHITE, width, height); + this._box.alpha = 0.3; + + //this._application.stage.addChild(this._box); + }; +} diff --git a/src/objects/events/EventManagerNode.ts b/src/objects/events/EventManagerNode.ts new file mode 100644 index 00000000..fb5444c2 --- /dev/null +++ b/src/objects/events/EventManagerNode.ts @@ -0,0 +1,62 @@ +import RBush from "rbush"; +import { Subscription } from "rxjs"; +import { Rectangle } from "../room/IRoomRectangle"; +import { IEventManagerNode } from "./interfaces/IEventManagerNode"; +import { IEventTarget } from "./interfaces/IEventTarget"; + +export class EventManagerNode implements IEventManagerNode { + private _rectangle: Rectangle | undefined; + private _subscription: Subscription; + + public get minX() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.x; + } + + public get maxX() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.x + this._rectangle.width; + } + + public get minY() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.y; + } + + public get maxY() { + if (this._rectangle == null) throw new Error("Rectangle wasn't set"); + + return this._rectangle.y + this._rectangle.height; + } + + constructor( + public readonly target: IEventTarget, + private _bush: RBush + ) { + this._subscription = target.getRectangleObservable().subscribe((value) => { + this._updateRectangle(value); + }); + } + + destroy(): void { + if (this._rectangle != null) { + this._bush.remove(this); + } + this._subscription.unsubscribe(); + } + + private _updateRectangle(rectangle: Rectangle | undefined): void { + if (this._rectangle != null) { + this._bush.remove(this); + } + + this._rectangle = rectangle; + + if (rectangle != null) { + this._bush.insert(this); + } + } +} diff --git a/src/objects/events/EventOverOutHandler.ts b/src/objects/events/EventOverOutHandler.ts new file mode 100644 index 00000000..20a95a78 --- /dev/null +++ b/src/objects/events/EventOverOutHandler.ts @@ -0,0 +1,148 @@ +import { HitSpriteEventMap } from "../hitdetection/HitSprite"; +import { EventEmitter } from "./EventEmitter"; +import { IEventManagerEvent } from "./interfaces/IEventManagerEvent"; + +type EventOverOutCallback = (event: IEventManagerEvent) => void; + +export class EventOverOutHandler { + private _eventEmitters: Map< + EventEmitter, + RegisteredOverOutHandler + > = new Map(); + private _overElements: Set> = new Set(); + private _hover = false; + private _onOverCallback: EventOverOutCallback | undefined; + private _onOutCallback: EventOverOutCallback | undefined; + private _timeout: any; + private _targetChanged = false; + + public get onOver() { + return this._onOverCallback; + } + + public set onOver(value) { + this._onOverCallback = value; + } + + public get onOut() { + return this._onOutCallback; + } + + public set onOut(value) { + this._onOutCallback = value; + } + + register(emitter: EventEmitter) { + if (this._eventEmitters.has(emitter)) return; + + this._eventEmitters.set( + emitter, + new RegisteredOverOutHandler( + emitter, + this._onOver, + this._onOut, + this._onTargetChanged + ) + ); + } + + remove(emitter: EventEmitter) { + const handler = this._eventEmitters.get(emitter); + if (handler == null) return; + + this._eventEmitters.delete(emitter); + this._overElements.delete(emitter); + + handler.destroy(); + } + + private _onOver = ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => { + this._overElements.add(emitter); + this._update(event); + }; + + private _onOut = ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => { + this._overElements.delete(emitter); + this._update(event); + }; + + private _onTargetChanged = ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => { + this._overElements.delete(emitter); + this._targetChanged = true; + this._update(event); + }; + + private _update(event: IEventManagerEvent) { + if (this._overElements.size > 0 && !this._hover) { + this._hover = true; + + if (!this._targetChanged) { + this.onOver && this.onOver(event); + } + + this._targetChanged = false; + } + + if (this._overElements.size < 1 && this._hover) { + this._hover = false; + if (!this._targetChanged) { + this.onOut && this.onOut(event); + } + } + } +} + +class RegisteredOverOutHandler { + constructor( + private emitter: EventEmitter, + private onOver: ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => void, + private onOut: ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => void, + private onTargetChanged: ( + emitter: EventEmitter, + event: IEventManagerEvent + ) => void + ) { + emitter.addEventListener("pointerout", this._handlePointerOut); + emitter.addEventListener("pointerover", this._handlePointerOver); + emitter.addEventListener( + "pointertargetchanged", + this._handlePointerTargetChanged + ); + } + + destroy() { + this.emitter.removeEventListener("pointerout", this._handlePointerOut); + this.emitter.removeEventListener("pointerover", this._handlePointerOver); + this.emitter.removeEventListener( + "pointertargetchanged", + this._handlePointerTargetChanged + ); + } + + private _handlePointerOut = (event: IEventManagerEvent) => { + this.onOut(this.emitter, event); + }; + + private _handlePointerOver = (event: IEventManagerEvent) => { + this.onOver(this.emitter, event); + }; + + private _handlePointerTargetChanged = (event: IEventManagerEvent) => { + this.onTargetChanged(this.emitter, event); + }; +} diff --git a/src/objects/events/interfaces/IEventGroup.ts b/src/objects/events/interfaces/IEventGroup.ts new file mode 100644 index 00000000..f7e45ea6 --- /dev/null +++ b/src/objects/events/interfaces/IEventGroup.ts @@ -0,0 +1,12 @@ +export interface IEventGroup { + getEventGroupIdentifier(): EventGroupIdentifier; +} + +export type EventGroupIdentifier = + | typeof FURNITURE + | typeof AVATAR + | typeof TILE_CURSOR; + +export const FURNITURE = Symbol("FURNITURE"); +export const AVATAR = Symbol("AVATAR"); +export const TILE_CURSOR = Symbol("TILE_CURSOR"); diff --git a/src/objects/events/interfaces/IEventHandler.ts b/src/objects/events/interfaces/IEventHandler.ts new file mode 100644 index 00000000..a9b47c70 --- /dev/null +++ b/src/objects/events/interfaces/IEventHandler.ts @@ -0,0 +1,10 @@ +import { IEventManagerEvent } from "./IEventManagerEvent"; + +export interface IEventHandler { + triggerClick(event: IEventManagerEvent): void; + triggerPointerDown(event: IEventManagerEvent): void; + triggerPointerUp(event: IEventManagerEvent): void; + triggerPointerOver(event: IEventManagerEvent): void; + triggerPointerOut(event: IEventManagerEvent): void; + triggerPointerTargetChanged(event: IEventManagerEvent): void; +} diff --git a/src/objects/events/interfaces/IEventHittable.ts b/src/objects/events/interfaces/IEventHittable.ts new file mode 100644 index 00000000..c339a3d9 --- /dev/null +++ b/src/objects/events/interfaces/IEventHittable.ts @@ -0,0 +1,10 @@ +import { Observable } from "rxjs"; +import { Rectangle } from "../../room/IRoomRectangle"; +import { IEventGroup } from "./IEventGroup"; + +export interface IEventHittable { + getGroup(): IEventGroup; + getRectangleObservable(): Observable; + getEventZOrder(): number; + hits(x: number, y: number): void; +} diff --git a/src/objects/events/interfaces/IEventManager.ts b/src/objects/events/interfaces/IEventManager.ts new file mode 100644 index 00000000..3d33d0e4 --- /dev/null +++ b/src/objects/events/interfaces/IEventManager.ts @@ -0,0 +1,7 @@ +import { IEventManagerNode } from "./IEventManagerNode"; +import { IEventTarget } from "./IEventTarget"; + +export interface IEventManager { + register(target: IEventTarget): IEventManagerNode; + remove(target: IEventTarget): void; +} diff --git a/src/objects/events/interfaces/IEventManagerEvent.ts b/src/objects/events/interfaces/IEventManagerEvent.ts new file mode 100644 index 00000000..fb11ac7f --- /dev/null +++ b/src/objects/events/interfaces/IEventManagerEvent.ts @@ -0,0 +1,15 @@ +import { InteractionEvent } from "pixi.js"; +import { EventGroupIdentifier } from "./IEventGroup"; + +export interface IEventManagerEvent { + tag?: string; + mouseEvent: MouseEvent | TouchEvent | PointerEvent; + interactionEvent: InteractionEvent; + stopPropagation(): void; + skip(...identifiers: EventGroupIdentifierParam[]): void; + skipExcept(...identifiers: EventGroupIdentifierParam[]): void; +} + +export type EventGroupIdentifierParam = + | EventGroupIdentifierParam[] + | EventGroupIdentifier; diff --git a/src/objects/events/interfaces/IEventManagerNode.ts b/src/objects/events/interfaces/IEventManagerNode.ts new file mode 100644 index 00000000..7ab96682 --- /dev/null +++ b/src/objects/events/interfaces/IEventManagerNode.ts @@ -0,0 +1,5 @@ +import { Rectangle } from "../../room/IRoomRectangle"; + +export interface IEventManagerNode { + destroy(): void; +} diff --git a/src/objects/events/interfaces/IEventTarget.ts b/src/objects/events/interfaces/IEventTarget.ts new file mode 100644 index 00000000..c3256c65 --- /dev/null +++ b/src/objects/events/interfaces/IEventTarget.ts @@ -0,0 +1,4 @@ +import { IEventHandler } from "./IEventHandler"; +import { IEventHittable } from "./IEventHittable"; + +export interface IEventTarget extends IEventHittable, IEventHandler {} diff --git a/src/objects/furniture/BaseFurniture.tsx b/src/objects/furniture/BaseFurniture.tsx index bb21ef76..73c88c78 100644 --- a/src/objects/furniture/BaseFurniture.tsx +++ b/src/objects/furniture/BaseFurniture.tsx @@ -14,14 +14,21 @@ import { import { FurnitureAsset } from "./data/interfaces/IFurnitureAssetsData"; import { FurnitureLayer } from "./data/interfaces/IFurnitureVisualizationData"; import { IAnimationTicker } from "../../interfaces/IAnimationTicker"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IRoomContext } from "../../interfaces/IRoomContext"; import { Shroom } from "../Shroom"; import { IFurnitureVisualization } from "./IFurnitureVisualization"; import { FurnitureSprite } from "./FurnitureSprite"; import { AnimatedFurnitureVisualization } from "./visualization/AnimatedFurnitureVisualization"; import { getDirectionForFurniture } from "./util/getDirectionForFurniture"; +import { IEventManager } from "../events/interfaces/IEventManager"; +import { + EventGroupIdentifier, + FURNITURE, + IEventGroup, +} from "../events/interfaces/IEventGroup"; +import { NOOP_EVENT_MANAGER } from "../events/EventManager"; import { FurnitureVisualizationView } from "./FurnitureVisualizationView"; +import { EventOverOutHandler } from "../events/EventOverOutHandler"; const highlightFilter = new HighlightFilter(0x999999, 0xffffff); @@ -39,8 +46,8 @@ interface BaseFurnitureDependencies { visualization: IFurnitureRoomVisualization; animationTicker: IAnimationTicker; furnitureLoader: IFurnitureLoader; - hitDetection: IHitDetection; application: PIXI.Application; + eventManager: IEventManager; } export interface BaseFurnitureProps { @@ -53,7 +60,7 @@ export interface BaseFurnitureProps { type ResolveLoadFurniResult = (result: LoadFurniResult) => void; -export class BaseFurniture implements IFurnitureEventHandlers { +export class BaseFurniture implements IFurnitureEventHandlers, IEventGroup { private _sprites: Map = new Map(); private _loadFurniResult: LoadFurniResult | undefined; @@ -65,7 +72,10 @@ export class BaseFurniture implements IFurnitureEventHandlers { private _type: FurnitureFetch; private _unknownTexture: PIXI.Texture | undefined; private _unknownSprite: FurnitureSprite | undefined; + private _clickHandler = new ClickHandler(); + private _overOutHandler = new EventOverOutHandler(); + private _loadFurniResultPromise: Promise; private _validDirections: number[] | undefined; private _resolveLoadFurniResult: ResolveLoadFurniResult | undefined; @@ -95,7 +105,7 @@ export class BaseFurniture implements IFurnitureEventHandlers { visualization: IFurnitureRoomVisualization; animationTicker: IAnimationTicker; furnitureLoader: IFurnitureLoader; - hitDetection: IHitDetection; + eventManager: IEventManager; }; constructor({ @@ -131,9 +141,9 @@ export class BaseFurniture implements IFurnitureEventHandlers { placeholder: context.configuration.placeholder, animationTicker: context.animationTicker, furnitureLoader: context.furnitureLoader, - hitDetection: context.hitDetection, visualization: context.visualization, application: context.application, + eventManager: context.eventManager, }, ...props, }); @@ -148,9 +158,9 @@ export class BaseFurniture implements IFurnitureEventHandlers { dependencies: { animationTicker: shroom.dependencies.animationTicker, furnitureLoader: shroom.dependencies.furnitureLoader, - hitDetection: shroom.dependencies.hitDetection, placeholder: shroom.dependencies.configuration.placeholder, application: shroom.dependencies.application, + eventManager: NOOP_EVENT_MANAGER, visualization: { container, addMask: () => { @@ -259,6 +269,22 @@ export class BaseFurniture implements IFurnitureEventHandlers { this._clickHandler.onPointerUp = value; } + public get onPointerOut() { + return this._overOutHandler.onOut; + } + + public set onPointerOut(value) { + this._overOutHandler.onOut = value; + } + + public get onPointerOver() { + return this._overOutHandler.onOver; + } + + public set onPointerOver(value) { + this._overOutHandler.onOver = value; + } + public get x() { return this._x; } @@ -322,6 +348,10 @@ export class BaseFurniture implements IFurnitureEventHandlers { this._getMaskId = value; } + getEventGroupIdentifier(): EventGroupIdentifier { + return FURNITURE; + } + destroy() { this._destroySprites(); @@ -401,7 +431,7 @@ export class BaseFurniture implements IFurnitureEventHandlers { if (this._unknownSprite == null) { this._unknownSprite = new FurnitureSprite({ - hitDetection: this.dependencies.hitDetection, + eventManager: this.dependencies.eventManager, group: this, }); @@ -465,8 +495,9 @@ export class BaseFurniture implements IFurnitureEventHandlers { this._view?.destroy(); const view = new FurnitureVisualizationView( - this.dependencies.hitDetection, + this.dependencies.eventManager, this._clickHandler, + this._overOutHandler, this.dependencies.visualization.container, loadFurniResult ); @@ -585,7 +616,7 @@ export class BaseFurniture implements IFurnitureEventHandlers { part: FurniDrawPart ): FurnitureSprite { const sprite = new FurnitureSprite({ - hitDetection: this.dependencies.hitDetection, + eventManager: this.dependencies.eventManager, mirrored: asset.flipH, tag: layer?.tag, group: this, @@ -594,15 +625,15 @@ export class BaseFurniture implements IFurnitureEventHandlers { const ignoreMouse = layer?.ignoreMouse != null && layer.ignoreMouse; sprite.ignoreMouse = ignoreMouse; - sprite.addEventListener("click", (event) => { + sprite.events.addEventListener("click", (event) => { this._clickHandler.handleClick(event); }); - sprite.addEventListener("pointerup", (event) => { + sprite.events.addEventListener("pointerup", (event) => { this._clickHandler.handlePointerUp(event); }); - sprite.addEventListener("pointerdown", (event) => { + sprite.events.addEventListener("pointerdown", (event) => { this._clickHandler.handlePointerDown(event); }); diff --git a/src/objects/furniture/FloorFurniture.tsx b/src/objects/furniture/FloorFurniture.tsx index b924bfb0..4a66ff3a 100644 --- a/src/objects/furniture/FloorFurniture.tsx +++ b/src/objects/furniture/FloorFurniture.tsx @@ -33,6 +33,8 @@ export class FloorFurniture private _onDoubleClick: HitEventHandler | undefined; private _onPointerDown: HitEventHandler | undefined; private _onPointerUp: HitEventHandler | undefined; + private _onPointerOver: HitEventHandler | undefined; + private _onPointerOut: HitEventHandler | undefined; constructor( options: { @@ -108,9 +110,9 @@ export class FloorFurniture this._baseFurniture.dependencies = { animationTicker: this.animationTicker, furnitureLoader: this.furnitureLoader, - hitDetection: this.hitDetection, placeholder: this.configuration.placeholder, visualization: this.roomVisualization, + eventManager: this.eventManager, }; this._moveAnimation = new ObjectAnimation( @@ -205,6 +207,24 @@ export class FloorFurniture this._baseFurniture.onPointerUp = this.onPointerUp; } + public get onPointerOver() { + return this._onPointerOver; + } + + public set onPointerOver(value) { + this._onPointerOver = value; + this._baseFurniture.onPointerOver = this.onPointerOver; + } + + public get onPointerOut() { + return this._onPointerOut; + } + + public set onPointerOut(value) { + this._onPointerOut = value; + this._baseFurniture.onPointerOut = this.onPointerOut; + } + /** * ID of the furniture */ diff --git a/src/objects/furniture/FurnitureVisualizationView.ts b/src/objects/furniture/FurnitureVisualizationView.ts index 544d3a42..7195680c 100644 --- a/src/objects/furniture/FurnitureVisualizationView.ts +++ b/src/objects/furniture/FurnitureVisualizationView.ts @@ -1,8 +1,14 @@ import * as PIXI from "pixi.js"; -import { IHitDetection } from "../../interfaces/IHitDetection"; +import { EventOverOutHandler } from "../events/EventOverOutHandler"; + +import { + EventGroupIdentifier, + FURNITURE, + IEventGroup, +} from "../events/interfaces/IEventGroup"; +import { IEventManager } from "../events/interfaces/IEventManager"; import { ClickHandler } from "../hitdetection/ClickHandler"; import { HitTexture } from "../hitdetection/HitTexture"; -import { BaseFurniture } from "./BaseFurniture"; import { FurnitureAsset } from "./data/interfaces/IFurnitureAssetsData"; import { IFurnitureVisualizationData } from "./data/interfaces/IFurnitureVisualizationData"; import { HighlightFilter } from "./filter/HighlightFilter"; @@ -17,7 +23,7 @@ import { LoadFurniResult } from "./util/loadFurni"; const highlightFilter = new HighlightFilter(0x999999, 0xffffff); export class FurnitureVisualizationView - implements IFurnitureVisualizationView, IBaseFurniture { + implements IFurnitureVisualizationView, IBaseFurniture, IEventGroup { private _direction: number | undefined; private _animation: string | undefined; @@ -81,12 +87,17 @@ export class FurnitureVisualizationView } constructor( - private _hitDetection: IHitDetection, + private _eventManager: IEventManager, private _clickHandler: ClickHandler, + private _overOutHandler: EventOverOutHandler, private _container: PIXI.Container, private _furniture: LoadFurniResult ) {} + getEventGroupIdentifier(): EventGroupIdentifier { + return FURNITURE; + } + getLayers(): IFurnitureVisualizationLayer[] { if (this._layers == null) throw new Error( @@ -132,8 +143,9 @@ export class FurnitureVisualizationView this, this._container, part, - this._hitDetection, + this._eventManager, this._clickHandler, + this._overOutHandler, (id) => this._furniture.getTexture(id) ) ); @@ -254,8 +266,9 @@ class FurnitureVisualizationLayer private _parent: FurnitureVisualizationView, private _container: PIXI.Container, private _part: FurniDrawPart, - private _hitDetection: IHitDetection, + private _eventManager: IEventManager, private _clickHandler: ClickHandler, + private _overOutHandler: EventOverOutHandler, private _getTexture: (id: string) => HitTexture | undefined ) { this.frameRepeat = _part.frameRepeat; @@ -283,6 +296,10 @@ class FurnitureVisualizationLayer if (newFrame != null) { this._setSpriteVisible(newFrame, true); this._addSprite(newFrame); + + if (this._color != null) { + newFrame.tint = this._color; + } } } @@ -308,11 +325,13 @@ class FurnitureVisualizationLayer this._mountedSprites.add(sprite); this._container.addChild(sprite); + this._overOutHandler.register(sprite.events); } private _destroySprites() { this._sprites.forEach((sprite) => { this._container.removeChild(sprite); + this._overOutHandler.remove(sprite.events); sprite.destroy(); }); this._sprites = new Map(); @@ -364,7 +383,9 @@ class FurnitureVisualizationLayer private _getSprite(frameIndex: number) { const current = this._sprites.get(frameIndex); - if (current != null) return current; + if (current != null) { + return current; + } const spriteInfo = this._getSpriteInfo(frameIndex); if (spriteInfo == null) return; @@ -375,7 +396,7 @@ class FurnitureVisualizationLayer const zIndex = (z ?? 0) + layerIndex * 0.01; const sprite = new FurnitureSprite({ - hitDetection: this._hitDetection, + eventManager: this._eventManager, mirrored: asset.flipH, tag: layer?.tag, group: this._parent, @@ -384,15 +405,15 @@ class FurnitureVisualizationLayer const ignoreMouse = layer?.ignoreMouse != null && layer.ignoreMouse; sprite.ignoreMouse = ignoreMouse; - sprite.addEventListener("click", (event) => { + sprite.events.addEventListener("click", (event) => { this._clickHandler.handleClick(event); }); - sprite.addEventListener("pointerup", (event) => { + sprite.events.addEventListener("pointerup", (event) => { this._clickHandler.handlePointerUp(event); }); - sprite.addEventListener("pointerdown", (event) => { + sprite.events.addEventListener("pointerdown", (event) => { this._clickHandler.handlePointerDown(event); }); @@ -468,6 +489,8 @@ class FurnitureVisualizationLayer this._setSpriteVisible(sprite, false); this._sprites.set(frameIndex, sprite); + this._overOutHandler.register(sprite.events); + return sprite; } diff --git a/src/objects/furniture/WallFurniture.tsx b/src/objects/furniture/WallFurniture.tsx index 6987fd4f..aaad0384 100644 --- a/src/objects/furniture/WallFurniture.tsx +++ b/src/objects/furniture/WallFurniture.tsx @@ -188,9 +188,9 @@ export class WallFurniture extends RoomObject { this._baseFurniture.dependencies = { animationTicker: this.animationTicker, furnitureLoader: this.furnitureLoader, - hitDetection: this.hitDetection, placeholder: undefined, visualization: this.roomVisualization, + eventManager: this.eventManager, }; this._updatePosition(); diff --git a/src/objects/furniture/util/IFurnitureEventHandlers.ts b/src/objects/furniture/util/IFurnitureEventHandlers.ts index 03517544..3f9306ac 100644 --- a/src/objects/furniture/util/IFurnitureEventHandlers.ts +++ b/src/objects/furniture/util/IFurnitureEventHandlers.ts @@ -1,8 +1,10 @@ -import { HitEvent } from "../../../interfaces/IHitDetection"; +import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; export interface IFurnitureEventHandlers { - onClick?: (event: HitEvent) => void; - onDoubleClick?: (event: HitEvent) => void; - onPointerDown?: (event: HitEvent) => void; - onPointerUp?: (event: HitEvent) => void; + onClick?: (event: IEventManagerEvent) => void; + onDoubleClick?: (event: IEventManagerEvent) => void; + onPointerDown?: (event: IEventManagerEvent) => void; + onPointerUp?: (event: IEventManagerEvent) => void; + onPointerOver?: (event: IEventManagerEvent) => void; + onPointerOut?: (event: IEventManagerEvent) => void; } diff --git a/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts b/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts index 6e684671..c773c457 100644 --- a/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts +++ b/src/objects/furniture/visualization/AnimatedFurnitureVisualization.ts @@ -164,6 +164,13 @@ export class AnimatedFurnitureVisualization extends FurnitureVisualization { this._updateFurniture(); } + private _updateLayers() { + if (this.modifier != null) { + const modifier = this.modifier; + this.view.getLayers().forEach((layer) => modifier(layer)); + } + } + private _updateFurniture() { if (!this.mounted) return; @@ -171,11 +178,7 @@ export class AnimatedFurnitureVisualization extends FurnitureVisualization { this.view.setDisplayAnimation(this.animationId?.toString()); this.view.updateDisplay(); - if (this.modifier != null) { - const modifier = this.modifier; - this.view.getLayers().forEach((layer) => modifier(layer)); - } - + this._updateLayers(); this._update(); } diff --git a/src/objects/hitdetection/ClickHandler.ts b/src/objects/hitdetection/ClickHandler.ts index 40c247ef..af721667 100644 --- a/src/objects/hitdetection/ClickHandler.ts +++ b/src/objects/hitdetection/ClickHandler.ts @@ -1,9 +1,9 @@ -import { HitEvent } from "../../interfaces/IHitDetection"; +import { IEventManagerEvent } from "../events/interfaces/IEventManagerEvent"; import { HitEventHandler } from "./HitSprite"; export class ClickHandler { private _doubleClickInfo?: { - initialEvent: HitEvent; + initialEvent: IEventManagerEvent; timeout: number; }; @@ -46,7 +46,7 @@ export class ClickHandler { this._onPointerUp = value; } - handleClick(event: HitEvent) { + handleClick(event: IEventManagerEvent) { if (this._doubleClickInfo == null) { this.onClick && this.onClick(event); @@ -59,15 +59,15 @@ export class ClickHandler { } } - handlePointerDown(event: HitEvent) { + handlePointerDown(event: IEventManagerEvent) { this.onPointerDown && this.onPointerDown(event); } - handlePointerUp(event: HitEvent) { + handlePointerUp(event: IEventManagerEvent) { this.onPointerUp && this.onPointerUp(event); } - private _performDoubleClick(event: HitEvent) { + private _performDoubleClick(event: IEventManagerEvent) { if (this._doubleClickInfo == null) return; this.onDoubleClick && @@ -85,7 +85,7 @@ export class ClickHandler { this._doubleClickInfo = undefined; } - private _startDoubleClick(event: HitEvent) { + private _startDoubleClick(event: IEventManagerEvent) { this._doubleClickInfo = { initialEvent: event, timeout: window.setTimeout(() => this._resetDoubleClick(), 350), diff --git a/src/objects/hitdetection/HitDetection.test.ts b/src/objects/hitdetection/HitDetection.test.ts deleted file mode 100644 index c457d78b..00000000 --- a/src/objects/hitdetection/HitDetection.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { HitDetection } from "./HitDetection"; - -const view = { - getBoundingClientRect: () => ({ x: 0, y: 0 }), - addEventListener: () => { - // Do nothing - }, -}; - -test("detects hits on multiple elements", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: () => { - firstClicked = true; - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => true, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 2, clientY: 2 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(true); -}); - -test("doesn't detect if element out of bounds", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: () => { - firstClicked = true; - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => false, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 101, clientY: 101 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(false); -}); - -test("doesn't detect if element in bounds but doesn't hit", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: () => { - firstClicked = true; - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => false, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 2, clientY: 2 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(false); -}); - -test("intercepts event if element stops propagating", () => { - const hitDetection = new HitDetection({ - view, - } as any); - - let firstClicked = false; - let secondClicked = false; - - hitDetection.register({ - getHitDetectionZIndex: () => 10, - hits: () => true, - trigger: (type, event) => { - firstClicked = true; - event.stopPropagation(); - }, - }); - - hitDetection.register({ - getHitDetectionZIndex: () => 5, - hits: () => true, - trigger: () => { - secondClicked = true; - }, - }); - - hitDetection.handleClick({ clientX: 2, clientY: 2 } as any); - - expect(firstClicked).toBe(true); - expect(secondClicked).toBe(false); -}); diff --git a/src/objects/hitdetection/HitDetection.ts b/src/objects/hitdetection/HitDetection.ts deleted file mode 100644 index c1c75d5d..00000000 --- a/src/objects/hitdetection/HitDetection.ts +++ /dev/null @@ -1,189 +0,0 @@ -import * as PIXI from "pixi.js"; -import { - HitDetectionElement, - HitDetectionNode, - HitEvent, - HitEventType, - IHitDetection, -} from "../../interfaces/IHitDetection"; - -export class HitDetection implements IHitDetection { - private _counter = 0; - private _map: Map = new Map(); - private _container: PIXI.Container | undefined; - - constructor(private _app: PIXI.Application) { - _app.view.addEventListener("click", (event) => this.handleClick(event), { - capture: true, - }); - - _app.view.addEventListener("pointerdown", (event) => - this.handlePointerDown(event) - ); - - _app.view.addEventListener("pointerup", (event) => - this.handlePointerUp(event) - ); - - _app.view.addEventListener( - "contextmenu", - (event) => this.handleClick(event), - { - capture: true, - } - ); - } - - static create(application: PIXI.Application) { - return new HitDetection(application); - } - - register(rectangle: HitDetectionElement): HitDetectionNode { - const id = this._counter++; - this._map.set(id, rectangle); - - return { - remove: () => { - this._map.delete(id); - }, - }; - } - - handleClick(event: MouseEvent) { - this._triggerEvent(event.clientX, event.clientY, "click", event); - } - - handlePointerDown(event: PointerEvent) { - this._triggerEvent(event.clientX, event.clientY, "pointerdown", event); - } - - handlePointerUp(event: PointerEvent) { - this._triggerEvent(event.clientX, event.clientY, "pointerup", event); - } - - private _triggerEvent( - x: number, - y: number, - eventType: HitEventType, - domEvent: MouseEvent - ) { - const start = performance.now(); - - const elements = this._performHitTest(x, y); - - const event = new HitEventPropagation(eventType, domEvent, elements); - event.resumePropagation(); - } - - private _performHitTest(x: number, y: number) { - const rect = this._app.view.getBoundingClientRect(); - - x = x - rect.x; - y = y - rect.y; - - const entries = Array.from(this._map.values()); - const ordered = entries.sort( - (a, b) => b.getHitDetectionZIndex() - a.getHitDetectionZIndex() - ); - - return ordered.filter((element) => { - return element.hits(x, y); - }); - } - - private _debugHitDetection() { - this._container?.destroy(); - const container = new PIXI.Container(); - - this._container = container; - - this._map.forEach((element) => { - if (element.createDebugSprite == null) return; - - const sprite = element.createDebugSprite(); - - if (sprite == null) return; - - container.addChild(sprite); - }); - - this._app.stage.addChild(container); - } -} - -class HitEventPropagation { - private _currentIndex = 0; - private _stopped = false; - private _groups: Set = new Set(); - - constructor( - private _eventType: HitEventType, - private _mouseEvent: MouseEvent, - private _path: HitDetectionElement[] - ) {} - - public get mouseEvent() { - return this._mouseEvent; - } - - stopPropagation(): void { - this._stopped = true; - } - - resumePropagation(): void { - this._stopped = false; - this._propagateEvent(); - } - - private _propagateEvent() { - for (let i = this._currentIndex; i < this._path.length; i++) { - this._currentIndex = i + 1; - - if (this._stopped) break; - - const element = this._path[i]; - - const group = element.group; - if (group == null || !this._groups.has(group)) { - element.trigger(this._eventType, new TargetedHitEvent(this, element)); - - if (group != null) { - this._groups.add(group); - } - } - } - } -} - -class TargetedHitEvent implements HitEvent { - private _tag: string | undefined; - - constructor( - private _base: HitEventPropagation, - private _target: HitDetectionElement - ) {} - - public get target() { - return this._target; - } - - public get mouseEvent() { - return this._base.mouseEvent; - } - - public get tag() { - return this._tag; - } - - public set tag(value) { - this._tag = value; - } - - stopPropagation(): void { - return this._base.stopPropagation(); - } - - resumePropagation(): void { - return this._base.resumePropagation(); - } -} diff --git a/src/objects/hitdetection/HitSprite.ts b/src/objects/hitdetection/HitSprite.ts index bc402b0d..b618a883 100644 --- a/src/objects/hitdetection/HitSprite.ts +++ b/src/objects/hitdetection/HitSprite.ts @@ -1,27 +1,30 @@ import * as PIXI from "pixi.js"; -import { - HitDetectionElement, - HitDetectionNode, - HitEvent, - HitEventType, - IHitDetection, - Rect, -} from "../../interfaces/IHitDetection"; +import { BehaviorSubject, Observable } from "rxjs"; +import { EventEmitter } from "../events/EventEmitter"; +import { IEventGroup } from "../events/interfaces/IEventGroup"; +import { IEventManager } from "../events/interfaces/IEventManager"; +import { IEventManagerEvent } from "../events/interfaces/IEventManagerEvent"; +import { IEventTarget } from "../events/interfaces/IEventTarget"; import { Hitmap } from "../furniture/util/loadFurni"; +import { Rectangle } from "../room/IRoomRectangle"; import { HitTexture } from "./HitTexture"; -export type HitEventHandler = (event: HitEvent) => void; +export type HitEventHandler = (event: IEventManagerEvent) => void; + +export class HitSprite extends PIXI.Sprite implements IEventTarget { + private _group: IEventGroup; -export class HitSprite extends PIXI.Sprite implements HitDetectionElement { - private _group: unknown; - private _hitDetectionNode: HitDetectionNode | undefined; - private _handlers = new Map>(); private _hitTexture: HitTexture | undefined; private _tag: string | undefined; private _mirrored: boolean; - private _mirrorNotVisually: boolean; private _ignore = false; private _ignoreMouse = false; + private _eventManager: IEventManager; + private _rectangleSubject = new BehaviorSubject( + undefined + ); + + private _eventEmitter = new EventEmitter(); private _getHitmap: | (() => ( @@ -31,33 +34,76 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { ) => boolean) | undefined; + public get events() { + return this._eventEmitter; + } + constructor({ - hitDetection, + eventManager, mirrored = false, - mirroredNotVisually = false, getHitmap, tag, group, }: { - hitDetection: IHitDetection; + eventManager: IEventManager; getHitmap?: () => Hitmap; mirrored?: boolean; - mirroredNotVisually?: boolean; tag?: string; - group?: unknown; + group: IEventGroup; }) { super(); - if (group != null) { - this._group = group; - } + this._group = group; this._mirrored = mirrored; - this._mirrorNotVisually = mirroredNotVisually; this._getHitmap = getHitmap; this._tag = tag; - this._hitDetectionNode = hitDetection.register(this); this.mirrored = this._mirrored; + this._eventManager = eventManager; + + eventManager.register(this); + } + + getGroup(): IEventGroup { + return this._group; + } + + getRectangleObservable(): Observable { + return this._rectangleSubject; + } + + getEventZOrder(): number { + return this.zIndex; + } + + triggerPointerTargetChanged(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointertargetchanged", event); + } + + triggerClick(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("click", event); + } + + triggerPointerDown(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerdown", event); + } + + triggerPointerUp(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerup", event); + } + + triggerPointerOver(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerover", event); + } + + triggerPointerOut(event: IEventManagerEvent): void { + event.tag = this._tag; + this._eventEmitter.trigger("pointerout", event); } createDebugSprite(): PIXI.Sprite | undefined { @@ -123,7 +169,7 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { transform: { x: number; y: number } ) => value.hits(x, y, transform, { - mirrorHorizonally: this._mirrored || this._mirrorNotVisually, + mirrorHorizonally: this._mirrored, }); } } @@ -132,53 +178,29 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { return this.zIndex; } - trigger(type: HitEventType, event: HitEvent): void { - const handlers = this._handlers.get(type); - - event.tag = this._tag; - - handlers?.forEach((handler) => handler(event)); - } - - removeAllEventListeners() { - this._handlers = new Map(); - } - - addEventListener(type: HitEventType, handler: HitEventHandler) { - const existingHandlers = this._handlers.get(type) ?? new Set(); - existingHandlers.add(handler); - - this._handlers.set(type, existingHandlers); - } - - removeEventListener(type: HitEventType, handler: HitEventHandler) { - const existingHandlers = this._handlers.get(type); - existingHandlers?.delete(handler); - } - destroy() { super.destroy(); - this._hitDetectionNode?.remove(); + this._eventManager.remove(this); } - getHitBox(): Rect { - if (this._mirrored || this._mirrorNotVisually) { + getHitBox(): Rectangle { + const pos = this.getGlobalPosition(); + + if (this._mirrored) { return { - x: this.worldTransform.tx - this.texture.width, - y: this.worldTransform.ty, + x: pos.x - this.texture.width, + y: pos.y, width: this.texture.width, height: this.texture.height, - zIndex: this.zIndex, }; } return { - x: this.worldTransform.tx, - y: this.worldTransform.ty, + x: pos.x, + y: pos.y, width: this.texture.width, height: this.texture.height, - zIndex: this.zIndex, }; } @@ -195,11 +217,26 @@ export class HitSprite extends PIXI.Sprite implements HitDetectionElement { if (inBoundsX && inBoundsY) { const hits = this._getHitmap(); return hits(x, y, { - x: this.worldTransform.tx, - y: this.worldTransform.ty, + x: this.getGlobalPosition().x, + y: this.getGlobalPosition().y, }); } return false; } + + updateTransform() { + super.updateTransform(); + + this._rectangleSubject.next(this.getHitBox()); + } } + +export type HitSpriteEventMap = { + click: IEventManagerEvent; + pointerup: IEventManagerEvent; + pointerdown: IEventManagerEvent; + pointerover: IEventManagerEvent; + pointerout: IEventManagerEvent; + pointertargetchanged: IEventManagerEvent; +}; diff --git a/src/objects/room/Room.ts b/src/objects/room/Room.ts index af72ecf3..966e4a9f 100644 --- a/src/objects/room/Room.ts +++ b/src/objects/room/Room.ts @@ -4,7 +4,6 @@ import { IAvatarLoader } from "../../interfaces/IAvatarLoader"; import { IConfiguration } from "../../interfaces/IConfiguration"; import { IFurnitureData } from "../../interfaces/IFurnitureData"; import { IFurnitureLoader } from "../../interfaces/IFurnitureLoader"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IRoomGeometry } from "../../interfaces/IRoomGeometry"; import { IRoomObject } from "../../interfaces/IRoomObject"; import { IRoomObjectContainer } from "../../interfaces/IRoomObjectContainer"; @@ -15,16 +14,15 @@ import { parseTileMapString } from "../../util/parseTileMapString"; import { Shroom } from "../Shroom"; import { ITileMap } from "../../interfaces/ITileMap"; import { RoomObjectContainer } from "./RoomObjectContainer"; -import { Subject } from "rxjs"; import { RoomModelVisualization } from "./RoomModelVisualization"; import { ParsedTileMap } from "./ParsedTileMap"; import { getTileColors, getWallColors } from "./util/getTileColors"; +import { EventManager } from "../events/EventManager"; export interface Dependencies { animationTicker: IAnimationTicker; avatarLoader: IAvatarLoader; furnitureLoader: IFurnitureLoader; - hitDetection: IHitDetection; configuration: IConfiguration; furnitureData?: IFurnitureData; application: PIXI.Application; @@ -60,7 +58,7 @@ export class Room private _animationTicker: IAnimationTicker; private _avatarLoader: IAvatarLoader; private _furnitureLoader: IFurnitureLoader; - private _hitDetection: IHitDetection; + private _eventManager: EventManager; private _configuration: IConfiguration; private _wallTexture: Promise | PIXI.Texture | undefined; @@ -88,7 +86,6 @@ export class Room avatarLoader, furnitureLoader, tilemap, - hitDetection, configuration, application, }: { @@ -103,12 +100,12 @@ export class Room this._animationTicker = animationTicker; this._furnitureLoader = furnitureLoader; this._avatarLoader = avatarLoader; - this._hitDetection = hitDetection; + this._eventManager = new EventManager(); this._configuration = configuration; this.application = application; this._visualization = new RoomModelVisualization( - this._hitDetection, + this._eventManager, this.application, new ParsedTileMap(normalizedTileMap) ); @@ -121,7 +118,7 @@ export class Room furnitureLoader: this._furnitureLoader, roomObjectContainer: this, avatarLoader: this._avatarLoader, - hitDetection: this._hitDetection, + eventManager: this._eventManager, configuration: this._configuration, tilemap: this, landscapeContainer: this._visualization, diff --git a/src/objects/room/RoomModelVisualization.ts b/src/objects/room/RoomModelVisualization.ts index 83bb2264..2f394f8b 100644 --- a/src/objects/room/RoomModelVisualization.ts +++ b/src/objects/room/RoomModelVisualization.ts @@ -1,6 +1,5 @@ import * as PIXI from "pixi.js"; import { Subject } from "rxjs"; -import { IHitDetection } from "../../interfaces/IHitDetection"; import { IRoomVisualization, MaskNode, @@ -9,6 +8,8 @@ import { import { RoomPosition } from "../../types/RoomPosition"; import { getZOrder } from "../../util/getZOrder"; import { ParsedTileType, ParsedTileWall } from "../../util/parseTileMap"; +import { EventManager } from "../events/EventManager"; +import { EventManagerContainer } from "../events/EventManagerContainer"; import { ILandscapeContainer } from "./ILandscapeContainer"; import { IRoomRectangle, Rectangle } from "./IRoomRectangle"; import { ParsedTileMap } from "./ParsedTileMap"; @@ -87,7 +88,7 @@ export class RoomModelVisualization private _rebuildRoom = false; constructor( - private _hitDetection: IHitDetection, + private _eventManager: EventManager, private _application: PIXI.Application, public readonly parsedTileMap: ParsedTileMap ) { @@ -113,6 +114,8 @@ export class RoomModelVisualization this.addChild(this._positionalContainer); + new EventManagerContainer(this._application, this._eventManager); + this._updateHeightmap(); this._application.ticker.add(this._handleTick); @@ -530,7 +533,7 @@ export class RoomModelVisualization const position: RoomPosition = { roomX: x, roomY: y, roomZ: z }; const cursor = new TileCursor( - this._hitDetection, + this._eventManager, position, () => { this._onTileClick.next(position); diff --git a/src/objects/room/parts/TileCursor.ts b/src/objects/room/parts/TileCursor.ts index cc02a4e5..20391677 100644 --- a/src/objects/room/parts/TileCursor.ts +++ b/src/objects/room/parts/TileCursor.ts @@ -1,22 +1,29 @@ import * as PIXI from "pixi.js"; -import { - HitDetectionElement, - HitDetectionNode, - HitEvent, - IHitDetection, -} from "../../../interfaces/IHitDetection"; +import { BehaviorSubject, Observable } from "rxjs"; import { RoomPosition } from "../../../types/RoomPosition"; - -export class TileCursor extends PIXI.Container implements HitDetectionElement { +import { isPointInside } from "../../../util/isPointInside"; +import { + EventGroupIdentifier, + IEventGroup, + TILE_CURSOR, +} from "../../events/interfaces/IEventGroup"; +import { IEventManager } from "../../events/interfaces/IEventManager"; +import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; +import { IEventTarget } from "../../events/interfaces/IEventTarget"; +import { Rectangle } from "../IRoomRectangle"; + +export class TileCursor + extends PIXI.Container + implements IEventTarget, IEventGroup { private _roomX: number; private _roomY: number; private _roomZ: number; private _graphics: PIXI.Graphics; private _hover = false; - private _node: HitDetectionNode; + private _subject = new BehaviorSubject(undefined); constructor( - hitDetection: IHitDetection, + private _eventManager: IEventManager, private _position: RoomPosition, private onClick: (position: RoomPosition) => void, private onOver: (position: RoomPosition) => void, @@ -31,31 +38,58 @@ export class TileCursor extends PIXI.Container implements HitDetectionElement { this.addChild(this._graphics); - this._node = hitDetection.register(this); + this._eventManager.register(this); } - createDebugSprite() { - return undefined; + getEventGroupIdentifier(): EventGroupIdentifier { + return TILE_CURSOR; } - trigger(type: "click", event: HitEvent): void { - switch (type) { - case "click": - this.onClick({ - roomX: this._roomX, - roomY: this._roomY, - roomZ: this._roomZ, - }); - break; - } + getGroup(): IEventGroup { + return this; + } + + getRectangleObservable(): Observable { + return this._subject; + } + + getEventZOrder(): number { + return -1000; + } + + triggerPointerTargetChanged(event: IEventManagerEvent): void {} + + triggerClick(event: IEventManagerEvent): void { + this.onClick({ + roomX: this._roomX, + roomY: this._roomY, + roomZ: this._roomZ, + }); + } + + triggerPointerDown(event: IEventManagerEvent): void {} + + triggerPointerUp(event: IEventManagerEvent): void {} + + triggerPointerOver(event: IEventManagerEvent): void { + this._updateHover(true); + this.onOver({ roomX: this._roomX, roomY: this._roomY, roomZ: this._roomZ }); + } + + triggerPointerOut(event: IEventManagerEvent): void { + this._updateHover(false); + this.onOut({ roomX: this._roomX, roomY: this._roomY, roomZ: this._roomZ }); + } + + createDebugSprite() { + return undefined; } hits(x: number, y: number): boolean { - const tx = this.worldTransform.tx; - const ty = this.worldTransform.ty; + const pos = this.getGlobalPosition(); - const diffX = x - tx; - const diffY = y - ty; + const diffX = x - pos.x; + const diffY = y - pos.y; return this._pointInside( [diffX, diffY], @@ -74,23 +108,31 @@ export class TileCursor extends PIXI.Container implements HitDetectionElement { destroy() { super.destroy(); - this._node.remove(); + this._graphics.destroy(); + this._eventManager.remove(this); + } + + updateTransform() { + super.updateTransform(); + + this._subject.next(this._getCurrentRectangle()); + } + + private _getCurrentRectangle(): Rectangle { + const position = this.getGlobalPosition(); + + return { + x: position.x, + y: position.y, + width: 64, + height: 32, + }; } private _createGraphics() { const graphics = new PIXI.Graphics(); - graphics.hitArea = new PIXI.Polygon([ - new PIXI.Point(points.p1.x, points.p1.y), - new PIXI.Point(points.p2.x, points.p2.y), - new PIXI.Point(points.p3.x, points.p3.y), - new PIXI.Point(points.p4.x, points.p4.y), - ]); - graphics.interactive = true; - graphics.addListener("mouseover", () => this._updateHover(true)); - graphics.addListener("mouseout", () => this._updateHover(false)); - return graphics; } @@ -120,25 +162,7 @@ export class TileCursor extends PIXI.Container implements HitDetectionElement { } private _pointInside(point: [number, number], vs: [number, number][]) { - // ray-casting algorithm based on - // https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html - - const x = point[0]; - const y = point[1]; - - let inside = false; - for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { - const xi = vs[i][0]; - const yi = vs[i][1]; - const xj = vs[j][0]; - const yj = vs[j][1]; - - const intersect = - yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; - if (intersect) inside = !inside; - } - - return inside; + return isPointInside(point, vs); } } diff --git a/src/objects/room/parts/WallLeft.ts b/src/objects/room/parts/WallLeft.ts index 5728447b..aa09f01b 100644 --- a/src/objects/room/parts/WallLeft.ts +++ b/src/objects/room/parts/WallLeft.ts @@ -74,24 +74,7 @@ export class WallLeft extends PIXI.Container implements IRoomPart { this.removeChildren(); - const hitArea = new PIXI.Polygon([ - new PIXI.Point( - this._getOffsetX() + this._borderWidth, - this._wallWidth / 2 - (this.props.cutawayHeight ?? 0) - ), - new PIXI.Point( - this._getOffsetX() + this._wallWidth + this._borderWidth, - -(this.props.cutawayHeight ?? 0) - ), - new PIXI.Point( - this._getOffsetX() + this._wallWidth + this._borderWidth, - -this._wallHeight - ), - new PIXI.Point( - this._getOffsetX() + this._borderWidth, - -this._wallHeight + this._wallWidth / 2 - ), - ]); + const hitArea = new PIXI.Polygon(this._getDisplayPoints()); this.hitArea = hitArea; @@ -135,6 +118,27 @@ export class WallLeft extends PIXI.Container implements IRoomPart { this.props.hitAreaContainer.addChild(this._hitAreaElement); } + private _getDisplayPoints() { + return [ + new PIXI.Point( + this._getOffsetX() + this._borderWidth, + this._wallWidth / 2 - (this.props.cutawayHeight ?? 0) + ), + new PIXI.Point( + this._getOffsetX() + this._wallWidth + this._borderWidth, + -(this.props.cutawayHeight ?? 0) + ), + new PIXI.Point( + this._getOffsetX() + this._wallWidth + this._borderWidth, + -this._wallHeight + ), + new PIXI.Point( + this._getOffsetX() + this._borderWidth, + -this._wallHeight + this._wallWidth / 2 + ), + ]; + } + private _getOffsetX() { return this.scale.x * this._offsets.x - this._borderWidth; } diff --git a/src/util/isPointInside.ts b/src/util/isPointInside.ts new file mode 100644 index 00000000..4c416cfa --- /dev/null +++ b/src/util/isPointInside.ts @@ -0,0 +1,21 @@ +export function isPointInside( + point: [number, number], + vs: [number, number][] +): boolean { + const x = point[0]; + const y = point[1]; + + let inside = false; + for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { + const xi = vs[i][0]; + const yi = vs[i][1]; + const xj = vs[j][0]; + const yj = vs[j][1]; + + const intersect = + yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + + return inside; +} diff --git a/storybook/stories/Documentation.stories.ts b/storybook/stories/Documentation.stories.ts index e0c6b902..b32bd017 100644 --- a/storybook/stories/Documentation.stories.ts +++ b/storybook/stories/Documentation.stories.ts @@ -102,6 +102,14 @@ export function ImplementingFurnitureLogic() { } }); + furniture.onPointerOver = () => { + console.log("OVER"); + }; + + furniture.onPointerOut = () => { + console.log("OUT"); + }; + room.addRoomObject(furniture); application.stage.addChild(room); diff --git a/storybook/stories/avatar/Avatar.stories.ts b/storybook/stories/avatar/Avatar.stories.ts index 2c16e84d..5c661e77 100644 --- a/storybook/stories/avatar/Avatar.stories.ts +++ b/storybook/stories/avatar/Avatar.stories.ts @@ -68,6 +68,14 @@ export function Default() { room.addRoomObject(avatar); avatars.push(avatar); + + avatar.onPointerOver = () => { + console.log("OVER"); + }; + + avatar.onPointerOut = () => { + console.log("OUT"); + }; } } diff --git a/storybook/stories/furniture/Furniture.stories.ts b/storybook/stories/furniture/Furniture.stories.ts index 0e1069e8..1671462c 100644 --- a/storybook/stories/furniture/Furniture.stories.ts +++ b/storybook/stories/furniture/Furniture.stories.ts @@ -17,6 +17,10 @@ import { import { createShroom } from "../common/createShroom"; import { action } from "@storybook/addon-actions"; import fetch from "node-fetch"; +import { + AVATAR, + FURNITURE, +} from "../../../dist/objects/events/interfaces/IEventGroup"; export default { title: "Furniture / General", @@ -996,6 +1000,8 @@ export function LoadTest() { `, }); + let activeFurniture: FloorFurniture | undefined; + fetch(`./furni.json`) .then((response) => response.json()) .then((furnitures: any[]) => { @@ -1022,6 +1028,20 @@ export function LoadTest() { obj.onClick = (event) => { console.log("Clicked"); + event.skip(FURNITURE, AVATAR); + }; + + obj.onPointerOver = (event) => { + if (activeFurniture != null) return; + + activeFurniture = obj; + console.log("Active Furniture", obj.type, obj.roomX, obj.roomY); + }; + + obj.onPointerOut = (event) => { + if (activeFurniture === obj) { + activeFurniture = undefined; + } }; room.addRoomObject(obj); @@ -1040,6 +1060,11 @@ export function LoadTest() { room.x = application.screen.width / 2 - room.roomWidth / 2; room.y = application.screen.height / 2 - room.roomHeight / 2; room.addRoomObject(furniture1); + + room.onTileClick = (event) => { + console.log(event); + }; + application.stage.addChild(room); }); }