diff --git a/src/core/game.ts b/src/core/game.ts index c46f3be..96eb3f9 100644 --- a/src/core/game.ts +++ b/src/core/game.ts @@ -23,7 +23,7 @@ import { BASE_FPS, BASE_MAX_COMMAND_EXECUTING_ON_TICK_LIMIT, isServer } from "@c import { BluePrintsFactory, EffectFactory, IteractionsFactory, QuestsFactory, SoundsFactory } from "@factories"; import { GlobalStore } from "@store"; import { baseChecksMiddleware, DropItemGuard, EntityInteractGuard, EquipItemGuard, MovementGuard, OpenChestGuard, PickUpGuard, ShootGuard, UseItemGuard } from "@middlewares"; -import { createPluginProto, extractMethodFromPlugin, extractPropertyFromPlugin } from "@utils"; +import { createPluginProto, extractMethodFromPlugin, extractPropertyFromPlugin, useLink, useValidation, useVisibility, useAttack } from "@utils"; import type { Entity, GameObject } from "@world"; import { ConflictResolverPlugin } from "@plugins"; @@ -378,6 +378,13 @@ export class Game implements IGame { if (options?.usingEntityMiddlewares) this.use([DropItemGuard, EntityInteractGuard, EquipItemGuard, MovementGuard, OpenChestGuard, PickUpGuard, UseItemGuard]) if (options?.usingObjectMiddlewares) this.use([ShootGuard]) if (!(options?.disableConflictResolver)) this.registerPlugin(new ConflictResolverPlugin()) + + if (!(options?.disableHooksHydration)) { + useLink.prototype.game = this + useValidation.prototype.game = this + useVisibility.prototype.game = this + useAttack.prototype.game = this + } } public on(event: keyof typeof GameEvent, cb: EventCallback) { diff --git a/src/interfaces/core/engine/engine-options.intefaces.ts b/src/interfaces/core/engine/engine-options.intefaces.ts index e834b12..2ee3382 100644 --- a/src/interfaces/core/engine/engine-options.intefaces.ts +++ b/src/interfaces/core/engine/engine-options.intefaces.ts @@ -43,6 +43,13 @@ export interface IGameOptions { */ readonly disableConflictResolver?: boolean; + /** + * Disable inject game into hooks for them functionallity. + * If true, you need inject game into hooks options manually, + * but this can be multiply perfomance + */ + readonly disableHooksHydration?: boolean; + /** * Optional command bus options */ diff --git a/src/interfaces/hooks/index.ts b/src/interfaces/hooks/index.ts index b01bac2..432f8be 100644 --- a/src/interfaces/hooks/index.ts +++ b/src/interfaces/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./use-visibility.hook.interface.js" -export * from "./use-validation.hook.interface.js" \ No newline at end of file +export * from "./use-validation.hook.interface.js" +export * from "./use-link.hook.interface.js" \ No newline at end of file diff --git a/src/interfaces/hooks/use-link.hook.interface.ts b/src/interfaces/hooks/use-link.hook.interface.ts new file mode 100644 index 0000000..0389c9e --- /dev/null +++ b/src/interfaces/hooks/use-link.hook.interface.ts @@ -0,0 +1,58 @@ +import type { Game } from "@core"; +import type { Linkable, UnlinkWhen } from "@types"; + +export interface ILink { + /** + * Parent of link + */ + readonly from: Linkable; + + /** + * Child of link + */ + readonly to: Linkable; + + /** + * Link destroy function + */ + readonly unLink: VoidFunction; + + /** + * Flag indicated link active status + */ + isActive: boolean; + + /** + * You can reactivate link, if then deleted + * @param options - Optional new options to link + * @returns { ILink | false } - ILink if relink success, false if link already exists + */ + readonly link: (options?: ILinkOptions) => ILink | false; +} + +export interface ILinkOptions { + /** + * Game reference, if you disabled + */ + readonly game?: Game; + + /** + * Max distance between Child and Parent + */ + readonly maxDistance?: number; + + /** + * Kill child of link, if parent was killed + */ + readonly killChild?: boolean; + + /** + * Delete child of link, if parent was deleted + */ + readonly deleteChild?: boolean; + + /** + * Auto delete link when + */ + readonly autoUnlinkOn?: UnlinkWhen[]; +} \ No newline at end of file diff --git a/src/interfaces/hooks/use-validation.hook.interface.ts b/src/interfaces/hooks/use-validation.hook.interface.ts index 74a172e..453ff31 100644 --- a/src/interfaces/hooks/use-validation.hook.interface.ts +++ b/src/interfaces/hooks/use-validation.hook.interface.ts @@ -1,4 +1,13 @@ -export interface IUseValidationResult { +export interface IUseValidationResult extends IUseValidationContext { + /** + * Callback executing (dispatch) action, returns true if success, else false + * @param data - Data to executing command + * @returns { boolean } - True if correct executing, else false + */ + readonly confirm: (data: T) => boolean; +} + +export interface IUseValidationContext { /** * Indicates, can this action will do */ @@ -8,11 +17,4 @@ export interface IUseValidationResult { * Array of errors, with reason, why action cant be executed */ readonly errors: string[]; - - /** - * Callback executing (dispatch) action, returns true if success, else false - * @param data - Data to executing command - * @returns { boolean } - True if correct executing, else false - */ - readonly confirm: (data: T) => boolean; } \ No newline at end of file diff --git a/src/types/hooks/index.ts b/src/types/hooks/index.ts new file mode 100644 index 0000000..c3476ef --- /dev/null +++ b/src/types/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./linkable.type.js" +export * from "./unlink-when.type.js" \ No newline at end of file diff --git a/src/types/hooks/linkable.type.ts b/src/types/hooks/linkable.type.ts new file mode 100644 index 0000000..31a7e60 --- /dev/null +++ b/src/types/hooks/linkable.type.ts @@ -0,0 +1,6 @@ +import type { Entity, GameObject } from "@world"; + +/** + * Linkable (Entity or GameObject) + */ +export type Linkable = (Entity | GameObject) \ No newline at end of file diff --git a/src/types/hooks/unlink-when.type.ts b/src/types/hooks/unlink-when.type.ts new file mode 100644 index 0000000..4beab56 --- /dev/null +++ b/src/types/hooks/unlink-when.type.ts @@ -0,0 +1,4 @@ +/** + * Link will be auto removed, when only of this events occured + */ +export type UnlinkWhen = "parentKilled" | "parentDeleted" | "childOutOfRange" \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 8686f26..fef2ffc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,4 +3,5 @@ export * from "./callbacks/index.js" export * from "./items/index.js" export * from "./factories/index.js" export * from "./decorators/index.js" -export * from "./global/index.js" \ No newline at end of file +export * from "./global/index.js" +export * from "./hooks/index.js" \ No newline at end of file diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 186dfd1..06407d0 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,3 +1,4 @@ export * from "./use-attack.hook.js" export * from "./use-visibility.hook.js" -export * from "./use-validation.hook.js" \ No newline at end of file +export * from "./use-validation.hook.js" +export * from "./use-link.hook.js" \ No newline at end of file diff --git a/src/utils/hooks/use-attack.hook.ts b/src/utils/hooks/use-attack.hook.ts index ed5e86e..4162afd 100644 --- a/src/utils/hooks/use-attack.hook.ts +++ b/src/utils/hooks/use-attack.hook.ts @@ -4,13 +4,15 @@ import type { IDeadData } from "@interfaces"; /** * Calc attack, emit DeadEvent, return victim dead state - * @param game - Game reference to emit event * @param dmg - Total damage (full calculated) * @param attacker - Attacker reference (another Entity, tower, etc.) * @param victim - Victim reference + * @param core - Game reference to emit event (if hydration disabled) * @returns { { isDead: boolean } } - GameObject with dead info */ -export function useAttack(game: Game, dmg: number, attacker: Entity | GameObject, victim: Entity): { isDead: boolean; } { +export function useAttack(dmg: number, attacker: Entity | GameObject, victim: Entity, core?: Game): { isDead: boolean; } { + const game = useAttack.prototype.game as Game || core + victim.health = victim.health - (dmg >= 0 ? dmg : 0) if (victim.health <= 0) { diff --git a/src/utils/hooks/use-link.hook.ts b/src/utils/hooks/use-link.hook.ts new file mode 100644 index 0000000..e930ac1 --- /dev/null +++ b/src/utils/hooks/use-link.hook.ts @@ -0,0 +1,67 @@ +import { anyWorldObjectIsGameObject, convertAnyPositionToPosition } from "@utils"; +import type { Linkable } from "@types"; +import type { IDeadData, ILink, ILinkOptions, IMovedData, IUseValidationContext } from "@interfaces"; +import { USE_VALIDATION_EVENT_PREFIX } from "@const"; +import { CommandType } from "@enums"; +import type { Game } from "@core"; + +/** + * Creates Link between two entities + * @param from - Linkable WO from + * @param to - Linkable WO to + * @returns { ILink } + */ +export function useLink(from: Linkable, to: Linkable, options?: ILinkOptions): ILink { + const game = useLink.prototype.game as Game + + const parentIsEntity = !anyWorldObjectIsGameObject(from) + const childIsEntity = !anyWorldObjectIsGameObject(to) + + let move: VoidFunction; + let kill: VoidFunction; + let deleting: VoidFunction; + + const link = { + from, + to, + isActive: true, + unLink: () => { + if (move) move() + if (kill) kill() + if (deleting) deleting() + + link.isActive = false + }, + link: (options?: ILinkOptions) => link.isActive ? false : useLink(from, to, options) + } as ILink + + if (childIsEntity && options?.maxDistance) move = game.registerCustomEvent(`${USE_VALIDATION_EVENT_PREFIX}:${CommandType.MOVE}`, (opt, event, data) => { + if ( + data.entity + && !anyWorldObjectIsGameObject(data.entity) + && data.entity.id === to.id + ) { + + const [x1, y1] = from.position + const [x2, y2] = convertAnyPositionToPosition(data.eventData.newPosition || (data.eventData as any).position) + + if (x2-x1 > options!.maxDistance! || y2-y1 > options!.maxDistance!) { + data.eventData.isAllowed = false + data.eventData.errors.push(`[useLink]: Max distance occured (${options!.maxDistance})`) + } + if (options.autoUnlinkOn?.includes("childOutOfRange")) link.unLink() + } + }) + if (parentIsEntity && childIsEntity) { + if (options?.killChild) kill = game.on("entityDead", (opt, event, data) => { + if (data.eventData.entity.id === from.id) game.options.manager.kill(to.id) + if (options.autoUnlinkOn?.includes("parentKilled")) link.unLink() + }) + if (options?.deleteChild) deleting = game.on<{}>("entityDeleted", (opt, event, data) => { + if (data.entity!.id === from.id) game.options.manager.delete(to.id) + if (options.autoUnlinkOn?.includes("parentDeleted")) link.unLink() + }) + } + + return link +} \ No newline at end of file diff --git a/src/utils/hooks/use-validation.hook.ts b/src/utils/hooks/use-validation.hook.ts index 00b1445..7d5ded3 100644 --- a/src/utils/hooks/use-validation.hook.ts +++ b/src/utils/hooks/use-validation.hook.ts @@ -8,13 +8,15 @@ import { anyWorldObjectIsGameObject } from "@utils"; /** * Throw validation event, all plugins can listen and block/unblock it - * @param game - Game reference * @param subject - Subject, who make action * @param action - Command to execute * @param ctx - Context in this hook + * @param core - Game reference (if hydration disabled) * @returns { IUseValidationResult } - Hook result */ -export function useValidation(game: Game, subject: Entity | GameObject, action: CommandType, ctx: CommandContext): IUseValidationResult { +export function useValidation(subject: Entity | GameObject, action: CommandType, ctx: CommandContext, core?: Game): IUseValidationResult { + const game = useValidation.prototype.game as Game || core + const resultContext = { ...ctx, isAllowed: true, diff --git a/src/utils/hooks/use-visibility.hook.ts b/src/utils/hooks/use-visibility.hook.ts index 9ded2be..744333b 100644 --- a/src/utils/hooks/use-visibility.hook.ts +++ b/src/utils/hooks/use-visibility.hook.ts @@ -6,12 +6,14 @@ import { USE_VISIBILITY_EVENT } from "@const"; /** * Util calculate how well observer can see target. Throwing useVisibility:calcVisibility custom event - * @param game - Game reference * @param observer - Who see * @param target - Target + * @param core - Game reference (if hydration disabled) * @returns { IUseVisibiltyResult } - Result of check */ -export function useVisibility(game: Game, observer: Entity | GameObject, target: Entity | GameObject): IUseVisibiltyResult { +export function useVisibility(observer: Entity | GameObject, target: Entity | GameObject, core?: Game): IUseVisibiltyResult { + const game = useVisibility.prototype.game as Game || core + if (canSee(observer.position, target.position, game.options.map)) { const context = { isVisible: true, diff --git a/src/world/entities/entity.ts b/src/world/entities/entity.ts index 72a1e03..f338a99 100644 --- a/src/world/entities/entity.ts +++ b/src/world/entities/entity.ts @@ -166,7 +166,7 @@ export class Entity implements ITarget { for (const entity of entities) { const totalDamage = this.fullDamage - entity.armorHealth - const { isDead } = useAttack(this.manager.game, totalDamage, this, entity) + const { isDead } = useAttack(totalDamage, this, entity, this.manager.game) if (isDead) counter++ } diff --git a/src/world/entities/object.ts b/src/world/entities/object.ts index b9685c8..5dce046 100644 --- a/src/world/entities/object.ts +++ b/src/world/entities/object.ts @@ -39,7 +39,7 @@ export class GameObject implements IGameObject { let counter = 0; for (const victim of entities) { - const { isDead } = useAttack(this.map.game, this.metadata.damage, this, victim) + const { isDead } = useAttack(this.metadata.damage, this, victim, this.map.game) if (isDead) counter++ } diff --git a/test/gametest.ts b/test/gametest.ts index 550e459..d9672f2 100644 --- a/test/gametest.ts +++ b/test/gametest.ts @@ -6,7 +6,7 @@ import { BASE_SEARCH_RADIUS, USE_VALIDATION_EVENT_PREFIX, USE_VISIBILITY_EVENT } import { BluePrintsFactory, EffectFactory, IteractionsFactory, QuestsFactory, SoundsFactory } from "@factories"; import { loggerMiddleware } from "@middlewares"; import { RegenerationPlugin, NetworkPlguin, AsyncPlugin, GraphicPlugin } from "@plugins"; -import { useVisibility, checkTwoPositions, useValidation } from "@utils"; +import { useVisibility, checkTwoPositions, useValidation, useLink } from "@utils"; const [game, manager, map] = createGame({ usingEntityMiddlewares: true, @@ -314,6 +314,12 @@ game.dispatch({ } }) +useLink(player, player_second, { maxDistance: 1, autoUnlinkOn: ["childOutOfRange", "parentDeleted", "parentKilled"], killChild: false, deleteChild: false }) + +console.log(useValidation(player_second, CommandType.MOVE, { + newPosition: [7, 7] +}), 'VALIDATION RES (BLOCKED!)') + const events = new Map() events.set(CommandType.SET_STATE, (ev, data) => { @@ -343,8 +349,8 @@ game.registerCustomEvent(`${USE_VALIDATION_EVENT_PREFIX}:$ d.eventData.errors.push('OUT OF REACH') }) -console.log(useVisibility(game, player, player_second)) -console.log(useValidation(game, player, CommandType.USE_ITEM, { +console.log(useVisibility(player, player_second)) +console.log(useValidation(player, CommandType.USE_ITEM, { forTest: true }))