diff --git a/src/engine/core/database/types.ts b/src/engine/core/database/types.ts index e9a79b55e..fdcad7165 100644 --- a/src/engine/core/database/types.ts +++ b/src/engine/core/database/types.ts @@ -126,13 +126,6 @@ export interface IRegistryObjectState extends Record; - /** - * ID of object currently looting object. - * Used to prevent looting of same object by multiple objects at once. - * - * todo: Move to loot scheme state, not store it in global. Probaly pstore is correct place. - */ - lootedByObject: Optional; /** * todo; */ diff --git a/src/engine/core/objects/binders/creature/StalkerBinder.ts b/src/engine/core/objects/binders/creature/StalkerBinder.ts index e7cfa4e6d..edca8e67e 100644 --- a/src/engine/core/objects/binders/creature/StalkerBinder.ts +++ b/src/engine/core/objects/binders/creature/StalkerBinder.ts @@ -495,6 +495,9 @@ export class StalkerBinder extends object_binder { this.state.stateManager!.animation.setState(null, true); } + this.updateLightState(this.object); + DropManager.getInstance().onObjectDeath(this.object); + if (this.state[EScheme.REACH_TASK]) { emitSchemeEvent(this.object, this.state[EScheme.REACH_TASK], ESchemeEvent.DEATH, victim, who); } @@ -507,9 +510,6 @@ export class StalkerBinder extends object_binder { emitSchemeEvent(this.object, this.state[this.state.activeScheme!]!, ESchemeEvent.DEATH, victim, who); } - this.updateLightState(this.object); - DropManager.getInstance().onObjectDeath(this.object); - unregisterHelicopterEnemy(this.helicopterEnemyIndex!); unregisterStalker(this, false); @@ -526,6 +526,7 @@ export class StalkerBinder extends object_binder { } EventsManager.emitEvent(EGameEvent.STALKER_KILLED, this.object, who); + ReleaseBodyManager.getInstance().addDeadBody(this.object); } @@ -533,7 +534,7 @@ export class StalkerBinder extends object_binder { * todo: Description. */ public onUse(object: ClientObject, who: ClientObject): void { - logger.info("Stalker use:", this.object.name(), "by", who.name()); + logger.info("Stalker used:", this.object.name(), "by", who.name()); if (this.object.alive()) { EventsManager.emitEvent(EGameEvent.STALKER_INTERACTION, object, who); diff --git a/src/engine/core/objects/state/add_state_manager.test.ts b/src/engine/core/objects/state/add_state_manager.test.ts index e96c34a8e..f47888c9e 100644 --- a/src/engine/core/objects/state/add_state_manager.test.ts +++ b/src/engine/core/objects/state/add_state_manager.test.ts @@ -178,6 +178,8 @@ describe("add_state_manager util", () => { "ToIdleItems", [ [EEvaluatorId.IS_STATE_IDLE_ITEMS, false], + [EEvaluatorId.IS_WOUNDED, false], + [EEvaluatorId.IS_WOUNDED_EXISTING, false], [mockStalkerIds.property_items, true], [mockStalkerIds.property_enemy, false], ], @@ -198,7 +200,15 @@ describe("add_state_manager util", () => { [[EEvaluatorId.IS_STATE_IDLE_ALIFE, true]] ); - checkAction(planner.action(EActionId.ALIFE), "generic", [[EEvaluatorId.IS_STATE_IDLE_ALIFE, true]], []); + checkAction( + planner.action(EActionId.ALIFE), + "generic", + [ + [EEvaluatorId.IS_STATE_IDLE_ALIFE, true], + [mockStalkerIds.property_items, false], + ], + [] + ); checkAction( planner.action(mockStalkerIds.action_gather_items), "generic", diff --git a/src/engine/core/objects/state/add_state_manager.ts b/src/engine/core/objects/state/add_state_manager.ts index 179947723..0e674b0cd 100644 --- a/src/engine/core/objects/state/add_state_manager.ts +++ b/src/engine/core/objects/state/add_state_manager.ts @@ -51,6 +51,8 @@ export function addStateManager(object: ClientObject): StalkerStateManager { actionItemsToIdle.add_precondition(new world_property(EEvaluatorId.IS_STATE_IDLE_ITEMS, false)); actionItemsToIdle.add_precondition(new world_property(stalker_ids.property_items, true)); actionItemsToIdle.add_precondition(new world_property(stalker_ids.property_enemy, false)); + actionItemsToIdle.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED, false)); + actionItemsToIdle.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, false)); actionItemsToIdle.add_effect(new world_property(EEvaluatorId.IS_STATE_IDLE_ITEMS, true)); planner.add_action(EActionId.STATE_TO_IDLE_ITEMS, actionItemsToIdle); @@ -68,6 +70,7 @@ export function addStateManager(object: ClientObject): StalkerStateManager { planner.add_action(EActionId.STATE_TO_IDLE_ALIFE, actionAlifeToIdle); planner.action(EActionId.ALIFE).add_precondition(new world_property(EEvaluatorId.IS_STATE_IDLE_ALIFE, true)); + planner.action(EActionId.ALIFE).add_precondition(new world_property(stalker_ids.property_items, false)); planner .action(stalker_ids.action_gather_items) diff --git a/src/engine/core/objects/state/state/ActionStateToIdle.ts b/src/engine/core/objects/state/state/ActionStateToIdle.ts index 50f2f820f..28275cf6e 100644 --- a/src/engine/core/objects/state/state/ActionStateToIdle.ts +++ b/src/engine/core/objects/state/state/ActionStateToIdle.ts @@ -36,6 +36,12 @@ export class ActionStateToIdle extends action_base { sendToNearestAccessibleVertex(this.object, this.object.level_vertex_id()); } + public override finalize(): void { + super.finalize(); + + logger.info("End state to idle for:", this.object.name(), this.name); + } + /** * Rest object state to idle. */ diff --git a/src/engine/core/objects/state/weapon/EvaluatorWeaponLocked.ts b/src/engine/core/objects/state/weapon/EvaluatorWeaponLocked.ts index 0259b8632..771dfe107 100644 --- a/src/engine/core/objects/state/weapon/EvaluatorWeaponLocked.ts +++ b/src/engine/core/objects/state/weapon/EvaluatorWeaponLocked.ts @@ -2,6 +2,7 @@ import { LuabindClass, property_evaluator } from "xray16"; import { StalkerStateManager } from "@/engine/core/objects/state/StalkerStateManager"; import { LuaLogger } from "@/engine/core/utils/logging"; +import { isWeapon } from "@/engine/core/utils/object"; import { ClientObject, Optional } from "@/engine/lib/types"; const logger: LuaLogger = new LuaLogger($filename); @@ -22,6 +23,16 @@ export class EvaluatorWeaponLocked extends property_evaluator { * todo: Description. */ public override evaluate(): boolean { + const bestWeapon: Optional = this.object.best_weapon(); + + if (bestWeapon === null) { + return false; + } + + if (!isWeapon(bestWeapon)) { + return false; + } + const isWeaponStrapped: boolean = this.object.weapon_strapped(); const isWeaponUnstrapped: boolean = this.object.weapon_unstrapped(); @@ -29,17 +40,9 @@ export class EvaluatorWeaponLocked extends property_evaluator { return true; } - const bestWeapon: Optional = this.object.best_weapon(); - - if (bestWeapon === null) { - return false; - } - const isWeaponGoingToBeStrapped: boolean = this.object.is_weapon_going_to_be_strapped(bestWeapon); - if (isWeaponGoingToBeStrapped && !isWeaponStrapped) { - return true; - } else if (!isWeaponGoingToBeStrapped && !isWeaponUnstrapped && this.object.active_item() !== null) { + if (isWeaponGoingToBeStrapped && (!isWeaponStrapped || isWeaponUnstrapped)) { return true; } diff --git a/src/engine/core/schemes/combat_idle/PostCombatIdle.ts b/src/engine/core/schemes/combat_idle/PostCombatIdle.ts index 1ff60d5ed..22de76f45 100644 --- a/src/engine/core/schemes/combat_idle/PostCombatIdle.ts +++ b/src/engine/core/schemes/combat_idle/PostCombatIdle.ts @@ -18,7 +18,7 @@ export class PostCombatIdle { * todo: Description. */ public static addPostCombatIdleWait(object: ClientObject): void { - logger.info("Add post-combat idle for:", object.name()); + // logger.info("Add post-combat idle for:", object.name()); const actionPlanner: ActionPlanner = object.motivation_action_manager(); const combatAction: ActionBase = actionPlanner.action(stalker_ids.action_combat_planner); diff --git a/src/engine/core/schemes/combat_idle/evaluators/EvaluatorHasEnemy.ts b/src/engine/core/schemes/combat_idle/evaluators/EvaluatorHasEnemy.ts index 95ee5288c..bd7cc19be 100644 --- a/src/engine/core/schemes/combat_idle/evaluators/EvaluatorHasEnemy.ts +++ b/src/engine/core/schemes/combat_idle/evaluators/EvaluatorHasEnemy.ts @@ -10,7 +10,7 @@ import { ClientObject, Optional, TDistance, TTimestamp } from "@/engine/lib/type const logger: LuaLogger = new LuaLogger($filename); /** - * Evaluator to check whether have any active enemy. + * Evaluator to check whether any enemy exists. */ @LuabindClass() export class EvaluatorHasEnemy extends property_evaluator { diff --git a/src/engine/core/schemes/corpse_detection/SchemeCorpseDetection.ts b/src/engine/core/schemes/corpse_detection/SchemeCorpseDetection.ts index 1304e92d1..445ca3569 100644 --- a/src/engine/core/schemes/corpse_detection/SchemeCorpseDetection.ts +++ b/src/engine/core/schemes/corpse_detection/SchemeCorpseDetection.ts @@ -47,21 +47,20 @@ export class SchemeCorpseDetection extends AbstractScheme { // Add evaluator to check if anything can be looted. planner.add_evaluator(EEvaluatorId.IS_CORPSE_EXISTING, new EvaluatorCorpseDetect(state)); - const actionSearchCorpse: ActionSearchCorpse = new ActionSearchCorpse(state); + const action: ActionSearchCorpse = new ActionSearchCorpse(state); - actionSearchCorpse.add_precondition(new world_property(stalker_ids.property_alive, true)); - actionSearchCorpse.add_precondition(new world_property(stalker_ids.property_enemy, false)); - actionSearchCorpse.add_precondition(new world_property(stalker_ids.property_danger, false)); - actionSearchCorpse.add_precondition(new world_property(stalker_ids.property_anomaly, false)); - actionSearchCorpse.add_precondition(new world_property(stalker_ids.property_items, false)); - actionSearchCorpse.add_precondition(new world_property(EEvaluatorId.IS_CORPSE_EXISTING, true)); - actionSearchCorpse.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED, false)); - actionSearchCorpse.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, false)); - // Mark as corpse not existing after search end. - actionSearchCorpse.add_effect(new world_property(EEvaluatorId.IS_CORPSE_EXISTING, false)); + action.add_precondition(new world_property(stalker_ids.property_alive, true)); + action.add_precondition(new world_property(stalker_ids.property_enemy, false)); + action.add_precondition(new world_property(stalker_ids.property_danger, false)); + action.add_precondition(new world_property(stalker_ids.property_anomaly, false)); + action.add_precondition(new world_property(EEvaluatorId.IS_CORPSE_EXISTING, true)); + action.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, false)); + action.add_precondition(new world_property(EEvaluatorId.IS_MEET_CONTACT, false)); + + action.add_effect(new world_property(EEvaluatorId.IS_CORPSE_EXISTING, false)); // Add alife action for execution when evaluator allows to do so. - planner.add_action(EActionId.SEARCH_CORPSE, actionSearchCorpse); + planner.add_action(EActionId.SEARCH_CORPSE, action); // Do not return to alife when searching for corpse. planner.action(EActionId.ALIFE).add_precondition(new world_property(EEvaluatorId.IS_CORPSE_EXISTING, false)); diff --git a/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts b/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts index d241c53a2..a2a5a5a96 100644 --- a/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts +++ b/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts @@ -1,11 +1,15 @@ import { action_base, LuabindClass } from "xray16"; -import { IRegistryObjectState, registry, setStalkerState } from "@/engine/core/database"; +import { setStalkerState } from "@/engine/core/database"; import { GlobalSoundManager } from "@/engine/core/managers/sounds/GlobalSoundManager"; import { EStalkerState } from "@/engine/core/objects/animation"; -import { ISchemeCorpseDetectionState } from "@/engine/core/schemes/corpse_detection"; +import { ISchemeCorpseDetectionState } from "@/engine/core/schemes/corpse_detection/ISchemeCorpseDetectionState"; +import { freeSelectedLootedObjectSpot } from "@/engine/core/schemes/corpse_detection/utils"; +import { LuaLogger } from "@/engine/core/utils/logging"; import { scriptSounds } from "@/engine/lib/constants/sound/script_sounds"; -import { Optional, Vector } from "@/engine/lib/types"; +import { EClientObjectPath, Optional, TNumberId, Vector } from "@/engine/lib/types"; + +const logger: LuaLogger = new LuaLogger($filename); /** * Action to go loot corpse by stalkers. @@ -14,39 +18,48 @@ import { Optional, Vector } from "@/engine/lib/types"; export class ActionSearchCorpse extends action_base { public readonly state: ISchemeCorpseDetectionState; + public isLootingSoundPlayed: boolean = false; + public lootingObjectId: Optional = null; + public constructor(state: ISchemeCorpseDetectionState) { super(null, ActionSearchCorpse.__name); this.state = state; } - /** - * Clean up action states. - */ - public override finalize(): void { - // Unmark corpse as selected by an object, if any exist. - if (this.state.selectedCorpseId !== null) { - const corpseState: Optional = registry.objects.get(this.state.selectedCorpseId); - - if (corpseState !== null) { - corpseState.lootedByObject = null; - } - } - - super.finalize(); - } - /** * Initialize object logics when it is capture by corpse loot action. */ public override initialize(): void { super.initialize(); + logger.info("Start search corpse:", this.object.name(), tostring(this.state.selectedCorpseId)); + this.object.set_desired_position(); this.object.set_desired_direction(); + this.object.set_path_type(EClientObjectPath.LEVEL_PATH); this.object.set_dest_level_vertex_id(this.state.selectedCorpseVertexId); setStalkerState(this.object, EStalkerState.PATROL); + + this.lootingObjectId = this.state.selectedCorpseId; + } + + /** + * Clean up action states. + */ + public override finalize(): void { + logger.info("Stop search corpse:", this.object.name(), tostring(this.state.selectedCorpseId)); + + // Unmark corpse as selected by an object, if any exist. + if (this.state.selectedCorpseId !== null) { + freeSelectedLootedObjectSpot(this.state.selectedCorpseId); + } + + this.isLootingSoundPlayed = false; + this.lootingObjectId = null; + + super.finalize(); } /** @@ -63,7 +76,16 @@ export class ActionSearchCorpse extends action_base { lookPosition: this.state.selectedCorpseVertexPosition, lookObjectId: null, }); - GlobalSoundManager.getInstance().playSound(this.object.id(), scriptSounds.corpse_loot_begin); + + // Play looting start sound once. + if (!this.isLootingSoundPlayed) { + GlobalSoundManager.getInstance().playSound(this.object.id(), scriptSounds.corpse_loot_begin); + this.isLootingSoundPlayed = true; + } + } else if (this.lootingObjectId !== this.state.selectedCorpseId) { + setStalkerState(this.object, EStalkerState.PATROL); } + + this.lootingObjectId = this.state.selectedCorpseId; } } diff --git a/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts b/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts index 803016553..521db6714 100644 --- a/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts +++ b/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts @@ -1,13 +1,16 @@ -import { level, LuabindClass, property_evaluator } from "xray16"; +import { LuabindClass, property_evaluator } from "xray16"; -import { IRegistryObjectState, registry } from "@/engine/core/database"; -import { IReleaseDescriptor, ReleaseBodyManager } from "@/engine/core/managers/world/ReleaseBodyManager"; -import { ISchemeCorpseDetectionState } from "@/engine/core/schemes/corpse_detection"; -import { isObjectWithValuableLoot, isObjectWounded } from "@/engine/core/utils/object"; -import { logicsConfig } from "@/engine/lib/configs/LogicsConfig"; +import { setPortableStoreValue } from "@/engine/core/database"; +import { ISchemeCorpseDetectionState } from "@/engine/core/schemes/corpse_detection/ISchemeCorpseDetectionState"; +import { freeSelectedLootedObjectSpot } from "@/engine/core/schemes/corpse_detection/utils"; +import { LuaLogger } from "@/engine/core/utils/logging"; +import { findNearestCorpseToLoot, isObjectWounded } from "@/engine/core/utils/object"; import { communities } from "@/engine/lib/constants/communities"; +import { LOOTING_DEAD_OBJECT_KEY } from "@/engine/lib/constants/portable_store_keys"; import { ACTOR_VISUAL_STALKER } from "@/engine/lib/constants/sections"; -import { ClientObject, LuaArray, Optional, TDistance, TNumberId, Vector } from "@/engine/lib/types"; +import { TNumberId } from "@/engine/lib/types"; + +const logger: LuaLogger = new LuaLogger($filename); /** * Evaluator to check whether object can find some corpse to loot and pick items from it. @@ -25,78 +28,46 @@ export class EvaluatorCorpseDetect extends property_evaluator { * Check if corpse with valuables is detected. */ public override evaluate(): boolean { - if ( + return ( // Dead cannot loot. - !this.object.alive() || + this.object.alive() && // Is in combat, cannot loot. - this.object.best_enemy() !== null || + this.object.best_enemy() === null && + // Looting logics should not be disabled + this.state.isCorpseDetectionEnabled !== false && // Is zombied, does not care. - this.object.character_community() === communities.zombied || - // Looting logics is disabled. - this.state.isCorpseDetectionEnabled === false || - // Is wounded, cannot do anything. - isObjectWounded(this.object.id()) || + this.object.character_community() !== communities.zombied && + // If wounded, cannot do anything. + !isObjectWounded(this.object.id()) && // Is cutscene participant, does not care about loot. - this.object.section() === ACTOR_VISUAL_STALKER - ) { - return false; - } - - const corpses: LuaArray = ReleaseBodyManager.getInstance().releaseObjectRegistry; - - let nearestCorpseDistSqr: TDistance = logicsConfig.SEARCH_CORPSE.DISTANCE_TO_SEARCH_SQR; - let nearestCorpseVertex: Optional = null; - let nearestCorpsePosition: Optional = null; - let corpseId: Optional = null; - - for (const it of $range(1, corpses.length())) { - const id: TNumberId = corpses.get(it).id; - const registryState: Optional = registry.objects.get(id); - const corpseObject: Optional = registryState !== null ? registryState.object! : null; - - if ( - // Is registered in client side. - corpseObject && - // Is not looted by anyone or looted by current object. - (registryState.lootedByObject === null || registryState.lootedByObject === this.object.id()) && - // Is visible so can be looted. - this.object.see(corpseObject) - ) { - if ( - // Is near enough. - this.object.position().distance_to_sqr(corpseObject.position()) < nearestCorpseDistSqr && - // Has valuable loot. - isObjectWithValuableLoot(corpseObject) - ) { - const corpseVertex: TNumberId = level.vertex_id(corpseObject.position()); + this.object.section() !== ACTOR_VISUAL_STALKER && + // Has anything to loot. + this.hasCorpseToLoot() + ); + } - // Can be reached by object. - if (this.object.accessible(corpseVertex)) { - nearestCorpseDistSqr = this.object.position().distance_to_sqr(corpseObject.position()); - nearestCorpseVertex = corpseVertex; - nearestCorpsePosition = corpseObject.position(); - corpseId = id; - } - } - } - } + /** + * todo; + * + * @returns whether any corpse nearby exists + */ + public hasCorpseToLoot(): boolean { + const [corpseObject, corpseVertexId, corpsePosition] = findNearestCorpseToLoot(this.object); - if (nearestCorpseVertex !== null) { - this.state.selectedCorpseVertexId = nearestCorpseVertex; - this.state.selectedCorpseVertexPosition = nearestCorpsePosition; + if (corpseVertexId !== null) { + const corpseId: TNumberId = corpseObject.id(); // Looted corpse before, mark it as not selected. if (this.state.selectedCorpseId !== null && this.state.selectedCorpseId !== corpseId) { - const lootedObjectState: Optional = registry.objects.get(this.state.selectedCorpseId); - - if (lootedObjectState !== null) { - lootedObjectState.lootedByObject = null; - } + freeSelectedLootedObjectSpot(this.state.selectedCorpseId); } + setPortableStoreValue(corpseId, LOOTING_DEAD_OBJECT_KEY, this.object.id()); + // Link looting state for current object and looted object. this.state.selectedCorpseId = corpseId; - registry.objects.get(corpseId as TNumberId).lootedByObject = this.object.id(); + this.state.selectedCorpseVertexId = corpseVertexId; + this.state.selectedCorpseVertexPosition = corpsePosition; return true; } diff --git a/src/engine/core/schemes/corpse_detection/utils/loot.ts b/src/engine/core/schemes/corpse_detection/utils/loot.ts index cec1b6ab9..f1bea36d2 100644 --- a/src/engine/core/schemes/corpse_detection/utils/loot.ts +++ b/src/engine/core/schemes/corpse_detection/utils/loot.ts @@ -1,11 +1,15 @@ -import { IRegistryObjectState, registry } from "@/engine/core/database"; +import { IRegistryObjectState, registry, setPortableStoreValue } from "@/engine/core/database"; import { GlobalSoundManager } from "@/engine/core/managers/sounds/GlobalSoundManager"; import { ISchemeCorpseDetectionState } from "@/engine/core/schemes/corpse_detection"; +import { LuaLogger } from "@/engine/core/utils/logging"; import { transferLoot } from "@/engine/core/utils/object/object_loot"; import { chance } from "@/engine/core/utils/random"; +import { LOOTING_DEAD_OBJECT_KEY } from "@/engine/lib/constants/portable_store_keys"; import { scriptSounds } from "@/engine/lib/constants/sound/script_sounds"; import { ClientObject, EScheme, LuaArray, Optional, TNumberId } from "@/engine/lib/types"; +const logger: LuaLogger = new LuaLogger($filename); + /** * Finish loot corpse action - transfer all the items from corpse and play sound notification about loot quality. * @@ -29,3 +33,14 @@ export function finishCorpseLooting(object: ClientObject): void { ); } } + +/** + * todo; + */ +export function freeSelectedLootedObjectSpot(lootedObject: TNumberId): void { + const lootedObjectState: Optional = registry.objects.get(lootedObject); + + if (lootedObjectState !== null) { + setPortableStoreValue(lootedObject, LOOTING_DEAD_OBJECT_KEY, null); + } +} diff --git a/src/engine/core/utils/object/object_loot.ts b/src/engine/core/utils/object/object_loot.ts index c8d911d13..f8b6d5b03 100644 --- a/src/engine/core/utils/object/object_loot.ts +++ b/src/engine/core/utils/object/object_loot.ts @@ -1,5 +1,12 @@ +import { level } from "xray16"; + +import { getPortableStoreValue, IRegistryObjectState, registry } from "@/engine/core/database"; +import { IReleaseDescriptor, ReleaseBodyManager } from "@/engine/core/managers/world/ReleaseBodyManager"; +import { isObjectWithValuableLoot } from "@/engine/core/utils/object/object_check"; import { isLootableItemSection } from "@/engine/core/utils/object/object_section"; -import { ClientObject, LuaArray } from "@/engine/lib/types"; +import { logicsConfig } from "@/engine/lib/configs/LogicsConfig"; +import { LOOTING_DEAD_OBJECT_KEY } from "@/engine/lib/constants/portable_store_keys"; +import { ClientObject, LuaArray, Optional, TDistance, TNumberId, Vector } from "@/engine/lib/types"; /** * Transfer all lootable items from one object to another. @@ -20,3 +27,54 @@ export function transferLoot(from: ClientObject, to: ClientObject): LuaArray { + const corpses: LuaArray = ReleaseBodyManager.getInstance().releaseObjectRegistry; + + let nearestCorpseDistSqr: TDistance = logicsConfig.SEARCH_CORPSE.DISTANCE_TO_SEARCH_SQR; + let nearestCorpseVertex: Optional = null; + let nearestCorpsePosition: Optional = null; + let nearestCorpseObject: Optional = null; + + for (const [, descriptor] of corpses) { + const id: TNumberId = descriptor.id; + const registryState: Optional = registry.objects.get(id); + const corpseObject: Optional = registryState !== null ? registryState.object : null; + + // Is registered in client side. + if (corpseObject) { + const isLootedBy: Optional = getPortableStoreValue(id, LOOTING_DEAD_OBJECT_KEY); + + if ( + // Is not looted by anyone or looted by current object. + (isLootedBy === null || isLootedBy === object.id()) && + // Seen dead object recently. + object.memory_position(corpseObject) !== null + ) { + const distanceBetween: TDistance = object.position().distance_to_sqr(corpseObject.position()); + + // Is near enough and has valuable loot. + if (distanceBetween < nearestCorpseDistSqr && isObjectWithValuableLoot(corpseObject)) { + const corpseVertex: TNumberId = level.vertex_id(corpseObject.position()); + + // Can be reached by object. + if (object.accessible(corpseVertex)) { + nearestCorpseDistSqr = distanceBetween; + nearestCorpseVertex = corpseVertex; + nearestCorpsePosition = corpseObject.position(); + nearestCorpseObject = corpseObject; + } + } + } + } + } + + return $multi(nearestCorpseObject, nearestCorpseVertex, nearestCorpsePosition) as LuaMultiReturn< + [ClientObject, TNumberId, Vector] + >; +}