From ba49a7b92512eb7fbccca90aa19cf85b5897f9c0 Mon Sep 17 00:00:00 2001 From: Neloreck Date: Tue, 25 Jul 2023 05:52:21 +0300 Subject: [PATCH] New utils. Kill object in debug. Animation manager adjustments. --- cli/utils/fs/get_game_paths.ts | 1 + .../core/objects/animation/animation_types.ts | 17 +- .../objects/state/StalkerAnimationManager.ts | 295 +++++++++--------- .../core/objects/state/StalkerStateManager.ts | 8 +- .../ui/debug/sections/DebugObjectSection.ts | 21 ++ .../core/utils/object/object_check.test.ts | 23 +- src/engine/core/utils/object/object_check.ts | 10 + .../core/utils/object/object_get.test.ts | 18 ++ src/engine/core/utils/object/object_get.ts | 17 + .../debug/DebugObjectSection.component.tsx | 11 + .../mocks/objects/client/game_object.mock.ts | 1 + 11 files changed, 256 insertions(+), 166 deletions(-) diff --git a/cli/utils/fs/get_game_paths.ts b/cli/utils/fs/get_game_paths.ts index ed65e71e2..0fd61d5cd 100644 --- a/cli/utils/fs/get_game_paths.ts +++ b/cli/utils/fs/get_game_paths.ts @@ -19,6 +19,7 @@ type TGamePaths = typeof GAME_PATHS; /** * Get absolute paths to the game assets/executables/directories. + * todo: Memoize paths? */ export async function getGamePaths(): Promise { const { path: gamePath } = await getAppPath(config.targets.stalker_game_steam_id).catch(() => { diff --git a/src/engine/core/objects/animation/animation_types.ts b/src/engine/core/objects/animation/animation_types.ts index 98b660f90..f36f12332 100644 --- a/src/engine/core/objects/animation/animation_types.ts +++ b/src/engine/core/objects/animation/animation_types.ts @@ -59,16 +59,21 @@ export type TAnimationSequenceElement = */ export type TAnimationSequenceElements = TAnimationSequenceElement | LuaArray; +/** + * List of properties configuring animation. + */ +export interface IAnimationDescriptorProperties { + maxidle: TDuration; + sumidle: TDuration; + rnd: Optional; + moving: Optional; +} + /** * Descriptor of in-game animation. */ export interface IAnimationDescriptor { - prop: { - maxidle: TDuration; - sumidle: TDuration; - rnd: Optional; - moving: Optional; - }; + prop: IAnimationDescriptorProperties; into: Optional>; out: Optional>; idle: Optional>; diff --git a/src/engine/core/objects/state/StalkerAnimationManager.ts b/src/engine/core/objects/state/StalkerAnimationManager.ts index a3db593e4..a95054f9c 100644 --- a/src/engine/core/objects/state/StalkerAnimationManager.ts +++ b/src/engine/core/objects/state/StalkerAnimationManager.ts @@ -12,8 +12,8 @@ import { animations } from "@/engine/core/objects/animation/animations"; import { animstates } from "@/engine/core/objects/animation/animstates"; import { EStalkerState } from "@/engine/core/objects/animation/state_types"; import { StalkerStateManager } from "@/engine/core/objects/state/StalkerStateManager"; -import { abort } from "@/engine/core/utils/assertion"; import { LuaLogger } from "@/engine/core/utils/logging"; +import { getObjectActiveWeaponSlot } from "@/engine/core/utils/object"; import { createVector, vectorRotateY } from "@/engine/core/utils/vector"; import { AnyCallable, @@ -25,6 +25,7 @@ import { TName, TRate, TTimestamp, + Vector, } from "@/engine/lib/types"; const logger: LuaLogger = new LuaLogger($filename); @@ -35,27 +36,25 @@ const logger: LuaLogger = new LuaLogger($filename); * Ties engine animation callbacks and game logic. */ export class StalkerAnimationManager { - public type: EAnimationType; - public object: ClientObject; - public stateManager: StalkerStateManager; - - public animations: LuaTable; - public state: IAnimationManagerState; + public readonly type: EAnimationType; + public readonly object: ClientObject; + public readonly stateManager: StalkerStateManager; + public readonly animations: LuaTable; + + public readonly state: IAnimationManagerState = { + lastIndex: null, + currentState: null, + targetState: null, + animationMarker: null, + nextRandomAt: null, + sequenceId: 1, + }; public constructor(object: ClientObject, stateManager: StalkerStateManager, type: EAnimationType) { this.type = type; this.object = object; this.stateManager = stateManager; this.animations = type === EAnimationType.ANIMATION ? animations : animstates; - - this.state = { - lastIndex: null, - currentState: null, - targetState: null, - animationMarker: null, - nextRandomAt: null, - sequenceId: 1, - }; } /** @@ -64,6 +63,7 @@ export class StalkerAnimationManager { public setControl(): void { this.object.set_callback(callback.script_animation, this.onAnimationCallback, this); + // On animation control also reset animstate. if (this.type === EAnimationType.ANIMATION) { this.stateManager.animstate.state.animationMarker = null; } @@ -73,17 +73,6 @@ export class StalkerAnimationManager { } } - /** - * Update state of current animation. - */ - public updateAnimation(): void { - const [animation, state] = this.selectAnimation(); - - if (animation !== null) { - this.addAnimation(animation, state); - } - } - /** * todo; */ @@ -96,14 +85,14 @@ export class StalkerAnimationManager { if (isForced === true) { this.object.clear_animations(); - const state = + const state: Optional = this.state.animationMarker === EAnimationMarker.IN ? this.animations.get(this.state.targetState!) : this.animations.get(this.state.currentState!); - if (state !== null && state.out !== null) { - const weaponSlot: TIndex = this.getActiveWeaponSlot(); - const animationForWeaponSlot = this.getAnimationForSlot(weaponSlot, state.out as any); + if (state?.out) { + const weaponSlot: TIndex = getObjectActiveWeaponSlot(this.object); + const animationForWeaponSlot = this.getAnimationForSlot(weaponSlot, state.out); if (animationForWeaponSlot !== null) { for (const [id, nextAnimation] of animationForWeaponSlot) { @@ -115,11 +104,9 @@ export class StalkerAnimationManager { } this.state.animationMarker = null; - this.state.currentState = newState; this.state.targetState = newState; this.state.sequenceId = 1; - this.state.nextRandomAt = now; return; @@ -129,10 +116,63 @@ export class StalkerAnimationManager { this.state.nextRandomAt = now; } + /** + * Update state of current animation. + */ + public updateAnimation(): void { + const [animation, state] = this.selectAnimation(); + + if (animation !== null) { + this.addAnimation(animation, state); + } + } + + /** + * todo; + */ + public addAnimation(animation: TName, state: IAnimationDescriptor): void { + const object: ClientObject = this.object; + const animationProperties = state.prop; + + if ( + this.stateManager.animationPosition && + this.stateManager.animationDirection && + !this.stateManager.isAnimationDirectionApplied + ) { + this.stateManager.isAnimationDirectionApplied = true; + + const direction: Vector = createVector( + 0, + -math.deg(math.atan2(this.stateManager.animationDirection.x, this.stateManager.animationDirection.z)), + 0 + ); + + object.add_animation(animation, true, this.stateManager.animationPosition, direction, true); + } else if (animationProperties === null || animationProperties.moving !== true) { + object.add_animation(animation, true, false); + } else { + object.add_animation(animation, true, true); + } + } + + /** + * todo; + */ + public getAnimationForSlot( + slot: TIndex, + animationsList: LuaArray + ): Optional> { + if (animationsList.get(slot) === null) { + slot = 0; + } + + return $fromArray(animationsList.get(slot) as any); + } + /** * todo; */ - public selectAnimation(): LuaMultiReturn<[Optional, any]> { + public selectAnimation(): LuaMultiReturn<[TName, IAnimationDescriptor] | [null, null]> { const states: IAnimationManagerState = this.state; // New animation detected: @@ -149,8 +189,8 @@ export class StalkerAnimationManager { states.animationMarker = EAnimationMarker.OUT; - const weaponSlot: TIndex = this.getActiveWeaponSlot(); - const animationForWeaponSlot = this.getAnimationForSlot(weaponSlot, animationDescriptor.out as any); + const weaponSlot: TIndex = getObjectActiveWeaponSlot(this.object); + const animationForWeaponSlot = this.getAnimationForSlot(weaponSlot, animationDescriptor.out); if (animationForWeaponSlot === null) { states.animationMarker = EAnimationMarker.OUT; @@ -163,12 +203,12 @@ export class StalkerAnimationManager { if (type(nextAnimation) === "table") { this.processSpecialAction(nextAnimation as any); - this.onAnimationCallback(); + this.onAnimationCallback(false); return $multi(null, null); } - return $multi(nextAnimation as any as string, animationDescriptor); + return $multi(nextAnimation as unknown as TName, animationDescriptor); } if (states.currentState === null) { @@ -183,8 +223,8 @@ export class StalkerAnimationManager { states.animationMarker = EAnimationMarker.IN; - const weaponSlot: TIndex = this.getActiveWeaponSlot(); - const animationForWeaponSlot = this.getAnimationForSlot(weaponSlot, state.into as any); + const weaponSlot: TIndex = getObjectActiveWeaponSlot(this.object); + const animationForWeaponSlot = this.getAnimationForSlot(weaponSlot, state.into); if (animationForWeaponSlot === null) { states.animationMarker = EAnimationMarker.IN; @@ -197,18 +237,18 @@ export class StalkerAnimationManager { if (type(nextAnimation) === "table") { this.processSpecialAction(nextAnimation as any); - this.onAnimationCallback(); + this.onAnimationCallback(false); return $multi(null, null); } - return $multi(nextAnimation as any as string, state); + return $multi(nextAnimation as unknown as TName, state); } } // Same non-null animation: if (states.targetState === states.currentState && states.currentState !== null) { - const activeWeaponSlot: TIndex = this.getActiveWeaponSlot(); + const activeWeaponSlot: TIndex = getObjectActiveWeaponSlot(this.object); const state: IAnimationDescriptor = this.animations.get(states.currentState); let animation; @@ -228,39 +268,12 @@ export class StalkerAnimationManager { states.animationMarker = EAnimationMarker.IDLE; } - return $multi(animation, state) as any; + return $multi(animation as TName, state); } return $multi(null, null); } - /** - * todo; - */ - public getActiveWeaponSlot(): TIndex { - const weapon: Optional = this.object.active_item(); - - if (weapon === null || this.object.weapon_strapped()) { - return 0; - } - - return weapon.animation_slot(); - } - - /** - * todo; - */ - public getAnimationForSlot( - slot: TIndex, - animationsList: LuaTable> - ): Optional> { - if (animationsList.get(slot) === null) { - slot = 0; - } - - return $fromArray(animationsList.get(slot) as any); - } - /** * todo; */ @@ -273,7 +286,10 @@ export class StalkerAnimationManager { return null; } - const animation = this.getAnimationForSlot(weaponSlot, animationStateDescriptor.rnd as any); + const animation: Optional> = this.getAnimationForSlot( + weaponSlot, + animationStateDescriptor.rnd as LuaArray + ); if (animation === null) { return null; @@ -301,40 +317,6 @@ export class StalkerAnimationManager { return animation.get(index); } - /** - * todo; - */ - public addAnimation(animation: TName, state: IAnimationDescriptor): void { - const object: ClientObject = this.object; - const animationProperties = state.prop; - - if (!(object.weapon_unstrapped() || object.weapon_strapped())) { - abort("[%s] Illegal call of add animation. Weapon is strapping now.", object.name()); - } - - if (animationProperties === null || animationProperties.moving !== true) { - object.add_animation(animation, true, false); - - return; - } - - if (this.stateManager.animationPosition === null || this.stateManager.isPositionDirectionApplied === true) { - object.add_animation(animation, true, true); - } else { - if (this.stateManager.animationDirection === null) { - abort("[%s] Animation direction is missing.", object.name()); - } - - const rotationY: TRate = -math.deg( - math.atan2(this.stateManager.animationDirection.x, this.stateManager.animationDirection.z) - ); - - object.add_animation(animation, true, this.stateManager.animationPosition, createVector(0, rotationY, 0), false); - - this.stateManager.isPositionDirectionApplied = true; - } - } - /** * todo; */ @@ -387,83 +369,86 @@ export class StalkerAnimationManager { * todo; */ public onAnimationCallback(skipMultiAnimationCheck?: boolean): void { - if (this.state.animationMarker === null || this.object.animation_count() !== 0) { + if (this.state.animationMarker === null || this.object.animation_count() > 0) { return; } const states: IAnimationManagerState = this.state; - if (states.animationMarker === EAnimationMarker.IN) { - states.animationMarker = null; + switch (this.state.animationMarker) { + case EAnimationMarker.IN: { + states.animationMarker = null; - if (skipMultiAnimationCheck !== true) { - let intoList: Optional> = new LuaTable(); - const targetAnimations = this.animations.get(states.targetState!); + if (skipMultiAnimationCheck !== true) { + let intoList: Optional> = new LuaTable(); + const targetAnimations = this.animations.get(states.targetState!); - if (targetAnimations !== null && targetAnimations.into !== null) { - intoList = this.getAnimationForSlot(this.getActiveWeaponSlot(), targetAnimations.into as any); - } + if (targetAnimations !== null && targetAnimations.into !== null) { + intoList = this.getAnimationForSlot(getObjectActiveWeaponSlot(this.object), targetAnimations.into); + } - if (intoList !== null && intoList.length() > states.sequenceId) { - states.sequenceId = states.sequenceId + 1; - this.updateAnimation(); + if (intoList !== null && intoList.length() > states.sequenceId) { + states.sequenceId += 1; + this.updateAnimation(); - return; + return; + } } - } - states.sequenceId = 1; - states.currentState = states.targetState; - this.updateAnimation(); + states.sequenceId = 1; + states.currentState = states.targetState; + this.updateAnimation(); - return; - } + return; + } - if (states.animationMarker === EAnimationMarker.IDLE) { - states.animationMarker = null; + case EAnimationMarker.IDLE: { + states.animationMarker = null; - const properties = this.animations.get(states.currentState!).prop; + const properties = this.animations.get(states.currentState!).prop; - if (properties.maxidle === 0) { - states.nextRandomAt = time_global() + properties.sumidle * 1000; - } else { - states.nextRandomAt = time_global() + (properties.sumidle + math.random(properties.maxidle)) * 1000; - } + if (properties.maxidle === 0) { + states.nextRandomAt = time_global() + properties.sumidle * 1000; + } else { + states.nextRandomAt = time_global() + (properties.sumidle + math.random(properties.maxidle)) * 1000; + } - this.updateAnimation(); + this.updateAnimation(); - return; - } + return; + } - if (states.animationMarker === EAnimationMarker.OUT) { - states.animationMarker = null; + case EAnimationMarker.OUT: { + states.animationMarker = null; - if (skipMultiAnimationCheck !== true) { - let outAnimationList: LuaTable = new LuaTable(); + if (skipMultiAnimationCheck !== true) { + let outAnimationList: Optional> = new LuaTable(); - if (this.animations.get(states.currentState!).out) { - outAnimationList = this.getAnimationForSlot( - this.getActiveWeaponSlot(), - this.animations.get(states.currentState!).out as any - ) as any; - } + if (this.animations.get(states.currentState as EStalkerState).out) { + outAnimationList = this.getAnimationForSlot( + getObjectActiveWeaponSlot(this.object), + this.animations.get(states.currentState!).out as LuaArray + ); + } - if (outAnimationList !== null && outAnimationList.length() > states.sequenceId) { - states.sequenceId = states.sequenceId + 1; - this.updateAnimation(); + if (outAnimationList !== null && outAnimationList.length() > states.sequenceId) { + states.sequenceId += 1; + this.updateAnimation(); - return; + return; + } } - } - states.sequenceId = 1; - states.currentState = null; + states.sequenceId = 1; + states.currentState = null; - if (this.type === EAnimationType.ANIMATION) { - if (this.stateManager.animstate !== null && this.stateManager.animstate.setControl !== null) { + // After out animation set control to animstate. + if (this.type === EAnimationType.ANIMATION) { this.stateManager.animstate.setControl(); // --this.mgr.animstate:update_anim() } + + return; } } } diff --git a/src/engine/core/objects/state/StalkerStateManager.ts b/src/engine/core/objects/state/StalkerStateManager.ts index 2a2163dab..5e86083e5 100644 --- a/src/engine/core/objects/state/StalkerStateManager.ts +++ b/src/engine/core/objects/state/StalkerStateManager.ts @@ -73,7 +73,7 @@ export class StalkerStateManager { public isForced: boolean = false; public isObjectPointDirectionLook: boolean = false; - public isPositionDirectionApplied: boolean = false; + public isAnimationDirectionApplied: boolean = false; public animation!: StalkerAnimationManager; public animstate!: StalkerAnimationManager; @@ -152,7 +152,7 @@ export class StalkerStateManager { this.isForced = extra.isForced === true; if ( - this.isPositionDirectionApplied === false || + this.isAnimationDirectionApplied === false || (this.animationPosition !== null && extra.animationPosition !== null && !areSameVectors(this.animationPosition, extra.animationPosition as Vector)) || @@ -162,12 +162,12 @@ export class StalkerStateManager { ) { this.animationPosition = extra.animationPosition as Optional; this.animationDirection = extra.animationDirection as Optional; - this.isPositionDirectionApplied = false; + this.isAnimationDirectionApplied = false; } } else { this.animationPosition = null; this.animationDirection = null; - this.isPositionDirectionApplied = false; + this.isAnimationDirectionApplied = false; this.isForced = false; } diff --git a/src/engine/core/ui/debug/sections/DebugObjectSection.ts b/src/engine/core/ui/debug/sections/DebugObjectSection.ts index 19b68e460..3c4617444 100644 --- a/src/engine/core/ui/debug/sections/DebugObjectSection.ts +++ b/src/engine/core/ui/debug/sections/DebugObjectSection.ts @@ -43,6 +43,8 @@ export class DebugObjectSection extends AbstractDebugSection { public uiSetNeutralButton!: CUI3tButton; public uiSetEnemyButton!: CUI3tButton; + public uiKillButton!: CUI3tButton; + public initializeControls(): void { resolveXmlFile(base, this.xml); @@ -63,6 +65,8 @@ export class DebugObjectSection extends AbstractDebugSection { this.uiSetNeutralButton = this.xml.Init3tButton("set_neutral_button", this); this.uiSetEnemyButton = this.xml.Init3tButton("set_enemy_button", this); + this.uiKillButton = this.xml.Init3tButton("kill_button", this); + this.owner.Register(this.uiUseTargetCheck, "use_target_object_check"); this.owner.Register(this.uiLogPlannerStateButton, "log_planner_state"); this.owner.Register(this.uiLogInventoryStateButton, "log_inventory_state"); @@ -73,6 +77,7 @@ export class DebugObjectSection extends AbstractDebugSection { this.owner.Register(this.uiSetFriendButton, "set_friend_button"); this.owner.Register(this.uiSetNeutralButton, "set_neutral_button"); this.owner.Register(this.uiSetEnemyButton, "set_enemy_button"); + this.owner.Register(this.uiKillButton, "kill_button"); } public initializeCallBacks(): void { @@ -105,6 +110,7 @@ export class DebugObjectSection extends AbstractDebugSection { () => this.onSetRelation(ERelation.ENEMY), this ); + this.owner.AddCallback("kill_button", ui_events.BUTTON_CLICKED, () => this.onKillObject(), this); } public initializeState(): void { @@ -217,6 +223,21 @@ export class DebugObjectSection extends AbstractDebugSection { } } + public onKillObject(): void { + if (!isGameStarted()) { + return logger.info("Cannot kill object while game is not started"); + } + + const targetObject: Optional = this.getCurrentObject(); + + if (targetObject) { + logger.info("Kill object:", targetObject.name()); + targetObject.kill(targetObject); + } else { + logger.info("No object found for killing"); + } + } + public getCurrentObject(): Optional { return this.uiUseTargetCheck.GetCheck() ? level.get_target_obj() : getNearestClientObject(); } diff --git a/src/engine/core/utils/object/object_check.test.ts b/src/engine/core/utils/object/object_check.test.ts index 9e5137e9d..bb5b42052 100644 --- a/src/engine/core/utils/object/object_check.test.ts +++ b/src/engine/core/utils/object/object_check.test.ts @@ -12,6 +12,7 @@ import { isObjectInjured, isObjectOnline, isObjectSeenByActor, + isObjectStrappingWeapon, isPlayingSound, isStalkerAlive, isSurgeEnabledOnLevel, @@ -19,6 +20,7 @@ import { } from "@/engine/core/utils/object/object_check"; import { classIds } from "@/engine/lib/constants/class_ids"; import { ClientObject, ServerHumanObject, TClassId } from "@/engine/lib/types"; +import { replaceFunctionMock } from "@/fixtures/utils"; import { CLIENT_SIDE_REGISTRY, MockActionPlanner, @@ -26,7 +28,6 @@ import { mockIniFile, mockServerAlifeHumanStalker, mockServerAlifeMonsterBase, - mockServerAlifeObject, } from "@/fixtures/xray"; describe("'object_check' utils", () => { @@ -52,6 +53,26 @@ describe("'object_check' utils", () => { expect(isObjectInCombat(object)).toBe(false); }); + it("'isObjectStrappingWeapon' should correctly check weapon strap state", () => { + const object: ClientObject = mockClientGameObject(); + + replaceFunctionMock(object.weapon_strapped, () => true); + replaceFunctionMock(object.weapon_unstrapped, () => false); + expect(isObjectStrappingWeapon(object)).toBe(false); + + replaceFunctionMock(object.weapon_strapped, () => false); + replaceFunctionMock(object.weapon_unstrapped, () => true); + expect(isObjectStrappingWeapon(object)).toBe(false); + + replaceFunctionMock(object.weapon_strapped, () => true); + replaceFunctionMock(object.weapon_unstrapped, () => true); + expect(isObjectStrappingWeapon(object)).toBe(false); + + replaceFunctionMock(object.weapon_strapped, () => false); + replaceFunctionMock(object.weapon_unstrapped, () => false); + expect(isObjectStrappingWeapon(object)).toBe(true); + }); + it("'isStalkerAlive' should correctly check stalker alive state", () => { const aliveStalkerServerObject: ServerHumanObject = mockServerAlifeHumanStalker({ alive: () => true, diff --git a/src/engine/core/utils/object/object_check.ts b/src/engine/core/utils/object/object_check.ts index 3dc5aa752..46a89bd34 100644 --- a/src/engine/core/utils/object/object_check.ts +++ b/src/engine/core/utils/object/object_check.ts @@ -36,6 +36,16 @@ export function isObjectInCombat(object: ClientObject): boolean { ); } +/** + * Check whether object is strapping weapon. + * + * @param object - target client object to check + * @returns whether strapping/unstrapping weapon is in process + */ +export function isObjectStrappingWeapon(object: ClientObject): boolean { + return !(object.weapon_unstrapped() || object.weapon_strapped()); +} + /** * Is provided target stalker and alive. * diff --git a/src/engine/core/utils/object/object_get.test.ts b/src/engine/core/utils/object/object_get.test.ts index fc396d163..09e97241d 100644 --- a/src/engine/core/utils/object/object_get.test.ts +++ b/src/engine/core/utils/object/object_get.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "@jest/globals"; import { + getObjectActiveWeaponSlot, getObjectCommunity, getObjectPositioning, getObjectSmartTerrain, @@ -104,4 +105,21 @@ describe("object get utils", () => { expect(getObjectCommunity(clientObject)).toBe("monolith"); expect(getObjectCommunity(serverObject)).toBe("army"); }); + + it("'getObjectActiveWeaponSlot' should correctly get slot", () => { + const object: ClientObject = mockClientGameObject(); + + replaceFunctionMock(object.active_item, () => null); + replaceFunctionMock(object.weapon_strapped, () => true); + expect(getObjectActiveWeaponSlot(object)).toBe(0); + + replaceFunctionMock(object.active_item, () => mockClientGameObject({ animation_slot: () => 4 })); + expect(getObjectActiveWeaponSlot(object)).toBe(0); + + replaceFunctionMock(object.weapon_strapped, () => false); + expect(getObjectActiveWeaponSlot(object)).toBe(4); + + replaceFunctionMock(object.active_item, () => mockClientGameObject({ animation_slot: () => 3 })); + expect(getObjectActiveWeaponSlot(object)).toBe(3); + }); }); diff --git a/src/engine/core/utils/object/object_get.ts b/src/engine/core/utils/object/object_get.ts index cefb53dec..fb739a4c8 100644 --- a/src/engine/core/utils/object/object_get.ts +++ b/src/engine/core/utils/object/object_get.ts @@ -14,6 +14,7 @@ import { ServerGroupObject, ServerHumanObject, ServerObject, + TIndex, TNumberId, Vector, } from "@/engine/lib/types"; @@ -99,3 +100,19 @@ export function getObjectCommunity(object: ClientObject | ServerHumanObject | Se return communities.monster; } + +/** + * Get active weapon slot of an object for animating. + * + * @param object - target client object to check + * @returns active weapon slot index + */ +export function getObjectActiveWeaponSlot(object: ClientObject): TIndex { + const weapon: Optional = object.active_item(); + + if (weapon === null || object.weapon_strapped()) { + return 0; + } + + return weapon.animation_slot(); +} diff --git a/src/engine/forms/menu/debug/DebugObjectSection.component.tsx b/src/engine/forms/menu/debug/DebugObjectSection.component.tsx index 27c4a5177..e690cfb5a 100644 --- a/src/engine/forms/menu/debug/DebugObjectSection.component.tsx +++ b/src/engine/forms/menu/debug/DebugObjectSection.component.tsx @@ -129,6 +129,17 @@ function renderRelationButtons(): JSXNode { textColor={WHITE} font={fonts.letterica16} /> + + ); } diff --git a/src/fixtures/xray/mocks/objects/client/game_object.mock.ts b/src/fixtures/xray/mocks/objects/client/game_object.mock.ts index c10ad423d..63e71d65e 100644 --- a/src/fixtures/xray/mocks/objects/client/game_object.mock.ts +++ b/src/fixtures/xray/mocks/objects/client/game_object.mock.ts @@ -95,6 +95,7 @@ export function mockClientGameObject({ active_item: rest.active_item || jest.fn(() => null), add_animation: rest.add_animation || jest.fn(), animation_count, + animation_slot: rest.animation_slot || jest.fn(() => 1), alive: rest.alive || jest.fn(() => true), accessible_nearest: rest.accessible_nearest || jest.fn(() => 15326), active_slot: rest.active_slot || jest.fn(() => 3),