4a8220ad3e
AbilityRef, AbilityCooldown, EffectiveAbilityStats, DefaultAbility deleted. GoInGameServerSystem seeds the per-frame 4-socket Spark loadout UNCONDITIONALLY (was gym-only); ClassSelectReceiveSystem + DebugOp.SetClass swap FrameId + re-seed sockets + zero SocketCooldown; ClassSwapUtil.Apply drops newAbilityId; EquipSystem weapons become stat-sticks (GrantedAbilityId removed from the item blob/authoring); StatRecomputeSystem folds CharacterStatsRef + sockets only; HUD cooldown bar reads socket 0 of SocketCooldown/EffectiveSocketStats; class HUD readers are FrameId-only (ClassForAbility/AbilityFor deleted); DebugModifierInjectionSystem drops CycleAbility. 390 tests green; Play-verified: a non-gym spawn gets frame=2 with Sparks [7,8,6,9] and no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 lines
4.3 KiB
C#
69 lines
4.3 KiB
C#
using Unity.Entities;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// The ONE in-place class-swap effect, shared by the editor dev tool (DebugOp.SetClass) and the player-facing
|
|
/// base ClassSelect (Staging). A class swap is much more than re-seeding: the pre-code review (DR-046) confirmed
|
|
/// that swapping only the class-seed band leaves the OLD class's PERMANENT META rows on the buffer and omits the
|
|
/// NEW class's — so a base swap would drain Aether into the wrong class's record and mis-set Max HP. This helper
|
|
/// mirrors the (previously editor-only) full swap: <see cref="ClassTraits.Reapply"/> (class-seed band) + the meta
|
|
/// band strip + per-class <see cref="MetaTierState"/> replay. The caller then writes AbilityRef/PlayerClass/
|
|
/// AbilityCooldown and calls <see cref="HealClamp"/> (a static can't resolve singletons or SystemAPI.SetComponent,
|
|
/// so the caller passes the resolved pieces). Server-authoritative + prediction-correct (StatRecomputeSystem
|
|
/// refolds EffectiveCharacterStats next tick).
|
|
/// </summary>
|
|
public static class ClassSwapUtil
|
|
{
|
|
/// <summary>Re-seed the class band + re-sync the permanent-meta band for <paramref name="rawClass"/> on
|
|
/// <paramref name="mods"/>. Returns the normalized class + its Fire ability id (the caller sets AbilityRef).
|
|
/// <paramref name="haveMeta"/> false (no catalog/record) skips the meta replay (the strip still runs).</summary>
|
|
/// <summary>Re-seed the class band + re-sync the permanent-meta band for <paramref name="rawClass"/> on
|
|
/// <paramref name="mods"/>. Returns the normalized class (the caller writes FrameId/PlayerClass + re-seeds
|
|
/// the socket loadout). <paramref name="haveMeta"/> false (no catalog/record) skips the meta replay (the
|
|
/// strip still runs).</summary>
|
|
public static void Apply(byte rawClass, DynamicBuffer<StatModifier> mods,
|
|
bool haveMeta, in MetaUpgradeCatalog metaCat, DynamicBuffer<MetaTierState> metaRecord,
|
|
out byte newClass)
|
|
{
|
|
newClass = ClassTraits.Normalize(rawClass);
|
|
ClassTraits.Reapply(newClass, mods);
|
|
|
|
// Strip the OLD class's meta rows (Reapply only touched the class-seed band), then replay the NEW class's
|
|
// persisted tiers (the GoInGame skip/clamp rules) so the permanent channel stays correct across the swap.
|
|
TimedModifierUtil.RemoveBySourceIdRange(mods, Tuning.MetaSourceIdBase,
|
|
Tuning.MetaSourceIdBase + Tuning.MetaSourceIdSpan);
|
|
if (haveMeta && metaCat.Value.IsCreated && metaRecord.IsCreated)
|
|
{
|
|
ref var metaPool = ref metaCat.Value.Value;
|
|
byte metaBit = BoonMath.MaskFor(newClass);
|
|
for (int mi = 0; mi < metaRecord.Length; mi++)
|
|
{
|
|
if (metaRecord[mi].ClassId != newClass || metaRecord[mi].Tier == 0) continue;
|
|
int defIdx = MetaMath.FindDef(ref metaPool, metaRecord[mi].UpgradeId);
|
|
if (defIdx < 0) continue;
|
|
if ((metaPool.Defs[defIdx].ClassMask & metaBit) == 0) continue;
|
|
byte metaTier = metaRecord[mi].Tier < metaPool.Defs[defIdx].MaxTier
|
|
? metaRecord[mi].Tier : metaPool.Defs[defIdx].MaxTier;
|
|
mods.Add(new StatModifier
|
|
{
|
|
Target = metaPool.Defs[defIdx].Target,
|
|
Op = metaPool.Defs[defIdx].Op,
|
|
Value = metaPool.Defs[defIdx].ValuePerTier * metaTier,
|
|
SourceId = Tuning.MetaSourceIdBase + metaRecord[mi].UpgradeId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Heal/down-clamp a LIVING player's Current to the new class's full max (blob base folded with the
|
|
/// just-reseeded <paramref name="mods"/>, like StatRecomputeSystem). Doubles as the down-clamp when the new
|
|
/// class's max is lower (Warrior +30 HP vs Ranger -15%). No-op on a corpse (Current<=0) — respawn refills.</summary>
|
|
public static void HealClamp(ref Health health, float baseMaxHealth, DynamicBuffer<StatModifier> mods)
|
|
{
|
|
if (health.Current > 0f)
|
|
health.Current = StatMath.Apply(baseMaxHealth, StatTarget.MaxHealth, mods);
|
|
}
|
|
}
|
|
}
|