diff --git a/docs/ecco_changelog.txt b/docs/ecco_changelog.txt index 29c74fa..312124f 100644 --- a/docs/ecco_changelog.txt +++ b/docs/ecco_changelog.txt @@ -34,6 +34,7 @@ v0.9.7: - added more MFC to stores for car refueling, as well as more healing kits and certain types of ammo - added bonus damage for fire-based traps with Pyromaniac perk - fixed incorrect car charging amount when using Cells on trunk +- tweaked AI weapon selection logic to avoid some illogical choices v0.9.6: diff --git a/root/mods/ecco/combat.ini b/root/mods/ecco/combat.ini index 3d25232..f41a3f6 100644 --- a/root/mods/ecco/combat.ini +++ b/root/mods/ecco/combat.ini @@ -102,13 +102,13 @@ weapon_drop_dist=2 weapon_drop_dir=0 ; % of times critter's weapon will be destroyed on death -destroy_weapon_percent=30 +destroy_weapon_percent=35 ; pid of junk item to spawn in place of destroyed weapon destroy_weapon_spawn_junk_pid=98 ; if weapon is destroyed, it's weight will be multiplied by this value and used as probability to spawn Junk in it's place -destroy_weapon_spawn_junk_chance_mult=4 +destroy_weapon_spawn_junk_chance_mult=5 ; comma-separated list of weapon PIDs that may be destroyed by "destroy_weapon_chance" destroy_weapon_list=5,6,8,9,10,11,12,13,15,16,18,22,23,24,28,94,115,116,118,122,143,160,233,235,242,268,283,287,296,299,300,313,332,350,351,352,353,354,355,385,387,388,389,391,392,394,395,396,397,398,399,400,401,402,403,404,405,406,407,500,522,617,629,630,634,638,639,640,643,644,646,647,648 diff --git a/scripts_src/_pbs_headers/ecco.h b/scripts_src/_pbs_headers/ecco.h index d06db20..37e414e 100644 --- a/scripts_src/_pbs_headers/ecco.h +++ b/scripts_src/_pbs_headers/ecco.h @@ -10,14 +10,13 @@ #include "../sfall/command_lite.h" #include "../sfall/define_extra.h" +#include "ecco_define.h" #include "ecco_ids.h" #include "ecco_ini.h" #include "ecco_log.h" #include "ecco_msg.h" -#define dude_skill(x) (has_skill(dude_obj, x)) - #define last_deathanim_critter (get_sfall_global_int(SGVAR_LAST_DEATHANIM_CRITTER)) #define last_deathanim (get_sfall_global_int(SGVAR_LAST_DEATHANIM)) @@ -39,15 +38,8 @@ #define get_proto_dmg_min(pid) (get_proto_data(pid, PROTO_WP_DMG_MIN)) #define get_proto_dmg_max(pid) (get_proto_data(pid, PROTO_WP_DMG_MAX)) -#define is_unarmed_weapon_pid(pid) (weapon_attack_mode1(pid) == ATTACK_MODE_PUNCH) -#define is_hidden_item(pid) ((get_proto_data(pid, PROTO_FLAG_EXT) bwand HIDDEN_ITEM) != 0) - -#define critter_dt_by_dmg_type(crit, type) (get_critter_stat(crit, STAT_dmg_thresh + type)) -#define critter_dr_by_dmg_type(crit, type) (get_critter_stat(crit, STAT_dmg_resist + type)) -#define critter_max_hp(crit) (get_critter_stat(crit, STAT_max_hp)) #define exp_for_kill_critter_pid(pid) (get_proto_data(pid, PROTO_CR_KILL_EXP)) #define critter_flags_by_pid(pid) (get_proto_data(pid, PROTO_CR_FLAGS)) -#define critter_proto_has_flag(pid, flg) ((get_proto_data(pid, PROTO_CR_FLAGS) bwand flg) != 0) #define can_steal_from_critter_pid(pid) (not critter_proto_has_flag(pid, CFLG_NOSTEAL)) #define critter_facing_dir(crit) (has_trait(TRAIT_OBJECT,crit,OBJECT_CUR_ROT)) @@ -137,8 +129,6 @@ #define is_humanoid(crit) (proto_data(obj_pid(crit), cr_body_type) == CR_BODY_BIPED) #define is_weapon_pid(pid) (proto_data(pid, it_type) == item_type_weapon) -#define obj_team(obj) (has_trait(TRAIT_OBJECT, obj, OBJECT_TEAM_NUM)) - #define actual_ammo_count(crit, obj) ((obj_is_carrying_obj(crit, obj) - 1)*get_proto_data(obj_pid(obj), PROTO_AM_PACK_SIZE) + get_weapon_ammo_count(obj)) #define is_using_ammo_pid(crit, pid) (get_weapon_ammo_pid(critter_inven_obj(crit, 1)) == pid or get_weapon_ammo_pid(critter_inven_obj(crit, 2)) == pid) diff --git a/scripts_src/_pbs_headers/ecco_define.h b/scripts_src/_pbs_headers/ecco_define.h new file mode 100644 index 0000000..3660f8d --- /dev/null +++ b/scripts_src/_pbs_headers/ecco_define.h @@ -0,0 +1,15 @@ +#ifndef ECCO_DEFINE_H +#define ECCO_DEFINE_H + +#define obj_team(obj) (has_trait(TRAIT_OBJECT, obj, OBJECT_TEAM_NUM)) + +#define critter_dt_by_dmg_type(crit, type) (get_critter_stat(crit, STAT_dmg_thresh + type)) +#define critter_dr_by_dmg_type(crit, type) (get_critter_stat(crit, STAT_dmg_resist + type)) +#define critter_max_hp(crit) (get_critter_stat(crit, STAT_max_hp)) +#define critter_proto_has_flag(pid, flg) ((get_proto_data(pid, PROTO_CR_FLAGS) bwand flg) != 0) + +#define proto_has_ext_flag(pid, flg) ((get_proto_data(pid, PROTO_FLAG_EXT) bwand flg) != 0) +#define item_proto_is_hidden(pid) proto_has_ext_flag(pid, HIDDEN_ITEM) + +#endif + \ No newline at end of file diff --git a/scripts_src/_pbs_main/gl_pc_armor_stats_fix.ssl b/scripts_src/_pbs_main/gl_party_armor_stats_fix.ssl similarity index 62% rename from scripts_src/_pbs_main/gl_pc_armor_stats_fix.ssl rename to scripts_src/_pbs_main/gl_party_armor_stats_fix.ssl index 09b9bb2..5a50c91 100644 --- a/scripts_src/_pbs_main/gl_pc_armor_stats_fix.ssl +++ b/scripts_src/_pbs_main/gl_party_armor_stats_fix.ssl @@ -11,6 +11,9 @@ #include "..\sfall\command_lite.h" #include "..\sfall\lib.arrays.h" +#define SCRIPT_REALNAME "party_armor_stats_fix" +#include "..\_pbs_headers\ecco_log.h" + #define MAX_PID (1000) #define STATS_ARR_NAME(pid) ("armor_stats_"+pid) #define STATS_AC (0) @@ -18,9 +21,6 @@ #define STATS_DT (1 + 7) #define STATS_SIZE (1 + 7*2) -#define debug_log(msg) debug_msg("pc_armor_stats_fix: "+msg) -#define debug_error(msg) debug_msg("! ERROR ! pc_armor_stats_fix: "+msg) - procedure save_armor_stats(variable armorPid) begin variable stats, i; @@ -46,40 +46,45 @@ procedure save_all_armor_stats begin debug_log("Saved armor stats for "+pids); end -procedure apply_armor_stats_difference begin - variable stats, armorPid, diff, i; - armorPid := obj_pid(dude_armor) if dude_armor else 0; - if (not armorPid) then return; +#define armor_pid_str(pid) (proto_data(pid, it_name) + "[" + pid + "]") +procedure apply_armor_stats_difference(variable critter) begin + variable stats, armor, armorPid, diff, i; + armor := get_armor(critter); + if (not armor) then return; + + armorPid := obj_pid(armor); stats := load_array(STATS_ARR_NAME(armorPid)); if (not stats) then begin - debug_error("Dude has armor but no saved armor stats! (possibly loaded an old save)"); + debug_err(obj_name(critter)+" has armor but no saved armor stats! (possibly loaded an old save)"); return; end diff := get_proto_data(armorPid, PROTO_AR_AC) - stats[STATS_AC]; if (diff != 0) then begin - set_critter_extra_stat(dude_obj, STAT_ac, get_critter_extra_stat(dude_obj, STAT_ac) + diff); - debug_log("Armor "+armorPid+" AC changed by "+diff+", applying difference."); + set_critter_extra_stat(critter, STAT_ac, get_critter_extra_stat(critter, STAT_ac) + diff); + debug_log(armor_pid_str(armorPid)+" AC changed by "+diff+", applying difference for "+obj_name(critter)); intface_redraw; end for (i := 0; i < 7; i++) begin diff := get_proto_data(armorPid, PROTO_AR_DR_NORMAL + i*4) - stats[STATS_DR + i]; if (diff != 0) then begin - set_critter_extra_stat(dude_obj, STAT_dmg_resist + i, get_critter_extra_stat(dude_obj, STAT_dmg_resist + i) + diff); - debug_log("Armor "+armorPid+" DR ("+i+") changed by "+diff+", applying difference."); + set_critter_extra_stat(critter, STAT_dmg_resist + i, get_critter_extra_stat(critter, STAT_dmg_resist + i) + diff); + debug_log(armor_pid_str(armorPid)+" DR ("+i+") changed by "+diff+", applying difference for "+obj_name(critter)); end diff := get_proto_data(armorPid, PROTO_AR_DT_NORMAL + i*4) - stats[STATS_DT + i]; if (diff != 0) then begin - set_critter_extra_stat(dude_obj, STAT_dmg_thresh + i, get_critter_extra_stat(dude_obj, STAT_dmg_thresh + i) + diff); - debug_log("Armor "+armorPid+" DT ("+i+") changed by "+diff+", applying difference."); + set_critter_extra_stat(critter, STAT_dmg_thresh + i, get_critter_extra_stat(critter, STAT_dmg_thresh + i) + diff); + debug_log(armor_pid_str(armorPid)+" DT ("+i+") changed by "+diff+", applying difference for "+obj_name(critter)); end end end procedure start begin if (game_loaded) then begin - call apply_armor_stats_difference; + foreach (variable crit in party_member_list_critters) begin + call apply_armor_stats_difference(crit); + end call save_all_armor_stats; end end diff --git a/scripts_src/_pbs_main/gl_pbs_ai.ssl b/scripts_src/_pbs_main/gl_pbs_ai.ssl index a8e975f..ee9d92f 100644 --- a/scripts_src/_pbs_main/gl_pbs_ai.ssl +++ b/scripts_src/_pbs_main/gl_pbs_ai.ssl @@ -1,6 +1,7 @@ /* AI improvements: - Configure certain parameters of called shot conditions, so it happens more consistently and frequently + - Improved best weapon selection logic (better expected damage estimation, range/distance factors, etc) */ @@ -10,8 +11,10 @@ #include "../sfall/command_lite.h" #include "../sfall/lib.arrays.h" +#include "../sfall/lib.obj.h" #include "../_pbs_headers/engine_funcs.h" +#include "../_pbs_headers/ecco_define.h" #include "../_pbs_headers/ecco_ini.h" #include "../_pbs_headers/ecco_log.h" @@ -188,6 +191,94 @@ procedure combatturn_handler begin end end + +procedure weapon_rank(variable critter, variable weapon, variable target) begin + // TODO: vanilla uses combat_safety_invalidate_weapon to account for potential friendly fire, this code does not + variable + pid := obj_pid(weapon), + avgDmg := (get_proto_data(pid, PROTO_WP_DMG_MIN) + get_proto_data(pid, PROTO_WP_DMG_MAX)) / 2, + burstSize := get_proto_data(pid, PROTO_WP_BURST) if (weapon_attack_mode1(pid) == ATTACK_MODE_BURST or weapon_attack_mode2(pid) == ATTACK_MODE_BURST) else 1, + avgBurstSize := (0.66 * burstSize) if burstSize > 1 else 1, + ammoPid := get_weapon_ammo_pid(weapon), + ammoDmgMult := (1.0 * get_proto_data(ammoPid, PROTO_AM_DMG_MULT) / get_proto_data(ammoPid, PROTO_AM_DMG_DIV) if (ammoPid > 0) else 1), + dmgType := get_proto_data(pid, PROTO_WP_DMG_TYPE), + perk := get_proto_data(pid, PROTO_WP_PERK), + targetDT := get_critter_stat(target, STAT_dmg_thresh + dmgType) if target != 0 else 0, + targetDR := get_critter_stat(target, STAT_dmg_resist + dmgType) if target != 0 else 0, + effectiveDT := (targetDT * 20 / 100) if perk == PERK_weapon_penetrate else targetDT, + expectedDmgPerAttack := (ammoDmgMult * avgDmg * avgBurstSize - effectiveDT) * (100 - targetDR) / 100, + attackAP := get_proto_data(pid, PROTO_WP_APCOST_2) if (weapon_attack_mode2(pid) == ATTACK_MODE_BURST) else get_proto_data(pid, PROTO_WP_APCOST_1), + expectedDmgPerAP := expectedDmgPerAttack / attackAP, + range := get_proto_data(pid, PROTO_WP_RANGE_1), + dmgFactor := expectedDmgPerAP * 10, + rangeFactor; + + if (target != 0) then begin + variable dist := tile_distance_objs(critter, target); + // Account for scope hit chance penalty + if (perk == PERK_weapon_scope_range and dist < 6) then + rangeFactor := -10; + else // Reduce range factor depending on actual distance to target + rangeFactor := range * 2 * math_min(dist, 50) / 50; + end else + rangeFactor := range; + + // TODO: get calculated damage from gl_pbs_damage_mod via exported proc? + return round(dmgFactor + rangeFactor); +end + +procedure weapon_type(variable weapon) begin + variable attackMode, type := WEAPON_TYPE_UNARMED; + + if weapon then begin + attackMode := weapon_attack_mode(obj_pid(weapon), ATKTYPE_RWEP1); + if (attackMode >= ATTACK_MODE_SINGLE) then + type := WEAPON_TYPE_RANGED; + else if (attackMode == ATTACK_MODE_THROW) then + type := WEAPON_TYPE_THROWN; + else if (attackMode >= ATTACK_MODE_SWING) then + type := WEAPON_TYPE_MELEE; + else if (attackMode == ATTACK_MODE_NONE) then + type := WEAPON_TYPE_NONE; + end + + return type; +end + +/* +HOOK_BESTWEAPON +Runs when the AI decides which weapon is the best while searching the inventory for a weapon to equip in combat. This also runs when the player presses the "Use Best Weapon" button on the party member control panel. + +Critter arg0 - the critter searching for a weapon +Item arg1 - the best weapon chosen from two items +Item arg2 - the first choice of weapon +Item arg3 - the second choice of weapon +Critter arg4 - the target of the critter (can be 0) + +Item ret0 - overrides the chosen best weapon +*/ +procedure bestweapon_handler begin + variable + critter := get_sfall_arg, + weaponChosen := get_sfall_arg, + weapon1 := get_sfall_arg, + weapon2 := get_sfall_arg, + target := get_sfall_arg; + + //display_msg(string_format("bestweapon for %s: %s vs %s -> %s (against %s)", obj_name_safe(critter), obj_name_safe(weapon1), obj_name_safe(weapon2), obj_name_safe(weaponChosen), obj_name_safe(target))); + + if (weapon1 == 0 or weapon2 == 0 + or weapon_type(weapon1) != weapon_type(weapon2) + or item_proto_is_hidden(obj_pid(weapon1)) or item_proto_is_hidden(obj_pid(weapon2))) then return; + + variable + weapon1rank := weapon_rank(critter, weapon1, target), + weapon2rank := weapon_rank(critter, weapon2, target); + + debug_log_fmt("%s's bestweapon: %s (%d) vs %s (%d)", obj_name_safe(critter), obj_name_safe(weapon1), weapon1rank, obj_name_safe(weapon2), weapon2rank); + set_sfall_return(weapon1 if weapon1rank > weapon2rank else weapon2); +end + #define INI_FILE INI_COMBAT #define INI_SECTION "AI" @@ -209,10 +300,11 @@ end procedure start begin if not game_loaded then return; - if (get_ini_value_def(INI_FILE, INI_SECTION, "called_tweaks", 0) == 0) then return; - - call load_called_shot_settings; + if (get_ini_value_def(INI_FILE, INI_SECTION, "called_tweaks", 0)) then begin + call load_called_shot_settings; + register_hook_proc(HOOK_TOHIT, tohit_handler); + register_hook_proc(HOOK_COMBATTURN, combatturn_handler); + end - register_hook_proc(HOOK_TOHIT, tohit_handler); - register_hook_proc(HOOK_COMBATTURN, combatturn_handler); + register_hook_proc(HOOK_BESTWEAPON, bestweapon_handler); end diff --git a/scripts_src/_pbs_main/gl_pbs_critter_loot.ssl b/scripts_src/_pbs_main/gl_pbs_critter_loot.ssl index 4f15ffc..f1a0317 100644 --- a/scripts_src/_pbs_main/gl_pbs_critter_loot.ssl +++ b/scripts_src/_pbs_main/gl_pbs_critter_loot.ssl @@ -150,6 +150,8 @@ procedure try_destroy_weapon(variable critter, variable weapon) begin return removed; end +#define is_unarmed_weapon_pid(pid) (weapon_attack_mode1(pid) == ATTACK_MODE_PUNCH) + procedure drop_weapons(variable critter) begin variable dist, @@ -165,7 +167,7 @@ procedure drop_weapons(variable critter) begin weapon := critter_inven_obj(critter, i); if (weapon and obj_item_subtype(weapon) == item_type_weapon and not is_unarmed_weapon_pid(obj_pid(weapon)) - and not is_hidden_item(obj_pid(weapon))) then + and not item_proto_is_hidden(obj_pid(weapon))) then break; else weapon := 0; @@ -201,7 +203,7 @@ procedure reduce_loot(variable critter) begin list := inven_as_array(critter); removeStats := ""; foreach item in list begin - if (item == 0 or is_hidden_item(obj_pid(item))) then + if (item == 0 or item_proto_is_hidden(obj_pid(item))) then continue; if (obj_item_subtype(item) == item_type_ammo) then begin diff --git a/scripts_src/_pbs_main/gl_pbs_weapons.ssl b/scripts_src/_pbs_main/gl_pbs_weapons.ssl index 999848a..904d3ee 100644 --- a/scripts_src/_pbs_main/gl_pbs_weapons.ssl +++ b/scripts_src/_pbs_main/gl_pbs_weapons.ssl @@ -33,7 +33,6 @@ procedure ammocost_handler; procedure combatdamage_handler; procedure deathanim2_handler; procedure itemdamage_handler; -procedure bestweapon_handler; procedure load_settings begin variable pid, pidList, fid; @@ -86,7 +85,6 @@ procedure start begin register_hook_proc(HOOK_COMBATDAMAGE, combatdamage_handler); register_hook_proc(HOOK_DEATHANIM2, deathanim2_handler); register_hook_proc(HOOK_ITEMDAMAGE, itemdamage_handler); - register_hook_proc(HOOK_BESTWEAPON, bestweapon_handler); end end @@ -296,75 +294,3 @@ procedure itemdamage_handler begin end end end - - -procedure weapon_type(variable weapon) begin - variable attackMode, type := WEAPON_TYPE_UNARMED; - - if weapon then begin - attackMode := weapon_attack_mode(obj_pid(weapon), ATKTYPE_RWEP1); - if (attackMode >= ATTACK_MODE_SINGLE) then - type := WEAPON_TYPE_RANGED; - else if (attackMode == ATTACK_MODE_THROW) then - type := WEAPON_TYPE_THROWN; - else if (attackMode >= ATTACK_MODE_SWING) then - type := WEAPON_TYPE_MELEE; - else if (attackMode == ATTACK_MODE_NONE) then - type := WEAPON_TYPE_NONE; - end - - return type; -end - -procedure weapon_rank(variable weapon, variable target) begin - variable - pid := obj_pid(weapon), - avgDmg := (get_proto_data(pid, PROTO_WP_DMG_MIN) + get_proto_data(pid, PROTO_WP_DMG_MAX)) / 2, - burstSize := get_proto_data(pid, PROTO_WP_BURST) if (weapon_attack_mode1(pid) == ATTACK_MODE_BURST or weapon_attack_mode2(pid) == ATTACK_MODE_BURST) else 1, - avgBurstSize := (0.66 * burstSize) if burstSize > 1 else 1, - ammoPid := get_weapon_ammo_pid(weapon), - ammoDmgMult := (1.0 * get_proto_data(ammoPid, PROTO_AM_DMG_MULT) / get_proto_data(ammoPid, PROTO_AM_DMG_DIV) if (ammoPid > 0) else 1), - dmgType := get_proto_data(pid, PROTO_WP_DMG_TYPE), - targetDT := get_critter_stat(target, STAT_dmg_thresh + dmgType) if target != 0 else 0, - targetDR := get_critter_stat(target, STAT_dmg_resist + dmgType) if target != 0 else 0, - attackAP := get_proto_data(pid, PROTO_WP_APCOST_2) if (weapon_attack_mode2(pid) == ATTACK_MODE_BURST) else get_proto_data(pid, PROTO_WP_APCOST_1), - expectedDmgPerAttack := (ammoDmgMult * avgDmg * avgBurstSize - targetDT) * (100 - targetDR) / 100, - expectedDmgPerAP := expectedDmgPerAttack / attackAP, - range := get_proto_data(pid, PROTO_WP_RANGE_1); - - // todo account for actual damage mod settings - return round(expectedDmgPerAP * 10 + range); -end - - -/* -HOOK_BESTWEAPON -Runs when the AI decides which weapon is the best while searching the inventory for a weapon to equip in combat. This also runs when the player presses the "Use Best Weapon" button on the party member control panel. - -Critter arg0 - the critter searching for a weapon -Item arg1 - the best weapon chosen from two items -Item arg2 - the first choice of weapon -Item arg3 - the second choice of weapon -Critter arg4 - the target of the critter (can be 0) - -Item ret0 - overrides the chosen best weapon -*/ -procedure bestweapon_handler begin - variable - critter := get_sfall_arg, - weaponChosen := get_sfall_arg, - weapon1 := get_sfall_arg, - weapon2 := get_sfall_arg, - target := get_sfall_arg; - - //display_msg(string_format("bestweapon for %s: %s vs %s -> %s (against %s)", obj_name_safe(critter), obj_name_safe(weapon1), obj_name_safe(weapon2), obj_name_safe(weaponChosen), obj_name_safe(target))); - - if (weapon1 == 0 or weapon2 == 0 or weapon_type(weapon1) != weapon_type(weapon2)) then return; - - variable - weapon1rank := weapon_rank(weapon1, target), - weapon2rank := weapon_rank(weapon2, target); - - debug_log_fmt("%s's bestweapon: %s (%d) vs %s (%d)", obj_name_safe(critter), obj_name_safe(weapon1), weapon1rank, obj_name_safe(weapon2), weapon2rank); - set_sfall_return(weapon1 if weapon1rank > weapon2rank else weapon2); -end \ No newline at end of file