diff --git a/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs index eb640d074..fe86ced87 100644 --- a/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Automation/ConveyorAuthoring.cs @@ -21,13 +21,7 @@ namespace ProjectM.Authoring public override void Bake(ConveyorAuthoring authoring) { var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - AddComponent(entity, new PlacedStructure - { - Type = StructureType.Conveyor, - Cell = default, - NextTick = 0u, - LastProcessedTick = 0u, - }); + this.AddPlacedStructure(entity, StructureType.Conveyor); AddComponent(entity, new Conveyor { Direction = authoring.Direction, diff --git a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs index c6947eae1..1f3a772cd 100644 --- a/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Automation/FabricatorAuthoring.cs @@ -30,13 +30,7 @@ namespace ProjectM.Authoring public override void Bake(FabricatorAuthoring authoring) { var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - AddComponent(entity, new PlacedStructure - { - Type = StructureType.Fabricator, - Cell = default, - NextTick = 0u, - LastProcessedTick = 0u, - }); + this.AddPlacedStructure(entity, StructureType.Fabricator); AddComponent(entity, new Fabricator { InResourceId = authoring.InResourceId, @@ -52,9 +46,7 @@ namespace ProjectM.Authoring // machine, diverging from DR-032. Now that it IS targetable, the DamageEvent buffer must exist // (an AI strike appends into it; absent = ECB-playback throw) + Destructible lets // HealthApplyDamageSystem destroy it at 0 (occupancy auto-frees). - AddComponent(entity, new Health { Current = authoring.MaxHp, Max = authoring.MaxHp }); - AddBuffer(entity); - AddComponent(entity); + this.AddDamageable(entity, authoring.MaxHp); } } } diff --git a/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs b/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs index 5e8ebc3b2..99d4dd316 100644 --- a/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Automation/HarvesterAuthoring.cs @@ -22,13 +22,7 @@ namespace ProjectM.Authoring public override void Bake(HarvesterAuthoring authoring) { var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - AddComponent(entity, new PlacedStructure - { - Type = StructureType.Harvester, - Cell = default, - NextTick = 0u, - LastProcessedTick = 0u, - }); + this.AddPlacedStructure(entity, StructureType.Harvester); AddComponent(entity, new Harvester { ResourceId = authoring.OutputResourceId, diff --git a/Assets/_Project/Scripts/Authoring/BakerStructureExt.cs b/Assets/_Project/Scripts/Authoring/BakerStructureExt.cs new file mode 100644 index 000000000..5962a3b66 --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/BakerStructureExt.cs @@ -0,0 +1,39 @@ +using ProjectM.Simulation; +using Unity.Entities; +using UnityEngine; + +namespace ProjectM.Authoring +{ + /// + /// Baker helpers shared by the build-structure / automation-machine bakers. These emit the SAME component data + /// the bakers wrote inline before (no serialized authoring field or baked component change) — they only + /// deduplicate the repeated stamp and the damageable triad + /// ( + buffer + ). Extension methods on + /// the concrete (not IBaker, whose AddComponent/AddBuffer are obsolete) so + /// the non-obsolete public Baker API resolves. + /// + public static class BakerStructureExt + { + /// Stamp PlacedStructure{Type=type} with the standard baked defaults (Cell/NextTick/LastProcessedTick set at placement). + public static void AddPlacedStructure(this Baker baker, Entity e, byte type) + where TAuthoring : Component + { + baker.AddComponent(e, new PlacedStructure + { + Type = type, + Cell = default, + NextTick = 0u, + LastProcessedTick = 0u, + }); + } + + /// Make an entity damageable/destructible: Health{Current=Max=maxHp} + the required DamageEvent buffer + Destructible tag. + public static void AddDamageable(this Baker baker, Entity e, float maxHp) + where TAuthoring : Component + { + baker.AddComponent(e, new Health { Current = maxHp, Max = maxHp }); + baker.AddBuffer(e); + baker.AddComponent(e); + } + } +} diff --git a/Assets/_Project/Scripts/Authoring/BakerStructureExt.cs.meta b/Assets/_Project/Scripts/Authoring/BakerStructureExt.cs.meta new file mode 100644 index 000000000..dde9dad55 --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/BakerStructureExt.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a54e2710fbe8cca4a9e84e9bbca4f7b9 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Authoring/Building/StructureAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/StructureAuthoring.cs index d566b2c9e..365c344d2 100644 --- a/Assets/_Project/Scripts/Authoring/Building/StructureAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Building/StructureAuthoring.cs @@ -24,19 +24,11 @@ namespace ProjectM.Authoring public override void Bake(StructureAuthoring authoring) { var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - AddComponent(entity, new PlacedStructure - { - Type = authoring.Kind, - Cell = default, - NextTick = 0u, - LastProcessedTick = 0u, - }); + this.AddPlacedStructure(entity, authoring.Kind); // EB-1: Wall/Pylon are damageable + destructible AI targets (a wall soaks Husk strikes that would // otherwise hit a turret). DamageEvent buffer MUST exist or an AI strike crashes at ECB playback. // No HitRadius -> ProjectileDamageSystem ignores them (no friendly projectile fire). - AddComponent(entity, new Health { Current = authoring.MaxHp, Max = authoring.MaxHp }); - AddBuffer(entity); - AddComponent(entity); + this.AddDamageable(entity, authoring.MaxHp); } } } diff --git a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs index 23a6bce17..e40be839a 100644 --- a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs @@ -21,13 +21,7 @@ namespace ProjectM.Authoring public override void Bake(TurretAuthoring authoring) { var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - AddComponent(entity, new PlacedStructure - { - Type = StructureType.Turret, - Cell = default, - NextTick = 0u, - LastProcessedTick = 0u, - }); + this.AddPlacedStructure(entity, StructureType.Turret); AddComponent(entity, new Turret { Range = authoring.Range, @@ -38,9 +32,7 @@ namespace ProjectM.Authoring // destroys a Destructible at Health<=0). The DamageEvent buffer MUST exist on the archetype or an // AI/turret strike crashes at ECB playback. NO HitRadius on purpose -> ProjectileDamageSystem (needs // Health+HitRadius) ignores structures, so player shots never friendly-fire your own turret. - AddComponent(entity, new Health { Current = authoring.MaxHp, Max = authoring.MaxHp }); - AddBuffer(entity); - AddComponent(entity); + this.AddDamageable(entity, authoring.MaxHp); } } } diff --git a/Assets/_Project/Scripts/Authoring/Combat/WaveDirectorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Combat/WaveDirectorAuthoring.cs index d34a02712..ae2c114ad 100644 --- a/Assets/_Project/Scripts/Authoring/Combat/WaveDirectorAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Combat/WaveDirectorAuthoring.cs @@ -60,15 +60,7 @@ namespace ProjectM.Authoring ClusterTightRadius = authoring.ClusterTightRadius, }); - var buffer = AddBuffer(entity); - if (authoring.EnemyPrefabs != null) - { - foreach (var prefab in authoring.EnemyPrefabs) - { - if (prefab != null) - buffer.Add(new WaveEnemyPrefab { Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic) }); - } - } + this.AddEnemyPrefabPool(entity, authoring.EnemyPrefabs, e => new WaveEnemyPrefab { Prefab = e }); AddComponent(entity, new WaveState { diff --git a/Assets/_Project/Scripts/Authoring/Combat/ZoneEnemyDirectorAuthoring.cs b/Assets/_Project/Scripts/Authoring/Combat/ZoneEnemyDirectorAuthoring.cs index f323ce11b..e546e5c36 100644 --- a/Assets/_Project/Scripts/Authoring/Combat/ZoneEnemyDirectorAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Combat/ZoneEnemyDirectorAuthoring.cs @@ -58,15 +58,7 @@ namespace ProjectM.Authoring ClusterTightRadius = authoring.ClusterTightRadius, }); - var buffer = AddBuffer(entity); - if (authoring.EnemyPrefabs != null) - { - foreach (var prefab in authoring.EnemyPrefabs) - { - if (prefab != null) - buffer.Add(new ZoneEnemyPrefab { Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic) }); - } - } + this.AddEnemyPrefabPool(entity, authoring.EnemyPrefabs, e => new ZoneEnemyPrefab { Prefab = e }); AddComponent(entity, new ZoneEnemyState { diff --git a/Assets/_Project/Scripts/Authoring/EnemyPrefabBakeExt.cs b/Assets/_Project/Scripts/Authoring/EnemyPrefabBakeExt.cs new file mode 100644 index 000000000..e62de872d --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/EnemyPrefabBakeExt.cs @@ -0,0 +1,31 @@ +using System; +using Unity.Entities; +using UnityEngine; + +namespace ProjectM.Authoring +{ + /// + /// Shared BAKE step for the two enemy directors (Wave + Zone): add the prefab-pool buffer and fill it from the + /// authoring GameObject[] exactly as before — always add the (possibly empty) buffer, skip null entries, and + /// GetEntity(prefab, Dynamic) per entry. The element type differs per director (WaveEnemyPrefab vs + /// ZoneEnemyPrefab), so the caller supplies a tiny factory wrapping the resolved Entity in its element struct; + /// nothing about the serialized inspector fields or the baked buffer contents changes. + /// + public static class EnemyPrefabBakeExt + { + public static void AddEnemyPrefabPool( + this Baker baker, Entity e, GameObject[] prefabs, Func make) + where TAuthoring : Component + where TElem : unmanaged, IBufferElementData + { + var buffer = baker.AddBuffer(e); + if (prefabs == null) + return; + foreach (var prefab in prefabs) + { + if (prefab != null) + buffer.Add(make(baker.GetEntity(prefab, TransformUsageFlags.Dynamic))); + } + } + } +} diff --git a/Assets/_Project/Scripts/Authoring/EnemyPrefabBakeExt.cs.meta b/Assets/_Project/Scripts/Authoring/EnemyPrefabBakeExt.cs.meta new file mode 100644 index 000000000..5452228b0 --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/EnemyPrefabBakeExt.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b1404309f9af2e1409511d4743e3eb99 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Editor/AnimRigUtil.cs b/Assets/_Project/Scripts/Editor/AnimRigUtil.cs new file mode 100644 index 000000000..11757c745 --- /dev/null +++ b/Assets/_Project/Scripts/Editor/AnimRigUtil.cs @@ -0,0 +1,45 @@ +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; + +namespace ProjectM.EditorTools +{ + /// + /// Shared editor-only helpers for the player/enemy Rukhanka rig tools (PlayerRigTools / EnemyRigTools): + /// AnimatorController parameter probing + the "overlay clip = full idle pose (every bone keyed, minus Root) + /// plus a Root-yaw twist" build. A Root-ONLY clip makes Rukhanka collapse every un-keyed bone to identity for + /// the state's duration -> the body sinks into the floor (writeDefaultValues does NOT prevent it), so both the + /// player swing and the enemy attack-windup base their clip on idle and drive only the Root yaw. The twist + /// keyframes differ per clip, so they are passed in. + /// + public static class AnimRigUtil + { + /// True if the controller already declares a parameter named . + public static bool HasParam(AnimatorController ac, string name) + { + foreach (var p in ac.parameters) if (p.name == name) return true; + return false; + } + + /// + /// Rebuild as the full pose (all bindings except Root) with + /// the supplied Root localEulerAnglesRaw.y curve on top; sets loopTime=false and + /// saves. Height-preserving (avoids the un-keyed-bone collapse). + /// + public static void BuildRootYawOverlayClip(AnimationClip clip, AnimationClip idle, AnimationCurve rootYaw) + { + clip.ClearCurves(); + foreach (var b in AnimationUtility.GetCurveBindings(idle)) + { + if (b.path == "Root") continue; // the Root is driven below + AnimationUtility.SetEditorCurve(clip, b, AnimationUtility.GetEditorCurve(idle, b)); + } + AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), rootYaw); + var s = AnimationUtility.GetAnimationClipSettings(clip); + s.loopTime = false; + AnimationUtility.SetAnimationClipSettings(clip, s); + EditorUtility.SetDirty(clip); + AssetDatabase.SaveAssets(); + } + } +} diff --git a/Assets/_Project/Scripts/Editor/AnimRigUtil.cs.meta b/Assets/_Project/Scripts/Editor/AnimRigUtil.cs.meta new file mode 100644 index 000000000..f61097620 --- /dev/null +++ b/Assets/_Project/Scripts/Editor/AnimRigUtil.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8824b7c4c8bbdd142858d1889c3ff308 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Editor/EnemyRigTools.cs b/Assets/_Project/Scripts/Editor/EnemyRigTools.cs index df878dc01..03dd4a347 100644 --- a/Assets/_Project/Scripts/Editor/EnemyRigTools.cs +++ b/Assets/_Project/Scripts/Editor/EnemyRigTools.cs @@ -97,15 +97,26 @@ namespace ProjectM.EditorTools static void MakeMat(string dst, string atlas, Color? tint) { - if (AssetDatabase.LoadAssetAtPath(PlayerMat) == null) + var src = AssetDatabase.LoadAssetAtPath(PlayerMat); + if (src == null) { Debug.LogError($"[EnemyRigTools] Source material missing: {PlayerMat}"); return; } - AssetDatabase.DeleteAsset(dst); - if (!AssetDatabase.CopyAsset(PlayerMat, dst)) - { Debug.LogError($"[EnemyRigTools] CopyAsset failed -> {dst}"); return; } - AssetDatabase.ImportAsset(dst); - + // GUID-preserving: overwrite an existing dst IN PLACE (CopySerialized keeps dst's GUID, so committed + // prefab material refs never break); only CopyAsset when dst is absent (first creation). The old + // DeleteAsset+CopyAsset minted a new GUID every run and silently orphaned those refs. var m = AssetDatabase.LoadAssetAtPath(dst); + if (m == null) + { + if (!AssetDatabase.CopyAsset(PlayerMat, dst)) + { Debug.LogError($"[EnemyRigTools] CopyAsset failed -> {dst}"); return; } + AssetDatabase.ImportAsset(dst); + m = AssetDatabase.LoadAssetAtPath(dst); + } + else + { + EditorUtility.CopySerialized(src, m); + } + var tex = AssetDatabase.LoadAssetAtPath(atlas); if (tex == null) Debug.LogWarning($"[EnemyRigTools] Atlas not found: {atlas}"); if (m.HasProperty("_BaseColorMap") && tex != null) m.SetTexture("_BaseColorMap", tex); @@ -128,26 +139,19 @@ namespace ProjectM.EditorTools if (idle == null) { Debug.LogError("[EnemyRigTools] No Idle clip to base the attack on."); return; } var clip = AssetDatabase.LoadAssetAtPath(AttackClip); if (clip == null) { clip = new AnimationClip { frameRate = 30f }; AssetDatabase.CreateAsset(clip, AttackClip); } - clip.ClearCurves(); - foreach (var cb in AnimationUtility.GetCurveBindings(idle)) - { if (cb.path == "Root") continue; AnimationUtility.SetEditorCurve(clip, cb, AnimationUtility.GetEditorCurve(idle, cb)); } var yaw = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.15f, -38f), new Keyframe(0.40f, 0f)); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), yaw); - var s = AnimationUtility.GetAnimationClipSettings(clip); - s.loopTime = false; - AnimationUtility.SetAnimationClipSettings(clip, s); - EditorUtility.SetDirty(clip); - AssetDatabase.SaveAssets(); + AnimRigUtil.BuildRootYawOverlayClip(clip, idle, yaw); if (AssetDatabase.LoadAssetAtPath(PlayerController) == null) { Debug.LogError($"[EnemyRigTools] Source controller missing: {PlayerController}"); return; } + // GUID caveat: DeleteAsset+CopyAsset re-mints AC_EnemyTopDown's GUID each run; left as-is because CopySerialized onto a composite AnimatorController risks orphaning its sub-assets (states/transitions), unlike MakeMat's flat Material. AssetDatabase.DeleteAsset(EnemyController); AssetDatabase.CopyAsset(PlayerController, EnemyController); AssetDatabase.ImportAsset(EnemyController); var ac = AssetDatabase.LoadAssetAtPath(EnemyController); - if (!HasParam(ac, "IsAttacking")) + if (!AnimRigUtil.HasParam(ac, "IsAttacking")) ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool); var sm = ac.layers[0].stateMachine; @@ -172,12 +176,6 @@ namespace ProjectM.EditorTools Debug.Log("[EnemyRigTools] AC_EnemyTopDown + EnemyAttackWindup clip built."); } - static bool HasParam(AnimatorController ac, string name) - { - foreach (var p in ac.parameters) if (p.name == name) return true; - return false; - } - // ---- 3. Prefabs -------------------------------------------------------------------------------------- [MenuItem("ProjectM/Animation/Enemy Rigs - 3 Build Prefabs")] diff --git a/Assets/_Project/Scripts/Editor/PlayerRigTools.cs b/Assets/_Project/Scripts/Editor/PlayerRigTools.cs index 4e070f7b9..ceb7618e3 100644 --- a/Assets/_Project/Scripts/Editor/PlayerRigTools.cs +++ b/Assets/_Project/Scripts/Editor/PlayerRigTools.cs @@ -30,22 +30,11 @@ namespace ProjectM.EditorTools // Full idle pose (every bone keyed) + a Root yaw twist. See the class summary for why a Root-only clip sinks. var clip = AssetDatabase.LoadAssetAtPath(SwingClip); if (clip == null) { clip = new AnimationClip { frameRate = 30f }; AssetDatabase.CreateAsset(clip, SwingClip); } - clip.ClearCurves(); - foreach (var b in AnimationUtility.GetCurveBindings(idle)) - { - if (b.path == "Root") continue; // the Root is driven below - AnimationUtility.SetEditorCurve(clip, b, AnimationUtility.GetEditorCurve(idle, b)); - } var yaw = new AnimationCurve( new Keyframe(0f, 0f), new Keyframe(0.06f, -25f), new Keyframe(0.16f, 48f), new Keyframe(0.30f, 0f)); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), yaw); - var s = AnimationUtility.GetAnimationClipSettings(clip); - s.loopTime = false; - AnimationUtility.SetAnimationClipSettings(clip, s); - EditorUtility.SetDirty(clip); - AssetDatabase.SaveAssets(); + AnimRigUtil.BuildRootYawOverlayClip(clip, idle, yaw); - if (!HasParam(ac, "IsAttacking")) + if (!AnimRigUtil.HasParam(ac, "IsAttacking")) ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool); var sm = ac.layers[0].stateMachine; @@ -80,12 +69,6 @@ namespace ProjectM.EditorTools return null; } - static bool HasParam(AnimatorController ac, string name) - { - foreach (var p in ac.parameters) if (p.name == name) return true; - return false; - } - static AnimatorState FindState(AnimatorStateMachine sm, string name) { foreach (var c in sm.states) if (c.state.name == name) return c.state; diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs b/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs index e597b1fcc..5e1834cba 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs @@ -143,17 +143,9 @@ namespace ProjectM.Simulation }); // C3: the cone reads as weak vs the melee cleave without knockback — stamp it like melee // (guarded: dummies lack KnockbackState → ECB throw; the boss is knockback-immune, A4). - if (m_KnockbackLookup.HasComponent(coneTargets[ci]) && !m_BossLookup.HasComponent(coneTargets[ci])) - { - float3 kd3 = coneTargetPos[ci] - xform.ValueRO.Position; - float2 kdir = math.lengthsq(kd3.xz) > 1e-6f ? math.normalize(kd3.xz) : cFace; - m_KnockbackLookup[coneTargets[ci]] = new KnockbackState - { - Dir = kdir, - Speed = Tuning.KnockbackSpeed, - UntilTick = TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)), - }; - } + KnockbackUtil.Stamp(ref m_KnockbackLookup, m_BossLookup, coneTargets[ci], + xform.ValueRO.Position, coneTargetPos[ci], cFace, Tuning.KnockbackSpeed, + TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks))); } } uint coneCd = (uint)math.max(1, eff.ValueRO.CooldownTicks); diff --git a/Assets/_Project/Scripts/Simulation/Combat/KnockbackUtil.cs b/Assets/_Project/Scripts/Simulation/Combat/KnockbackUtil.cs new file mode 100644 index 000000000..471261f03 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Combat/KnockbackUtil.cs @@ -0,0 +1,28 @@ +using Unity.Entities; +using Unity.Mathematics; + +namespace ProjectM.Simulation +{ + /// + /// Shared knockback stamp for melee/cone hits. Guarded exactly as the two call sites were: the target must own + /// (dummies/structures lacking it would throw at ECB playback if written) and must + /// NOT be a boss ( = knockback-immune, A4). The planar (XZ) heading is + /// normalize(targetPos - sourcePos), falling back to when that delta is + /// degenerate. Deduplicates the identical stamps in (Warrior cone) and + /// (melee cleave). Callers still gate their own speed/window (e.g. the melee + /// KnockSpeed > 0 check) before calling. + /// + static class KnockbackUtil + { + public static void Stamp(ref ComponentLookup lookup, in ComponentLookup bossLookup, + Entity target, float3 sourcePos, float3 targetPos, float2 faceFallback, float speed, uint untilTick) + { + if (!lookup.HasComponent(target) || bossLookup.HasComponent(target)) + return; + + float3 delta = targetPos - sourcePos; + float2 dir = math.lengthsq(delta.xz) > 1e-6f ? math.normalize(delta.xz) : faceFallback; + lookup[target] = new KnockbackState { Dir = dir, Speed = speed, UntilTick = untilTick }; + } + } +} diff --git a/Assets/_Project/Scripts/Simulation/Combat/KnockbackUtil.cs.meta b/Assets/_Project/Scripts/Simulation/Combat/KnockbackUtil.cs.meta new file mode 100644 index 000000000..6344e2224 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Combat/KnockbackUtil.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fa42de08cd15e1841853823f81f94fa3 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Player/MeleeComboSystem.cs b/Assets/_Project/Scripts/Simulation/Player/MeleeComboSystem.cs index 3e7219d73..c26f07735 100644 --- a/Assets/_Project/Scripts/Simulation/Player/MeleeComboSystem.cs +++ b/Assets/_Project/Scripts/Simulation/Player/MeleeComboSystem.cs @@ -248,13 +248,9 @@ namespace ProjectM.Simulation SourceNetworkId = c.OwnerId, SourceTick = c.Stamp, }); - if (c.KnockSpeed > 0f && m_KnockbackLookup.HasComponent(target) && !m_BossLookup.HasComponent(target)) - { - float3 d3 = enemyPositions[i] - c.From; - float2 kdir = new float2(d3.x, d3.z); - kdir = math.lengthsq(kdir) > 1e-6f ? math.normalize(kdir) : c.Face; - m_KnockbackLookup[target] = new KnockbackState { Dir = kdir, Speed = c.KnockSpeed, UntilTick = c.KnockUntil }; - } + if (c.KnockSpeed > 0f) + KnockbackUtil.Stamp(ref m_KnockbackLookup, m_BossLookup, 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