Re: [MOD] Meaner Monsters
Posted: Mon Mar 02, 2020 3:03 pm
Myself at least, I would be "balancing" the monster stats around the fact that the player is now less carried by the weapon material in terms of hit-chance, so I would think possibly making a second version of the mod could be a thing, or even as you said renaming it to something more encompassing. Cause no matter what, the monster stats will NEED to be adjusted, because the default ones are just way to junky and random to be difficult, even if they are harder to hit, like the damage and health values for even the moderately high level monsters just makes no sense. They seemed to think that 5-15 damage was some sort of sweet spot, but it's pretty pathetic when you look at even the more flimsy character builds at that level, same goes with most of the monster health values.I am debating with myself if I should include this in Meaner Monsters or LevelUp Adjuster (which I'm thinking of renaming to just "Mechanics Adjuster"). Problem is, I'll balance Meaner Monsters for the modded mechanic, so having them seperate could be awkward.
It's all still up in the air, but I'm thinking there should be at least one mod alternative to change how combat is calculated.Hazelnut wrote: ↑Mon Mar 02, 2020 8:54 pm Wow guys... I didn't have time to respond before work today and wanted to mull it over anyway. In the meantime you seem to have made some progress/decisions?
Anyway I am troubled by the modifiers added for weapons, I can only assume this matches classic (seems to match UESP at least) but it seems like material to hit mods are a bit over the top. This is something I would consider rebalancing as a module to R&R to promote role playing. Prior to this, except for re-factoring the formula by request (UV IIRC) I've never looked too hard at this calculation.
Yeah, I was not really suggesting that the formula is inaccurate to what Classic had, but just from the perspective of a player, the weapon material modifier is just vastly more impactful to accuracy compared to other elements that, in my opinion, would make more sense, as well as make the game more difficult throughout the game, rather than just becoming a non-factor once you get closer to the max level. If anything, once the player gets a Daedric weapon, almost no matter what their skills are, they will be hitting the cap of 97% against most enemies. To me, this is just a bit crazy and explains why Daggerfall on DOS started to become so crazy easy once you got better weapons, you just stun-lock the dumb AI death from ALWAYS hitting them.Hazelnut wrote: ↑Mon Mar 02, 2020 8:54 pm Wow guys... I didn't have time to respond before work today and wanted to mull it over anyway. In the meantime you seem to have made some progress/decisions?
Anyway I am troubled by the modifiers added for weapons, I can only assume this matches classic (seems to match UESP at least) but it seems like material to hit mods are a bit over the top. This is something I would consider rebalancing as a module to R&R to promote role playing. Prior to this, except for re-factoring the formula by request (UV IIRC) I've never looked too hard at this calculation.
Code: Select all
#region Formula Overrides
/// <summary>
/// Registers an override for a formula using a generic `System.Func{T}` callback
/// with the same signature as the method it overrides
/// (i.e. `RegisterOverride{Func{int, int, float}}("FormulaName", (a, b) => (float)a / b);`).
/// The invocation will fail if signature is not correct, meaning if the delegate
/// is not one of the variation of `Func` with the expected arguments.
/// </summary>
/// <param name="provider">The mod that provides this override; used to enforce load order.</param>
/// <param name="formulaName">The name of the method that provides the formula.</param>
/// <param name="formula">A callback that implements the formula.</param>
/// <typeparam name="TDelegate">One of the available generic Func delegates.</typeparam>
/// <exception cref="ArgumentNullException">`formulaName` or `formula` is null.</exception>
/// <exception cref="InvalidCastException">Type is not a delegate.</exception>
public static void RegisterOverride<TDelegate>(Mod provider, string formulaName, TDelegate formula)
where TDelegate : class
{
if (formulaName == null)
throw new ArgumentNullException("formulaName");
if (formula == null)
throw new ArgumentNullException("formula");
var del = formula as Delegate;
if (del == null)
throw new InvalidCastException("formula is not a delegate.");
FormulaOverride formulaOverride;
if (!overrides.TryGetValue(formulaName, out formulaOverride) || formulaOverride.Provider.LoadPriority < provider.LoadPriority)
overrides[formulaName] = new FormulaOverride(del, provider);
}
/// <summary>
/// Gets an override for a formula.
/// </summary>
/// <param name="formulaName">The name of the method that provides the formula.</param>
/// <param name="formula">A callback that implements the formula.</param>
/// <typeparam name="TDelegate">One of the available generic Func delegates.</typeparam>
/// <returns>True if formula is found.</returns>
private static bool TryGetOverride<TDelegate>(string formulaName, out TDelegate formula)
where TDelegate : class
{
FormulaOverride formulaOverride;
if (overrides.TryGetValue(formulaName, out formulaOverride))
{
if ((formula = formulaOverride.Formula as TDelegate) != null)
return true;
const string errorMessage = "Removed override for formula {0} provided by {1} because signature doesn't match (expected {2} and got {3}).";
Debug.LogErrorFormat(errorMessage, formulaName, formulaOverride.Provider.Title, typeof(TDelegate), formulaOverride.Formula.GetType());
overrides.Remove(formulaName);
}
formula = default(TDelegate);
return false;
}
#endregion
}
Code: Select all
FormulaHelper.RegisterOverride<TDelegate>(Mod_Name, string CalculateSuccessfulHit, TDelegate Modded_Formula){
"Modded Formula Code, mostly unchanged code from CalculateSuccessfulHit"
}
True.Hazelnut wrote: ↑Mon Mar 02, 2020 9:33 pm Ralzar, there's definitely more work to break down the combat formula to be more modular, but it can be done on a case by case basis. The focus was on reproducing classic and we need to be careful to preserve that effort while making it moddable. Overriding the big calc damage formula is really only appropriate for complete replacement mechanics which is why you found it so hard. It would be useful to me if you were to suggest logical blocks of formula for separation, however this is probably not the right place for that discussion.
It will work with either, but not both. I cannot recall if this was intentional or not, as this was mid 2018 I did this refactoring, but it seems sensible to me. I can already see several places where it needs refinement in its current form even before being broken down any more.
Code: Select all
// Project: UnleveledMobs mod for Daggerfall Unity (http://www.dfworkshop.net)
// Copyright: Copyright (C) 2019 JayH
// License: MIT License (http://www.opensource.org/licenses/mit-license.php)
// Author: JayH
// Modifier: Kirk.O (Minor Tweeks Done to most values, 3/2/2020, 12:00 PM EST)
using DaggerfallConnect;
using DaggerfallWorkshop;
using DaggerfallWorkshop.Game;
using DaggerfallWorkshop.Game.Formulas;
using DaggerfallWorkshop.Game.Utility.ModSupport;
using DaggerfallWorkshop.Utility;
using UnityEngine;
using System.Collections;
using System.Collections.Generic
namespace WeaponModBalancer
{
public class WeaponModBalancer : MonoBehaviour
{
static Mod mod;
[Invoke(StateManager.StateTypes.Start, 0)]
public static void Init(InitParams initParams)
{
mod = initParams.Mod;
var go = new GameObject(mod.Title);
go.AddComponent<WeaponModBalancer>();
}
void Awake()
{
InitMod();
mod.IsReady = true;
}
private static void InitMod()
{
Debug.Log("Begin mod init: WeaponModBalancer");
FormulaHelper.RegisterOverride<Func{DaggerfallEntity, DaggerfallEntity, int, int, DaggerfallUnityItem, int}>(WeaponModBalancer, AdjustWeaponHitChanceMod, ModdedHitChanceFormula);
public static int ModdedHitChanceFormula(attacker, target, chanceToHitMod, weaponAnimTime, weapon)
{
int chanceToHitMod = 0;
chanceToHitMod = attacker.Skills.GetLiveSkillValue(skillID);
if (attacker == player)
{
// Apply swing modifiers.
FPSWeapon onscreenWeapon = GameManager.Instance.WeaponManager.ScreenWeapon;
if (onscreenWeapon != null)
{
// The Daggerfall manual groups diagonal slashes to the left and right as if they are the same, but they are different.
// Classic does not apply swing modifiers to unarmed attacks.
if (onscreenWeapon.WeaponState == WeaponStates.StrikeUp)
{
damageModifiers += -4;
chanceToHitMod += 10;
}
if (onscreenWeapon.WeaponState == WeaponStates.StrikeDownRight)
{
damageModifiers += -2;
chanceToHitMod += 5;
}
if (onscreenWeapon.WeaponState == WeaponStates.StrikeDownLeft)
{
damageModifiers += 2;
chanceToHitMod += -5;
}
if (onscreenWeapon.WeaponState == WeaponStates.StrikeDown)
{
damageModifiers += 4;
chanceToHitMod += -10;
}
}
if (weapon != null)
{
// Apply weapon proficiency
if (((int)attacker.Career.ExpertProficiencies & weapon.GetWeaponSkillUsed()) != 0)
{
damageModifiers += ((attacker.Level / 3) + 1);
chanceToHitMod += attacker.Level;
}
}
// Apply hand-to-hand proficiency. Hand-to-hand proficiency is not applied in classic.
else if (((int)attacker.Career.ExpertProficiencies & (int)(DFCareer.ProficiencyFlags.HandToHand)) != 0)
{
damageModifiers += ((attacker.Level / 3) + 1);
chanceToHitMod += attacker.Level;
}
// Apply racial bonuses
if (weapon != null)
{
if (player.RaceTemplate.ID == (int)Races.DarkElf)
{
damageModifiers += (attacker.Level / 4);
chanceToHitMod += (attacker.Level / 4);
}
else if (skillID == (short)DFCareer.Skills.Archery)
{
if (player.RaceTemplate.ID == (int)Races.WoodElf)
{
damageModifiers += (attacker.Level / 3);
chanceToHitMod += (attacker.Level / 3);
}
}
else if (player.RaceTemplate.ID == (int)Races.Redguard)
{
damageModifiers += (attacker.Level / 3);
chanceToHitMod += (attacker.Level / 3);
}
}
backstabChance = CalculateBackstabChance(player, null, enemyAnimStateRecord);
chanceToHitMod += backstabChance;
}
// Choose struck body part
int struckBodyPart = CalculateStruckBodyPart();
// Get damage for weaponless attacks
if (skillID == (short)DFCareer.Skills.HandToHand)
{
if (attacker == player || (AIAttacker != null && AIAttacker.EntityType == EntityTypes.EnemyClass))
{
if (CalculateSuccessfulHit(attacker, target, chanceToHitMod, struckBodyPart) > 0)
{
minBaseDamage = CalculateHandToHandMinDamage(attacker.Skills.GetLiveSkillValue(DFCareer.Skills.HandToHand));
maxBaseDamage = CalculateHandToHandMaxDamage(attacker.Skills.GetLiveSkillValue(DFCareer.Skills.HandToHand));
damage = UnityEngine.Random.Range(minBaseDamage, maxBaseDamage + 1);
// Apply damage modifiers.
damage += damageModifiers;
// Apply strength modifier. It is not applied in classic despite what the in-game description for the Strength attribute says.
if (attacker == player)
damage += DamageModifier(attacker.Stats.LiveStrength);
// Handle backstabbing
damage = CalculateBackstabDamage(damage, backstabChance);
}
}
else if (AIAttacker != null) // attacker is monster
{
// Handle multiple attacks by AI
int attackNumber = 0;
while (attackNumber < 3) // Classic supports up to 5 attacks but no monster has more than 3
{
if (attackNumber == 0)
{
minBaseDamage = AIAttacker.MobileEnemy.MinDamage;
maxBaseDamage = AIAttacker.MobileEnemy.MaxDamage;
}
else if (attackNumber == 1)
{
minBaseDamage = AIAttacker.MobileEnemy.MinDamage2;
maxBaseDamage = AIAttacker.MobileEnemy.MaxDamage2;
}
else if (attackNumber == 2)
{
minBaseDamage = AIAttacker.MobileEnemy.MinDamage3;
maxBaseDamage = AIAttacker.MobileEnemy.MaxDamage3;
}
int reflexesChance = 50 - (10 * ((int)player.Reflexes - 2));
if (DFRandom.rand() % 100 < reflexesChance && minBaseDamage > 0 && CalculateSuccessfulHit(attacker, target, chanceToHitMod, struckBodyPart) > 0)
{
int hitDamage = UnityEngine.Random.Range(minBaseDamage, maxBaseDamage + 1);
// Apply special monster attack effects
if (hitDamage > 0)
OnMonsterHit(AIAttacker, target, hitDamage);
damage += hitDamage;
}
++attackNumber;
}
}
}
// Handle weapon attacks
else if (weapon != null)
{
// Apply weapon material modifier.
if (weapon.GetWeaponMaterialModifier() > 0)
{
chanceToHitMod += (weapon.GetWeaponMaterialModifier() * 10);
}
}
return chanceToHitMod;
}
Debug.Log("Finished mod init: WeaponModBalancer");
}
}
}