using System; using Unity.Entities; namespace ProjectM.Simulation { /// /// Slice 2 — pure mapping from a chosen class (a byte) to its spawn-time setup: the /// Fire-slot ability id + the permanent trait s (tagged with the reserved /// range, NEVER stripped by the generic modifier systems). Applied by /// GoInGameServerSystem on the just-spawned player and unit-tested. Burst-safe (byte/uint only, no managed types). /// /// Trait deltas seed onto the character (no per-class blob row needed — the /// deltas ride the replicated StatModifier buffer, OwnerSendType.All, so the owning client folds the correct /// EffectiveCharacterStats). The DRG-asymmetry (operator-locked): Warrior = melee bruiser (tankier, /// slower, harder + longer-reach melee via MeleeDamage/MeleeRange); Ranger = ranged anchor (faster, /// squishier, longer projectile range + a wider auto-target assist — the co-op synergy hook the Warrior's /// knockback feeds). The Warrior's Fire = the aim-directed cone (); the /// Ranger's Fire = the default projectile (). /// /// /// swaps an already-spawned player's class IN PLACE (strip the old class seeds, re-seed /// the new ones) — used by the editor-only class-switch dev tool. The single seed set lives in /// so the spawn path and the swap path can never drift; couples the strip range /// () to the number of seeds emitted. /// /// public static class ClassTraits { public const byte WarriorClass = (byte)CharacterId.Warrior; public const byte RangerClass = (byte)CharacterId.Ranger; /// How many trait modifiers a class seeds (each on a distinct SourceId at ClassSourceId + i). public const int ClassSeedCount = 4; /// Normalize a wire ClassId to a known class (0 / unknown -> Warrior). public static byte Normalize(byte classId) => classId == RangerClass ? RangerClass : WarriorClass; /// The Fire-slot ability id for a class (Warrior = cone, Ranger = the default projectile). public static byte AbilityFor(byte classId) => classId == RangerClass ? (byte)AbilityId.Primary : (byte)AbilityId.WarriorCone; /// True when a modifier's SourceId is in the reserved class-seed range [ClassSourceId, +ClassSeedCount). public static bool IsClassSeed(uint sourceId) => sourceId >= Tuning.ClassSourceId && sourceId < Tuning.ClassSourceId + (uint)ClassSeedCount; /// /// The permanent trait deltas for a class — the SINGLE source shared by both the spawn path /// () and the in-place swap path /// ( / ), so they can't drift. /// Fills exactly entries, each tagged ClassSourceId + i. /// static void Seeds(byte classId, Span seeds) { uint src = Tuning.ClassSourceId; if (classId == RangerClass) { seeds[0] = new StatModifier { Target = (byte)StatTarget.MoveSpeed, Op = (byte)ModOp.PercentMult, Value = 0.15f, SourceId = src }; seeds[1] = new StatModifier { Target = (byte)StatTarget.MaxHealth, Op = (byte)ModOp.PercentMult, Value = -0.15f, SourceId = src + 1u }; seeds[2] = new StatModifier { Target = (byte)StatTarget.Range, Op = (byte)ModOp.PercentAdd, Value = 0.30f, SourceId = src + 2u }; seeds[3] = new StatModifier { Target = (byte)StatTarget.AutoTargetRange, Op = (byte)ModOp.Flat, Value = 3f, SourceId = src + 3u }; } else { seeds[0] = new StatModifier { Target = (byte)StatTarget.MaxHealth, Op = (byte)ModOp.Flat, Value = 30f, SourceId = src }; seeds[1] = new StatModifier { Target = (byte)StatTarget.MoveSpeed, Op = (byte)ModOp.PercentMult, Value = -0.15f, SourceId = src + 1u }; seeds[2] = new StatModifier { Target = (byte)StatTarget.MeleeDamage, Op = (byte)ModOp.Flat, Value = 6f, SourceId = src + 2u }; seeds[3] = new StatModifier { Target = (byte)StatTarget.MeleeRange, Op = (byte)ModOp.Flat, Value = 0.8f, SourceId = src + 3u }; } } /// Append a class's permanent trait modifiers onto a player's StatModifier buffer (via ECB at spawn). public static void AppendSeeds(byte classId, Entity player, EntityCommandBuffer ecb) { Span seeds = stackalloc StatModifier[ClassSeedCount]; Seeds(classId, seeds); for (int i = 0; i < ClassSeedCount; i++) ecb.AppendToBuffer(player, seeds[i]); } /// Append a class's permanent trait modifiers directly onto a live StatModifier buffer (in-place swap). public static void AppendSeeds(byte classId, DynamicBuffer mods) { Span seeds = stackalloc StatModifier[ClassSeedCount]; Seeds(classId, seeds); for (int i = 0; i < ClassSeedCount; i++) mods.Add(seeds[i]); } /// /// Swap an already-spawned player's class IN PLACE: strip whatever class seeds are present, then re-seed the /// target class. Order-independent (StatMath folds by Target/Op), so the RemoveAtSwapBack reordering is safe, /// and idempotent (switching to the same class still leaves exactly seeds). /// Foreign modifiers (upgrades, equipment, timed buffs — all on disjoint SourceIds) are preserved. /// public static void Reapply(byte classId, DynamicBuffer mods) { for (int i = mods.Length - 1; i >= 0; i--) if (IsClassSeed(mods[i].SourceId)) mods.RemoveAtSwapBack(i); AppendSeeds(classId, mods); } } }