From 52a18efcdc2ec7ec3c4a1ee6245095cdb3a49af8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 19 Sep 2026 13:02:37 +0200 Subject: [PATCH] Require a targeting system on every armed loadout; AI aims through reveal LoadoutGenerator.FillInterior equips a targeting-system product, required: true, on any entity EquipHardpoints already gave a weapon -- Q4's "not an upgrade" ruling made structural. CombatState now selects the target's most dangerous revealed weapon item each tick through the same IsRevealed predicate and TrySelectTargetItem writer the player path uses, instead of never touching TargetItem at all. Co-Authored-By: Claude Opus 5 --- .../ServerShared/Agents/States/Combat.cs | 19 ++++++++++++++++++ .../Scripts/ServerShared/LoadoutGenerator.cs | 20 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/Assets/Scripts/ServerShared/Agents/States/Combat.cs b/Assets/Scripts/ServerShared/Agents/States/Combat.cs index 6feeed610..0450c50d7 100644 --- a/Assets/Scripts/ServerShared/Agents/States/Combat.cs +++ b/Assets/Scripts/ServerShared/Agents/States/Combat.cs @@ -33,6 +33,25 @@ public class CombatState : BaseState var toTarget = target.Position - _agent.Ship.Position; var targetDistance = length(toTarget); + + // Cut 2 (docs/fire-control-cut.md): aim at whichever revealed weapon on the target hits hardest at + // this range, or clear the aim point when nothing qualifies -- the same predicate + // (FireControl.IsRevealed) and the same writer (TrySelectTargetItem) the player path uses. + EquippedItem bestTargetItem = null; + var bestTargetDps = float.MinValue; + foreach (var targetItem in target.Equipment) + { + if (!(targetItem.Behaviors.FirstOrDefault(b => b is Weapon) is Weapon targetWeapon)) continue; + if (!FireControl.IsRevealed(_agent.Ship, targetItem)) continue; + var dps = targetWeapon.RangeDamagePerSecond(targetDistance); + if (dps > bestTargetDps) + { + bestTargetDps = dps; + bestTargetItem = targetItem; + } + } + _agent.Ship.TrySelectTargetItem(bestTargetItem); + for (var i = 0; i < _agent.Ship.WeaponGroups.Length; i++) { var group = _agent.Ship.WeaponGroups[i]; diff --git a/Assets/Scripts/ServerShared/LoadoutGenerator.cs b/Assets/Scripts/ServerShared/LoadoutGenerator.cs index da432b7c2..242854d20 100644 --- a/Assets/Scripts/ServerShared/LoadoutGenerator.cs +++ b/Assets/Scripts/ServerShared/LoadoutGenerator.cs @@ -258,6 +258,26 @@ public class LoadoutGenerator capacitor.Rotation = capacitorRotation; if (!entity.TryEquip(capacitor, capacitorPosition)) throw new InvalidLoadoutException("Failed to equip selected capacitor!"); + + // Cut 2 (docs/fire-control-cut.md, Q4): required equipment for anything that can fire -- unaided + // fire is a last resort, not an alternative loadout choice. EquipHardpoints runs before FillInterior + // (OutfitEntity), so entity.Weapons is already populated here. Zenith and anything else with no + // weapon hardpoints gets none. + if (entity.Weapons.Any()) + { + emptyShape = entity.UnoccupiedSpace; + + var (targetingProduct, targetingData) = RandomProduct(2, item => + item.Behaviors.Any(b => b is TargetingSystemData) && + item.Shape.FitsWithin(emptyShape, out _, out _), required: true); + if (targetingData == null) throw new InvalidLoadoutException("No compatible targeting system found for entity!"); + + targetingData.Shape.FitsWithin(emptyShape, out var targetingRotation, out var targetingPosition); + var targetingSystem = ItemManager.CreateInstance(targetingProduct) as EquippableItem; + targetingSystem.Rotation = targetingRotation; + if (!entity.TryEquip(targetingSystem, targetingPosition)) + throw new InvalidLoadoutException("Failed to equip selected targeting system!"); + } } }