From f88d1fc1b3346fb1c31d596dee75116005ea198d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 13:02:26 +0200 Subject: [PATCH] Mining Cut 3 fix: launchers do not count toward rock reach (Q12 A) Weapon.CanMine is the one mining predicate; LockWeapon overrides it to false. Entity.VisibleChunksInReach counts only weapons that can mine. Tests: LaunchersDoNotCountTowardChunkReach (a launcher-only ship is offered no rock; a gun of 200 beside a launcher of 400 is offered the near rock only) and AnOfflineWeaponAddsNoReach (Soul F8: reach reads Active, not Enabled). Co-Authored-By: Claude Opus 5.5 --- .../ServerShared/Behaviors/LockWeapon.cs | 3 + .../Scripts/ServerShared/Behaviors/Weapon.cs | 5 ++ Assets/Scripts/ServerShared/Entity.cs | 8 +-- .../Aetheria.Shared.Tests/MiningCut3Tests.cs | 72 +++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs index a290d1481..6b88f4ba4 100644 --- a/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs @@ -65,6 +65,9 @@ public class LockWeapon : InstantWeapon // before a shot is ever attempted. public bool IsLocked => _lock > .99f; + // Q12 A: a launcher cannot mine. + public override bool CanMine => false; + public LockWeapon(LockWeaponData data, EquippedItem item) : base(data, item) { diff --git a/Assets/Scripts/ServerShared/Behaviors/Weapon.cs b/Assets/Scripts/ServerShared/Behaviors/Weapon.cs index b8f70699c..450490cfe 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Weapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Weapon.cs @@ -88,6 +88,11 @@ public abstract class Weapon : Behavior, IActivatedBehavior public float Spread { get; protected set; } public float Velocity { get; protected set; } + // Mining Cut 3 fix (Q12 A, "launchers cannot mine"): whether this weapon can mine a chunk. The one mining + // predicate: chunk reach (Entity.VisibleChunksInReach) reads it, and so does anything that decides a weapon + // may work a chunk. A launcher overrides it. + public virtual bool CanMine => true; + protected bool _firing; public bool Firing diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 48142c9ca..908741173 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -371,14 +371,14 @@ public abstract class Entity // enters VisibleEntities: its info exceeds TargetDetectionInfoThreshold. public bool ChunkVisible(ChunkId chunk) => ChunkInfo(chunk) > ItemManager.GameplaySettings.TargetDetectionInfoThreshold; - // Mining Cut 3 (Q13 A): the reach for picking a chunk -- the longest range among this entity's active weapons. - // Every visible chunk within it, written into `into` (cleared first). Cycling and any programmatic chunk pick - // read this, so there is one reach rule. + // Mining Cut 3 (Q13 A): the reach for picking a chunk -- the longest range among this entity's active weapons + // that can mine (Weapon.CanMine, Q12 A). Every visible chunk within it, written into `into` (cleared first). + // Cycling and any programmatic chunk pick read this, so there is one reach rule. public void VisibleChunksInReach(List into) { var reach = 0f; foreach (var weapon in _weapons) - if (weapon.Item == null || weapon.Item.Active.Value) + if (weapon.CanMine && (weapon.Item == null || weapon.Item.Active.Value)) reach = max(reach, weapon.Range); Zone.ChunksNear(Position.xz, reach, into); into.RemoveAll(chunk => !ChunkVisible(chunk)); diff --git a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs index 04536d829..1caee68d1 100644 --- a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs +++ b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs @@ -777,4 +777,76 @@ public sealed class MiningCut3Tests : IDisposable } Assert.True(belts > 0, "no seed generated a belt; the generation path went unexercised"); } + + // ==== Mining Cut 3 fix batch (Soul's findings) ==== + + // Q12 A ("launchers cannot mine") in the reach rule: reach counts only weapons that can mine (Weapon.CanMine). + // Two visible rocks, 150 and 250 away. A launcher of range 400 alone is offered neither; a gun of range 200 + // beside it is offered the near one only, so the launcher's range still counts for nothing. + [Fact] + public void LaunchersDoNotCountTowardChunkReach() + { + var kind = Kind("Asteroid", 40f); + var eyeAt = float2(150, -250); + var nearAt = float2(150, -100); + var s = BuildScene(new[] { kind }, Belt(kind, Rock(150f), Rock(length(nearAt), 1f + atan2(nearAt.y, nearAt.x) / (2 * PI)))); + var far = new ChunkId(s.Belts[0], 0); + var near = new ChunkId(s.Belts[0], 1); + Assert.Equal(250f, length(At(s, far) - eyeAt), 2); + Assert.Equal(150f, length(At(s, near) - eyeAt), 2); + var launcher = Gear("Launcher", new LockWeaponData { Range = Constant(400f), LockSpeed = Constant(.5f), LockAngle = Constant(30f), DirectionImpact = Constant(1f) }); + s.Cache.Upsert(launcher); + var gun = Gear("Gun 200", new InstantWeaponData { Range = Constant(200f) }); + s.Cache.Upsert(gun); + + Ship Fit(params GearData[] weapons) + { + var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings()); + Assert.True(ship.TryEquip(Mint(s, s.Eye))); + foreach (var weapon in weapons) Assert.True(ship.TryEquip(Mint(s, weapon))); + s.Zone.Entities.Add(ship); + Hold(ship, eyeAt); + ship.LookDirection = float3(0, 0, 1); + ship.Activate(); + Tick((ship, eyeAt)); // weapons read their ranges when they run + return ship; + } + + var offered = new List(); + var launcherOnly = Fit(launcher); + Assert.Equal(400f, launcherOnly.Weapons.Single().Range); + Assert.True(launcherOnly.ChunkVisible(far)); + Assert.True(launcherOnly.ChunkVisible(near)); + launcherOnly.VisibleChunksInReach(offered); + Assert.Empty(offered); + + var withGun = Fit(launcher, gun); + withGun.VisibleChunksInReach(offered); + Assert.Equal(new[] { near }, offered); + } + + // Q13 A reads Active, not Enabled (Soul F8): a gun that is switched on but offline (worn out) adds no reach. + [Fact] + public void AnOfflineWeaponAddsNoReach() + { + var kind = Kind("Asteroid", 4f); + var s = BuildScene(new[] { kind }, + Belt(kind, Rock(150f), Rock(length(float2(150, 100)), atan2(100f, 150f) / (2 * PI)))); + var near = new ChunkId(s.Belts[0], 0); + var farther = new ChunkId(s.Belts[0], 1); + var eyeAt = float2(150, -200); + var observer = SpawnShip(s, eyeAt, sensor: true, weaponRanges: new[] { 250f, 350f }); + Tick((observer, eyeAt)); + var offered = new List(); + observer.VisibleChunksInReach(offered); + Assert.Equal(new[] { near, farther }, offered); + + var longGun = observer.Equipment.Single(item => item.Data.Name == "Gun 350"); + longGun.EquippableItem.Durability = 0f; + Tick((observer, eyeAt)); + Assert.True(longGun.Enabled.Value); + Assert.False(longGun.Active.Value); + observer.VisibleChunksInReach(offered); + Assert.Equal(new[] { near }, offered); + } }