diff --git a/src/character_editor.cc b/src/character_editor.cc index f01bc91..fc14d74 100644 --- a/src/character_editor.cc +++ b/src/character_editor.cc @@ -2,6 +2,7 @@ #include "art.h" #include "color.h" +#include "combat.h" #include "core.h" #include "critter.h" #include "cycle.h" @@ -2801,7 +2802,13 @@ static void characterEditorDrawDerivedStats() sprintf(t, "%s", messageListItemText); fontDrawText(gCharacterEditorWindowBuffer + 640 * y + 194, t, 640, 640, color); - compat_itoa(critterGetStat(gDude, STAT_MELEE_DAMAGE), t, 10); + // SFALL: Display melee damage without "Bonus HtH Damage" bonus. + int meleeDamage = critterGetStat(gDude, STAT_MELEE_DAMAGE); + if (!damageModGetDisplayBonusDamage()) { + meleeDamage -= 2 * perkGetRank(gDude, PERK_BONUS_HTH_DAMAGE); + } + + compat_itoa(meleeDamage, t, 10); fontDrawText(gCharacterEditorWindowBuffer + 640 * y + 288, t, 640, 640, color); // Damage Resistance @@ -4404,13 +4411,19 @@ static int characterPrintToFile(const char* fileName) fileWriteString(title1, stream); fileWriteString("\n", stream); + // SFALL: Display melee damage without "Bonus HtH Damage" bonus. + int meleeDamage = critterGetStat(gDude, STAT_MELEE_DAMAGE); + if (!damageModGetDisplayBonusDamage()) { + meleeDamage -= 2 * perkGetRank(gDude, PERK_BONUS_HTH_DAMAGE); + } + // Charisma / Melee Damage / Carry Weight sprintf(title1, "%s %.2d %s %.2d %s %.3d lbs.", getmsg(&gCharacterEditorMessageList, &gCharacterEditorMessageListItem, 633), critterGetStat(gDude, STAT_CHARISMA), getmsg(&gCharacterEditorMessageList, &gCharacterEditorMessageListItem, 634), - critterGetStat(gDude, STAT_MELEE_DAMAGE), + meleeDamage, getmsg(&gCharacterEditorMessageList, &gCharacterEditorMessageListItem, 635), critterGetStat(gDude, STAT_CARRY_WEIGHT)); fileWriteString(title1, stream); diff --git a/src/combat.cc b/src/combat.cc index 9aab99c..cc72f77 100644 --- a/src/combat.cc +++ b/src/combat.cc @@ -47,6 +47,13 @@ #define CALLED_SHOT_WINDOW_WIDTH (504) #define CALLED_SHOT_WINDOW_HEIGHT (309) +typedef enum DamageCalculationType { + DAMAGE_CALCULATION_TYPE_VANILLA = 0, + DAMAGE_CALCULATION_TYPE_GLOVZ = 1, + DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK = 2, + DAMAGE_CALCULATION_TYPE_YAAM = 5, +} DamageCalculationType; + typedef struct CombatAiInfo { Object* friendlyDead; Object* lastTarget; @@ -67,6 +74,17 @@ typedef struct UnarmedHitDescription { bool isSecondary; } UnarmedHitDescription; +typedef struct DamageCalculationContext { + Attack* attack; + int* damagePtr; + int ammoQuantity; + int damageResistance; + int damageThreshold; + int damageBonus; + int bonusDamageMultiplier; + int combatDifficultyDamageModifier; +} DamageCalculationContext; + static bool _combat_safety_invalidate_weapon_func(Object* critter, Object* weapon, int hitMode, Object* a4, int* a5, Object* a6); static int aiInfoCopy(int srcIndex, int destIndex); static void _combat_begin(Object* a1); @@ -113,6 +131,10 @@ static void unarmedInit(); static void unarmedInitVanilla(); static void unarmedInitCustom(); static int unarmedGetHitModeInRange(int firstHitMode, int lastHitMode, bool isSecondary); +static void damageModInit(); +static void damageModCalculateGlovz(DamageCalculationContext* context); +static int damageModGlovzDivRound(int dividend, int divisor); +static void damageModCalculateYaam(DamageCalculationContext* context); // 0x500B50 static char _a_1[] = "."; @@ -1950,6 +1972,9 @@ static int gBurstModCenterDivisor = SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_DIVISO static int gBurstModTargetMultiplier = SFALL_CONFIG_BURST_MOD_DEFAULT_TARGET_MULTIPLIER; static int gBurstModTargetDivisor = SFALL_CONFIG_BURST_MOD_DEFAULT_TARGET_DIVISOR; static UnarmedHitDescription gUnarmedHitDescriptions[HIT_MODE_COUNT]; +static int gDamageCalculationType; +static bool gBonusHthDamageFix; +static bool gDisplayBonusDamage; // combat_init // 0x420CC0 @@ -1993,6 +2018,7 @@ int combatInit() criticalsInit(); burstModInit(); unarmedInit(); + damageModInit(); return 0; } @@ -4464,41 +4490,57 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag } } - damageResistance += weaponGetAmmoDamageResistanceModifier(attack->weapon); - if (damageResistance > 100) { - damageResistance = 100; - } else if (damageResistance < 0) { - damageResistance = 0; - } + // SFALL: Damage mod. + DamageCalculationContext context; + context.attack = attack; + context.damagePtr = damagePtr; + context.damageResistance = damageResistance; + context.damageThreshold = damageThreshold; + context.damageBonus = damageBonus; + context.bonusDamageMultiplier = bonusDamageMultiplier; + context.combatDifficultyDamageModifier = combatDifficultyDamageModifier; - int damageMultiplier = bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(attack->weapon); - int damageDivisor = weaponGetAmmoDamageDivisor(attack->weapon); - - for (int index = 0; index < ammoQuantity; index++) { - int damage = weaponGetMeleeDamage(attack->attacker, attack->hitMode); - - damage += damageBonus; - - damage *= damageMultiplier; - - if (damageDivisor != 0) { - damage /= damageDivisor; + if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ || gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK) { + damageModCalculateGlovz(&context); + } else if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_YAAM) { + damageModCalculateYaam(&context); + } else { + damageResistance += weaponGetAmmoDamageResistanceModifier(attack->weapon); + if (damageResistance > 100) { + damageResistance = 100; + } else if (damageResistance < 0) { + damageResistance = 0; } - // TODO: Why we're halving it? - damage /= 2; + int damageMultiplier = bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(attack->weapon); + int damageDivisor = weaponGetAmmoDamageDivisor(attack->weapon); - damage *= combatDifficultyDamageModifier; - damage /= 100; + for (int index = 0; index < ammoQuantity; index++) { + int damage = weaponGetMeleeDamage(attack->attacker, attack->hitMode); - damage -= damageThreshold; + damage += damageBonus; - if (damage > 0) { - damage -= damage * damageResistance / 100; - } + damage *= damageMultiplier; - if (damage > 0) { - *damagePtr += damage; + if (damageDivisor != 0) { + damage /= damageDivisor; + } + + // TODO: Why we're halving it? + damage /= 2; + + damage *= combatDifficultyDamageModifier; + damage /= 100; + + damage -= damageThreshold; + + if (damage > 0) { + damage -= damage * damageResistance / 100; + } + + if (damage > 0) { + *damagePtr += damage; + } } } @@ -6508,3 +6550,180 @@ static int unarmedGetHitModeInRange(int firstHitMode, int lastHitMode, bool isSe return hitMode; } + +static void damageModInit() +{ + gDamageCalculationType = DAMAGE_CALCULATION_TYPE_VANILLA; + configGetInt(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_DAMAGE_MOD_FORMULA_KEY, &gDamageCalculationType); + + gBonusHthDamageFix = true; + configGetBool(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_BONUS_HTH_DAMAGE_FIX_KEY, &gBonusHthDamageFix); + + gDisplayBonusDamage = false; + configGetBool(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_DISPLAY_BONUS_DAMAGE_KEY, &gDisplayBonusDamage); +} + +bool damageModGetBonusHthDamageFix() +{ + return gBonusHthDamageFix; +} + +bool damageModGetDisplayBonusDamage() +{ + return gDisplayBonusDamage; +} + +static void damageModCalculateGlovz(DamageCalculationContext* context) +{ + int ammoX = weaponGetAmmoDamageMultiplier(context->attack->weapon); + if (ammoX <= 0) { + ammoX = 1; + } + + int ammoY = weaponGetAmmoDamageDivisor(context->attack->weapon); + if (ammoY <= 0) { + ammoY = 1; + } + + int ammoDamageResistance = weaponGetAmmoDamageResistanceModifier(context->attack->weapon); + if (ammoDamageResistance > 0) { + ammoDamageResistance = -ammoDamageResistance; + } + + int calculatedDamageThreshold = context->damageThreshold; + if (calculatedDamageThreshold > 0) { + calculatedDamageThreshold = damageModGlovzDivRound(calculatedDamageThreshold, ammoY); + } + + int calculatedDamageResistance = context->damageResistance; + if (calculatedDamageResistance > 0) { + if (context->combatDifficultyDamageModifier > 100) { + calculatedDamageResistance -= 20; + } else if (context->combatDifficultyDamageModifier < 100) { + calculatedDamageResistance += 20; + } + + calculatedDamageResistance += ammoDamageResistance; + + calculatedDamageResistance = damageModGlovzDivRound(calculatedDamageResistance, ammoX); + + if (calculatedDamageResistance >= 100) { + return; + } + } + + for (int index = 0; index < context->ammoQuantity; index++) { + int damage = weaponGetMeleeDamage(context->attack->attacker, context->attack->hitMode); + + damage += context->damageBonus; + if (damage <= 0) { + continue; + } + + if (context->damageThreshold > 0) { + damage -= calculatedDamageThreshold; + if (damage <= 0) { + continue; + } + } + + if (context->damageResistance > 0) { + damage -= damageModGlovzDivRound(damage * calculatedDamageResistance, 100); + if (damage <= 0) { + continue; + } + } + + if (context->damageThreshold <= 0 && context->damageResistance <= 0) { + if (ammoX > 1 && ammoY > 1) { + damage += damageModGlovzDivRound(damage * 15, 100); + } else if (ammoX > 1) { + damage += damageModGlovzDivRound(damage * 20, 100); + } else if (ammoY > 1) { + damage += damageModGlovzDivRound(damage * 10, 100); + } + } + + if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK) { + damage += damageModGlovzDivRound(damage * context->bonusDamageMultiplier * 25, 100); + } else { + damage += damage * context->bonusDamageMultiplier / 2; + } + + if (damage > 0) { + *context->damagePtr += damage; + } + } +} + +static int damageModGlovzDivRound(int dividend, int divisor) +{ + if (dividend < divisor) { + return dividend != divisor && dividend * 2 <= divisor ? 0 : 1; + } + + int quotient = dividend / divisor; + dividend %= divisor; + + if (dividend == 0) { + return quotient; + } + + dividend *= 2; + + if (dividend > divisor || (dividend == divisor && (quotient & 1) != 0)) { + quotient += 1; + } + + return quotient; +} + +static void damageModCalculateYaam(DamageCalculationContext* context) +{ + int damageMultiplier = context->bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(context->attack->weapon); + int damageDivisor = weaponGetAmmoDamageDivisor(context->attack->weapon); + + int ammoDamageResistance = weaponGetAmmoDamageResistanceModifier(context->attack->weapon); + + int calculatedDamageThreshold = context->damageThreshold - ammoDamageResistance; + int damageResistance = calculatedDamageThreshold; + + if (calculatedDamageThreshold >= 0) { + damageResistance = 0; + } else { + calculatedDamageThreshold = 0; + damageResistance *= 10; + } + + int calculatedDamageResistance = context->damageResistance + damageResistance; + if (calculatedDamageResistance < 0) { + calculatedDamageResistance = 0; + } else if (calculatedDamageResistance >= 100) { + return; + } + + for (int index = 0; index < context->ammoQuantity; index++) { + int damage = weaponGetMeleeDamage(context->attack->weapon, context->attack->hitMode); + damage += context->damageBonus; + + damage -= calculatedDamageThreshold; + if (damage <= 0) { + continue; + } + + damage *= damageMultiplier; + if (damageDivisor != 0) { + damage /= damageDivisor; + } + + damage /= 2; + damage *= context->combatDifficultyDamageModifier; + damage /= 100; + + damage -= damage * damageResistance / 100; + + if (damage > 0) { + context->damagePtr += damage; + } + } +} diff --git a/src/combat.h b/src/combat.h index 652c3fb..158a76e 100644 --- a/src/combat.h +++ b/src/combat.h @@ -66,6 +66,8 @@ bool unarmedIsPenetrating(int hitMode); int unarmedGetPunchHitMode(bool isSecondary); int unarmedGetKickHitMode(bool isSecondary); bool unarmedIsPenetrating(int hitMode); +bool damageModGetBonusHthDamageFix(); +bool damageModGetDisplayBonusDamage(); static inline bool isInCombat() { diff --git a/src/inventory.cc b/src/inventory.cc index 9beaaa5..b9ffe05 100644 --- a/src/inventory.cc +++ b/src/inventory.cc @@ -2544,6 +2544,11 @@ static void inventoryRenderSummary() int meleeDamage; if (attackType == ATTACK_TYPE_MELEE || attackType == ATTACK_TYPE_UNARMED) { meleeDamage = critterGetStat(_stack[0], STAT_MELEE_DAMAGE); + + // SFALL: Display melee damage without "Bonus HtH Damage" bonus. + if (damageModGetBonusHthDamageFix() && !damageModGetDisplayBonusDamage()) { + meleeDamage -= 2 * perkGetRank(gDude, PERK_BONUS_HTH_DAMAGE); + } } else { meleeDamage = 0; } @@ -2551,11 +2556,33 @@ static void inventoryRenderSummary() messageListItem.num = 15; // Dmg: if (messageListGetItem(&gInventoryMessageList, &messageListItem)) { if (attackType != 4 && range <= 1) { + // SFALL: Display bonus damage. + if (damageModGetBonusHthDamageFix() && damageModGetDisplayBonusDamage()) { + // CE: Just in case check for attack type, however it looks + // like we cannot be here with anything besides melee or + // unarmed. + if (_stack[0] == gDude && (attackType == ATTACK_TYPE_MELEE || attackType == ATTACK_TYPE_UNARMED)) { + // See explanation in `weaponGetMeleeDamage`. + damageMin += 2 * perkGetRank(gDude, PERK_BONUS_HTH_DAMAGE); + } + } sprintf(formattedText, "%s %d-%d", messageListItem.text, damageMin, damageMax + meleeDamage); } else { MessageListItem rangeMessageListItem; rangeMessageListItem.num = 16; // Rng: if (messageListGetItem(&gInventoryMessageList, &rangeMessageListItem)) { + // SFALL: Display bonus damage. + if (damageModGetDisplayBonusDamage()) { + // CE: There is a bug in Sfall diplaying wrong damage + // bonus for melee weapons with range > 1 (spears, + // sledgehammers) and throwables (secondary mode). + if (_stack[0] == gDude && attackType == ATTACK_TYPE_RANGED) { + int damageBonus = 2 * perkGetRank(gDude, PERK_BONUS_RANGED_DAMAGE); + damageMin += damageBonus; + damageMax += damageBonus; + } + } + sprintf(formattedText, "%s %d-%d %s %d", messageListItem.text, damageMin, damageMax + meleeDamage, rangeMessageListItem.text, range); } } diff --git a/src/item.cc b/src/item.cc index f4d3489..c688f2e 100644 --- a/src/item.cc +++ b/src/item.cc @@ -1267,20 +1267,35 @@ int weaponGetMeleeDamage(Object* critter, int hitMode) Object* weapon = critterGetWeaponForHitMode(critter, hitMode); if (weapon != NULL) { - Proto* proto; - protoGetProto(weapon->pid, &proto); - - minDamage = proto->item.data.weapon.minDamage; - maxDamage = proto->item.data.weapon.maxDamage; + // NOTE: Uninline. + weaponGetDamageMinMax(weapon, &minDamage, &maxDamage); int attackType = weaponGetAttackTypeForHitMode(weapon, hitMode); if (attackType == ATTACK_TYPE_MELEE || attackType == ATTACK_TYPE_UNARMED) { meleeDamage = critterGetStat(critter, STAT_MELEE_DAMAGE); + + // SFALL: Bonus HtH Damage fix. + if (damageModGetBonusHthDamageFix()) { + if (critter == gDude) { + // See explanation below. + minDamage += 2 * perkGetRank(gDude, PERK_BONUS_HTH_DAMAGE); + } + } } } else { // SFALL bonusDamage = unarmedGetDamage(hitMode, &minDamage, &maxDamage); meleeDamage = critterGetStat(critter, STAT_MELEE_DAMAGE); + + // SFALL: Bonus HtH Damage fix. + if (damageModGetBonusHthDamageFix()) { + if (critter == gDude) { + // Increase only min damage. Max damage should not be changed. + // It is calculated later by adding `meleeDamage` which already + // includes damage bonus (via `perkAddEffect`). + minDamage += 2 * perkGetRank(gDude, PERK_BONUS_HTH_DAMAGE); + } + } } return randomBetween(bonusDamage + minDamage, bonusDamage + meleeDamage + maxDamage); diff --git a/src/sfall_config.h b/src/sfall_config.h index 569cf09..865a5ad 100644 --- a/src/sfall_config.h +++ b/src/sfall_config.h @@ -42,6 +42,9 @@ #define SFALL_CONFIG_EXPLOSION_EMITS_LIGHT_KEY "ExplosionsEmitLight" #define SFALL_CONFIG_CITY_REPUTATION_LIST_KEY "CityRepsList" #define SFALL_CONFIG_UNARMED_FILE_KEY "UnarmedFile" +#define SFALL_CONFIG_DAMAGE_MOD_FORMULA_KEY "DamageFormula" +#define SFALL_CONFIG_BONUS_HTH_DAMAGE_FIX_KEY "BonusHtHDamageFix" +#define SFALL_CONFIG_DISPLAY_BONUS_DAMAGE_KEY "DisplayBonusDamage" #define SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_MULTIPLIER 1 #define SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_DIVISOR 3