From aed269ad94190eeaabb3a14d1413dd87d9b997d9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 03:04:45 +0200 Subject: [PATCH] Fire clamps the delivery range to Range; smoke-rounds keeps the template's falloff ratio; fixtures follow the gate Not yet verified together. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/FireControl.cs | 5 +++-- .../FireControlPerWeaponTests.cs | 3 ++- tests/Aetheria.Shared.Tests/FireControlTests.cs | 5 ++++- tools/AetherDb/Program.cs | 12 +++++++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 4e1afeb84..5e3082066 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -723,8 +723,9 @@ public static class FireControl Target = engaged, Weapon = item, Aimed = source.ResolvedTargetItem, - // The one place a shot's damage meets range, frozen here: BurstReach is where the payload is delivered. - Damage = (damageOverride ?? weapon.Damage) * weapon.RangeFactor(solution.BurstReach), + // The one place a shot's damage meets range, frozen here: BurstReach is where the payload is delivered. A burst is + // never farther than Range (it is pulled back to it), so rounding in its distance must not read as beyond it. + Damage = (damageOverride ?? weapon.Damage) * weapon.RangeFactor(min(solution.BurstReach, weapon.Range)), Penetration = weapon.Penetration, DamageSpread = weapon.DamageSpread, DamageType = weapon.WeaponData.DamageType, diff --git a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs index ed8716cff..3bf5e08ce 100644 --- a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs @@ -588,7 +588,8 @@ public sealed class FireControlPerWeaponTests : IDisposable public void CombatDoesNotPickAGroupWhoseWeaponCannotReachTheTarget() { var r = Build(range: 10f, targetRange: 20f, decoy: true); - Assert.True(r.Gun.Weapon.RangeDamagePerSecond(20f) > r.Decoy.Weapon.RangeDamagePerSecond(20f), "fixture: the short gun is the stronger group"); + Assert.True(r.Gun.Weapon.RangeDamagePerSecond(5f) > r.Decoy.Weapon.RangeDamagePerSecond(5f), "fixture: the short gun is the stronger group"); + Assert.Equal(0f, r.Gun.Weapon.RangeDamagePerSecond(20f)); // the damage function already reads 0 beyond Range r.Zone.Agents.Add(new Minion(r.Shooter)); r.Zone.Update(1f); diff --git a/tests/Aetheria.Shared.Tests/FireControlTests.cs b/tests/Aetheria.Shared.Tests/FireControlTests.cs index 6044d1a23..3efcb9b58 100644 --- a/tests/Aetheria.Shared.Tests/FireControlTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlTests.cs @@ -377,8 +377,11 @@ public sealed class FireControlTests : IDisposable public void TheAIHoldsAtItsEffectiveRange() { var (shooter, _, zone) = BuildCombatEncounter(float3(0, 0, 10), effectiveRange: 500f); - zone.Update(1f); // enters CombatState, which samples DPS over the evaluated stats + zone.Update(1f); // enters CombatState, which samples before the weapons' first stat evaluation zone.Update(1f); + var item = shooter.Weapons.Single().Item; + item.Online.Value = false; // an Online change is what makes Combat sample again, over the evaluated stats + item.Online.Value = true; var agent = zone.Agents.Single(); var state = typeof(Agent).GetField("_currentState", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(agent); diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 775513333..141ffc6e7 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1774,6 +1774,12 @@ public static class Program return design; } static PerformanceStat Fixed(float value) => new PerformanceStat { Min = value, Max = value, Terms = new List() }; + // A refused weapon keeps its template's ratio of effective range to range, so range-falloff leaves it as authored. + static void Shorten(WeaponData weapon) + { + weapon.EffectiveRange = Fixed(20f * weapon.EffectiveRange.Max / weapon.Range.Max); + weapon.Range = Fixed(20f); + } var proximity = Copy("Spectra", names[0], "A Spectra that bursts by proximity, blast 8. For scenarios; no one builds it.", WeaponFuse.Proximity, 8f); var delayed = Copy("Spectra", names[1], "A Spectra that bursts after two cells of metal, blast 4. For scenarios; no one builds it.", WeaponFuse.Delayed, 4f); @@ -1791,15 +1797,15 @@ public static class Program data.Energy = Fixed(4f); } delayed.Behaviors.OfType().Single().Penetration = Fixed(2f); - refused.Behaviors.OfType().Single().Range = Fixed(20f); - charge.Behaviors.OfType().Single().Range = Fixed(20f); + Shorten(refused.Behaviors.OfType().Single()); + Shorten(charge.Behaviors.OfType().Single()); // The beam keeps the Spectra's gear (shape, hardpoint, thermal envelope, roles) and swaps its behaviour for a constant one. var instant = beam.Behaviors.OfType().Single(); var constant = new ConstantWeaponData { DamageType = instant.DamageType, Damage = instant.Damage, Penetration = instant.Penetration, DamageSpread = instant.DamageSpread, - MinRange = instant.MinRange, Range = Fixed(20f), EffectiveRange = instant.EffectiveRange, DamageAtMaxRange = instant.DamageAtMaxRange, EffectPrefab = instant.EffectPrefab, + MinRange = instant.MinRange, Range = Fixed(20f), EffectiveRange = Fixed(20f * instant.EffectiveRange.Max / instant.Range.Max), DamageAtMaxRange = instant.DamageAtMaxRange, EffectPrefab = instant.EffectPrefab, Energy = instant.Energy, Heat = instant.Heat, Visibility = instant.Visibility, AmmoType = instant.AmmoType, MagazineSize = instant.MagazineSize, ReloadTime = instant.ReloadTime, Spread = instant.Spread, Velocity = instant.Velocity, Tracking = instant.Tracking