void RandomizeEnemySpeed(int enemyID) { // Randomizes speeds of relevant animations in enemy TAE. Also adjusts attack damage based on new speed (excludes Bullets). if (enemyID == 0) { throw new ArgumentException("`RandomizeEnemySpeed` is not for the player."); } TAE tae = Mod[enemyID].GetTAE(enemyID); foreach (var animation in tae.Animations) { var info = new NPCAnimationInfo(animation, enemyID, Mod); if (info.HasInvokeBehaviorEvent && 3000 <= animation.ID && animation.ID <= 3999) { RandomizeBehaviorAnimationSpeed(animation, info); } } }
void RandomizeBehaviorAnimationSpeed(TAE.Animation behaviorAnim, NPCAnimationInfo info, bool isPlayer = false) { /* Apply random speed modifiers throughout given attack animation (includes behaviors that trigger Bullets and SpEffects). * - The earliest possible time for the hitbox behavior to start is Min(oldStartTime, Max(0.5, 0.5 * oldStartTime, hitboxRadius)). * - The latest possible time for the hitbox is 0.5 * (oldStart - minStart), clamped between [minMax] and [maxMax]. * - RoryAlgorithm is more likely to add speed to valid frames that already have more speed, which leads to less noisy functions. * - Hitbox radius of bullets is estimated, but does not take projectile speed into account. Fast, small bullets may see large speed boosts. * - Non-Bullet attack damage is scaled by [minAttackScale] (if earliest possible time) to [maxAttackScale] (if latest possible time), rounded to the nearest 0.1. * - Bullets themselves are unchanged here and randomized separately. */ float hitboxRadius = 1.0f; // default float behaviorStartTime = -1.0f; float behaviorEndTime = -1.0f; if (info.InvokeAttackEvent != null) { Behavior attackBehavior = info.GetAttackBehavior(); if (attackBehavior == null) { return; // Missing Behavior param, which means it is most likely unused. No randomization. } Attack attack = info.GetAttack(); if (attack == null) { return; // Missing Attack param, which means it is most likely unused. No randomization. } hitboxRadius = attack.Hitbox0Radius; behaviorStartTime = info.InvokeAttackEvent.StartTime; behaviorEndTime = info.InvokeAttackEvent.EndTime; } else if (info.InvokeBulletEvent != null) { Behavior bulletBehavior = info.GetBulletBehavior(); if (bulletBehavior == null) { return; // Missing Behavior param, which means it is most likely unused. No randomization. } if (bulletBehavior.ReferenceType == 1) { Bullet bullet = info.GetBullet(); if (bullet == null) { return; // Missing Bullet param. } hitboxRadius = Tools.GuessBulletRadius(bullet, Mod); // Console.WriteLine($" Final bullet ({bullet.ID}) radius of animation {behaviorAnim.ID}: {hitboxRadius:0.00}"); if (hitboxRadius == -1.0f) { hitboxRadius = 1.0f; // default for bullets with no final radius } } else if (bulletBehavior.ReferenceType == 2) { SpEffect spEffect = info.GetSpEffect(); if (spEffect == null) { return; // Missing SpEffect param. } hitboxRadius = 1.0f; // leave as default for SpEffect } behaviorStartTime = info.InvokeBulletEvent.StartTime; behaviorEndTime = info.InvokeBulletEvent.EndTime; } if (behaviorStartTime == -1.0f || behaviorEndTime == -1.0f) { throw new ArgumentException($"Behavior start/end times were not set."); } float minBehaviorStartTime = Math.Min(behaviorStartTime, Math.Max(0.5f, Math.Max(0.5f * behaviorStartTime, MinAttackBehaviorHitboxScalar * hitboxRadius))); float maxBehaviorStartTime = behaviorStartTime + Math.Max(MinMaxAttackBehaviorDelay, Math.Min(0.5f * (behaviorStartTime - minBehaviorStartTime), MaxMaxAttackBehaviorDelay)); float newBehaviorStartTime = minBehaviorStartTime + (float)Rand.NextDouble() * (maxBehaviorStartTime - minBehaviorStartTime); List <int> preAttackSpeedFunction = GetRandomSpeedFunction(0.0f, behaviorStartTime, newBehaviorStartTime); ApplySpeedFunction(behaviorAnim, preAttackSpeedFunction); if (DEBUG) { Console.WriteLine($"\nANIMATION {behaviorAnim.ID}"); Console.WriteLine($" Attack start time: {behaviorStartTime} => {newBehaviorStartTime}"); Console.WriteLine($" Random min/max: {minBehaviorStartTime}, {maxBehaviorStartTime}"); Tools.DrawSpeedFunction(preAttackSpeedFunction, MaxSpeedMultiplier, QuantizedSpeed); } if (info.InvokeAttackEvent != null) { float attackPowerFactor = (newBehaviorStartTime - minBehaviorStartTime) / (maxBehaviorStartTime - minBehaviorStartTime); float attackDamageMultiplier = MinAttackScale + attackPowerFactor * (MaxAttackScale - MinAttackScale); ApplySpeedAttackDamageMultiplier(behaviorAnim, attackDamageMultiplier, behaviorStartTime - FrameDuration, behaviorEndTime + FrameDuration); } // Mild random speed change during InvokeBehaviorEvent itself (multiplier of 1.0, 1.1, or 1.2). int duringSpeedOptionCount = (int)(0.2f / QuantizedSpeed) + 1; float duringAttackSpeed = 1.0f + (Rand.Next(duringSpeedOptionCount) * QuantizedSpeed); int duringSpEffectID = SpeedEffects[(float)Math.Round(duringAttackSpeed, 2)]; behaviorAnim.ApplyEffect(duringSpEffectID, behaviorStartTime, behaviorEndTime); // Post-attack speed function (if cancel event is present to approximate end of animation). if (info.AnimationCancelEventEnd != -1.0f) { float animationEndTime = info.AnimationCancelEventEnd; float minEndRealTime = behaviorEndTime + (0.5f * (animationEndTime - behaviorEndTime)); float maxEndRealTime = behaviorEndTime + (1.3f * (animationEndTime - behaviorEndTime)); float newAnimationEndTime = minEndRealTime + (float)Rand.NextDouble() * (maxEndRealTime - minEndRealTime); if (DEBUG) { Console.WriteLine($" Attack end time: {behaviorEndTime} (before pre-attack speed change)"); Console.WriteLine($" Animation end time: {animationEndTime} => {newAnimationEndTime}"); Console.WriteLine($" Random min/max: {minEndRealTime}, {maxEndRealTime}"); } List <int> postAttackSpeedFunction = GetRandomSpeedFunction(behaviorEndTime, animationEndTime, newAnimationEndTime); ApplySpeedFunction(behaviorAnim, postAttackSpeedFunction); } }