diff --git a/sfall/Game/items.cpp b/sfall/Game/items.cpp index 0249233a..776fc973 100644 --- a/sfall/Game/items.cpp +++ b/sfall/Game/items.cpp @@ -13,6 +13,7 @@ #include "..\Modules\HookScripts\InventoryHs.h" #include "..\Modules\HookScripts\ObjectHs.h" #include "..\Modules\Perks.h" +#include "..\Modules\Unarmed.h" #include "..\Game\stats.h" @@ -23,7 +24,8 @@ namespace game namespace sf = sfall; -constexpr int reloadCostAP = 2; // engine default reload AP cost +constexpr int reloadAPCost = 2; // engine default reload AP cost +constexpr int unarmedAPCost = 3; // engine default use item AP cost static std::array healingItemPids = {fo::PID_STIMPAK, fo::PID_SUPER_STIMPAK, fo::PID_HEALING_POWDER}; @@ -121,7 +123,34 @@ long Items::item_weapon_range(fo::GameObject* source, fo::GameObject* weapon, lo // return item_weapon_range(source, fo::func::item_hit_with(source, hitMode), hitMode); //} -static bool fastShotTweak = false; +static int fastShotTweak; + +static long item_w_mp_cost_sub(fo::GameObject* source, fo::GameObject* item, long hitMode, long isCalled, long cost) { + if (isCalled) cost++; + if (cost < 0) cost = 0; + + long type = fo::func::item_w_subtype(item, hitMode); + + if (source->protoId == fo::ProtoID::PID_Player && sf::Perks::DudeHasTrait(fo::Trait::TRAIT_fast_shot)) { + // Alternative behaviors of the Fast Shot trait + if (item && fastShotTweak > 2) { // Fallout 1 behavior (allowed for all weapons) + cost--; + } else if (fastShotTweak == 2) { // Alternative behavior (allowed for all attacks) + cost--; + } else if (fastShotTweak < 2 && type > fo::AttackSubType::MELEE && fo::func::item_w_range(source, hitMode) >= 2) { // Fallout 2 behavior (with Haenlomal's fix) + cost--; + } + } + if ((type == fo::AttackSubType::MELEE || type == fo::AttackSubType::UNARMED) && Stats::perk_level(source, fo::Perk::PERK_bonus_hth_attacks)) { + cost--; + } + if (type == fo::AttackSubType::GUNS && Stats::perk_level(source, fo::Perk::PERK_bonus_rate_of_fire)) { + cost--; + } + if (cost < 1) cost = 1; + + return cost; +} // Implementation of item_w_primary_mp_cost_ and item_w_secondary_mp_cost_ engine functions in a single function with the HOOK_CALCAPCOST hook long __fastcall Items::item_weapon_mp_cost(fo::GameObject* source, fo::GameObject* weapon, long hitMode, long isCalled) { @@ -139,40 +168,18 @@ long __fastcall Items::item_weapon_mp_cost(fo::GameObject* source, fo::GameObjec case fo::AttackType::ATKTYPE_LWEAPON_RELOAD: case fo::AttackType::ATKTYPE_RWEAPON_RELOAD: if (weapon && weapon->protoId != fo::ProtoID::PID_SOLAR_SCORCHER) { // Solar Scorcher has no reload AP cost - cost = reloadCostAP; + cost = reloadAPCost; if (fo::util::GetProto(weapon->protoId)->item.weapon.perk == fo::Perk::PERK_weapon_fast_reload) { cost--; } } + goto endReload; } - if (hitMode < fo::AttackType::ATKTYPE_LWEAPON_RELOAD) { - if (isCalled) cost++; - if (cost < 0) cost = 0; - long type = fo::func::item_w_subtype(weapon, hitMode); + cost = item_w_mp_cost_sub(source, weapon, hitMode, isCalled, cost); - if (source->protoId == fo::ProtoID::PID_Player && sf::Perks::DudeHasTrait(fo::Trait::TRAIT_fast_shot)) { - if (fastShotTweak || // Fallout 1 behavior and Alternative behavior (allowed for all weapons) - (fo::func::item_w_range(source, hitMode) >= 2 && type > fo::AttackSubType::MELEE)) // Fallout 2 behavior (with fix) and Haenlomal's tweak - { - cost--; - } - } - if ((type == fo::AttackSubType::MELEE || type == fo::AttackSubType::UNARMED) && Stats::perk_level(source, fo::Perk::PERK_bonus_hth_attacks)) { - cost--; - } - if (type == fo::AttackSubType::GUNS && Stats::perk_level(source, fo::Perk::PERK_bonus_rate_of_fire)) { - cost--; - } - if (cost < 1) cost = 1; - } - return sf::CalcApCostHook_Invoke(source, hitMode, isCalled, cost, weapon); -} - -// Implementation of item_w_mp_cost_ engine function with the HOOK_CALCAPCOST hook -long Items::item_w_mp_cost(fo::GameObject* source, long hitMode, long isCalled) { - long cost = fo::func::item_w_mp_cost(source, hitMode, isCalled); - return sf::CalcApCostHook_Invoke(source, hitMode, isCalled, cost, nullptr); +endReload: + return sf::CalcApCostHook_Invoke(source, hitMode, isCalled, cost, weapon); // return cost } static void __declspec(naked) ai_search_inven_weap_hook() { @@ -187,12 +194,63 @@ static void __declspec(naked) ai_search_inven_weap_hook() { } } +// Implementation of item_w_mp_cost_ engine function with the HOOK_CALCAPCOST hook +long __fastcall Items::item_w_mp_cost(fo::GameObject* source, long hitMode, long isCalled) { + fo::GameObject* handItem = nullptr; + + switch (hitMode) { + case fo::AttackType::ATKTYPE_LWEAPON_PRIMARY: + case fo::AttackType::ATKTYPE_LWEAPON_SECONDARY: + case fo::AttackType::ATKTYPE_LWEAPON_RELOAD: + handItem = fo::func::inven_left_hand(source); + break; + case fo::AttackType::ATKTYPE_RWEAPON_PRIMARY: + case fo::AttackType::ATKTYPE_RWEAPON_SECONDARY: + case fo::AttackType::ATKTYPE_RWEAPON_RELOAD: + handItem = fo::func::inven_right_hand(source); + break; + default: + break; + } + if (handItem) { + return Items::item_weapon_mp_cost(source, handItem, hitMode, isCalled); + } + + // unarmed hits + long cost = unarmedAPCost; + if (hitMode == fo::AttackType::ATKTYPE_PUNCH || hitMode == fo::AttackType::ATKTYPE_KICK || hitMode >= fo::AttackType::ATKTYPE_STRONGPUNCH) { + cost = sf::Unarmed::GetHitAPCost((fo::AttackType)hitMode); + } + + // return cost + return sf::CalcApCostHook_Invoke( + source, + hitMode, + isCalled, + item_w_mp_cost_sub(source, nullptr, hitMode, isCalled, cost), + nullptr + ); +} + +static void __declspec(naked) item_w_mp_cost_hack() { + using namespace fo; + __asm { + push ebx; // isCalled + mov ecx, eax; // source + call Items::item_w_mp_cost; + pop ecx; + retn; + } +} + void Items::init() { // Replace the item_w_primary_mp_cost_ function with the sfall implementation sf::HookCall(0x429A08, ai_search_inven_weap_hook); - int fastShotFix = sf::IniReader::GetConfigInt("Misc", "FastShotFix", 0); - fastShotTweak = (fastShotFix > 0 && fastShotFix <= 3); + // Replace the item_w_mp_cost_ function with the sfall implementation + sf::MakeJump(fo::funcoffs::item_w_mp_cost_ + 1, item_w_mp_cost_hack); // 0x478B25 + + fastShotTweak = sf::IniReader::GetConfigInt("Misc", "FastShotFix", 0); } } diff --git a/sfall/Game/items.h b/sfall/Game/items.h index 2c78aa42..e8e697e8 100644 --- a/sfall/Game/items.h +++ b/sfall/Game/items.h @@ -37,8 +37,8 @@ public: static long __fastcall item_weapon_mp_cost(fo::GameObject* source, fo::GameObject* weapon, long hitMode, long isCalled); // Implementation of item_w_mp_cost_ engine function with the HOOK_CALCAPCOST hook - // Note: Use the generic item_mp_cost function which has a hook call - static long item_w_mp_cost(fo::GameObject* source, long hitMode, long isCalled); + // Note: Can use the generic item_mp_cost_ function which has a hook call + static long __fastcall item_w_mp_cost(fo::GameObject* source, long hitMode, long isCalled); }; } \ No newline at end of file diff --git a/sfall/Modules/BugFixes.cpp b/sfall/Modules/BugFixes.cpp index e6f22942..eabfa185 100644 --- a/sfall/Modules/BugFixes.cpp +++ b/sfall/Modules/BugFixes.cpp @@ -120,7 +120,7 @@ isFloat: jmp NegateFixHack_Back; } } - +/* static void __declspec(naked) compute_attack_hack() { static const DWORD UnarmedAttacksFixEnd = 0x423A0D; __asm { @@ -157,7 +157,7 @@ end: jmp UnarmedAttacksFixEnd; } } - +*/ static void __declspec(naked) SharpShooterFix() { __asm { call fo::funcoffs::stat_level_ // Perception @@ -3186,7 +3186,7 @@ void BugFixes::init() //if (IniReader::GetConfigInt("Misc", "SpecialUnarmedAttacksFix", 1)) { dlog("Applying Special Unarmed Attacks fix.", DL_FIX); - MakeJump(0x42394D, compute_attack_hack); + //MakeJump(0x42394D, compute_attack_hack); - implementation moved to Unarmed module dlogr(" Done", DL_FIX); //} diff --git a/sfall/Modules/DamageMod.cpp b/sfall/Modules/DamageMod.cpp index bcd84326..92e45ba2 100644 --- a/sfall/Modules/DamageMod.cpp +++ b/sfall/Modules/DamageMod.cpp @@ -22,6 +22,8 @@ #include "..\Logging.h" #include "HookScripts.h" +#include "..\Game\stats.h" + #include "DamageMod.h" namespace sfall @@ -320,7 +322,7 @@ static __declspec(naked) void CommonDmgRngDispFix_hook() { retn; } } - +/* static __declspec(naked) void HtHDamageFix1a_hack() { using namespace fo; __asm { @@ -337,7 +339,7 @@ fix: retn; } } - +*/ static __declspec(naked) void HtHDamageFix1b_hook() { using namespace fo; __asm { @@ -399,6 +401,14 @@ static void __declspec(naked) DisplayBonusHtHDmg2_hack() { } } +static bool bonusHtHDamageFix = false; + +long DamageMod::GetHtHMinDamageBonus(fo::GameObject* source) { + return (bonusHtHDamageFix) + ? game::Stats::perk_level(source, fo::Perk::PERK_bonus_hth_damage) << 1 // Rank_of_Bonus_HtH_Damage_perk *= 2 + : 0; +} + void DamageMod::init() { if (formula = IniReader::GetConfigInt("Misc", "DamageFormula", 0)) { switch (formula) { @@ -414,7 +424,9 @@ void DamageMod::init() { int BonusHtHDmgFix = IniReader::GetConfigInt("Misc", "BonusHtHDamageFix", 1); int DisplayBonusDmg = IniReader::GetConfigInt("Misc", "DisplayBonusDamage", 0); + if (BonusHtHDmgFix) { + bonusHtHDamageFix = true; dlog("Applying Bonus HtH Damage Perk fix.", DL_INIT); if (DisplayBonusDmg == 0) { // Subtract damage from perk bonus (vanilla displaying) HookCalls(MeleeDmgDisplayPrintFix_hook, { @@ -426,7 +438,7 @@ void DamageMod::init() { 0x472546 // Unarmed (display_stats_) }); } - MakeCall(0x478492, HtHDamageFix1a_hack); // Unarmed (item_w_damage_) + //MakeCall(0x478492, HtHDamageFix1a_hack); // Unarmed (item_w_damage_) HookCall(0x47854C, HtHDamageFix1b_hook); // MeleeWeap (item_w_damage_) dlogr(" Done", DL_INIT); } diff --git a/sfall/Modules/DamageMod.h b/sfall/Modules/DamageMod.h index 91d8ec26..90438aef 100644 --- a/sfall/Modules/DamageMod.h +++ b/sfall/Modules/DamageMod.h @@ -20,7 +20,7 @@ #include "Module.h" -namespace sfall +namespace sfall { class DamageMod : public Module { @@ -31,6 +31,8 @@ public: static int formula; static void DamageGlovz(fo::ComputeAttackResult &ctd, DWORD &accumulatedDamage, int rounds, int armorDT, int armorDR, int bonusRangedDamage, int multiplyDamage, int difficulty); static void DamageYAAM(fo::ComputeAttackResult &ctd, DWORD &accumulatedDamage, int rounds, int armorDT, int armorDR, int bonusRangedDamage,int multiplyDamage, int difficulty); + + static long GetHtHMinDamageBonus(fo::GameObject* source); }; } diff --git a/sfall/Modules/HookScripts/CombatHs.cpp b/sfall/Modules/HookScripts/CombatHs.cpp index f8cafccf..527160e8 100644 --- a/sfall/Modules/HookScripts/CombatHs.cpp +++ b/sfall/Modules/HookScripts/CombatHs.cpp @@ -102,7 +102,7 @@ long CalcApCostHook_Invoke(fo::GameObject* source, long hitMode, long isCalled, ? CalcApCostHook_Script(source, hitMode, isCalled, cost, weapon) : cost; } - +/* static void __declspec(naked) CalcApCostHook() { __asm { HookBegin; @@ -129,7 +129,7 @@ static void __declspec(naked) CalcApCostHook() { retn; } } - +*/ // this is for using non-weapon items, always 2 AP in vanilla static void __declspec(naked) CalcApCostHook2() { __asm { @@ -281,13 +281,11 @@ static void __fastcall FindTargetHook_Script(DWORD* target, fo::GameObject* atta } EndHook(); } - /* void FindTargetHook_Invoke(fo::GameObject* targets[], fo::GameObject* attacker) { if (HookScripts::HookHasScript(HOOK_FINDTARGET)) FindTargetHook_Script((DWORD*)targets, attacker); } */ - static void __declspec(naked) FindTargetHook() { __asm { push eax; @@ -311,9 +309,10 @@ static void __declspec(naked) ItemDamageHook() { } argCount = 6; - if (args[2] == 0) { // weapon arg - args[4] += 8; // type arg - } + // tweak for 0x4784AA (obsolete) + //if (args[2] == 0) { // weapon arg + // args[4] += 8; // type arg + //} RunHookScript(HOOK_ITEMDAMAGE); @@ -610,7 +609,6 @@ static void __declspec(naked) ai_search_inven_weap_hook() { retn; } } - /* fo::GameObject* BestWeaponHook_Invoke(fo::GameObject* bestWeapon, fo::GameObject* source, fo::GameObject* weapon1, fo::GameObject* weapon2, fo::GameObject* target) { return (HookScripts::HookHasScript(HOOK_BESTWEAPON)) @@ -618,7 +616,6 @@ fo::GameObject* BestWeaponHook_Invoke(fo::GameObject* bestWeapon, fo::GameObject : bestWeapon; } */ - static bool __stdcall CanUseWeaponHook_Script(bool result, fo::GameObject* source, fo::GameObject* weapon, long hitMode) { BeginHook(); argCount = 4; @@ -695,18 +692,18 @@ void Inject_AfterHitRollHook() { } void Inject_CalcApCostHook() { - HookCalls(CalcApCostHook, { - 0x42307A, - 0x42669F, - 0x42687B, - 0x42A625, - 0x42A655, - 0x42A686, - 0x42AE32, - 0x42AE71, - 0x460048, - 0x47807B - }); + //HookCalls(CalcApCostHook, { + // 0x42307A, + // 0x42669F, + // 0x42687B, + // 0x42A625, + // 0x42A655, + // 0x42A686, + // 0x42AE32, + // 0x42AE71, + // 0x460048, + // 0x47807B + //}); MakeCall(0x478083, CalcApCostHook2); } diff --git a/sfall/Modules/Perks.cpp b/sfall/Modules/Perks.cpp index 096622e8..f817ea56 100644 --- a/sfall/Modules/Perks.cpp +++ b/sfall/Modules/Perks.cpp @@ -1073,7 +1073,7 @@ static void PerkAndTraitSetup() { HookCall(0x4248F9, BlockedTrait); // compute_damage_ break; case fo::Trait::TRAIT_fast_shot: - HookCall(0x478C8A, BlockedTrait); // item_w_mp_cost_ + //HookCall(0x478C8A, BlockedTrait); // item_w_mp_cost_ (obsolete) HookCall(0x478E70, BlockedTrait); // item_w_called_shot_ break; case fo::Trait::TRAIT_bloody_mess: @@ -1121,7 +1121,7 @@ dlgExit: jmp perks_dialog_Ret; } } - +/* static void __declspec(naked) item_w_mp_cost_hook() { __asm { call fo::funcoffs::item_w_range_; @@ -1136,7 +1136,7 @@ checkType: jmp fo::funcoffs::item_w_subtype_; // eax - item } } - +*/ // Haenlomal's tweak static void __declspec(naked) item_w_called_shot_hack() { static const DWORD FastShotTraitFix_End = 0x478E7F; @@ -1174,18 +1174,20 @@ static void FastShotTraitFix() { goto fix; case 2: dlog("Applying Fast Shot trait patch (Alternative behavior).", DL_INIT); - SafeWrite16(0x478C9F, 0x9090); - HookCalls((void*)0x478C7D, {0x478BB8, 0x478BC7, 0x478BD6, 0x478BEA, 0x478BF9, 0x478C08, 0x478C2F}); + /* Implemented in sfall item_w_mp_cost function */ + //SafeWrite16(0x478C9F, 0x9090); // item_w_mp_cost_ + //HookCalls((void*)0x478C7D, {0x478BB8, 0x478BC7, 0x478BD6, 0x478BEA, 0x478BF9, 0x478C08, 0x478C2F}); // jmp 0x478C7D goto done; case 3: dlog("Applying Fast Shot trait patch (Fallout 1 behavior).", DL_INIT); - HookCall(0x478C97, (void*)fo::funcoffs::item_hit_with_); - SafeWrite16(0x478C9E, CodeType::JumpZ << 8); // ignore all unarmed attacks (cmp eax, 0; jz) + /* Implemented in sfall item_w_mp_cost function */ + //HookCall(0x478C97, (void*)fo::funcoffs::item_hit_with_); + //SafeWrite16(0x478C9E, CodeType::JumpZ << 8); // ignore all unarmed attacks (cmp eax, 0; jz) goto done; default: dlog("Applying Fast Shot trait fix.", DL_INIT); fix: - HookCall(0x478C97, item_w_mp_cost_hook); + //HookCall(0x478C97, item_w_mp_cost_hook); - Fix implemented in sfall item_w_mp_cost function done: dlogr(" Done", DL_INIT); } diff --git a/sfall/Modules/Unarmed.cpp b/sfall/Modules/Unarmed.cpp new file mode 100644 index 00000000..d8cc173d --- /dev/null +++ b/sfall/Modules/Unarmed.cpp @@ -0,0 +1,419 @@ +/* + * sfall + * Copyright (C) 2008-2021 The sfall team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "..\main.h" +#include "..\FalloutEngine\Fallout2.h" + +#include "DamageMod.h" + +#include "Unarmed.h" + +namespace sfall +{ + +class Hits { +public: + static const long count = 20; + +private: + struct HitsData { + long reqLevel = 0; // intface_update_items_ + long reqSkill = 0; + long reqStat[7] = {0}; + long minDamage = 1; // default for all + long maxDamage = 2; // default for all (maxDamage + STAT_melee_dmg) + long bonusDamage = 0; // item_w_damage_ + long bonusCrit = 0; // compute_attack_ + long apCost = 3; // item_w_mp_cost_ + bool isPenetrate = false; // compute_damage_ + bool isSecondary = false; + }; + + struct SortHits { + char level; + char hit; // index in Hits class + }; + + // sorted in descending order of the required stats in group hits + SortHits sortHits[Hits::count - fo::AttackType::ATKTYPE_STRONGPUNCH]; + + HitsData hit[Hits::count]; + + long skillLevel; // SKILL_UNARMED_COMBAT + long psStat[fo::Stat::STAT_base_count]; + +public: + class Types{ + public: enum : long { + punch = fo::AttackType::ATKTYPE_PUNCH, + kick = fo::AttackType::ATKTYPE_KICK, + + strong_punch = fo::AttackType::ATKTYPE_STRONGPUNCH, + hammer_punch, + haymaker, + jab, + palm_strike, + piercing_strike, + + strong_kick, + snap_kick, + power_kick, + hip_kick, + hook_kick, + piercing_kick + }; + }; + + Hits() { + // punch primary 1 + hit[Types::strong_punch].reqLevel = 1; + hit[Types::strong_punch].reqSkill = 55; + hit[Types::strong_punch].reqStat[fo::Stat::STAT_ag] = 6; + hit[Types::strong_punch].bonusDamage = 3; + // secondary 1 + hit[Types::jab].reqLevel = 5; + hit[Types::jab].reqSkill = 75; + hit[Types::jab].reqStat[fo::Stat::STAT_st] = 4; + hit[Types::jab].reqStat[fo::Stat::STAT_ag] = 6; + hit[Types::jab].bonusDamage = 3; + hit[Types::jab].bonusCrit = 10; + hit[Types::jab].isSecondary = true; + + // primary 2 + hit[Types::hammer_punch].reqLevel = 6; + hit[Types::hammer_punch].reqSkill = 75; + hit[Types::hammer_punch].reqStat[fo::Stat::STAT_st] = 5; + hit[Types::hammer_punch].reqStat[fo::Stat::STAT_ag] = 6; + hit[Types::hammer_punch].bonusDamage = 5; + hit[Types::hammer_punch].bonusCrit = 5; + // secondary 2 + hit[Types::palm_strike].reqLevel = 12; + hit[Types::palm_strike].reqSkill = 115; + hit[Types::palm_strike].reqStat[fo::Stat::STAT_st] = 5; + hit[Types::palm_strike].reqStat[fo::Stat::STAT_ag] = 7; + hit[Types::palm_strike].bonusDamage = 7; + hit[Types::palm_strike].bonusCrit = 20; + hit[Types::palm_strike].apCost = 6; + hit[Types::palm_strike].isPenetrate = true; + hit[Types::palm_strike].isSecondary = true; + + // primary 3 + hit[Types::haymaker].reqLevel = 9; + hit[Types::haymaker].reqSkill = 100; + hit[Types::haymaker].reqStat[fo::Stat::STAT_st] = 5; + hit[Types::haymaker].reqStat[fo::Stat::STAT_ag] = 7; + hit[Types::haymaker].bonusDamage = 7; + hit[Types::haymaker].bonusCrit = 15; + // secondary 3 + hit[Types::piercing_strike].reqLevel = 16; + hit[Types::piercing_strike].reqSkill = 130; + hit[Types::piercing_strike].reqStat[fo::Stat::STAT_st] = 5; + hit[Types::piercing_strike].reqStat[fo::Stat::STAT_ag] = 7; + hit[Types::piercing_strike].bonusDamage = 10; + hit[Types::piercing_strike].bonusCrit = 40; + hit[Types::piercing_strike].apCost = 8; + hit[Types::piercing_strike].isPenetrate = true; + hit[Types::piercing_strike].isSecondary = true; + + // kick primary 1 + hit[Types::strong_kick].reqLevel = 1; + hit[Types::strong_kick].reqSkill = 40; + hit[Types::strong_kick].reqStat[fo::Stat::STAT_ag] = 6; + hit[Types::strong_kick].bonusDamage = 5; + hit[Types::strong_kick].apCost = 4; + // secondary 1 + hit[Types::hip_kick].reqLevel = 6; + hit[Types::hip_kick].reqSkill = 60; + hit[Types::hip_kick].reqStat[fo::Stat::STAT_st] = 6; + hit[Types::hip_kick].reqStat[fo::Stat::STAT_ag] = 7; + hit[Types::hip_kick].bonusDamage = 7; + hit[Types::hip_kick].apCost = 7; + hit[Types::hip_kick].isSecondary = true; + + // primary 2 + hit[Types::snap_kick].reqLevel = 6; + hit[Types::snap_kick].reqSkill = 60; + hit[Types::snap_kick].reqStat[fo::Stat::STAT_ag] = 6; + hit[Types::snap_kick].bonusDamage = 7; + hit[Types::snap_kick].apCost = 4; + // secondary 2 + hit[Types::hook_kick].reqLevel = 12; + hit[Types::hook_kick].reqSkill = 100; + hit[Types::hook_kick].reqStat[fo::Stat::STAT_st] = 6; + hit[Types::hook_kick].reqStat[fo::Stat::STAT_ag] = 7; + hit[Types::hook_kick].bonusDamage = 9; + hit[Types::hook_kick].bonusCrit = 10; + hit[Types::hook_kick].apCost = 7; + hit[Types::hook_kick].isPenetrate = true; + hit[Types::hook_kick].isSecondary = true; + + // primary 3 + hit[Types::power_kick].reqLevel = 9; + hit[Types::power_kick].reqSkill = 80; + hit[Types::power_kick].reqStat[fo::Stat::STAT_st] = 6; + hit[Types::power_kick].reqStat[fo::Stat::STAT_ag] = 6; + hit[Types::power_kick].bonusDamage = 9; + hit[Types::power_kick].bonusCrit = 5; + hit[Types::power_kick].apCost = 4; + // secondary 3 + hit[Types::piercing_kick].reqLevel = 15; + hit[Types::piercing_kick].reqSkill = 125; + hit[Types::piercing_kick].reqStat[fo::Stat::STAT_st] = 6; + hit[Types::piercing_kick].reqStat[fo::Stat::STAT_ag] = 8; + hit[Types::piercing_kick].bonusDamage = 12; + hit[Types::piercing_kick].bonusCrit = 50; + hit[Types::piercing_kick].apCost = 9; + hit[Types::piercing_kick].isPenetrate = true; + hit[Types::piercing_kick].isSecondary = true; + } + + HitsData& Hit(fo::AttackType i) { return hit[i]; } + + // Get hit by index + HitsData& Hit(size_t index) { return hit[index]; } + + fo::AttackType GetSortHit(size_t index) { return (fo::AttackType)sortHits[index].hit; } + + void Sort() { + for (char i = fo::AttackType::ATKTYPE_STRONGPUNCH, j = 0; i < Hits::count; i++, j++) { + sortHits[j].level = (char)hit[i].reqLevel; + sortHits[j].hit = i; + } + + auto greater_comp = [&](const SortHits &a, const SortHits &b) { + if (a.level == b.level) { + return hit[a.hit].reqSkill > hit[b.hit].reqSkill; + } + return a.level > b.level; + }; + + std::sort(&sortHits[0], &sortHits[6], greater_comp); + std::sort(&sortHits[6], &sortHits[12], greater_comp); + } + + void GetDudeStats(long sLevel) { + skillLevel = sLevel; + for (size_t stat = 0; stat < fo::Stat::STAT_base_count; stat++) { + psStat[stat] = fo::func::stat_level(fo::var::obj_dude, stat); + } + } + + long SkillLevel() { return skillLevel; } + long DudeStat(long stat) { return psStat[stat]; } +}; + +Hits unarmed; + +static bool UnarmedReqStats(fo::AttackType hit) { + if (unarmed.SkillLevel() >= unarmed.Hit(hit).reqSkill && (long)fo::var::Level_pc >= unarmed.Hit(hit).reqLevel) { + for (size_t stat = 0; stat < fo::Stat::STAT_base_count; stat++) { + if (unarmed.Hit(hit).reqStat[stat] == 0) continue; + if (unarmed.Hit(hit).reqStat[stat] > unarmed.DudeStat(stat)) { + return false; + } + } + return true; + } + return false; +} + +static bool Punching(bool isPrimary) { + for (size_t i = 0; i < 6; i++) { + fo::AttackType hit = unarmed.GetSortHit(i); + if (unarmed.Hit(hit).isSecondary == isPrimary) continue; + + if (UnarmedReqStats(hit)) { + if (unarmed.Hit(hit).isSecondary) { + fo::var::itemButtonItems[fo::HandSlot::Left].secondaryAttack = hit; + } else { + fo::var::itemButtonItems[fo::HandSlot::Left].primaryAttack = hit; + } + return true; + } + } + return false; +} + +// Punch hits +static void __fastcall check_unarmed_left_slot(long skillLevel) { + unarmed.GetDudeStats(skillLevel); + + if (!Punching(true)) fo::var::itemButtonItems[fo::HandSlot::Left].primaryAttack = fo::AttackType::ATKTYPE_PUNCH; + if (!Punching(false)) fo::var::itemButtonItems[fo::HandSlot::Left].secondaryAttack = fo::AttackType::ATKTYPE_PUNCH; +} + +static void __declspec(naked) intface_update_items_hack_punch() { + __asm { + push 0x45F1D7; + jmp check_unarmed_left_slot; + } +} + +static bool Kicking(bool isPrimary) { + for (size_t i = 6; i < 12; i++) { + fo::AttackType hit = unarmed.GetSortHit(i); + if (unarmed.Hit(hit).isSecondary == isPrimary) continue; + + if (UnarmedReqStats(hit)) { + if (unarmed.Hit(hit).isSecondary) { + fo::var::itemButtonItems[fo::HandSlot::Right].secondaryAttack = hit; + } else { + fo::var::itemButtonItems[fo::HandSlot::Right].primaryAttack = hit; + } + return true; + } + } + return false; +} + +// Kick hits +static void check_unarmed_right_slot() { + if (!Kicking(true)) fo::var::itemButtonItems[fo::HandSlot::Right].primaryAttack = fo::AttackType::ATKTYPE_KICK; + if (!Kicking(false)) fo::var::itemButtonItems[fo::HandSlot::Right].secondaryAttack = fo::AttackType::ATKTYPE_KICK; +} + +static void __declspec(naked) intface_update_items_hack_kick() { + __asm { + push 0x45F380; + jmp check_unarmed_right_slot; + } +} + +static long __fastcall get_unarmed_crit_chance(long &chanceOut, fo::AttackType hit) { + chanceOut = unarmed.Hit(hit).bonusCrit; + return (chanceOut > 0) + ? 0x4239F4 // + : 0x423A0D; // skip random roll +} + +static void __declspec(naked) compute_attack_hack() { + __asm { + sub esp, 4; + mov ecx, esp; // chanceOut ref + call get_unarmed_crit_chance; // edx - hit + pop ecx; // chance + jmp eax; + } +} + +static long __fastcall get_unarmed_damage(fo::GameObject* source, fo::AttackType hit, long &minOut, long &maxOut) { + minOut = unarmed.Hit(hit).minDamage + DamageMod::GetHtHMinDamageBonus(source); + maxOut = unarmed.Hit(hit).maxDamage + fo::func::stat_level(source, fo::Stat::STAT_melee_dmg); + return unarmed.Hit(hit).bonusDamage; +} + +static void __declspec(naked) item_w_damage_hack() { + static DWORD item_w_damage_hack_ret = 0x478553; + __asm { + lea eax, [esp + 4]; // min_DMG + lea edx, [esp + 0]; // max_DMG + push ecx; + push edx; // maxOut ref + push eax; // minOut ref + mov edx, esi; // hit + call get_unarmed_damage; + mov ebx, eax; // bonus + pop ecx; + jmp item_w_damage_hack_ret; + } +} + +static long __fastcall check_unarmed_penetrate(fo::AttackType hit) { + return static_cast(unarmed.Hit(hit).isPenetrate); +} + +long Unarmed::GetHitAPCost(fo::AttackType hit) { + return unarmed.Hit(hit).apCost; +} + +void Unarmed::init() { + unarmed = Hits(); + + auto unarmedFile = IniReader::GetConfigString("Misc", "UnarmedFile", "", MAX_PATH); + if (!unarmedFile.empty()) { + const char* file = unarmedFile.insert(0, ".\\").c_str(); + if (GetFileAttributes(file) != INVALID_FILE_ATTRIBUTES) { // check if file exists + char stat[6] = "Stat0"; + char sHit[4] = "0"; + for (size_t i = fo::AttackType::ATKTYPE_PUNCH; i < Hits::count; _itoa(++i, sHit, 10)) { + if (i < fo::ATKTYPE_STRONGPUNCH && i != fo::ATKTYPE_PUNCH && i != fo::ATKTYPE_KICK) continue; + + auto& hit = unarmed.Hit(i); + + int val = IniReader::GetInt(sHit, "ReqLevel", -1, file); + if (val >= 0) hit.reqLevel = val; + + val = IniReader::GetInt(sHit, "SkillLevel", -1, file); + if (val >= 0) hit.reqSkill = val; + + val = IniReader::GetInt(sHit, "MinDamage", -1, file); + if (val >= 0) hit.minDamage = val; + + val = IniReader::GetInt(sHit, "MaxDamage", -1, file); + if (val >= 0) hit.maxDamage = val; + + val = IniReader::GetInt(sHit, "BonusDamage", -1, file); + if (val >= 0) hit.bonusDamage = val; + + val = IniReader::GetInt(sHit, "BonusCrit", -1, file); + if (val >= 0) hit.bonusCrit = val; + + val = IniReader::GetInt(sHit, "APCost", -1, file); + if (val >= 0) hit.apCost = val; + + val = IniReader::GetInt(sHit, "Penetrate", -1, file); + if (val >= 0) hit.isPenetrate = (val != 0); + + val = IniReader::GetInt(sHit, "Second", -1, file); + if (val >= 0) hit.isSecondary = (val != 0); + + for (size_t s = 0; s < fo::Stat::STAT_base_count; _itoa(++s, &stat[4], 10)) { + val = IniReader::GetInt(sHit, stat, -1, file); + if (val >= 0) hit.reqStat[s] = val; + } + stat[4] = '0'; + } + + // Only if a custom file is used, otherwise use the engine function code + MakeJump(0x45F0DF, intface_update_items_hack_punch); + MakeJump(0x45F278, intface_update_items_hack_kick); + } + } + unarmed.Sort(); + + // Get critical chance hack + MakeJump(0x42394D, compute_attack_hack); + SafeWrite16(0x423A03, 0xC839); // cmp eax, 50 -> cmp eax, ecx + SafeWrite8(0x423A05, CodeType::Nop); + + // Get damage hack + MakeJump(0x478492, item_w_damage_hack); + + // compute_damage_ penetrate hack + SafeWrite8(0x4248B4, 0x4E); // mov ecx, [hit] + MakeCall(0x4248B6, check_unarmed_penetrate, 5); + SafeWrite16(0x4248C1, 0x01F8); // cmp eax, 1 + SafeWrite8(0x4248C8, CodeType::JumpShort); +} + +//void Unarmed::exit() { +//} + +} diff --git a/sfall/Modules/Unarmed.h b/sfall/Modules/Unarmed.h new file mode 100644 index 00000000..f7b8fce1 --- /dev/null +++ b/sfall/Modules/Unarmed.h @@ -0,0 +1,35 @@ +/* + * sfall + * Copyright (C) 2008-2021 The sfall team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "Module.h" + +namespace sfall +{ + +class Unarmed : public Module { +public: + const char* name() { return "Unarmed"; } + void init(); + //void exit() override; + + static long GetHitAPCost(fo::AttackType hit); +}; + +} diff --git a/sfall/ddraw.vcxproj b/sfall/ddraw.vcxproj index 9b14d50f..d377513e 100644 --- a/sfall/ddraw.vcxproj +++ b/sfall/ddraw.vcxproj @@ -335,6 +335,7 @@ + @@ -449,6 +450,7 @@ + diff --git a/sfall/ddraw.vcxproj.filters b/sfall/ddraw.vcxproj.filters index 8daa8cee..6a618c96 100644 --- a/sfall/ddraw.vcxproj.filters +++ b/sfall/ddraw.vcxproj.filters @@ -364,6 +364,9 @@ Game\AI + + Modules + @@ -669,6 +672,9 @@ Game\AI + + Modules + diff --git a/sfall/main.cpp b/sfall/main.cpp index d2cf47e6..436733e0 100644 --- a/sfall/main.cpp +++ b/sfall/main.cpp @@ -74,6 +74,7 @@ #include "Modules\Stats.h" #include "Modules\TalkingHeads.h" #include "Modules\Tiles.h" +#include "Modules\Unarmed.h" #include "Modules\Worldmap.h" #include "CRC.h" @@ -121,6 +122,7 @@ static void InitModules() { manager.add(); manager.add(); manager.add(); + manager.add(); manager.add(); manager.add();