Skip to content

Commit ce00cdd

Browse files
committed
AI combat behavior: Update code.
1 parent a4ade0d commit ce00cdd

6 files changed

Lines changed: 74 additions & 36 deletions

File tree

sfall/Modules/AI.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fo::GameObject* AI::CheckShootAndTeamCritterOnLineOfFire(fo::GameObject* object,
5656
object = CheckShootAndTeamCritterOnLineOfFire(obj, targetTile, team);
5757
}
5858
if (object) {
59-
DEV_PRINTF3("\n[AI] TeamCritterOnLineOfFire: %s (team: %d==%d)", fo::func::critter_name(object), object->critter.teamNum, team);
59+
fo::func::dev_printf("\n[AI] TeamCritterOnLineOfFire: %s (team: %d==%d)", fo::func::critter_name(object), object->critter.teamNum, team);
6060
}
6161
return object; // friendly critter, any object or null
6262
}
@@ -654,6 +654,12 @@ void AI::init() {
654654
dlogr(" Done", DL_INIT);
655655
}
656656

657+
// Allows the for all critters or party members with "whomever" to search for a new target each turn of the combat
658+
if (IniReader::GetConfigInt("CombatAI", "ReFindTargets", 0) > 0) {
659+
SafeWrite16(0x4290B3, 0xDFEB); // jmp 0x429094
660+
SafeWrite8(0x4290B5, CodeType::Nop);
661+
}
662+
657663
// Fix AI weapon switching when not having enough AP to make an attack
658664
// AI will try to change attack mode before deciding to switch weapon
659665
if (IniReader::GetConfigInt("CombatAI", "NPCSwitchingWeaponFix", 1)) {
@@ -679,7 +685,7 @@ void AI::init() {
679685

680686
/////////////////////// Combat behavior AI fixes ///////////////////////
681687
#ifndef NDEBUG
682-
if (IniReader::GetConfigInt("Debugging", "AIBugFixes", 1) == 0) return;
688+
if (IniReader::GetConfigInt("Debugging", "AIFixes", 1) == 0) return;
683689
#endif
684690

685691
// Fix for NPCs not fully reloading a weapon if it has more ammo capacity than a box of ammo

sfall/Modules/SubModules/AI.Behavior.cpp

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ static long __fastcall AIPickupWeaponFix(fo::GameObject* critter, long distance
8989
fo::GameObject* item = fo::func::inven_right_hand(critter);
9090
if (item && AIHelpers::IsGunOrThrowingWeapon(item)) return 1; // allow pickup
9191
}
92-
return (allowPickUp) ? 1 : 0; // false - next item
92+
DEV_PRINTF1("\nai_search_environ: ItemPickUpFix is allow: %d\n", allowPickUp);
93+
return 0 | allowPickUp; // false - next item
9394
}
9495

9596
static void __declspec(naked) ai_search_environ_hook() {
@@ -380,7 +381,7 @@ static fo::GameObject* AISearchBestWeaponInCorpses(fo::GameObject* source, fo::G
380381
if (!game::CombatAI::ai_can_use_weapon(source, itemGround, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY)) continue;
381382

382383
// проверяем наличее и количество имеющихся патронов
383-
if (!AIInventory::AICheckAmmo(itemGround, source) && Combat::check_item_ammo_cost(itemGround, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) <= 0) {
384+
if (itemGround->item.ammoPid != -1 && !AIInventory::AICheckAmmo(itemGround, source) && Combat::check_item_ammo_cost(itemGround, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) <= 0) {
384385
continue;
385386
}
386387

@@ -438,7 +439,7 @@ static fo::GameObject* AISearchBestWeaponOnGround(fo::GameObject* source, fo::Ga
438439

439440
if (game::CombatAI::ai_can_use_weapon(source, itemGround, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) &&
440441
// проверяем наличее и количество имеющихся патронов
441-
AIInventory::AICheckAmmo(itemGround, source) && Combat::check_item_ammo_cost(itemGround, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) > 0)
442+
itemGround->item.ammoPid == -1 || AIInventory::AICheckAmmo(itemGround, source) && Combat::check_item_ammo_cost(itemGround, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) > 0)
442443
{
443444
if (AIInventory::BestWeapon(source, item, itemGround, target) == itemGround) {
444445
DEV_PRINTF2("\n[AI] SearchBestWeaponOnGround: %d (%s)", itemGround->protoId, fo::func::critter_name(itemGround));
@@ -935,23 +936,24 @@ static void __declspec(naked) ai_try_attack_hack_after_attack() {
935936

936937
// Событие перед атакой, когда шанс поразить цель является минимальным установленному значению 'min_to_hit' в AI.txt
937938
static fo::GameObject* __fastcall AIBadToHit(fo::GameObject* source, fo::GameObject* target, fo::AttackType hitMode) {
938-
DEV_PRINTF2("\n[AI] AIBadToHit: %s attack bad to hit my target (%s)\n", fo::func::critter_name(source), fo::func::critter_name(target));
939+
DEV_PRINTF2("\n[AI] BadToHit: %s attack bad to hit my (%s) target.\n", fo::func::critter_name(source), fo::func::critter_name(target));
939940

940-
//TODO: попробовать сменить оружие?
941+
//TODO: попробовать сменить оружие
941942

942943
AICombat::AttackerSetHitMode(hitMode);
943944

944945
fo::GameObject* newTarget = AISearchTarget::AIDangerSource(source, 8); // попробовать найти другую цель со значеннием шанса выше 'min_to_hit'
945946
if (!newTarget || newTarget == target) {
946947
// новая цель не была найдена, проверяем боевой рейтинг цели
947-
if (fo::func::combatai_rating(target) > (fo::func::combatai_rating(source) * 2)) {
948+
fo::GameObject* lastTarget = fo::func::combatAIInfoGetLastTarget(target);
949+
if (lastTarget == source && fo::func::combatai_rating(target) > (fo::func::combatai_rating(source) * 2)) {
948950
source->critter.combatState |= fo::CombatStateFlag::ReTarget;
949951
return nullptr; // цель сильна - убегаем
950952
}
951-
DEV_PRINTF("\n[AI] AIBadToHit: No alternative target. Attack my target with bad to hit.\n");
953+
DEV_PRINTF("\n[AI] BadToHit: No alternative target. Attack my target with bad to hit.\n");
952954
return (fo::GameObject*)-1; // продолжаем атаковать цель несмотря на низкий шанс
953955
}
954-
DEV_PRINTF1("\n[AI] AIBadToHit: Found alternative (%s) target.\n", fo::func::critter_name(newTarget));
956+
DEV_PRINTF1("\n[AI] BadToHit: Found alternative (%s) target.\n", fo::func::critter_name(newTarget));
955957
return newTarget;
956958
}
957959

@@ -995,16 +997,18 @@ static void ClearWalkThru() {
995997
static fo::GameObject* AIClearMovePath(fo::GameObject* source, fo::GameObject* target) {
996998
fo::var::moveBlockObj = 0; // всегда содержит криттер или null
997999

1000+
__asm call fo::funcoffs::gmouse_bk_process_;
1001+
9981002
// check the path is clear
9991003
if (fo::func::make_path_func(source, source->tile, target->tile, 0, 0, AIHelpers::obj_ai_move_blocking_at_) > 0) {
1000-
DEV_PRINTF("\n[AI] AIClearMovePath: free.");
1004+
DEV_PRINTF("\n[AI] ClearMovePath: free.");
10011005
// TODO: найти причину по которой путь строится для obj_ai_move_blocking_at_ но для obj_blocking_at_ это блокировано
10021006
if (fo::func::make_path_func(source, source->tile, target->tile, 0, 0, (void*)fo::funcoffs::obj_blocking_at_) > 0) {
10031007
return target;
10041008
}
10051009
if (++recursiveDepth > 10) return nullptr;
10061010

1007-
DEV_PRINTF("\n[AI] AIClearMovePath: ClearWalkThru.");
1011+
DEV_PRINTF("\n[AI] ClearMovePath: ClearWalkThru.");
10081012
ClearWalkThru();
10091013
// BREAKPOINT;
10101014
return AIClearMovePath(source, target);
@@ -1023,7 +1027,7 @@ static fo::GameObject* AIClearMovePath(fo::GameObject* source, fo::GameObject* t
10231027
#endif
10241028
if (fo::var::moveBlockObj) {
10251029
fo::GameObject* blockCritter = fo::var::moveBlockObj;
1026-
DEV_PRINTF1("\n[AI] AIClearMovePath: Blocked: %s", fo::func::critter_name(blockCritter));
1030+
DEV_PRINTF2("\n[AI] ClearMovePath: Blocked: %s ID: %d", fo::func::critter_name(blockCritter), blockCritter->id);
10271031

10281032
if (!(blockCritter->flags & fo::ObjectFlag::WalkThru)) {
10291033
blockCritter->flags |= fo::ObjectFlag::WalkThru; // устанавливаем флаг для obj_ai_move_blocking_at_
@@ -1043,7 +1047,7 @@ static fo::GameObject* AIClearMovePath(fo::GameObject* source, fo::GameObject* t
10431047
char rotation[800];
10441048
len = fo::func::make_path_func(source, source->tile, blockCritter->tile, rotation, 0, (void*)fo::funcoffs::obj_blocking_at_);
10451049
if (len == 0) {
1046-
DEV_PRINTF("\n[AI] AIClearMovePath: don't make path from source to block critter.");
1050+
DEV_PRINTF("\n[AI] ClearMovePath: don't make path from source to block critter.");
10471051
return nullptr; // нельзя!!!
10481052
}
10491053
if (len > source->critter.getMoveAP()) return blockCritter;
@@ -1065,11 +1069,13 @@ static fo::GameObject* AIClearMovePath(fo::GameObject* source, fo::GameObject* t
10651069
// recursively calling functions until the path is freed or completely blocked
10661070
return AIClearMovePath(source, target);
10671071
}
1068-
DEV_PRINTF("\n[AI] AIClearMovePath: null.");
1072+
DEV_PRINTF("\n[AI] ClearMovePath: null.");
10691073
return nullptr;
10701074
}
10711075

1072-
static void __fastcall GetMoveObject(fo::GameObject* source, fo::GameObject* target, fo::GameObject* nearCritter) {
1076+
static void __fastcall GetMoveObject(fo::GameObject* source, fo::GameObject* target, fo::GameObject* nearCritter, long isMouse3d) {
1077+
if (isMouse3d) target->flags &= ~fo::ObjectFlag::Mouse_3d;
1078+
10731079
recursiveDepth = 0;
10741080
DEV_PRINTF2("\n[AI] TargetСritter: %s / NearСritter: %s", fo::func::critter_name(target), fo::func::critter_name(nearCritter));
10751081
fo::GameObject* blockObject = AIClearMovePath(source, target);
@@ -1082,13 +1088,14 @@ static void __fastcall GetMoveObject(fo::GameObject* source, fo::GameObject* tar
10821088
? blockObject
10831089
: nearCritter;
10841090

1085-
DEV_PRINTF1("\n[AI] MoveToСritter: %s\n", fo::func::critter_name(fo::var::moveBlockObj));
1091+
DEV_PRINTF2("\n[AI] MoveToСritter: %s ID:%d\n", fo::func::critter_name(fo::var::moveBlockObj), fo::var::moveBlockObj->id);
10861092

10871093
fo::func::register_begin(fo::RB_RESERVED);
10881094
}
10891095

10901096
static void __declspec(naked) ai_move_steps_closer_hack() {
10911097
__asm {
1098+
push [esp + 0x1C - 0x10 + 4]; // multiHexIsMouse3d
10921099
push edx; // block critter to move
10931100
mov ecx, esi; // source
10941101
mov edx, edi; // target
@@ -1124,7 +1131,7 @@ void AIBehavior::init(bool smartBehavior) {
11241131

11251132
// Реализация функции освобождения пути криттера блокирующего путь к цели
11261133
MakeCall(0x42A0D6, ai_move_steps_closer_hack, 5);
1127-
BlockCall(0x42A06A); // unused code
1134+
SafeWrite16(0x42A0BF, 0x9090);
11281135

11291136
//////////////////// Combat AI improved behavior //////////////////////////
11301137

@@ -1133,7 +1140,7 @@ void AIBehavior::init(bool smartBehavior) {
11331140

11341141
if (smartBehavior) {
11351142
// Before starting his turn npc will always check if it has better weapons in inventory, than there is a current weapon
1136-
LookupOnGround = (IniReader::GetConfigInt("CombatAI", "TakeBetterWeapons", 1) > 1); // always check the items available on the ground
1143+
LookupOnGround = (IniReader::GetConfigInt("CombatAI", "TakeBetterWeapons", 0) > 0); // always check the items available on the ground
11371144

11381145
HookCall(0x42A6BF, ai_called_shot_hook);
11391146

sfall/Modules/SubModules/AI.Combat.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,12 @@ static void CombatAI_Extended(fo::GameObject* source, fo::GameObject* target) {
588588
{
589589
fo::func::debug_printf("\n[AI] %s FLEEING: I'm Hurt!", attacker.name);
590590

591-
if (!target) target = AIHelpers::GetNearestEnemyCritter(source); // получить самого ближнего криттера не из своей команды
592-
fo::func::ai_run_away(source, target); // убегаем от цели или от игрока если цель не была назначена
591+
if (!target) {
592+
target = AIHelpers::GetNearestEnemyCritter(source); // получить самого ближнего криттера не из своей команды
593+
}
594+
if (fo::func::obj_dist(source, fo::var::obj_dude) <= attacker.cap->max_dist * 2) {
595+
fo::func::ai_run_away(source, target); // убегаем от цели или от игрока если цель не была назначена
596+
}
593597
return;
594598
}
595599

@@ -618,8 +622,11 @@ static void CombatAI_Extended(fo::GameObject* source, fo::GameObject* target) {
618622
fo::func::ai_move_steps_closer(source, fo::var::obj_dude, source->critter.getAP(), 0); // !!! здесь диспозиция stay не позволяет бежать к игроку !!!
619623
} else {
620624
if (!lastTarget) lastTarget = AIHelpers::GetNearestEnemyCritter(source); // получить самого ближнего криттера не из своей команды
625+
626+
if (fo::func::obj_dist(source, lastTarget) <= 1) fo::func::ai_try_attack(source, lastTarget);
627+
621628
fo::func::ai_run_away(source, lastTarget); // убегаем от потенцеально опасного криттера
622-
source->critter.combatState ^= (fo::CombatStateFlag::EnemyOutOfRange | fo::CombatStateFlag::InFlee); // снять флаги после ai_run_away
629+
source->critter.combatState &= ~(fo::CombatStateFlag::EnemyOutOfRange | fo::CombatStateFlag::InFlee); // снять флаги после ai_run_away
623630
}
624631
return;
625632
}

sfall/Modules/SubModules/AI.FuncHelpers.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,16 @@ fo::GameObject* AIHelpers::AICheckWeaponSkill(fo::GameObject* source, fo::GameOb
127127
if (hSkill == sSkill) return sWeapon;
128128

129129
int hLevel = fo::func::skill_level(source, hSkill);
130-
int sLevel = fo::func::skill_level(source, sSkill) + 10;
130+
int sLevel = fo::func::skill_level(source, sSkill);
131131

132-
return (hLevel > sLevel) ? hWeapon : sWeapon;
132+
fo::func::dev_printf("\n[AI] Check weapon skill: %d vs %d", hLevel, sLevel);
133+
134+
// если разница в навыках не большая то отдаем предпочитение стрелковому оружию
135+
if (std::abs(hLevel - sLevel) <= 10) {
136+
if (sSkill >= fo::Skill::SKILL_SMALL_GUNS && sSkill <= fo::Skill::SKILL_ENERGY_WEAPONS) return sWeapon;
137+
if (hSkill >= fo::Skill::SKILL_SMALL_GUNS && hSkill <= fo::Skill::SKILL_ENERGY_WEAPONS) return hWeapon;
138+
}
139+
return (hLevel > (sLevel + 10)) ? hWeapon : sWeapon;
133140
}
134141

135142
long AIHelpers::GetCurrenShootAPCost(fo::GameObject* source, long modeHit, long isCalled) {

sfall/Modules/SubModules/AI.Inventory.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fo::GameObject* AIInventory::SearchInventoryItemType(fo::GameObject* source, lon
5252
case fo::ItemType::item_type_weapon:
5353
if (!game::CombatAI::ai_can_use_weapon(source, item, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY)) continue;
5454
// проверяем наличее и количество имеющихся патронов
55-
if (!AICheckAmmo(item, source) && Combat::check_item_ammo_cost(item, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) <= 0) continue;
55+
if (item->item.ammoPid != -1 && !AICheckAmmo(item, source) && Combat::check_item_ammo_cost(item, fo::AttackType::ATKTYPE_RWEAPON_PRIMARY) <= 0) continue;
5656
break;
5757
case fo::ItemType::item_type_ammo:
5858
if (!fo::func::item_w_can_reload(weapon, item)) continue;
@@ -459,7 +459,9 @@ long AIInventory::ai_search_environ_corpse(fo::GameObject* source, long itemType
459459
}
460460

461461
static long __fastcall AISearchCorpseWeapon(fo::GameObject* target, fo::GameObject* source, fo::GameObject* &weapon, long &hitMode, fo::GameObject* itemEnv) {
462-
if (itemEnv) DEV_PRINTF1("\n[AI] ai_search_environ: weapon: %s", fo::func::critter_name(itemEnv));
462+
if (itemEnv) {
463+
DEV_PRINTF1("\n[AI] ai_search_environ: weapon: %s", fo::func::critter_name(itemEnv));
464+
}
463465

464466
fo::GameObject* outItem = itemEnv;
465467
if (AIInventory::ai_search_environ_corpse(source, fo::ItemType::item_type_weapon, outItem, weapon) < 0 || outItem == itemEnv) return 1; // default

sfall/Modules/SubModules/AI.SearchTarget.cpp

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace sfall
2626
{
2727

2828
static bool npcAttackWhoFix = false;
29-
static bool reFindNewTargets = false;
29+
static long reFindNewTargets = 0;
3030

3131
fo::GameObject* rememberTarget = nullptr;
3232

@@ -328,6 +328,7 @@ fo::GameObject* AISearchTarget::AIDangerSource(fo::GameObject* source, long type
328328
if (attack_who == fo::AIpref::attack_who::whomever || attack_who == fo::AIpref::attack_who::no_attack_mode) {
329329
// NPCs (or PM with whomever) always attack the target that is set to who_hit_me
330330
// [add ext] Additional function allows to analyze the current target (TryToFindTargets option)
331+
if (reFindNewTargets > 1) goto ReFindNewTargets;
331332
if (reFindNewTargets && AICheckCurrentAttackerTarget(source, sourceTarget)) {
332333
if (!(type & 8)) type |= 4;
333334
goto ReFindNewTargets;
@@ -431,27 +432,35 @@ static void __declspec(naked) ai_danger_source_replacement() {
431432
}
432433
}
433434

435+
static void __declspec(naked) ai_danger_source_hack() {
436+
__asm {
437+
mov eax, esi;
438+
call fo::funcoffs::ai_get_attack_who_value_;
439+
mov dword ptr [esp + 0x34 - 0x1C + 4], eax; // attack_who
440+
retn;
441+
}
442+
}
443+
434444
void AISearchTarget::init(bool smartBehavior) {
445+
// Enables the ability to use the AttackWho value from the AI-packet for the NPC
446+
npcAttackWhoFix = (IniReader::GetConfigInt("CombatAI", "NPCAttackWhoFix", 0) > 0);
435447

436448
if (smartBehavior) {
437449
MakeJump(fo::funcoffs::ai_danger_source_ + 1, ai_danger_source_replacement); // 0x428F4C
438450
SafeWrite8(0x428F4C, 0x52); // push edx
439451

440-
reFindNewTargets = (IniReader::GetConfigInt("CombatAI", "TryToFindTargets", 1) > 0);
441-
442-
///for TryToFindTargets=2 w/o logic
443-
///SafeWrite16(0x4290B3, 0xDFEB); // jmp 0x429094
444-
///SafeWrite8(0x4290B5, 0x90);
452+
if (IniReader::GetConfigInt("CombatAI", "TryToFindTargets", 1) > 0) {
453+
reFindNewTargets = 1;
454+
} else if (IniReader::GetConfigInt("CombatAI", "ReFindTargets", 0)) {
455+
reFindNewTargets = 2; // w/o logic
456+
}
445457
} else {
446458
/// Changes the behavior of the AI so that the AI moves to its target to perform an attack/shot when the range of its weapon is less than
447459
/// the distance to the target or the AI will choose the nearest target if any other targets are available
448460

449461
HookCall(0x42B240, combat_ai_hook_revert_target); // also need for TryToFindTargets option
450-
}
451462

452-
// Enables the ability to use the AttackWho value from the AI-packet for the NPC
453-
if (IniReader::GetConfigInt("CombatAI", "NPCAttackWhoFix", 0)) {
454-
npcAttackWhoFix = true;
463+
if (npcAttackWhoFix) MakeCall(0x428F70, ai_danger_source_hack, 3);
455464
}
456465
}
457466

0 commit comments

Comments
 (0)