62e48a3b0b
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>
414 lines
24 KiB
C#
414 lines
24 KiB
C#
using Unity.Burst;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>One server-side cleave queued from a player's swing-start this tick, resolved after the player loop so
|
|
/// the living-enemy snapshot is gathered ONCE and ONLY when a swing actually fired (never on the client, never on an
|
|
/// idle tick). Blittable (Burst-friendly).</summary>
|
|
struct PendingCleave
|
|
{
|
|
public float3 From;
|
|
public float2 Face;
|
|
public float Damage;
|
|
public float Range;
|
|
public float KnockSpeed;
|
|
public int OwnerId;
|
|
public uint Stamp;
|
|
public uint KnockUntil;
|
|
public bool IsFinisher; // Phase 1.7: this swing is the combo finisher
|
|
}
|
|
|
|
/// <summary>
|
|
/// MC-4 — the predicted melee combo (Hades-style 2-3 hit chain; the player's PRIMARY verb). On a fresh
|
|
/// <see cref="PlayerInput.Attack"/> press (not locked, not mid-dash) it ADVANCES <see cref="MeleeCombo.Step"/>
|
|
/// (chain if re-pressed inside [LockUntilTick, LockUntilTick + grace), else reset to 1) and opens a movement-commit
|
|
/// lock; the finisher (Step == ComboLength) hits bigger. The Step/SwingStartTick/LockUntilTick anchor is
|
|
/// owner-replicated (<see cref="MeleeCombo"/> [GhostField]s) so a rollback restores the authoritative combo
|
|
/// position; every write is an ABSOLUTE function of (restored Step, tick) — re-running a tick re-derives identical
|
|
/// state (the DashSystem idempotency idiom; never an in-place prev+1 of a non-restored field — MC-4 review PRED-1).
|
|
/// <para>
|
|
/// Runs in <see cref="PredictedSimulationSystemGroup"/> AFTER <see cref="PlayerControlSystem"/> (it scales the
|
|
/// MoveVelocity that system wrote) and BEFORE <see cref="DashSystem"/> (a dash OVERRIDES the swing's movement =
|
|
/// dash-cancel) — and so before HealthApplyDamageSystem ([UpdateAfter(DashSystem)]) which drains the cleave's
|
|
/// DamageEvent the same tick. Movement-commit re-applies every predicted pass, lower-bounded on SwingStartTick (a
|
|
/// re-simulated pre-swing tick must NOT inherit the scale — the DashSystem inDashWindow fix). DAMAGE is SERVER-ONLY
|
|
/// (enemies are interpolated ghosts; the client never predicts enemy health) and mirrors ProjectileDamageSystem:
|
|
/// queue each swing, then collect ALL living enemies in the per-step cone (<see cref="MeleeConeMath"/>) ONCE, append
|
|
/// a SourceTick-stamped DamageEvent + stamp KnockbackState. All ticks via TickUtil.NonZero, compared with
|
|
/// NetworkTick only; feel knobs live in <see cref="TuningConfig"/> (MC-0, fallback to Defaults()).
|
|
/// </para>
|
|
/// </summary>
|
|
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
|
[UpdateAfter(typeof(PlayerControlSystem))]
|
|
[UpdateBefore(typeof(DashSystem))]
|
|
[BurstCompile]
|
|
public partial struct MeleeComboSystem : ISystem
|
|
{
|
|
ComponentLookup<KnockbackState> m_KnockbackLookup;
|
|
|
|
BufferLookup<StatModifier> m_StatModLookup;
|
|
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>
|
|
const float k_DetonateRadius = 3.5f;
|
|
|
|
/// <summary>07-20 G2.1: build a cleave payload from LIVE player state at the moment it FIRES (contact tick,
|
|
/// flush, or legacy same-tick) -- the cast-turn keeps steering the cone until the blade lands. Static with
|
|
/// explicit params (no captures) so Burst lowering stays trivial. Reach uses the RANGE-only finisher mult
|
|
/// (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, bool hasMods, DynamicBuffer<StatModifier> mods)
|
|
{
|
|
bool fin = step >= comboLen;
|
|
float d = math.max(0f, hasMods ? StatMath.Apply(baseDamage, StatTarget.MeleeDamage, mods) : baseDamage);
|
|
float r = math.max(0f, hasMods ? StatMath.Apply(baseRange, StatTarget.MeleeRange, mods) : baseRange);
|
|
return new PendingCleave
|
|
{
|
|
From = from,
|
|
Face = FacingMath.ResolveAim(aim, facingDir),
|
|
Damage = fin ? d * finisherMult : d,
|
|
Range = fin ? r * finisherRangeMult : r,
|
|
KnockSpeed = fin ? knockSpeed * finisherMult : knockSpeed,
|
|
OwnerId = ownerId,
|
|
Stamp = stamp,
|
|
KnockUntil = knockUntil,
|
|
IsFinisher = fin,
|
|
// Detonate/Pull came from boon flags (deleted 2026-08-07 audit purge).
|
|
};
|
|
}
|
|
|
|
[BurstCompile]
|
|
public void OnCreate(ref SystemState state)
|
|
{
|
|
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
|
|
|
|
m_StatModLookup = state.GetBufferLookup<StatModifier>(isReadOnly: true);
|
|
m_PendingLookup = state.GetComponentLookup<MeleeCleavePending>(isReadOnly: false);
|
|
state.RequireForUpdate<NetworkTime>();
|
|
}
|
|
|
|
[BurstCompile]
|
|
public void OnUpdate(ref SystemState state)
|
|
{
|
|
if (!SystemAPI.TryGetSingleton<NetworkTime>(out var netTime) || !netTime.ServerTick.IsValid)
|
|
return;
|
|
var serverTick = netTime.ServerTick;
|
|
uint now = serverTick.TickIndexForValidTick;
|
|
|
|
var t = SystemAPI.TryGetSingleton<TuningConfig>(out var tc) ? tc : TuningConfig.Defaults();
|
|
float baseDamage = math.max(0f, t.MeleeDamage);
|
|
float baseRange = math.max(0f, t.MeleeRange);
|
|
float cosHalf = math.cos(math.max(0f, t.MeleeConeHalfAngleRad));
|
|
uint recoverTicks = (uint)math.max(1f, t.MeleeRecoverTicks);
|
|
uint graceTicks = (uint)math.max(1f, t.MeleeChainGraceTicks);
|
|
float moveScale = math.max(0f, t.MeleeSwingMoveScale);
|
|
float knockSpeed = math.max(0f, t.MeleeKnockbackSpeed);
|
|
float finisherMult = math.max(1f, t.MeleeFinisherMult);
|
|
float finisherRangeMult = math.max(0f, t.MeleeFinisherRangeMult); // 07-20 G2.2: REACH-only finisher scaling
|
|
float bufferKnob = math.max(0f, t.MeleeBufferTicks); // 07-20 G7: 0 = buffer off
|
|
float contactKnob = math.max(0f, t.MeleeContactTicks); // 07-20 G2.1: 0 = immediate (legacy)
|
|
byte comboLen = (byte)math.clamp((int)t.MeleeComboLength, 1, 3);
|
|
uint stamp = TickUtil.NonZero(now);
|
|
uint knockUntil = TickUtil.NonZero(now + (uint)math.max(1, Tuning.KnockbackDurationTicks));
|
|
|
|
bool isServer = state.WorldUnmanaged.IsServer();
|
|
|
|
// Server-only queue of cleaves to resolve after the player loop (so enemies are gathered ONCE, and only
|
|
// 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_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
|
|
SystemAPI.Query<RefRW<MeleeCombo>, RefRW<CharacterControl>, RefRO<PlayerInput>,
|
|
RefRO<PlayerFacing>, RefRO<LocalTransform>, RefRO<GhostOwner>, RefRO<DashState>>()
|
|
.WithAll<Simulate>().WithDisabled<Dead>().WithEntityAccess())
|
|
{
|
|
// A dash window (i-frame OR recovery) active = dash owns movement + blocks a swing start (dash-cancel).
|
|
bool dashActive = ds.ValueRO.StartTick != 0u
|
|
&& !new NetworkTick(ds.ValueRO.StartTick).IsNewerThan(serverTick)
|
|
&& ds.ValueRO.RecoverUntilTick != 0u
|
|
&& new NetworkTick(ds.ValueRO.RecoverUntilTick).IsNewerThan(serverTick);
|
|
|
|
// Locked: mid swing / recovery (now < LockUntilTick).
|
|
bool locked = mc.ValueRO.LockUntilTick != 0u
|
|
&& new NetworkTick(mc.ValueRO.LockUntilTick).IsNewerThan(serverTick);
|
|
|
|
// --- 07-20 G2.1: fire a DUE scheduled cleave BEFORE anything can overwrite the swing this tick
|
|
// (wrap-safe elapsed compare = tick-batch-proof; consumed by zeroing -- the AttackWindup idiom,
|
|
// design review wf_000bc247 C11). Server-only; the pending slot never replicates.
|
|
bool hasPending = m_PendingLookup.HasComponent(entity);
|
|
if (isServer && hasPending)
|
|
{
|
|
var pend = m_PendingLookup[entity];
|
|
if (pend.ResolveTick != 0u && !new NetworkTick(pend.ResolveTick).IsNewerThan(serverTick))
|
|
{
|
|
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_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
|
|
m_PendingLookup[entity] = default;
|
|
}
|
|
}
|
|
|
|
// --- 07-20 G7 buffer: a press in the lock's LAST MeleeBufferTicks is remembered; validity anchors
|
|
// to the UNLOCK edge so server tick-batching can't drop an edge press (review wf_000bc247 C1). ---
|
|
uint bufTicks = (uint)bufferKnob;
|
|
if (bufTicks > 0u && input.ValueRO.Attack.IsSet && locked)
|
|
{
|
|
var lockEnd = new NetworkTick(mc.ValueRO.LockUntilTick);
|
|
if (lockEnd.IsValid && lockEnd.TicksSince(serverTick) <= (int)bufTicks)
|
|
mc.ValueRW.BufferedAttackTick = TickUtil.NonZero(now);
|
|
}
|
|
bool buffered = false;
|
|
if (!locked && mc.ValueRO.BufferedAttackTick != 0u)
|
|
{
|
|
var lockEnd = new NetworkTick(mc.ValueRO.LockUntilTick);
|
|
if (bufTicks > 0u && lockEnd.IsValid && serverTick.TicksSince(lockEnd) <= (int)MeleeTiming.BatchSlackTicks)
|
|
buffered = true;
|
|
else
|
|
mc.ValueRW.BufferedAttackTick = 0u; // stale (missed the slack / buffer tuned off) -- drop
|
|
}
|
|
|
|
// --- ADVANCE (idempotent ABSOLUTE writes; dash wins same-tick ties via !Dash.IsSet; a buffered
|
|
// press counts as a press -- G7) ---
|
|
bool swingStarted = false;
|
|
byte swingStep = 0;
|
|
if ((input.ValueRO.Attack.IsSet || buffered) && !locked && !dashActive && !input.ValueRO.Dash.IsSet)
|
|
{
|
|
bool inChainWindow = false;
|
|
if (mc.ValueRO.LockUntilTick != 0u)
|
|
{
|
|
var deadline = new NetworkTick(TickUtil.NonZero(mc.ValueRO.LockUntilTick + graceTicks));
|
|
inChainWindow = deadline.IsValid && deadline.IsNewerThan(serverTick); // now < lock+grace (now >= lock since !locked)
|
|
}
|
|
byte prev = mc.ValueRO.Step;
|
|
swingStep = (inChainWindow && prev >= 1 && prev < comboLen) ? (byte)(prev + 1) : (byte)1;
|
|
|
|
bool isFin = swingStep >= comboLen;
|
|
uint stepRecover = isFin ? (uint)math.max(1f, math.round(recoverTicks * finisherMult)) : recoverTicks;
|
|
|
|
mc.ValueRW.Step = swingStep;
|
|
mc.ValueRW.SwingStartTick = TickUtil.NonZero(now);
|
|
mc.ValueRW.LockUntilTick = TickUtil.NonZero(now + stepRecover);
|
|
mc.ValueRW.BufferedAttackTick = 0u; // consumed (or superseded by a fresh press)
|
|
swingStarted = true;
|
|
}
|
|
|
|
// --- MOVEMENT COMMIT (every pass; lower-bounded on SwingStartTick; DashSystem overrides later) ---
|
|
bool inSwingWindow = mc.ValueRO.SwingStartTick != 0u
|
|
&& !new NetworkTick(mc.ValueRO.SwingStartTick).IsNewerThan(serverTick)
|
|
&& mc.ValueRO.LockUntilTick != 0u
|
|
&& new NetworkTick(mc.ValueRO.LockUntilTick).IsNewerThan(serverTick);
|
|
if (inSwingWindow && !dashActive)
|
|
control.ValueRW.MoveVelocity *= moveScale;
|
|
|
|
// --- SERVER: schedule the cleave at the CONTACT tick (07-20 G2.1) -- or fire same-tick when the
|
|
// knob is 0 (legacy) / the entity has no pending slot (plain test worlds). Damage stays SERVER-only;
|
|
// payload is built from LIVE state at the moment it fires (the cast-turn steers until the blade lands).
|
|
if (swingStarted && isServer)
|
|
{
|
|
uint ct = MeleeTiming.ContactTicks(swingStep, contactKnob);
|
|
if (ct > 0u && hasPending)
|
|
{
|
|
// FLUSH a still-armed older cleave first (pathological knob combos can push contact past the
|
|
// recover lock -- fire it early rather than lose it; review wf_000bc247 C0/C12).
|
|
var pend = m_PendingLookup[entity];
|
|
if (pend.ResolveTick != 0u)
|
|
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_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
|
|
m_PendingLookup[entity] = new MeleeCleavePending { ResolveTick = TickUtil.NonZero(now + ct), Step = swingStep };
|
|
}
|
|
else
|
|
{
|
|
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_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve queued cleaves SERVER-ONLY and only when one actually fired: gather living enemies ONCE, then
|
|
// append a SourceTick-stamped DamageEvent + stamp KnockbackState for each enemy in each swing's cone.
|
|
if (isServer && cleaves.IsCreated && cleaves.Length > 0)
|
|
{
|
|
var enemyEntities = new NativeList<Entity>(Allocator.Temp);
|
|
var enemyPositions = new NativeList<float3>(Allocator.Temp);
|
|
foreach (var (xform, health, enemyEntity) in
|
|
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>>()
|
|
.WithAll<EnemyTag>()
|
|
.WithEntityAccess())
|
|
{
|
|
if (health.ValueRO.Current <= 0f)
|
|
continue; // skip already-dead enemies (about to despawn)
|
|
enemyEntities.Add(enemyEntity);
|
|
enemyPositions.Add(xform.ValueRO.Position);
|
|
}
|
|
// Gather harvest targets (resource nodes + Blight clutter) ONCE so "any attack harvests": a swing
|
|
// depletes every node/clutter in its cone, crediting the shared ResourceLedger (the build currency
|
|
// 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);
|
|
DynamicBuffer<StorageEntry> ledger = default;
|
|
if (haveLedger) ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
|
|
var harvEntity = new NativeList<Entity>(Allocator.Temp);
|
|
var harvPos = new NativeList<float3>(Allocator.Temp);
|
|
var harvRemaining = new NativeList<int>(Allocator.Temp);
|
|
var harvYieldId = new NativeList<byte>(Allocator.Temp);
|
|
var harvPerHit = new NativeList<float>(Allocator.Temp);
|
|
var harvIsClutter = new NativeList<bool>(Allocator.Temp);
|
|
var harvVariant = new NativeList<byte>(Allocator.Temp);
|
|
foreach (var (hx, node, he) in
|
|
SystemAPI.Query<RefRO<LocalTransform>, RefRO<ResourceNode>>().WithEntityAccess())
|
|
{
|
|
if (node.ValueRO.Remaining <= 0) continue; // spent is not a target
|
|
|
|
harvEntity.Add(he);
|
|
harvPos.Add(hx.ValueRO.Position);
|
|
harvRemaining.Add(node.ValueRO.Remaining);
|
|
harvYieldId.Add(node.ValueRO.ResourceId);
|
|
harvPerHit.Add(node.ValueRO.HarvestPerHit);
|
|
harvIsClutter.Add(false);
|
|
harvVariant.Add(0);
|
|
}
|
|
foreach (var (hx, clutter, he) in
|
|
SystemAPI.Query<RefRO<LocalTransform>, RefRO<BlightClutter>>().WithEntityAccess())
|
|
{
|
|
if (clutter.ValueRO.Remaining <= 0) continue; // lit-fuse barrel: unhittable, detonation owns it
|
|
|
|
harvEntity.Add(he);
|
|
harvPos.Add(hx.ValueRO.Position);
|
|
harvRemaining.Add(clutter.ValueRO.Remaining);
|
|
harvYieldId.Add(clutter.ValueRO.ScrapResourceId);
|
|
harvPerHit.Add(clutter.ValueRO.ScrapPerHit);
|
|
harvIsClutter.Add(true);
|
|
harvVariant.Add(clutter.ValueRO.Variant);
|
|
}
|
|
var harvDestroyed = new NativeArray<bool>(harvEntity.Length, Allocator.Temp);
|
|
|
|
|
|
m_KnockbackLookup.Update(ref state);
|
|
|
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
|
for (int s = 0; s < cleaves.Length; s++)
|
|
{
|
|
var c = cleaves[s];
|
|
for (int i = 0; i < enemyEntities.Length; i++)
|
|
{
|
|
if (!MeleeConeMath.InCone(c.From, c.Face, c.Range, cosHalf, enemyPositions[i]))
|
|
continue;
|
|
var target = enemyEntities[i];
|
|
ecb.AppendToBuffer(target, new DamageEvent
|
|
{
|
|
Amount = c.Damage,
|
|
SourceNetworkId = c.OwnerId,
|
|
SourceTick = c.Stamp,
|
|
});
|
|
if (c.KnockSpeed > 0f)
|
|
KnockbackUtil.Stamp(ref m_KnockbackLookup, target,
|
|
c.From, enemyPositions[i], c.Face, c.KnockSpeed, c.KnockUntil);
|
|
}
|
|
}
|
|
|
|
// HARVEST: deplete every node/clutter in each swing's cone, crediting the shared ledger; write
|
|
// Remaining back so the [GhostField] replicates -> WorldFeedbackSystem chips fire on melee mining.
|
|
for (int s = 0; s < cleaves.Length; s++)
|
|
{
|
|
var hc = cleaves[s];
|
|
for (int i = 0; i < harvEntity.Length; i++)
|
|
{
|
|
if (harvDestroyed[i])
|
|
continue;
|
|
if (!MeleeConeMath.InCone(hc.From, hc.Face, hc.Range, cosHalf, harvPos[i]))
|
|
continue;
|
|
|
|
int amount = math.max(1, (int)harvPerHit[i]);
|
|
// DECOUPLED deposit (cover review): cover has ScrapPerHit=0 — breaks in Remaining hits,
|
|
// yields nothing. Nodes/clutter (PerHit >= 1) keep deposit == decrement as before.
|
|
// 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];
|
|
// 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, ledger, haveLedger);
|
|
if (!deposited)
|
|
continue; // never consume a YIELDING target for zero credit
|
|
}
|
|
|
|
int rem = harvRemaining[i] - amount;
|
|
harvRemaining[i] = rem;
|
|
if (rem <= 0)
|
|
{
|
|
harvDestroyed[i] = true;
|
|
if (harvIsClutter[i] && harvVariant[i] == 3)
|
|
{
|
|
// EXPLOSIVE pop (Variant 3): light the fuse instead of destroying — mirrors the
|
|
// projectile pop site; HazardExplosionSystem detonates (Exploding_Barrels_Build_Spec).
|
|
ecb.SetComponent(harvEntity[i], new BlightClutter
|
|
{
|
|
Remaining = 0,
|
|
Variant = harvVariant[i],
|
|
ScrapResourceId = harvYieldId[i],
|
|
ScrapPerHit = harvPerHit[i],
|
|
});
|
|
ecb.AddComponent(harvEntity[i], new BarrelFuse
|
|
{
|
|
ExplodeTick = TickUtil.NonZero(now + Tuning.BarrelFuseTicks),
|
|
});
|
|
}
|
|
else
|
|
ecb.DestroyEntity(harvEntity[i]);
|
|
}
|
|
else if (harvIsClutter[i])
|
|
{
|
|
ecb.SetComponent(harvEntity[i], new BlightClutter
|
|
{
|
|
Remaining = rem,
|
|
Variant = harvVariant[i],
|
|
ScrapResourceId = yieldId,
|
|
ScrapPerHit = harvPerHit[i],
|
|
});
|
|
}
|
|
else
|
|
{
|
|
ecb.SetComponent(harvEntity[i], new ResourceNode
|
|
{
|
|
ResourceId = yieldId,
|
|
Remaining = rem,
|
|
HarvestPerHit = harvPerHit[i],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
ecb.Playback(state.EntityManager);
|
|
ecb.Dispose();
|
|
enemyEntities.Dispose();
|
|
enemyPositions.Dispose();
|
|
harvEntity.Dispose();
|
|
harvPos.Dispose();
|
|
harvRemaining.Dispose();
|
|
harvYieldId.Dispose();
|
|
harvPerHit.Dispose();
|
|
harvIsClutter.Dispose();
|
|
harvVariant.Dispose();
|
|
harvDestroyed.Dispose();
|
|
}
|
|
|
|
if (cleaves.IsCreated)
|
|
cleaves.Dispose();
|
|
}
|
|
}
|
|
}
|