From 15564dc0aba9ce6a71bf81f8f4722edcb5511fd9 Mon Sep 17 00:00:00 2001 From: NovaRain Date: Sun, 24 Jan 2021 11:45:05 +0800 Subject: [PATCH] Added an option about max HP calculation to the stats ini file Added an update to the HP stats when the map is loaded first time. Minor code edits. --- artifacts/config_files/Stats.ini | 4 ++ sfall/FalloutEngine/Functions_def.h | 1 + sfall/Modules/AI.cpp | 2 +- sfall/Modules/BugFixes.cpp | 2 +- sfall/Modules/DebugEditor.cpp | 2 +- sfall/Modules/Objects.cpp | 12 ++-- sfall/Modules/Scripting/Handlers/Objects.cpp | 8 +-- sfall/Modules/Scripting/Handlers/Perks.cpp | 10 +-- sfall/Modules/Scripting/Handlers/Stats.cpp | 20 +++--- sfall/Modules/Stats.cpp | 68 +++++++++++++++----- sfall/Modules/Stats.h | 2 + 11 files changed, 90 insertions(+), 41 deletions(-) diff --git a/artifacts/config_files/Stats.ini b/artifacts/config_files/Stats.ini index 82b86166..447764a6 100644 --- a/artifacts/config_files/Stats.ini +++ b/artifacts/config_files/Stats.ini @@ -6,6 +6,10 @@ ;multi[x] - where x is from 0 to 6, the multiplier of st/pe/en/etc. ;shift[x] - shifts the base stat before the multiplication +[Main] +;Set to 1 to take the bonus values of SPECIAL stats into account when calculating maximum hit points +;Set to 0 for the original behavior (bonus stats do not affect maximum hit points) +HPDependOnBonusStats=0 ;max hp [7] diff --git a/sfall/FalloutEngine/Functions_def.h b/sfall/FalloutEngine/Functions_def.h index 381760a9..f269fec5 100644 --- a/sfall/FalloutEngine/Functions_def.h +++ b/sfall/FalloutEngine/Functions_def.h @@ -254,6 +254,7 @@ WRAP_WATCOM_FUNC2(long, skill_dec_point_force, GameObject*, critter, long, skill WRAP_WATCOM_FUNC2(long, skill_inc_point_force, GameObject*, critter, long, skill) WRAP_WATCOM_FUNC1(long, skill_is_tagged, long, skill) WRAP_WATCOM_FUNC2(long, skill_level, GameObject*, critter, long, statID) +WRAP_WATCOM_FUNC2(long, stat_get_base, GameObject*, critter, long, statID) WRAP_WATCOM_FUNC2(long, stat_get_base_direct, GameObject*, critter, long, statID) WRAP_WATCOM_FUNC2(long, stat_get_bonus, GameObject*, critter, long, statID) WRAP_WATCOM_FUNC3(long, stat_set_bonus, GameObject*, critter, long, statID, long, amount) diff --git a/sfall/Modules/AI.cpp b/sfall/Modules/AI.cpp index dee6be9f..ff2ee259 100644 --- a/sfall/Modules/AI.cpp +++ b/sfall/Modules/AI.cpp @@ -36,7 +36,7 @@ static std::unordered_map sources; // Returns the friendly critter or any blocking object in the line of fire fo::GameObject* AI::CheckShootAndFriendlyInLineOfFire(fo::GameObject* object, long targetTile, long team) { - if (object && object->Type() == ObjType::OBJ_TYPE_CRITTER && object->critter.teamNum != team) { // is not friendly fire + if (object && object->IsCritter() && object->critter.teamNum != team) { // is not friendly fire long objTile = object->tile; if (objTile == targetTile) return nullptr; diff --git a/sfall/Modules/BugFixes.cpp b/sfall/Modules/BugFixes.cpp index 4ce9ee73..7d664697 100644 --- a/sfall/Modules/BugFixes.cpp +++ b/sfall/Modules/BugFixes.cpp @@ -2075,7 +2075,7 @@ static void __stdcall combat_attack_gcsd() { if (fo::var::main_ctd.targetDamage > fo::var::gcsd->maxDamage) { fo::var::main_ctd.targetDamage = fo::var::gcsd->maxDamage; } - if (damage > fo::var::main_ctd.targetDamage && fo::var::main_ctd.target->Type() == fo::ObjType::OBJ_TYPE_CRITTER) { + if (damage > fo::var::main_ctd.targetDamage && fo::var::main_ctd.target->IsCritter()) { long cHP = fo::var::main_ctd.target->critter.health; if (cHP > fo::var::gcsd->maxDamage && cHP <= damage) { fo::var::main_ctd.targetFlags &= ~fo::DamageFlag::DAM_DEAD; // unset diff --git a/sfall/Modules/DebugEditor.cpp b/sfall/Modules/DebugEditor.cpp index 2a751157..57cbfe75 100644 --- a/sfall/Modules/DebugEditor.cpp +++ b/sfall/Modules/DebugEditor.cpp @@ -107,7 +107,7 @@ static void RunEditorInternal(SOCKET &s) { for (int tile = 0; tile < 40000; tile++) { fo::GameObject* obj = fo::func::obj_find_first_at_tile(elv, tile); while (obj) { - if ((obj->Type()) == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { vec.push_back(reinterpret_cast(obj)); } obj = fo::func::obj_find_next_at_tile(); diff --git a/sfall/Modules/Objects.cpp b/sfall/Modules/Objects.cpp index b0c3751d..5aac9db6 100644 --- a/sfall/Modules/Objects.cpp +++ b/sfall/Modules/Objects.cpp @@ -19,6 +19,7 @@ #include "..\main.h" #include "..\FalloutEngine\Fallout2.h" #include "LoadGameHook.h" +#include "Stats.h" #include "Objects.h" @@ -93,14 +94,17 @@ pickNewID: // skip PM range (18000 - 83535) } } -// Reassigns object IDs to all critters upon first loading a map +// Reassigns object IDs to all critters upon first loading a map and updates HP stats static void map_fix_critter_id() { long npcStartID = 4096; fo::GameObject* obj = fo::func::obj_find_first(); while (obj) { - if (obj->Type() == fo::OBJ_TYPE_CRITTER && obj->id < fo::PLAYER_ID) { - obj->id = npcStartID++; - SetScriptObjectID(obj); + if (obj->IsCritter()) { + if (obj->id < fo::PLAYER_ID) { + obj->id = npcStartID++; + SetScriptObjectID(obj); + } + Stats::UpdateHPStat(obj); } obj = fo::func::obj_find_next(); } diff --git a/sfall/Modules/Scripting/Handlers/Objects.cpp b/sfall/Modules/Scripting/Handlers/Objects.cpp index 7ce28c25..c6825c22 100644 --- a/sfall/Modules/Scripting/Handlers/Objects.cpp +++ b/sfall/Modules/Scripting/Handlers/Objects.cpp @@ -69,7 +69,7 @@ void op_set_script(OpcodeContext& ctx) { fo::func::scr_remove(object->scriptId); object->scriptId = 0xFFFFFFFF; } - if (object->Type() == fo::ObjType::OBJ_TYPE_CRITTER) { + if (object->IsCritter()) { scriptType = fo::Scripts::ScriptTypes::SCRIPT_CRITTER; } else { scriptType = fo::Scripts::ScriptTypes::SCRIPT_ITEM; @@ -182,7 +182,7 @@ void op_make_path(OpcodeContext& ctx) { auto func = getBlockingFunc(type); // if the object is not a critter, then there is no need to check tile (tileTo) for blocking - long checkFlag = (objFrom->Type() == fo::OBJ_TYPE_CRITTER); + long checkFlag = (objFrom->IsCritter()); char pathData[800]; long pathLength = fo::func::make_path_func(objFrom, objFrom->tile, tileTo, pathData, checkFlag, (void*)func); @@ -225,7 +225,7 @@ void op_get_party_members(OpcodeContext& ctx) { auto partyMemberList = fo::var::partyMemberList; for (int i = 0; i < actualCount; i++) { auto obj = reinterpret_cast(partyMemberList[i * 4]); - if (includeHidden || (obj->Type() == fo::OBJ_TYPE_CRITTER && !fo::func::critter_is_dead(obj) && !(obj->flags & fo::ObjectFlag::Mouse_3d))) { + if (includeHidden || (obj->IsCritter() && !fo::func::critter_is_dead(obj) && !(obj->flags & fo::ObjectFlag::Mouse_3d))) { arrays[arrayId].push_back((long)obj); } } @@ -307,7 +307,7 @@ void mf_item_weight(OpcodeContext& ctx) { void mf_set_dude_obj(OpcodeContext& ctx) { auto obj = ctx.arg(0).object(); - if (obj == nullptr || obj->Type() == fo::ObjType::OBJ_TYPE_CRITTER) { + if (obj == nullptr || obj->IsCritter()) { //if (!InCombat && obj && obj != PartyControl::RealDudeObject()) { // ctx.printOpcodeError("%s() - controlling of the critter is only allowed in combat mode.", ctx.getMetaruleName()); //} else { diff --git a/sfall/Modules/Scripting/Handlers/Perks.cpp b/sfall/Modules/Scripting/Handlers/Perks.cpp index bd746f11..d225e807 100644 --- a/sfall/Modules/Scripting/Handlers/Perks.cpp +++ b/sfall/Modules/Scripting/Handlers/Perks.cpp @@ -116,7 +116,7 @@ const char* notPartyMemberErr = "%s() - the object is not a party member."; void mf_set_selectable_perk_npc(OpcodeContext& ctx) { auto obj = ctx.arg(0).object(); - if (obj->Type() == fo::ObjType::OBJ_TYPE_CRITTER && fo::func::isPartyMember(obj)) { + if (obj->IsCritter() && fo::func::isPartyMember(obj)) { Perks::SetSelectablePerk(ctx.arg(1).strValue(), ctx.arg(2).rawValue(), ctx.arg(3).rawValue(), ctx.arg(4).strValue(), (obj->id != fo::PLAYER_ID) ? obj->id : 0); } else { ctx.printOpcodeError(notPartyMemberErr, ctx.getMetaruleName()); @@ -126,7 +126,7 @@ void mf_set_selectable_perk_npc(OpcodeContext& ctx) { void mf_set_fake_perk_npc(OpcodeContext& ctx) { auto obj = ctx.arg(0).object(); - if (obj->Type() == fo::ObjType::OBJ_TYPE_CRITTER && fo::func::isPartyMember(obj)) { + if (obj->IsCritter() && fo::func::isPartyMember(obj)) { Perks::SetFakePerk(ctx.arg(1).strValue(), ctx.arg(2).rawValue(), ctx.arg(3).rawValue(), ctx.arg(4).strValue(), (obj->id != fo::PLAYER_ID) ? obj->id : 0); } else { ctx.printOpcodeError(notPartyMemberErr, ctx.getMetaruleName()); @@ -136,7 +136,7 @@ void mf_set_fake_perk_npc(OpcodeContext& ctx) { void mf_set_fake_trait_npc(OpcodeContext& ctx) { auto obj = ctx.arg(0).object(); - if (obj->Type() == fo::ObjType::OBJ_TYPE_CRITTER && fo::func::isPartyMember(obj)) { + if (obj->IsCritter() && fo::func::isPartyMember(obj)) { Perks::SetFakeTrait(ctx.arg(1).strValue(), ctx.arg(2).rawValue(), ctx.arg(3).rawValue(), ctx.arg(4).strValue(), (obj->id != fo::PLAYER_ID) ? obj->id : 0); } else { ctx.printOpcodeError(notPartyMemberErr, ctx.getMetaruleName()); @@ -204,7 +204,7 @@ void op_has_fake_trait(OpcodeContext& ctx) { void mf_has_fake_perk_npc(OpcodeContext& ctx) { long result = 0; auto obj = ctx.arg(0).object(); - if (obj->Type() == fo::ObjType::OBJ_TYPE_CRITTER && fo::func::isPartyMember(obj)) { + if (obj->IsCritter() && fo::func::isPartyMember(obj)) { result = Perks::HasFakePerkOwner(ctx.arg(1).strValue(), (obj->id != fo::PLAYER_ID) ? obj->id : 0); } else { ctx.printOpcodeError(notPartyMemberErr, ctx.getMetaruleName()); @@ -215,7 +215,7 @@ void mf_has_fake_perk_npc(OpcodeContext& ctx) { void mf_has_fake_trait_npc(OpcodeContext& ctx) { long result = 0; auto obj = ctx.arg(0).object(); - if (obj->Type() == fo::ObjType::OBJ_TYPE_CRITTER && fo::func::isPartyMember(obj)) { + if (obj->IsCritter() && fo::func::isPartyMember(obj)) { result = Perks::HasFakeTraitOwner(ctx.arg(1).strValue(), (obj->id != fo::PLAYER_ID) ? obj->id : 0); } else { ctx.printOpcodeError(notPartyMemberErr, ctx.getMetaruleName()); diff --git a/sfall/Modules/Scripting/Handlers/Stats.cpp b/sfall/Modules/Scripting/Handlers/Stats.cpp index 5ef38a85..c0de131e 100644 --- a/sfall/Modules/Scripting/Handlers/Stats.cpp +++ b/sfall/Modules/Scripting/Handlers/Stats.cpp @@ -79,7 +79,7 @@ void op_get_pc_extra_stat(OpcodeContext& ctx) { void op_set_critter_base_stat(OpcodeContext& ctx) { fo::GameObject* obj = ctx.arg(0).object(); - if (obj && obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj && obj->IsCritter()) { int stat = ctx.arg(1).rawValue(); if (stat >= 0 && stat < fo::STAT_max_stat) { CritterStats::SetStat(obj, stat, ctx.arg(2).rawValue(), 9); @@ -93,7 +93,7 @@ void op_set_critter_base_stat(OpcodeContext& ctx) { void op_set_critter_extra_stat(OpcodeContext& ctx) { fo::GameObject* obj = ctx.arg(0).object(); - if (obj && obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj && obj->IsCritter()) { int stat = ctx.arg(1).rawValue(); if (stat >= 0 && stat < fo::STAT_max_stat) { CritterStats::SetStat(obj, stat, ctx.arg(2).rawValue(), 44); @@ -108,7 +108,7 @@ void op_set_critter_extra_stat(OpcodeContext& ctx) { void op_get_critter_base_stat(OpcodeContext& ctx) { int result = 0; fo::GameObject* obj = ctx.arg(0).object(); - if (obj && obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj && obj->IsCritter()) { int stat = ctx.arg(1).rawValue(); if (stat >= 0 && stat < fo::STAT_max_stat) { result = CritterStats::GetStat(obj, stat, 9); @@ -124,7 +124,7 @@ void op_get_critter_base_stat(OpcodeContext& ctx) { void op_get_critter_extra_stat(OpcodeContext& ctx) { int result = 0; fo::GameObject* obj = ctx.arg(0).object(); - if (obj && obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj && obj->IsCritter()) { int stat = ctx.arg(1).rawValue(); if (stat >= 0 && stat < fo::STAT_max_stat) { result = CritterStats::GetStat(obj, stat, 44); @@ -145,7 +145,7 @@ void op_set_critter_skill_points(OpcodeContext& ctx) { } fo::GameObject* obj = ctx.arg(0).object(); - if (obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { fo::Proto* proto = fo::GetProto(obj->protoId); if (proto) proto->critter.skills[skill] = ctx.arg(2).rawValue(); } else { @@ -161,7 +161,7 @@ void op_get_critter_skill_points(OpcodeContext& ctx) { } fo::GameObject* obj = ctx.arg(0).object(); - if (obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { fo::Proto* proto = fo::GetProto(obj->protoId); if (proto) ctx.setReturn(proto->critter.skills[skill]); } else { @@ -232,7 +232,7 @@ fail: void op_set_critter_current_ap(OpcodeContext& ctx) { fo::GameObject* obj = ctx.arg(0).object(); - if (obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { long ap = ctx.arg(1).rawValue(); if (ap < 0) ap = 0; obj->critter.movePoints = ap; @@ -285,7 +285,7 @@ end: void op_set_critter_hit_chance_mod(OpcodeContext& ctx) { fo::GameObject* obj = ctx.arg(0).object(); - if (obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { SetHitChanceMax(obj, ctx.arg(1).rawValue(), ctx.arg(2).rawValue()); } else { ctx.printOpcodeError(objNotCritter, ctx.getOpcodeName()); @@ -312,7 +312,7 @@ end: void op_set_critter_pickpocket_mod(OpcodeContext& ctx) { fo::GameObject* obj = ctx.arg(0).object(); - if (obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { SetPickpocketMax(obj, ctx.arg(1).rawValue(), ctx.arg(2).rawValue()); } else { ctx.printOpcodeError(objNotCritter, ctx.getOpcodeName()); @@ -339,7 +339,7 @@ end: void op_set_critter_skill_mod(OpcodeContext& ctx) { fo::GameObject* obj = ctx.arg(0).object(); - if (obj->Type() == fo::OBJ_TYPE_CRITTER) { + if (obj->IsCritter()) { SetSkillMax(obj, ctx.arg(1).rawValue()); } else { ctx.printOpcodeError(objNotCritter, ctx.getOpcodeName()); diff --git a/sfall/Modules/Stats.cpp b/sfall/Modules/Stats.cpp index f51179a5..0a294f3f 100644 --- a/sfall/Modules/Stats.cpp +++ b/sfall/Modules/Stats.cpp @@ -26,6 +26,9 @@ namespace sfall { +bool engineDerivedStats = true; +bool derivedHPwBonus = false; // recalculate the hit points with bonus stat values + static DWORD statMaximumsPC[fo::STAT_max_stat]; static DWORD statMinimumsPC[fo::STAT_max_stat]; static DWORD statMaximumsNPC[fo::STAT_max_stat]; @@ -144,23 +147,31 @@ end: } } +static long RecalcStat(int stat, int statsValue[]) { + double sum = 0; + for (int i = fo::Stat::STAT_st; i <= fo::Stat::STAT_lu; i++) { + sum += (statsValue[i] + statFormulas[stat].shift[i]) * statFormulas[stat].multi[i]; + } + long calcStatValue = statFormulas[stat].base + (int)floor(sum); + return (calcStatValue < statFormulas[stat].min) ? statFormulas[stat].min : calcStatValue; +} + static void __stdcall StatRecalcDerived(fo::GameObject* critter) { - int baseStats[7]; - for (int stat = fo::Stat::STAT_st; stat <= fo::Stat::STAT_lu; stat++) baseStats[stat] = fo::func::stat_level(critter, stat); - long* proto = CritterStats::GetProto(critter); - if (!proto) fo::func::proto_ptr(critter->protoId, (fo::Proto**)&proto); + if (!proto && fo::func::proto_ptr(critter->protoId, (fo::Proto**)&proto) == -1) return; - for (int i = fo::Stat::STAT_max_hit_points; i <= fo::Stat::STAT_poison_resist; i++) { - if (i >= fo::Stat::STAT_dmg_thresh && i <= fo::Stat::STAT_dmg_resist_explosion) continue; + int baseStats[7], levelStats[7]; + for (int stat = fo::Stat::STAT_st; stat <= fo::Stat::STAT_lu; stat++) { + levelStats[stat] = fo::func::stat_level(critter, stat); + if (!derivedHPwBonus) baseStats[stat] = fo::func::stat_get_base(critter, stat); + } - double sum = 0; - for (int stat = fo::Stat::STAT_st; stat <= fo::Stat::STAT_lu; stat++) { - sum += (baseStats[stat] + statFormulas[i].shift[stat]) * statFormulas[i].multi[stat]; - } - long calcStat = statFormulas[i].base + (int)floor(sum); - if (calcStat < statFormulas[i].min) calcStat = statFormulas[i].min; - proto[OffsetStat::base + i] = calcStat; // offset from base_stat_srength + ((fo::Proto*)proto)->critter.base.health = RecalcStat(fo::Stat::STAT_max_hit_points, (derivedHPwBonus) ? levelStats : baseStats); + + for (int stat = fo::Stat::STAT_max_move_points; stat <= fo::Stat::STAT_poison_resist; stat++) { + if (stat >= fo::Stat::STAT_dmg_thresh && stat <= fo::Stat::STAT_dmg_resist_explosion) continue; + // offset from base_stat_srength + proto[OffsetStat::base + stat] = RecalcStat(stat, levelStats); } } @@ -176,6 +187,29 @@ static void __declspec(naked) stat_recalc_derived_hack() { } } +void Stats::UpdateHPStat(fo::GameObject* critter) { + if (engineDerivedStats) return; + + fo::Proto* proto; + if (fo::func::proto_ptr(critter->protoId, &proto) == -1) return; + + auto getStatFunc = (derivedHPwBonus) ? fo::func::stat_level : fo::func::stat_get_base; + + double sum = 0; + for (int stat = fo::Stat::STAT_st; stat <= fo::Stat::STAT_lu; stat++) { + sum += (getStatFunc(critter, stat) + statFormulas[fo::Stat::STAT_max_hit_points].shift[stat]) * statFormulas[fo::Stat::STAT_max_hit_points].multi[stat]; + } + long calcStatValue = statFormulas[fo::Stat::STAT_max_hit_points].base + (int)floor(sum); + if (calcStatValue < statFormulas[fo::Stat::STAT_max_hit_points].min) calcStatValue = statFormulas[fo::Stat::STAT_max_hit_points].min; + + if (proto->critter.base.health != calcStatValue) { + fo::func::debug_printf("\nWarning: critter PID: %d, ID: %d, has an incorrect base value %d (must be %d) of the max HP stat.", + critter->protoId, critter->id, proto->critter.base.health, calcStatValue); + } + proto->critter.base.health = calcStatValue; + critter->critter.health = calcStatValue + proto->critter.bonus.health; +} + static void __declspec(naked) stat_set_base_hack_allow() { static const DWORD StatSetBaseRet = 0x4AF559; using namespace fo; @@ -240,7 +274,7 @@ void Stats::init() { MakeCall(0x4AF09C, CalcApToAcBonus, 3); // stat_level_ - // Allow set_critter_stat function to change STAT_unused and STAT_dmg_* stats for the player + // Allow set_critter_stat function to change the base stats of STAT_unused and STAT_dmg_* for the player MakeCall(0x4AF54E, stat_set_base_hack_allow); MakeCall(0x455D65, op_set_critter_stat_hack); // STAT_unused for other critters @@ -263,6 +297,7 @@ void Stats::init() { auto statsFile = GetConfigString("Misc", "DerivedStats", "", MAX_PATH); if (!statsFile.empty()) { + engineDerivedStats = false; MakeJump(0x4AF6FC, stat_recalc_derived_hack); // overrides function // STAT_st + STAT_en * 2 + 15 @@ -293,10 +328,13 @@ void Stats::init() { // STAT_en * 5 statFormulas[STAT_poison_resist].multi[STAT_en] = 5; // poison resist - char key[6], buf2[256], buf3[256]; const char* statFile = statsFile.insert(0, ".\\").c_str(); if (GetFileAttributes(statFile) == INVALID_FILE_ATTRIBUTES) return; + derivedHPwBonus = (iniGetInt("Main", "HPDependOnBonusStats", 0, statFile) != 0); + + char key[6], buf2[256], buf3[256]; + for (int i = fo::Stat::STAT_max_hit_points; i <= fo::Stat::STAT_poison_resist; i++) { if (i >= fo::Stat::STAT_dmg_thresh && i <= fo::Stat::STAT_dmg_resist_explosion) continue; diff --git a/sfall/Modules/Stats.h b/sfall/Modules/Stats.h index 17f90cf8..5bac62a1 100644 --- a/sfall/Modules/Stats.h +++ b/sfall/Modules/Stats.h @@ -34,6 +34,8 @@ public: static long GetStatMax(int stat, int isNPC); static long GetStatMin(int stat, int isNPC); + + static void UpdateHPStat(fo::GameObject* critter); }; void __stdcall SetPCStatMax(int stat, int value);