Slice 2 (WIP): class data layer + melee-augment routing

Foundation for Two Classes (DR-037). New ids (CharacterId.Warrior/Ranger,
AbilityId.WarriorCone, StatTarget.MeleeDamage/MeleeRange); CharacterStatsRef.Id ->
[GhostField] so the owning client folds the right class stats; MeleeComboSystem
folds per-player MeleeDamage/MeleeRange off the replicated StatModifier buffer
(HasBuffer-guarded -> identity without class seeds, so behavior-preserving).
345/345 EditMode. Slice 2 design review + locked forks logged in the session note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 00:23:56 -07:00
parent f98125f0b2
commit d9d67c4e78
4 changed files with 33 additions and 9 deletions
@@ -1,15 +1,16 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Which authored character-stats definition this entity uses - a light key into the CharacterStats
/// blob, replacing M2's inlined PlayerMoveStats values. Not replicated (baked identically on both
/// worlds); promote to a GhostField if runtime character changes are ever needed. <c>Id</c> stores a
/// blob, replacing M2's inlined PlayerMoveStats values. NOW a [GhostField] (Slice 2 classes) so the
/// server-written per-player class id replicates -> the owning client folds correct stats. <c>Id</c> stores a
/// <see cref="CharacterId"/>.
/// </summary>
public struct CharacterStatsRef : IComponentData
{
public byte Id;
[GhostField] public byte Id;
}
}
@@ -51,6 +51,7 @@ namespace ProjectM.Simulation
ComponentLookup<KnockbackState> m_KnockbackLookup;
ComponentLookup<RegionTag> m_RegionLookup;
BufferLookup<InventorySlot> m_InvLookup;
BufferLookup<StatModifier> m_StatModLookup;
[BurstCompile]
public void OnCreate(ref SystemState state)
@@ -58,6 +59,7 @@ namespace ProjectM.Simulation
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
m_RegionLookup = state.GetComponentLookup<RegionTag>(isReadOnly: true);
m_InvLookup = state.GetBufferLookup<InventorySlot>(isReadOnly: false);
m_StatModLookup = state.GetBufferLookup<StatModifier>(isReadOnly: true);
state.RequireForUpdate<NetworkTime>();
}
@@ -87,11 +89,12 @@ namespace ProjectM.Simulation
// 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); // Slice 2: per-player melee stat fold (read inside the player loop)
foreach (var (mc, control, input, facing, xform, owner, ds) in
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>())
.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
@@ -138,14 +141,19 @@ namespace ProjectM.Simulation
if (swingStarted && isServer)
{
bool isFin = swingStep >= comboLen;
// Slice 2: fold the player's class/run StatModifiers onto the live-tunable melee base so the
// PRIMARY verb scales with class identity (Warrior +MeleeDamage/+reach) + run augments.
bool hasMods = m_StatModLookup.HasBuffer(entity);
float pDamage = math.max(0f, hasMods ? StatMath.Apply(baseDamage, StatTarget.MeleeDamage, m_StatModLookup[entity]) : baseDamage);
float pRange = math.max(0f, hasMods ? StatMath.Apply(baseRange, StatTarget.MeleeRange, m_StatModLookup[entity]) : baseRange);
float2 face = facing.ValueRO.Direction;
face = math.lengthsq(face) < 1e-6f ? new float2(0f, 1f) : math.normalize(face);
cleaves.Add(new PendingCleave
{
From = xform.ValueRO.Position,
Face = face,
Damage = isFin ? baseDamage * finisherMult : baseDamage,
Range = isFin ? baseRange * finisherMult : baseRange,
Damage = isFin ? pDamage * finisherMult : pDamage,
Range = isFin ? pRange * finisherMult : pRange,
KnockSpeed = isFin ? knockSpeed * finisherMult : knockSpeed,
OwnerId = owner.ValueRO.NetworkId,
Stamp = stamp,