/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System.Linq; using CultMath; using static CultMath.math; // Cut 1 (docs/fire-control-cut.md): geometry -- whether a weapon bears on a point is decided here and // nowhere else -- no prefab, transform, ArticulationPoint or renderer may influence it (R6, R7). Mount // direction and arc are both derived per read from catalog data and the item's authored rotation; neither // is ever stored on the entity. // Cut 2 adds the reveal rule (R5) and the resolved targeting-system stats an entity fires with, both // likewise derived on every read and never cached. public static class FireControl { // The planar unit vector a hardpoint's weapon points along, derived solely from the item's authored // rotation (R6, the same convention reaction thrusters already use) -- never from a live Unity // transform. This is the item-rotation path Behavior.Direction now uses unconditionally, after the // HardpointTransforms readback branch is deleted. public static float3 MountDirection(EquippedItem item) { var itemDirection = item.Entity.Direction.Rotate(item.EquippableItem.Rotation); return normalize(float3(itemDirection.x, 0, itemDirection.y)); } // Full arc width in degrees: the per-hardpoint override when authored (nonzero), else // GameplaySettings.FiringArc. Zero on the hardpoint means "use the default," not "no arc." public static float ArcFor(EquippedItem item) { var hardpoint = item.Entity.Hardpoints[item.Position.x, item.Position.y]; var arc = hardpoint?.FiringArc ?? 0f; return arc > 0 ? arc : item.ItemManager.GameplaySettings.FiringArc; } // Planar bearing test (R7: the simulation is 2D, the 3D is set dressing -- target height never enters // this). An arc of 360 degrees or more passes unconditionally, which is how a turret hardpoint // (authored FiringArc: 360, Q1) tracks all the way around without a second type. public static bool InArc(EquippedItem weapon, float3 toTarget) { var arc = ArcFor(weapon); if (arc >= 360f) return true; var planarTarget = normalize(float3(toTarget.x, 0, toTarget.z)); return dot(MountDirection(weapon), planarTarget) >= cos(radians(arc / 2f)); } // Cut 2, R5: whether `observer` has resolved `item` (which must belong to some entity in the zone) well // enough to aim at it. Derived fresh from EntityInfoGathered on every call -- nothing caches a reveal // result, so decay drops it the moment info falls back below tier, with no clearing loop anywhere. // // Ranking: the target's own non-hull equipment, hardpoint-mounted gear first (the big obvious things), // then by size descending, ties broken by equipment order (OrderByDescending is a stable sort, so this // needs no explicit tie-break key). Item i of N reveals once info crosses // lerp(TargetArmorInfoThreshold, TargetGearInfoThreshold, i / max(1, N - 1)) -- the first item to reveal // does so at the armor tier, the last at the gear tier. public static bool IsRevealed(Entity observer, EquippedItem item) { var target = item.Entity; var ranked = target.Equipment .Where(e => e.Data.HardpointType != HardpointType.Hull) .OrderByDescending(e => e.Data.HardpointType != HardpointType.Tool) .ThenByDescending(e => e.Data.Shape.Coordinates.Length) .ToList(); var index = ranked.IndexOf(item); if (index < 0) return false; // the hull itself, or an item that is no longer on this target var settings = observer.ItemManager.GameplaySettings; var info = observer.EntityInfoGathered.TryGetValue(target, out var gathered) ? gathered : 0f; var tier = lerp(settings.TargetArmorInfoThreshold, settings.TargetGearInfoThreshold, (float) index / max(1, ranked.Count - 1)); return info >= tier; } // Cut 2, Q4: the stats an entity actually fires with. A working targeting system (equipped, online -- // not destroyed, not shut down) supplies its own resolved stats; anything else falls back to the // authored unaided figures, which make aiming without one a last resort rather than an alternative. // Cut 3's roll is the only planned reader. public static float Accuracy(Entity entity) { var system = entity.GetBehavior(); return system != null && system.Item.Active.Value ? system.Accuracy : entity.ItemManager.GameplaySettings.UnaidedAccuracy; } public static float Resolution(Entity entity) { var system = entity.GetBehavior(); return system != null && system.Item.Active.Value ? system.Resolution : 1f; } public static float Precision(Entity entity) { var system = entity.GetBehavior(); return system != null && system.Item.Active.Value ? system.Precision : 0f; } public static float Tracking(Entity entity) { var system = entity.GetBehavior(); return system != null && system.Item.Active.Value ? system.Tracking : 0f; } }