LANTERN purge: delete the superseded base/expedition shell (audit H1/H3/M5)

The 2026-08-06 audit found the shipping scene was still the abandoned
co-op-Hades game with LANTERN combat bolted on, and that a third of the
codebase was live code for a direction abandoned on 2026-07-13. Operator
chose deletion over freezing: "everything is saved in source control if
needed. I want the project to be clean."

DELETED (~140 source files, Scripts 335->231, Tests 77->43):
- Enemy variants + boss (H3). ChargerAuthoring / SpitterAuthoring /
  SwarmerAuthoring were attached to ZERO prefabs, so LungeState /
  SpitterState / SwarmerTag were never baked: ~272 lines of Bursted AI
  passes, BossAISystem (261 lines) and the whole MixBands escalation
  curve could not match a single chunk at runtime, while 734 lines of
  green tests certified them. Both shipping enemy prefabs were already
  byte-identical in stats.
- Run/room lifecycle: RunDirector FSM, RunInfo/RunMap/RoomPlan/RoomTag,
  route select, portal interact, ready-check, room field/teardown.
- Meta shop, prep loadout, boons (incl. KillRewardSystem and
  DashTrailDamageSystem, which existed only to serve boon flags).
- Build palette + structures, shared storage, inventory/equipment
  (already recorded PAUSED in CLAUDE.md).
- The HUD panels driving all of the above (HudSystem 1168 -> 610).

KEPT deliberately: BaseGridMath + BaseAnchor (8 systems use PlotCenter
for spawn rings, respawn and dynamic light), the resource ledger +
StorageMath, the save system, region/relevancy. Three of these were in
the delete set until I checked their consumers — worth remembering that
the file-level manifest was wrong about them.

Also folds in audit finding M5: PlayerClass was a second, server-only
copy of the byte FrameId already replicates. It existed for the meta
shop; with that gone, FrameId is the single frame identity.

Harvest is now single-sink (ledger). HarvestMath keeps its shape so
LANTERN's carried-vs-banked cargo split lands in one place, not two.

295/295 EditMode green, zero compile errors. Subscene re-bake and Play
validation follow in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 12:59:39 -07:00
parent 6a412fe3e7
commit 62e48a3b0b
304 changed files with 260 additions and 14591 deletions
@@ -1,28 +0,0 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// A player's private choice-of-3 boon offer for the just-cleared room. OWNER-ONLY replication
/// (<see cref="SendToOwnerType.SendToOwner"/>): the offer is an observe-only HUD read of the LOCAL player — no
/// prediction, no teammate read — so the traffic-minimal owner-only path is correct (Play-validated at Step 9;
/// the proven fallback is <see cref="SendToOwnerType.All"/>, which for HUD purposes still only surfaces each
/// player's component on the client that owns that ghost). Written by <c>BoonOfferSystem</c> on the RoomReward
/// entry edge (options drawn deterministically from Hash(RunSeed, room, NetworkId)); <see cref="Pending"/> is
/// cleared by <c>BoonApplySystem</c> on a valid pick and zeroed by the Returning-edge strip. INERT until Step 9 —
/// baked at Step 3 so the player ghost re-bakes exactly ONCE for the whole redesign.
/// </summary>
[GhostComponent(OwnerSendType = SendToOwnerType.SendToOwner)]
public struct BoonOffer : IComponentData
{
/// <summary>1 = awaiting this player's pick.</summary>
[GhostField] public byte Pending;
/// <summary>Boon catalog id of option 0.</summary>
[GhostField] public byte Option0;
/// <summary>Boon catalog id of option 1.</summary>
[GhostField] public byte Option1;
/// <summary>Boon catalog id of option 2.</summary>
[GhostField] public byte Option2;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c3c9cb8a1b819fd4b8474206e766076e
@@ -1,21 +0,0 @@
using Unity.Collections;
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Phase 1.7 Blade-Dash bookkeeping — SERVER-ONLY, plain (NOT a <c>[GhostField]</c>, so no ghost-hash impact;
/// it piggybacks the <see cref="BoonEffects"/> player re-bake). Keys the per-dash "hit once" dedup to
/// <see cref="DashState.StartTick"/> (which is <c>TickUtil.NonZero(now)</c> on every dash and cannot be relied
/// upon to reset) rather than to any DashState clear edge: <c>DashTrailDamageSystem</c> clears <see cref="Hit"/>
/// whenever the current StartTick differs from <see cref="LastStartTick"/>. Server-only (no rollback) so the
/// accumulator is safe to persist across ticks.
/// </summary>
public struct DashTrailState : IComponentData
{
/// <summary>The <see cref="DashState.StartTick"/> the <see cref="Hit"/> set currently belongs to.</summary>
public uint LastStartTick;
/// <summary>Enemies already struck by the CURRENT dash's trail (one hit per enemy per dash).</summary>
public FixedList64Bytes<Entity> Hit;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 248bd87d96cfc5b43b4681a203756c5c
@@ -21,8 +21,6 @@ namespace ProjectM.Simulation
public uint Stamp;
public uint KnockUntil;
public bool IsFinisher; // Phase 1.7: this swing is the combo finisher
public bool Detonate; // Phase 1.7: attacker has the FinisherDetonate boon
public bool Pull; // Phase 1.7: attacker has the KnockToPull boon
}
/// <summary>
@@ -52,12 +50,8 @@ namespace ProjectM.Simulation
public partial struct MeleeComboSystem : ISystem
{
ComponentLookup<KnockbackState> m_KnockbackLookup;
ComponentLookup<BossState> m_BossLookup; // A4: the boss is knockback-immune (no melee stunlock out of its slams)
ComponentLookup<RegionTag> m_RegionLookup;
BufferLookup<InventorySlot> m_InvLookup;
BufferLookup<StatModifier> m_StatModLookup;
ComponentLookup<BoonEffects> m_BoonEffectsLookup; // Phase 1.7 (player query is at the 7-type cap -> lookup)
ComponentLookup<MeleeCleavePending> m_PendingLookup; // 07-20 G2.1 scheduled cleave (query at cap -> lookup)
/// <summary>Phase 1.7 Detonating Finisher blast radius (planar, tunable).</summary>
@@ -69,7 +63,7 @@ namespace ProjectM.Simulation
/// (G2.2); damage/knockback keep the classic finisher mult.</summary>
static PendingCleave BuildCleave(byte step, byte comboLen, float baseDamage, float baseRange, float knockSpeed,
float finisherMult, float finisherRangeMult, uint stamp, uint knockUntil, float3 from, float2 aim,
float2 facingDir, int ownerId, byte bflags, bool hasMods, DynamicBuffer<StatModifier> mods)
float2 facingDir, int ownerId, bool hasMods, DynamicBuffer<StatModifier> mods)
{
bool fin = step >= comboLen;
float d = math.max(0f, hasMods ? StatMath.Apply(baseDamage, StatTarget.MeleeDamage, mods) : baseDamage);
@@ -85,8 +79,7 @@ namespace ProjectM.Simulation
Stamp = stamp,
KnockUntil = knockUntil,
IsFinisher = fin,
Detonate = (bflags & BoonFlag.FinisherDetonate) != 0,
Pull = (bflags & BoonFlag.KnockToPull) != 0,
// Detonate/Pull came from boon flags (deleted 2026-08-07 audit purge).
};
}
@@ -94,12 +87,8 @@ namespace ProjectM.Simulation
public void OnCreate(ref SystemState state)
{
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
m_BossLookup = state.GetComponentLookup<BossState>(isReadOnly: true);
m_RegionLookup = state.GetComponentLookup<RegionTag>(isReadOnly: true);
m_InvLookup = state.GetBufferLookup<InventorySlot>(isReadOnly: false);
m_StatModLookup = state.GetBufferLookup<StatModifier>(isReadOnly: true);
m_BoonEffectsLookup = state.GetComponentLookup<BoonEffects>(isReadOnly: true);
m_PendingLookup = state.GetComponentLookup<MeleeCleavePending>(isReadOnly: false);
state.RequireForUpdate<NetworkTime>();
}
@@ -134,7 +123,6 @@ namespace ProjectM.Simulation
// when at least one swing actually started — no per-tick enemy gather on idle/client ticks).
var cleaves = isServer ? new NativeList<PendingCleave>(Allocator.Temp) : default;
m_StatModLookup.Update(ref state);
m_BoonEffectsLookup.Update(ref state); // Phase 1.7: per-player boon flags (read inside the player loop)
m_PendingLookup.Update(ref state); // 07-20 G2.1: scheduled cleave slots (server-only writes)
foreach (var (mc, control, input, facing, xform, owner, ds, entity) in
@@ -164,7 +152,6 @@ namespace ProjectM.Simulation
cleaves.Add(BuildCleave(pend.Step, comboLen, baseDamage, baseRange, knockSpeed,
finisherMult, finisherRangeMult, stamp, knockUntil, xform.ValueRO.Position,
input.ValueRO.Aim, facing.ValueRO.Direction, owner.ValueRO.NetworkId,
m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0,
m_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
m_PendingLookup[entity] = default;
}
@@ -237,7 +224,6 @@ namespace ProjectM.Simulation
cleaves.Add(BuildCleave(pend.Step, comboLen, baseDamage, baseRange, knockSpeed,
finisherMult, finisherRangeMult, stamp, knockUntil, xform.ValueRO.Position,
input.ValueRO.Aim, facing.ValueRO.Direction, owner.ValueRO.NetworkId,
m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0,
m_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
m_PendingLookup[entity] = new MeleeCleavePending { ResolveTick = TickUtil.NonZero(now + ct), Step = swingStep };
}
@@ -246,7 +232,6 @@ namespace ProjectM.Simulation
cleaves.Add(BuildCleave(swingStep, comboLen, baseDamage, baseRange, knockSpeed,
finisherMult, finisherRangeMult, stamp, knockUntil, xform.ValueRO.Position,
input.ValueRO.Aim, facing.ValueRO.Direction, owner.ValueRO.NetworkId,
m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0,
m_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
}
}
@@ -273,14 +258,8 @@ namespace ProjectM.Simulation
// pool) just like a base projectile hit. SERVER-ONLY (this whole block) — interpolated node ghosts
// are never rolled back, so the deposit + destroy fire exactly once per swing.
bool haveLedger = SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerEntity);
bool haveDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var itemDb);
DynamicBuffer<StorageEntry> ledger = default;
if (haveLedger) ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
m_RegionLookup.Update(ref state);
m_InvLookup.Update(ref state);
var meleePlayerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
foreach (var (po, pe) in SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, InventorySlot>().WithEntityAccess())
meleePlayerByConn[po.ValueRO.NetworkId] = pe;
var harvEntity = new NativeList<Entity>(Allocator.Temp);
var harvPos = new NativeList<float3>(Allocator.Temp);
var harvRemaining = new NativeList<int>(Allocator.Temp);
@@ -288,7 +267,6 @@ namespace ProjectM.Simulation
var harvPerHit = new NativeList<float>(Allocator.Temp);
var harvIsClutter = new NativeList<bool>(Allocator.Temp);
var harvVariant = new NativeList<byte>(Allocator.Temp);
var harvToLedger = new NativeList<bool>(Allocator.Temp);
foreach (var (hx, node, he) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<ResourceNode>>().WithEntityAccess())
{
@@ -301,7 +279,6 @@ namespace ProjectM.Simulation
harvPerHit.Add(node.ValueRO.HarvestPerHit);
harvIsClutter.Add(false);
harvVariant.Add(0);
harvToLedger.Add(m_RegionLookup.HasComponent(he) && m_RegionLookup[he].Region == RegionId.Base);
}
foreach (var (hx, clutter, he) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<BlightClutter>>().WithEntityAccess())
@@ -315,13 +292,11 @@ namespace ProjectM.Simulation
harvPerHit.Add(clutter.ValueRO.ScrapPerHit);
harvIsClutter.Add(true);
harvVariant.Add(clutter.ValueRO.Variant);
harvToLedger.Add(m_RegionLookup.HasComponent(he) && m_RegionLookup[he].Region == RegionId.Base);
}
var harvDestroyed = new NativeArray<bool>(harvEntity.Length, Allocator.Temp);
m_KnockbackLookup.Update(ref state);
m_BossLookup.Update(ref state);
var ecb = new EntityCommandBuffer(Allocator.Temp);
for (int s = 0; s < cleaves.Length; s++)
@@ -339,29 +314,8 @@ namespace ProjectM.Simulation
SourceTick = c.Stamp,
});
if (c.KnockSpeed > 0f)
KnockbackUtil.Stamp(ref m_KnockbackLookup, m_BossLookup, target,
c.From, enemyPositions[i], c.Face, c.KnockSpeed, c.KnockUntil, c.Pull);
}
}
// Phase 1.7 Detonating Finisher: a finisher swing with the boon blasts a planar AoE around its
// origin (mirrors HazardExplosionSystem). Cone+blast overlap is the normal DamageEvent-summation.
for (int s = 0; s < cleaves.Length; s++)
{
var dc = cleaves[s];
if (!dc.IsFinisher || !dc.Detonate)
continue;
float detRadSq = k_DetonateRadius * k_DetonateRadius;
for (int i = 0; i < enemyEntities.Length; i++)
{
float2 dd = new float2(enemyPositions[i].x - dc.From.x, enemyPositions[i].z - dc.From.z);
if (math.lengthsq(dd) > detRadSq)
continue;
ecb.AppendToBuffer(enemyEntities[i], new DamageEvent
{
Amount = dc.Damage,
SourceNetworkId = dc.OwnerId,
SourceTick = dc.Stamp,
});
KnockbackUtil.Stamp(ref m_KnockbackLookup, target,
c.From, enemyPositions[i], c.Face, c.KnockSpeed, c.KnockUntil);
}
}
@@ -383,17 +337,12 @@ namespace ProjectM.Simulation
// zero means ZERO (cover); any POSITIVE yield still credits >= 1 (the fractional-yield guard).
int deposit = harvPerHit[i] > 0f ? amount : 0;
byte yieldId = harvYieldId[i];
// Route by region: Base nodes credit the shared ledger DIRECTLY (the build pool); an
// expedition / un-tagged target goes to the swinging player's PERSONAL inventory (spill to
// ledger), mirroring ResourceHarvestSystem. Only deplete if the yield landed somewhere —
// never consume a node for zero credit (e.g. no ledger singleton present).
Entity meleeHarvester = Entity.Null;
if (meleePlayerByConn.TryGetValue(hc.OwnerId, out var meleePlayer))
meleeHarvester = meleePlayer;
// All yield credits the shared ledger — the PERSONAL inventory sink was deleted with the
// superseded shell (2026-08-07 audit). Only deplete if the yield landed somewhere; never
// consume a node for zero credit (e.g. no ledger singleton present).
if (deposit > 0)
{
bool deposited = HarvestMath.DepositYield(yieldId, deposit, harvToLedger[i], meleeHarvester,
m_InvLookup, ledger, haveLedger, haveDb, itemDb);
bool deposited = HarvestMath.DepositYield(yieldId, deposit, ledger, haveLedger);
if (!deposited)
continue; // never consume a YIELDING target for zero credit
}
@@ -455,8 +404,6 @@ namespace ProjectM.Simulation
harvIsClutter.Dispose();
harvVariant.Dispose();
harvDestroyed.Dispose();
harvToLedger.Dispose();
meleePlayerByConn.Dispose();
}
if (cleaves.IsCreated)
@@ -1,20 +0,0 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// The player's expedition ready-check flag — a plain send-to-all <c>[GhostField]</c> byte on the player ghost so
/// EVERY client can render the "N/M READY" staging panel (owner-only would hide teammates' readiness). Written
/// server-only by <c>ReadyToggleSystem</c> from the <see cref="ReadyToggleRequest"/> RPC — honored during Staging
/// AND the Launching countdown (an un-ready during the countdown is the launch-abort escape hatch); cleared for
/// all players by <c>RunDirectorSystem</c> on the Returning edge. The N/M derivation counts live PlayerTag ghosts,
/// which is valid ONLY because the party is co-located at base while Staging (the co-location invariant — N7);
/// ready toggles are refused in every other lifecycle state.
/// </summary>
public struct PlayerReady : IComponentData
{
/// <summary>1 = ready to launch; 0 = not ready.</summary>
[GhostField] public byte Value;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 5423c043f8023a246b4244eb34e5fbdd