diff --git a/src/engine/configs/scripts/README.md b/src/engine/configs/scripts/README.md index b9883b816..82341d8d7 100644 --- a/src/engine/configs/scripts/README.md +++ b/src/engine/configs/scripts/README.md @@ -14,7 +14,7 @@ - `combat_ignore_keep_when_attacked` - ??? - `meet` - ??? - `invulnerable` - ??? -- `gather_items_enabled` - ??? -- `help_wounded_enabled` - ??? +- `gather_items_enabled` - whether stalker can loot items from corpses nearby if any detected, true by default +- `help_wounded_enabled` - whether stalker can help wounded if injured nearby are detected, true by default - `corpse_detection_enabled` - ??? - `use_camp` - [boolean] whether object can use camp logic (stories, guitar, harmonica), true by default diff --git a/src/engine/core/database/types.ts b/src/engine/core/database/types.ts index 4a6ba01ae..c9fb3f16b 100644 --- a/src/engine/core/database/types.ts +++ b/src/engine/core/database/types.ts @@ -130,7 +130,7 @@ export interface IRegistryObjectState extends Record; /** diff --git a/src/engine/core/objects/animation/animations/base.ts b/src/engine/core/objects/animation/animations/base.ts index 8d5f1159c..3f2f4ad20 100644 --- a/src/engine/core/objects/animation/animations/base.ts +++ b/src/engine/core/objects/animation/animations/base.ts @@ -1,7 +1,7 @@ import { IAnimationDescriptor } from "@/engine/core/objects/animation/animation_types"; import { EStalkerState } from "@/engine/core/objects/animation/state_types"; import { finishCorpseLooting } from "@/engine/core/schemes/corpse_detection/utils"; -import { SchemeHelpWounded } from "@/engine/core/schemes/help_wounded"; +import { finishHelpWounded } from "@/engine/core/schemes/help_wounded/utils"; import { createSequence } from "@/engine/core/utils/animation"; import { getExtern } from "@/engine/core/utils/binding"; import { startPlayingGuitar, startPlayingHarmonica } from "@/engine/core/utils/camp"; @@ -1015,8 +1015,9 @@ export const baseAnimations: LuaTable = $fromObject into: createSequence([ "dinamit_1", { + // When animation ends, finish help wounded and heal up. f: (object: ClientObject) => { - SchemeHelpWounded.helpWounded(object); + finishHelpWounded(object); }, }, ]), diff --git a/src/engine/core/objects/binders/creature/StalkerBinder.ts b/src/engine/core/objects/binders/creature/StalkerBinder.ts index 8f27714cd..986fd6dc0 100644 --- a/src/engine/core/objects/binders/creature/StalkerBinder.ts +++ b/src/engine/core/objects/binders/creature/StalkerBinder.ts @@ -49,7 +49,7 @@ import { SchemeHear } from "@/engine/core/schemes/hear/SchemeHear"; import { SchemeMeet } from "@/engine/core/schemes/meet/SchemeMeet"; import { SchemeReachTask } from "@/engine/core/schemes/reach_task/SchemeReachTask"; import { SchemeLight } from "@/engine/core/schemes/sr_light/SchemeLight"; -import { SchemeWounded } from "@/engine/core/schemes/wounded/SchemeWounded"; +import { ISchemeWoundedState } from "@/engine/core/schemes/wounded"; import { pickSectionFromCondList, readIniString, TConditionList } from "@/engine/core/utils/ini"; import { IObjectJobDescriptor } from "@/engine/core/utils/job"; import { LuaLogger } from "@/engine/core/utils/logging"; @@ -555,7 +555,7 @@ export class StalkerBinder extends object_binder { } if (amount > 0) { - SchemeWounded.onHit(object.id()); + (this.state[EScheme.WOUNDED] as ISchemeWoundedState)?.woundManager.onHit(); } EventsManager.emitEvent(EGameEvent.STALKER_HIT, this.object, amount, direction, who, boneIndex); diff --git a/src/engine/core/schemes/base/id/evaluator_ids.ts b/src/engine/core/schemes/base/id/evaluator_ids.ts index fb701e242..32632b61f 100644 --- a/src/engine/core/schemes/base/id/evaluator_ids.ts +++ b/src/engine/core/schemes/base/id/evaluator_ids.ts @@ -12,7 +12,9 @@ const BASE: TNumberId = stalker_ids.property_script || 74; * todo; */ export enum EEvaluatorId { + // Whether any corpse to loot exists nearby. IS_CORPSE_EXISTING = BASE + 50, // 124 + // Whether any wounded stalker to help exists nearby. IS_WOUNDED_EXISTING = BASE + 55, // 129 IS_STATE_IDLE_COMBAT = BASE + 101, // 175 IS_STATE_IDLE_ALIFE = BASE + 102, // 176 diff --git a/src/engine/core/schemes/corpse_detection/ISchemeCorpseDetectionState.ts b/src/engine/core/schemes/corpse_detection/ISchemeCorpseDetectionState.ts index 9521dee78..590ab91ac 100644 --- a/src/engine/core/schemes/corpse_detection/ISchemeCorpseDetectionState.ts +++ b/src/engine/core/schemes/corpse_detection/ISchemeCorpseDetectionState.ts @@ -5,12 +5,12 @@ import { Optional, TNumberId, Vector } from "@/engine/lib/types"; * State of corpse looting scheme. */ export interface ISchemeCorpseDetectionState extends IBaseSchemeState { + // Whether object can detect and search nearby corpses for loot. + isCorpseDetectionEnabled: Optional; // Selected corpse vertex id to loot. selectedCorpseVertexId: TNumberId; // Selected corpse vertex position to loot. selectedCorpseVertexPosition: Optional; // Selected corpse ID to loot. selectedCorpseId: Optional; - // Whether object can detect and search nearby corpses for loot. - isCorpseDetectionEnabled: Optional; } diff --git a/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts b/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts index c9a91799b..2a9474b3e 100644 --- a/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts +++ b/src/engine/core/schemes/corpse_detection/actions/ActionSearchCorpse.ts @@ -58,7 +58,7 @@ export class ActionSearchCorpse extends action_base { super.execute(); // Start playing looting animation when actually reach destination point. - if (this.object.position().distance_to_sqr(this.state.selectedCorpseVertexPosition as Vector) < 2) { + if (this.object.position().distance_to_sqr(this.state.selectedCorpseVertexPosition as Vector) <= 2) { setStalkerState(this.object, EStalkerState.SEARCH_CORPSE, null, null, { lookPosition: this.state.selectedCorpseVertexPosition, lookObject: null, diff --git a/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts b/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts index 7b3494fe2..c5b79c832 100644 --- a/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts +++ b/src/engine/core/schemes/corpse_detection/evaluators/EvaluatorCorpseDetect.ts @@ -56,10 +56,10 @@ export class EvaluatorCorpseDetect extends property_evaluator { if ( // Is registered in client side. corpseObject && - // Is visible so can be looted. - this.object.see(corpseObject) && // Is not looted by anyone or looted by current object. - (registryState.lootedByObject === null || registryState.lootedByObject === this.object.id()) + (registryState.lootedByObject === null || registryState.lootedByObject === this.object.id()) && + // Is visible so can be looted. + this.object.see(corpseObject) ) { if ( // Is near enough. diff --git a/src/engine/core/schemes/help_wounded/ISchemeHelpWoundedState.ts b/src/engine/core/schemes/help_wounded/ISchemeHelpWoundedState.ts index 005f9bdc9..f2826d459 100644 --- a/src/engine/core/schemes/help_wounded/ISchemeHelpWoundedState.ts +++ b/src/engine/core/schemes/help_wounded/ISchemeHelpWoundedState.ts @@ -2,11 +2,15 @@ import { IBaseSchemeState } from "@/engine/core/schemes/base"; import { TNumberId, Vector } from "@/engine/lib/types"; /** - * todo; + * State of helping wounded scheme. */ export interface ISchemeHelpWoundedState extends IBaseSchemeState { - help_wounded_enabled: boolean; - vertex_id: TNumberId; - vertex_position: Vector; - selected_id: TNumberId; + // Whether object can detect and help nearby wounded stalkers. + isHelpingWoundedEnabled: boolean; + // Selected wounded stalker vertex id to help. + selectedWoundedVertexId: TNumberId; + // Selected wounded stalker position to help. + selectedWoundedVertexPosition: Vector; + // Selected wounded stalker ID to help. + selectedWoundedId: TNumberId; } diff --git a/src/engine/core/schemes/help_wounded/SchemeHelpWounded.ts b/src/engine/core/schemes/help_wounded/SchemeHelpWounded.ts index ea0cdba1e..bf3a3c567 100644 --- a/src/engine/core/schemes/help_wounded/SchemeHelpWounded.ts +++ b/src/engine/core/schemes/help_wounded/SchemeHelpWounded.ts @@ -1,36 +1,34 @@ -import { alife, stalker_ids, world_property } from "xray16"; +import { stalker_ids, world_property } from "xray16"; -import { IRegistryObjectState, registry } from "@/engine/core/database"; -import { GlobalSoundManager } from "@/engine/core/managers/sounds/GlobalSoundManager"; +import { IRegistryObjectState } from "@/engine/core/database"; import { AbstractScheme, EActionId, EEvaluatorId } from "@/engine/core/schemes"; import { ActionHelpWounded } from "@/engine/core/schemes/help_wounded/actions"; import { EvaluatorWoundedExist } from "@/engine/core/schemes/help_wounded/evaluators"; import { ISchemeHelpWoundedState } from "@/engine/core/schemes/help_wounded/ISchemeHelpWoundedState"; -import { SchemeWounded } from "@/engine/core/schemes/wounded/SchemeWounded"; import { readIniBoolean } from "@/engine/core/utils/ini/ini_read"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { scriptSounds } from "@/engine/lib/constants/sound/script_sounds"; -import { ActionPlanner, ClientObject, IniFile, Optional, TNumberId } from "@/engine/lib/types"; +import { ActionPlanner, ClientObject, IniFile, Optional } from "@/engine/lib/types"; import { EScheme, ESchemeType, TSection } from "@/engine/lib/types/scheme"; const logger: LuaLogger = new LuaLogger($filename); /** - * todo; + * Scheme describing object logics for helping friendly injured stalkers. + * Part of shared generics logics available for all stalker objects. */ export class SchemeHelpWounded extends AbstractScheme { public static override readonly SCHEME_SECTION: EScheme = EScheme.HELP_WOUNDED; public static override readonly SCHEME_TYPE: ESchemeType = ESchemeType.STALKER; /** - * todo: Description. + * Activate section with help wounded for the object. */ public static override activate(object: ClientObject, ini: IniFile, scheme: EScheme, section: Optional) { AbstractScheme.assign(object, ini, scheme, section); } /** - * todo: Description. + * Add scheme generic states / evaluators / actions for the object. */ public static override add( object: ClientObject, @@ -41,6 +39,7 @@ export class SchemeHelpWounded extends AbstractScheme { ): void { const actionPlanner: ActionPlanner = object.motivation_action_manager(); + // Add custom evaluator to check if wounded stalkers exist nearby. actionPlanner.add_evaluator(EEvaluatorId.IS_WOUNDED_EXISTING, new EvaluatorWoundedExist(state)); const action: ActionHelpWounded = new ActionHelpWounded(state); @@ -51,53 +50,36 @@ export class SchemeHelpWounded extends AbstractScheme { action.add_precondition(new world_property(stalker_ids.property_anomaly, false)); action.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, true)); action.add_precondition(new world_property(EEvaluatorId.IS_WOUNDED, false)); + // Clean up wounded stalkers search once action is finished. action.add_effect(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, false)); + // Help stalkers nearby if conditions are met. actionPlanner.add_action(EActionId.HELP_WOUNDED, action); + + // Do not allow alife activity before finish helping all stalkers nearby. actionPlanner.action(EActionId.ALIFE).add_precondition(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, false)); + + // Do not allow alife idle activity before finish helping all stalkers nearby. actionPlanner .action(EActionId.STATE_TO_IDLE_ALIFE) .add_precondition(new world_property(EEvaluatorId.IS_WOUNDED_EXISTING, false)); } /** - * todo: Description. + * Reset scheme state and read configuration from current logics section for the object. */ - public static override reset(object: ClientObject, scheme: EScheme, state: IRegistryObjectState, section: TSection) { - (state[SchemeHelpWounded.SCHEME_SECTION] as ISchemeHelpWoundedState).help_wounded_enabled = readIniBoolean( - state.ini!, + public static override reset( + object: ClientObject, + scheme: EScheme, + state: IRegistryObjectState, + section: TSection + ): void { + (state[SchemeHelpWounded.SCHEME_SECTION] as ISchemeHelpWoundedState).isHelpingWoundedEnabled = readIniBoolean( + state.ini, section, "help_wounded_enabled", false, true ); } - - /** - * todo: Description. - */ - public static helpWounded(object: ClientObject): void { - const state: IRegistryObjectState = registry.objects.get(object.id()); - const selectedId: TNumberId = (state[EScheme.HELP_WOUNDED] as ISchemeHelpWoundedState).selected_id; - const selectedObject: Optional = - registry.objects.get(selectedId) && registry.objects.get(selectedId).object!; - - if (selectedObject === null) { - return; - } - - alife().create( - "medkit_script", - selectedObject.position(), - selectedObject.level_vertex_id(), - selectedObject.game_vertex_id(), - selectedId - ); - - SchemeWounded.unlockMedkit(selectedObject); - - registry.objects.get(selectedId).wounded_already_selected = -1; - - GlobalSoundManager.getInstance().playSound(object.id(), scriptSounds.wounded_medkit, null, null); - } } diff --git a/src/engine/core/schemes/help_wounded/actions/ActionHelpWounded.ts b/src/engine/core/schemes/help_wounded/actions/ActionHelpWounded.ts index 63eb6b973..c0b479ce4 100644 --- a/src/engine/core/schemes/help_wounded/actions/ActionHelpWounded.ts +++ b/src/engine/core/schemes/help_wounded/actions/ActionHelpWounded.ts @@ -8,7 +8,7 @@ import { LuaLogger } from "@/engine/core/utils/logging"; const logger: LuaLogger = new LuaLogger($filename); /** - * todo; + * Action class describing how stalkers help each other when one of them is wounded. */ @LuabindClass() export class ActionHelpWounded extends action_base { @@ -20,35 +20,32 @@ export class ActionHelpWounded extends action_base { } /** - * todo: Description. + * On init set destination vertex of wounded object and try to reach it. */ public override initialize(): void { super.initialize(); this.object.set_desired_position(); this.object.set_desired_direction(); - this.object.set_dest_level_vertex_id(this.state.vertex_id); + + this.object.set_dest_level_vertex_id(this.state.selectedWoundedVertexId); setStalkerState(this.object, EStalkerState.PATROL); } /** - * todo: Description. + * Wait for object to reach target location. + * Then run heal up animation. + * On animation end separate callback to heal target will be called. */ public override execute(): void { super.execute(); - if (this.object.position().distance_to_sqr(this.state.vertex_position) > 2) { - return; + if (this.object.position().distance_to_sqr(this.state.selectedWoundedVertexPosition) <= 2) { + setStalkerState(this.object, EStalkerState.HELP_WOUNDED, null, null, { + lookPosition: this.state.selectedWoundedVertexPosition, + lookObject: null, + }); } - - setStalkerState( - this.object, - EStalkerState.HELP_WOUNDED, - null, - null, - { lookPosition: this.state.vertex_position, lookObject: null }, - null - ); } } diff --git a/src/engine/core/schemes/help_wounded/evaluators/EvaluatorWoundedExist.ts b/src/engine/core/schemes/help_wounded/evaluators/EvaluatorWoundedExist.ts index 8dd211246..8e9ba6be0 100644 --- a/src/engine/core/schemes/help_wounded/evaluators/EvaluatorWoundedExist.ts +++ b/src/engine/core/schemes/help_wounded/evaluators/EvaluatorWoundedExist.ts @@ -6,98 +6,102 @@ import { ISchemeWoundedState } from "@/engine/core/schemes/wounded"; import { LuaLogger } from "@/engine/core/utils/logging"; import { isObjectWounded } from "@/engine/core/utils/object"; import { communities } from "@/engine/lib/constants/communities"; -import { ClientObject, EScheme, TDistance, TNumberId, Vector } from "@/engine/lib/types"; +import { ACTOR_VISUAL_STALKER } from "@/engine/lib/constants/sections"; +import { ClientObject, EScheme, Optional, TDistance, TNumberId, Vector } from "@/engine/lib/types"; const logger: LuaLogger = new LuaLogger($filename); /** - * todo; + * Check if any wounded stalker to help exists nearby. */ @LuabindClass() export class EvaluatorWoundedExist extends property_evaluator { public readonly state: ISchemeHelpWoundedState; - /** - * todo: Description. - */ public constructor(storage: ISchemeHelpWoundedState) { super(null, EvaluatorWoundedExist.__name); this.state = storage; } /** - * todo: Description. + * Evaluate whether object can detect any wounded stalkers nearby. */ public override evaluate(): boolean { const object: ClientObject = this.object; const objectId: TNumberId = object.id(); const objectPosition: Vector = object.position(); - if (!object.alive()) { - return false; - } else if (object.best_enemy() !== null) { - return false; - } else if (object.character_community() === communities.zombied) { - return false; - } else if (this.state.help_wounded_enabled === false) { - return false; - } - - if (isObjectWounded(object.id())) { - return false; - } else if (object.section() === "actor_visual_stalker") { + if ( + // Cannot help, scheme is disabled. + !this.state.isHelpingWoundedEnabled || + // Is not alive and cannot help. + !object.alive() || + // Have enemies to fight. + object.best_enemy() !== null || + // Zombied cannot help others. + object.character_community() === communities.zombied || + // Nearby stalkers shoul be wounded. + isObjectWounded(object.id()) || + // Cutscene object, should follow scenarios. + object.section() === ACTOR_VISUAL_STALKER + ) { return false; } let nearestDistance: TDistance = 900; // sqr -> 30*30 - let nearestVertex = null; - let nearestPosition = null; - let selectedId = null; + let nearestVertexId: Optional = null; + let nearestPosition: Optional = null; + let selectedObjectId: Optional = null; + // Iterate all seen objects in nearest time. for (const memoryVisibleObject of object.memory_visible_objects()) { const visibleObject: ClientObject = memoryVisibleObject.object(); const visibleObjectId: TNumberId = visibleObject.id(); const visibleObjectState: IRegistryObjectState = registry.objects.get(visibleObjectId); if ( - object.see(visibleObject) && + // Is alive. + visibleObject.alive() && + // Is wounded. isObjectWounded(visibleObject.id()) && + // Is not selected by others to help or selected by current object. (visibleObjectState.wounded_already_selected === null || visibleObjectState.wounded_already_selected === objectId) && - visibleObject.alive() + // Is seen. + object.see(visibleObject) && + // Is not marked as excluded. + (visibleObjectState[EScheme.WOUNDED] as ISchemeWoundedState).notForHelp !== true ) { - if ((visibleObjectState[EScheme.WOUNDED] as ISchemeWoundedState).not_for_help !== true) { - const visibleObjectPosition: Vector = visibleObject.position(); - const distanceBetweenObjects: TDistance = objectPosition.distance_to_sqr(visibleObjectPosition); - - if (distanceBetweenObjects < nearestDistance) { - const vertexId: TNumberId = level.vertex_id(visibleObjectPosition); - - if (object.accessible(vertexId)) { - nearestDistance = distanceBetweenObjects; - nearestVertex = vertexId; - nearestPosition = visibleObjectPosition; - selectedId = visibleObjectId; - } + const visibleObjectPosition: Vector = visibleObject.position(); + const distanceBetweenObjects: TDistance = objectPosition.distance_to_sqr(visibleObjectPosition); + + if (distanceBetweenObjects < nearestDistance) { + const vertexId: TNumberId = level.vertex_id(visibleObjectPosition); + + if (object.accessible(vertexId)) { + nearestDistance = distanceBetweenObjects; + nearestVertexId = vertexId; + nearestPosition = visibleObjectPosition; + selectedObjectId = visibleObjectId; } } } } - if (nearestVertex !== null) { - this.state.vertex_id = nearestVertex; - this.state.vertex_position = nearestPosition!; + if (nearestVertexId) { + this.state.selectedWoundedVertexId = nearestVertexId; + this.state.selectedWoundedVertexPosition = nearestPosition!; - if ( - this.state.selected_id !== null && - this.state.selected_id !== selectedId && - registry.objects.get(this.state.selected_id) !== null - ) { - registry.objects.get(this.state.selected_id).wounded_already_selected = null; + if (this.state.selectedWoundedId !== null && this.state.selectedWoundedId !== selectedObjectId) { + const previousWoundedState: Optional = registry.objects.get(this.state.selectedWoundedId); + + if (previousWoundedState !== null) { + registry.objects.get(this.state.selectedWoundedId).wounded_already_selected = null; + } } - this.state.selected_id = selectedId!; - registry.objects.get(this.state.selected_id!).wounded_already_selected = object.id(); + this.state.selectedWoundedId = selectedObjectId as TNumberId; + registry.objects.get(selectedObjectId as TNumberId).wounded_already_selected = object.id(); return true; } diff --git a/src/engine/core/schemes/help_wounded/index.ts b/src/engine/core/schemes/help_wounded/index.ts index 0e7aafa03..a33c318d6 100644 --- a/src/engine/core/schemes/help_wounded/index.ts +++ b/src/engine/core/schemes/help_wounded/index.ts @@ -1,2 +1,3 @@ export * from "@/engine/core/schemes/help_wounded/SchemeHelpWounded"; export * from "@/engine/core/schemes/help_wounded/ISchemeHelpWoundedState"; +export * from "@/engine/core/schemes/help_wounded/utils"; diff --git a/src/engine/core/schemes/help_wounded/utils/help_wounded.ts b/src/engine/core/schemes/help_wounded/utils/help_wounded.ts new file mode 100644 index 000000000..bda681396 --- /dev/null +++ b/src/engine/core/schemes/help_wounded/utils/help_wounded.ts @@ -0,0 +1,36 @@ +import { alife } from "xray16"; + +import { IRegistryObjectState, registry } from "@/engine/core/database"; +import { GlobalSoundManager } from "@/engine/core/managers/sounds/GlobalSoundManager"; +import { ISchemeHelpWoundedState } from "@/engine/core/schemes/help_wounded"; +import { ISchemeWoundedState } from "@/engine/core/schemes/wounded"; +import { scriptSounds } from "@/engine/lib/constants/sound/script_sounds"; +import { ClientObject, EScheme, Maybe, Optional, TNumberId } from "@/engine/lib/types"; + +/** + * todo: Description. + */ +export function finishHelpWounded(object: ClientObject): void { + const state: IRegistryObjectState = registry.objects.get(object.id()); + + const selectedObjectId: TNumberId = (state[EScheme.HELP_WOUNDED] as ISchemeHelpWoundedState).selectedWoundedId; + const selectedObjectState: Optional = registry.objects.get(selectedObjectId); + const selectedObject: Optional = selectedObjectState?.object as Optional; + + if (selectedObject) { + // Give script medkit to heal up for an object. + alife().create( + "medkit_script", + selectedObject.position(), + selectedObject.level_vertex_id(), + selectedObject.game_vertex_id(), + selectedObjectId + ); + + (selectedObjectState?.wounded as Maybe)?.woundManager.unlockMedkit(); + selectedObjectState.wounded_already_selected = -1; + + // Say thank you. + GlobalSoundManager.getInstance().playSound(object.id(), scriptSounds.wounded_medkit); + } +} diff --git a/src/engine/core/schemes/help_wounded/utils/index.ts b/src/engine/core/schemes/help_wounded/utils/index.ts new file mode 100644 index 000000000..be92507e6 --- /dev/null +++ b/src/engine/core/schemes/help_wounded/utils/index.ts @@ -0,0 +1 @@ +export * from "@/engine/core/schemes/help_wounded/utils/help_wounded"; diff --git a/src/engine/core/schemes/wounded/ISchemeWoundedState.ts b/src/engine/core/schemes/wounded/ISchemeWoundedState.ts index 5a8c2db51..1877c52d9 100644 --- a/src/engine/core/schemes/wounded/ISchemeWoundedState.ts +++ b/src/engine/core/schemes/wounded/ISchemeWoundedState.ts @@ -3,7 +3,8 @@ import type { WoundManager } from "@/engine/core/schemes/wounded/WoundManager"; import type { LuaArray, Optional, TNumberId } from "@/engine/lib/types"; /** - * todo; + * State of object wounded scheme. + * Configures how stalker should behave once it is wounded. */ export interface ISchemeWoundedState extends IBaseSchemeState { woundManager: WoundManager; @@ -18,7 +19,8 @@ export interface ISchemeWoundedState extends IBaseSchemeState { help_start_dialog: Optional; use_medkit: Optional; help_dialog: TNumberId; - not_for_help: Optional; + // Whether object should not be helped by other stalkers. + notForHelp: Optional; autoheal: boolean; enable_talk: boolean; } diff --git a/src/engine/core/schemes/wounded/SchemeWounded.ts b/src/engine/core/schemes/wounded/SchemeWounded.ts index 13a51b644..4a93febac 100644 --- a/src/engine/core/schemes/wounded/SchemeWounded.ts +++ b/src/engine/core/schemes/wounded/SchemeWounded.ts @@ -180,7 +180,7 @@ export class SchemeWounded extends AbstractScheme { state.use_medkit = defaults.use_medkit; state.autoheal = true; state.enable_talk = true; - state.not_for_help = defaults.not_for_help; + state.notForHelp = defaults.not_for_help; } else { state.hp_state = SchemeWounded.parseData(readIniString(ini, section, "hp_state", false, "", defaults.hp_state)); state.hp_state_see = SchemeWounded.parseData( @@ -199,60 +199,12 @@ export class SchemeWounded extends AbstractScheme { state.use_medkit = readIniBoolean(ini, section, "use_medkit", false, defaults.use_medkit); state.autoheal = readIniBoolean(ini, section, "autoheal", false, true); state.enable_talk = readIniBoolean(ini, section, "enable_talk", false, true); - state.not_for_help = readIniBoolean(ini, section, "not_for_help", false, defaults.not_for_help); + state.notForHelp = readIniBoolean(ini, section, "not_for_help", false, defaults.not_for_help); } state.wounded_set = true; } - /** - * todo: Description. - */ - public static unlockMedkit(object: ClientObject): void { - const state: Optional = registry.objects.get(object.id()); - - (state?.wounded as Maybe)?.woundManager.unlockMedkit(); - } - - /** - * todo: Description. - */ - public static eatMedkit(object: ClientObject): void { - const state: Optional = registry.objects.get(object.id()); - - (state?.wounded as Maybe)?.woundManager.eatMedkit(); - } - - /** - * todo: Description. - */ - public static onHit(objectId: TNumberId): void { - const state: Optional = registry.objects.get(objectId); - - (state?.wounded as Maybe)?.woundManager.onHit(); - } - - /** - * todo: Description. - */ - public static isPsyWoundedById(objectId: TNumberId): boolean { - const state: Optional = registry.objects.get(objectId); - - if (state.wounded !== null) { - const woundState = (state?.wounded as Maybe)?.woundManager.woundState; - - return ( - woundState === "psy_pain" || - woundState === "psy_armed" || - woundState === "psy_shoot" || - woundState === "psycho_pain" || - woundState === "psycho_shoot" - ); - } - - return false; - } - /** * todo; */ diff --git a/src/engine/core/schemes/wounded/WoundManager.ts b/src/engine/core/schemes/wounded/WoundManager.ts index 960752577..54f922c68 100644 --- a/src/engine/core/schemes/wounded/WoundManager.ts +++ b/src/engine/core/schemes/wounded/WoundManager.ts @@ -7,11 +7,13 @@ import { AbstractSchemeManager } from "@/engine/core/schemes"; import { ISchemeWoundedState } from "@/engine/core/schemes/wounded/ISchemeWoundedState"; import { pickSectionFromCondList } from "@/engine/core/utils/ini/ini_config"; import { drugs } from "@/engine/lib/constants/items/drugs"; +import { scriptSounds } from "@/engine/lib/constants/sound/script_sounds"; import { FALSE, NIL, TRUE } from "@/engine/lib/constants/words"; import { AlifeSimulator, LuaArray, Optional, TCount, TIndex, TRate, TTimestamp } from "@/engine/lib/types"; /** - * todo; + * Manager to handle wounded state of stalkers. + * On low HP eat medkits / lay on the floor and call for help. */ export class WoundManager extends AbstractSchemeManager { public canUseMedkit: boolean = false; @@ -23,7 +25,7 @@ export class WoundManager extends AbstractSchemeManager { public woundState!: string; /** - * todo: Description. + * Handle recalculation of wounded state on each iteration. */ public update(): void { const hp: TCount = 100 * this.object.health; @@ -84,7 +86,7 @@ export class WoundManager extends AbstractSchemeManager { const beginAt: Optional = getPortableStoreValue(this.object, "begin_wounded"); if (beginAt !== null && now - beginAt <= 60_000) { - GlobalSoundManager.getInstance().playSound(this.object.id(), "help_thanks", null, null); + GlobalSoundManager.getInstance().playSound(this.object.id(), scriptSounds.help_thanks); } setPortableStoreValue(this.object, "begin_wounded", null); @@ -200,14 +202,11 @@ export class WoundManager extends AbstractSchemeManager { } /** - * todo: Description. + * Handle object being hit. + * Recalculate wounded states. */ public override onHit(): void { - if (!this.object.alive()) { - return; - } - - if (this.object.critically_wounded()) { + if (!this.object.alive() || this.object.critically_wounded()) { return; } diff --git a/src/engine/core/schemes/wounded/evaluators/EvaluatorCanFight.ts b/src/engine/core/schemes/wounded/evaluators/EvaluatorCanFight.ts index 6af8e03bd..6d89c787c 100644 --- a/src/engine/core/schemes/wounded/evaluators/EvaluatorCanFight.ts +++ b/src/engine/core/schemes/wounded/evaluators/EvaluatorCanFight.ts @@ -8,7 +8,7 @@ import { FALSE } from "@/engine/lib/constants/words"; const logger: LuaLogger = new LuaLogger($filename); /** - * todo; + * Evaluator to check whether object is wounded and cannot fight. */ @LuabindClass() export class EvaluatorCanFight extends property_evaluator { @@ -21,12 +21,9 @@ export class EvaluatorCanFight extends property_evaluator { /** * Evaluate whether object can fight. + * Allows overriding it with config and still fighting when wounded_fight is enabled with ltx. */ public override evaluate(): boolean { - if (this.object.critically_wounded()) { - return true; - } - - return getPortableStoreValue(this.object, "wounded_fight") !== FALSE; + return this.object.critically_wounded() || getPortableStoreValue(this.object, "wounded_fight") !== FALSE; } } diff --git a/src/engine/core/schemes/wounded/evaluators/EvaluatorWounded.ts b/src/engine/core/schemes/wounded/evaluators/EvaluatorWounded.ts index 74af0acb1..eb3139aab 100644 --- a/src/engine/core/schemes/wounded/evaluators/EvaluatorWounded.ts +++ b/src/engine/core/schemes/wounded/evaluators/EvaluatorWounded.ts @@ -9,7 +9,7 @@ import { ActionPlanner, Optional } from "@/engine/lib/types"; const logger: LuaLogger = new LuaLogger($filename); /** - * todo; + * Evaluator to check if object is wounded critically. */ @LuabindClass() export class EvaluatorWounded extends property_evaluator { @@ -22,25 +22,25 @@ export class EvaluatorWounded extends property_evaluator { } /** - * todo: Description. + * Perform wounded state check. */ public override evaluate(): boolean { - if (this.object.in_smart_cover()) { - return false; - } else if (this.state.wounded_set !== true) { + // If scheme is not activated or object is in smart cover (animation state is captured). + if (!this.state.wounded_set || this.object.in_smart_cover()) { return false; } this.state.woundManager.update(); - if (this.actionPlanner === null) { - this.actionPlanner = this.object.motivation_action_manager(); - } - if (this.object.critically_wounded()) { return false; } + if (this.actionPlanner === null) { + this.actionPlanner = this.object.motivation_action_manager(); + } + + // If fighting and wounded_fight is set to 'true' still fight: if ( this.actionPlanner.evaluator(stalker_ids.property_enemy).evaluate() && getPortableStoreValue(this.object, "wounded_fight") === TRUE @@ -48,6 +48,7 @@ export class EvaluatorWounded extends property_evaluator { return false; } + // Wounded state is set for an object, consider it wounded. return tostring(getPortableStoreValue(this.object, "wounded_state")) !== NIL; } } diff --git a/src/engine/core/schemes/wounded/index.ts b/src/engine/core/schemes/wounded/index.ts index 96f90d2ea..418651ce1 100644 --- a/src/engine/core/schemes/wounded/index.ts +++ b/src/engine/core/schemes/wounded/index.ts @@ -1,2 +1,2 @@ -export * from "@/engine/core/schemes/wounded/SchemeWounded"; export * from "@/engine/core/schemes/wounded/ISchemeWoundedState"; +export * from "@/engine/core/schemes/wounded/SchemeWounded"; diff --git a/src/engine/core/utils/object/index.ts b/src/engine/core/utils/object/index.ts index e043bb263..1f8408f17 100644 --- a/src/engine/core/utils/object/index.ts +++ b/src/engine/core/utils/object/index.ts @@ -14,3 +14,4 @@ export * from "@/engine/core/utils/object/object_sound"; export * from "@/engine/core/utils/object/object_spawn"; export * from "@/engine/core/utils/object/object_state"; export * from "@/engine/core/utils/object/object_task_reward"; +export * from "@/engine/core/utils/object/object_wounds"; diff --git a/src/engine/core/utils/object/object_wounds.ts b/src/engine/core/utils/object/object_wounds.ts new file mode 100644 index 000000000..0ccc235c7 --- /dev/null +++ b/src/engine/core/utils/object/object_wounds.ts @@ -0,0 +1,36 @@ +import { IRegistryObjectState, registry } from "@/engine/core/database"; +import { ISchemeWoundedState } from "@/engine/core/schemes/wounded"; +import { ClientObject, Maybe, Optional } from "@/engine/lib/types"; + +/** + * Allow wounded object to heal. + * + * todo; + */ +export function enableObjectWoundedHealing(object: ClientObject): void { + const state: Optional = registry.objects.get(object.id()); + + (state?.wounded as Maybe)?.woundManager.unlockMedkit(); +} + +/** + * todo: Description. + */ +export function isObjectPsyWounded(object: ClientObject): boolean { + const state: Optional = registry.objects.get(object.id()); + + if (state.wounded !== null) { + const woundState: Optional = (state?.wounded as Maybe)?.woundManager + .woundState as Optional; + + return ( + woundState === "psy_pain" || + woundState === "psy_armed" || + woundState === "psy_shoot" || + woundState === "psycho_pain" || + woundState === "psycho_shoot" + ); + } + + return false; +} diff --git a/src/engine/scripts/declarations/dialogs/dialogs.ts b/src/engine/scripts/declarations/dialogs/dialogs.ts index 3d9bea2ee..09b380c1e 100644 --- a/src/engine/scripts/declarations/dialogs/dialogs.ts +++ b/src/engine/scripts/declarations/dialogs/dialogs.ts @@ -9,12 +9,12 @@ import { updateStalkerLogic } from "@/engine/core/objects/binders/creature/Stalk import { ISchemeMeetState } from "@/engine/core/schemes/meet"; import { SchemeMeet } from "@/engine/core/schemes/meet/SchemeMeet"; import { ISchemeWoundedState } from "@/engine/core/schemes/wounded"; -import { SchemeWounded } from "@/engine/core/schemes/wounded/SchemeWounded"; import { extern } from "@/engine/core/utils/binding"; import { createGameAutoSave } from "@/engine/core/utils/game"; import { LuaLogger } from "@/engine/core/utils/logging"; import { actorHasMedKit, + enableObjectWoundedHealing, getActorAvailableMedKit, getNpcSpeaker, getObjectCommunity, @@ -103,7 +103,7 @@ extern("dialogs.transfer_medkit", (firstSpeaker: ClientObject, secondSpeaker: Cl secondSpeaker.id() ); - SchemeWounded.unlockMedkit(secondSpeaker); + enableObjectWoundedHealing(secondSpeaker); if (secondSpeaker.relation(firstSpeaker) !== EClientObjectRelation.ENEMY) { secondSpeaker.set_relation(EClientObjectRelation.FRIEND, firstSpeaker);