0ff11fe2a9
SwordCombat React (1.4x, 0.3s pulse) + Stagger (1.2x, 0.55s) as Any-State states on AC_EnemyTopDown via EnemyRigTools.WireEnemyHitReacts; driven by EnemyAnimationDriveSystem's cache (now Pos/Hp/ReactUntil/Heavy) off replicated Health drops. Windup-honesty gate: light reacts require !IsAttacking (a sub-poise hit never visually cancels a live windup); the heavy tier tracks the server's B2 poise break (stagger threshold 50 = finisher staggers, light flinches at the locked tuning). Knobs: HitReactSeconds (0=off) / HitStaggerSeconds / HitReactStaggerDamage. Replaces the review-cut positional vibrate (Rukhanka inverse-fold no-op). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
449 lines
27 KiB
C#
449 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
|
|
namespace ProjectM.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// Reusable editor pipeline that turns Synty Polygon character meshes into ANIMATED interpolated-ghost enemy
|
|
/// prefabs, mirroring the DR-022 player-rig recipe so the same Rukhanka client-derived animation works for
|
|
/// Husks (see DR-023). Three idempotent, re-runnable steps (menu: ProjectM/Animation):
|
|
/// 1. Build deformation materials (duplicate the player's AnimatedLitShader mat, swap the pack atlas).
|
|
/// 2. Build AC_EnemyTopDown (fork AC_PlayerTopDown + an Attack state) and the EnemyAttackWindup clip.
|
|
/// 3. Build the 3 animated enemy prefabs (clone the capsule ghost template, strip the capsule, flatten the
|
|
/// Synty skeleton + SMRs under the ghost root, add Animator + RigDefinitionAuthoring, set the deformation
|
|
/// material, offset the Root bone for feet-on-ground).
|
|
/// RigDefinitionAuthoring is set via reflection (no Rukhanka asmdef ref needed — same "by name" tactic as
|
|
/// ServerStripAnimationSystem). Outputs are NEW prefabs (templates stay pristine capsules); re-point the
|
|
/// gameplay subscene WaveDirector.EnemyPrefabs[] at them.
|
|
/// </summary>
|
|
public static class EnemyRigTools
|
|
{
|
|
const string PlayerMat = "Assets/_Project/Materials/M_SpaceSoldier_Animated.mat";
|
|
const string PlayerController = "Assets/_Project/Animation/AC_PlayerTopDown.controller";
|
|
const string EnemyController = "Assets/_Project/Animation/AC_EnemyTopDown.controller";
|
|
const string AttackClip = "Assets/_Project/Animation/EnemyAttackWindup.anim";
|
|
const string HitReactFbx = "Assets/Synty/AnimationSwordCombat/Animations/Polygon/Hit/HitReact/A_Hit_F_React_Sword.fbx";
|
|
const string HitStaggerFbx = "Assets/Synty/AnimationSwordCombat/Animations/Polygon/Hit/HitStagger/A_Hit_F_Stagger_Sword.fbx";
|
|
|
|
const string WerewolfAtlas = "Assets/Synty/PolygonWerewolf/Textures/PolygonWerewolf_01_A.png";
|
|
const string KaijuAtlas = "Assets/Synty/PolygonKaiju/Textures/PolygonKaiju_01.png";
|
|
|
|
const string MatWerewolf = "Assets/_Project/Materials/M_Enemy_Werewolf_Animated.mat";
|
|
const string MatWerewolfUndead = "Assets/_Project/Materials/M_Enemy_WerewolfUndead_Animated.mat";
|
|
const string MatKaiju = "Assets/_Project/Materials/M_Enemy_Kaiju_Animated.mat";
|
|
|
|
const string SyntyWerewolf = "Assets/Synty/PolygonWerewolf/Prefabs/Characters/SM_Chr_Werewolf_01.prefab";
|
|
const string SyntyKaiju = "Assets/Synty/PolygonKaiju/Prefabs/Characters/SM_Chr_Kaiju_01.prefab";
|
|
|
|
// MC-1 Charger (SciFi-City Muscle — verified Generic rig, distinct charging silhouette; see Synty_Asset_Inventory).
|
|
const string ChargerAtlas = "Assets/Synty/PolygonSciFiCity/Textures/Alts/PolygonScifi_01_A.png";
|
|
const string MatCharger = "Assets/_Project/Materials/M_Enemy_Charger_Animated.mat";
|
|
const string SyntyMuscle = "Assets/Synty/PolygonSciFiCity/Prefabs/Characters/SM_Chr_Muscle_Male_01.prefab";
|
|
|
|
struct Variant
|
|
{
|
|
public string Name, Template, Synty, Output, Material;
|
|
public float RootY; // Root-bone local Y (capsule-center -> feet; tuned per scale in Play)
|
|
public float Scale; // 0 = keep the template's baked scale
|
|
}
|
|
|
|
static Variant[] Variants() => new[]
|
|
{
|
|
new Variant { Name = "Grunt (Werewolf)", Template = "Assets/_Project/Prefabs/Enemy.prefab", Synty = SyntyWerewolf, Output = "Assets/_Project/Prefabs/EnemyWerewolf.prefab", Material = MatWerewolf, RootY = -1.25f, Scale = 0f },
|
|
new Variant { Name = "Swarmer (Werewolf Undead)", Template = "Assets/_Project/Prefabs/EnemySwarmer.prefab", Synty = SyntyWerewolf, Output = "Assets/_Project/Prefabs/EnemyWerewolfUndead.prefab", Material = MatWerewolfUndead, RootY = -1.67f, Scale = 0f },
|
|
new Variant { Name = "Brute (Kaiju)", Template = "Assets/_Project/Prefabs/EnemyBrute.prefab", Synty = SyntyKaiju, Output = "Assets/_Project/Prefabs/EnemyKaiju.prefab", Material = MatKaiju, RootY = -0.52f, Scale = 0f },
|
|
ChargerVariant(),
|
|
};
|
|
|
|
/// <summary>MC-1 Charger: SciFi-City Muscle silhouette via the standard DR-023 pipeline (the inventory's prescribed next-faction path). Template scale 1.0 -> RootY -1.0 (humanoid -1/scale rule); fine-tune feet in Play.</summary>
|
|
static Variant ChargerVariant() => new Variant { Name = "Charger (SciFi Muscle)", Template = "Assets/_Project/Prefabs/EnemyCharger.prefab", Synty = SyntyMuscle, Output = "Assets/_Project/Prefabs/EnemyChargerMuscle.prefab", Material = MatCharger, RootY = -1.00f, Scale = 0f };
|
|
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - Build All")]
|
|
public static void BuildAll()
|
|
{
|
|
BuildMaterials();
|
|
BuildControllerAndClip();
|
|
BuildPrefabs();
|
|
Debug.Log("[EnemyRigTools] Build All complete. Re-point WaveDirector.EnemyPrefabs[] at the new prefabs and re-bake the gameplay subscene.");
|
|
}
|
|
|
|
/// <summary>MC-1: build ONLY the Charger material + prefab (leaves the committed Werewolf/Kaiju outputs untouched).</summary>
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - Build Charger (MC-1)")]
|
|
public static void BuildCharger()
|
|
{
|
|
MakeMat(MatCharger, ChargerAtlas, new Color(1f, 0.42f, 0.36f, 1f)); // red-shifted SciFi atlas = danger read
|
|
AssetDatabase.SaveAssets();
|
|
BuildOne(ChargerVariant());
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Debug.Log("[EnemyRigTools] Charger built -> EnemyChargerMuscle.prefab; add it to WaveDirector.EnemyPrefabs[] in the gameplay subscene.");
|
|
}
|
|
|
|
/// <summary>LANTERN Drowner: SciFiSpace Crew_Male body (SAME rig/atlas/avatar as the player) via the standard
|
|
/// DR-023 path, gloam-pallor tint. Reuses the committed AC_EnemyTopDown controller. Template = grunt Enemy.prefab
|
|
/// (kind Grunt). RootY -1.13 (~ -0.9 player offset / 0.8 template scale) - tune feet in Play.</summary>
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - Build Drowner (LANTERN)")]
|
|
public static void BuildDrowner()
|
|
{
|
|
const string atlas = "Assets/Synty/PolygonSciFiSpace/Textures/PolygonSciFiSpace_Texture_01_A.png";
|
|
const string mat = "Assets/_Project/Materials/M_Enemy_Drowner_Animated.mat";
|
|
const string synty = "Assets/Synty/PolygonSciFiSpace/Prefabs/Characters/SM_Chr_Crew_Male_01.prefab";
|
|
MakeMat(mat, atlas, new Color(0.42f, 0.68f, 0.6f, 1f)); // drowned gloam pallor (cold sickly teal-green)
|
|
AssetDatabase.SaveAssets();
|
|
BuildOne(new Variant { Name = "Drowner (SciFiSpace Crew)", Template = "Assets/_Project/Prefabs/Enemy.prefab", Synty = synty, Output = "Assets/_Project/Prefabs/EnemyDrowner.prefab", Material = mat, RootY = -1.13f, Scale = 0f });
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Debug.Log("[EnemyRigTools] Drowner built -> EnemyDrowner.prefab; add to WaveDirector.EnemyPrefabs[] to spawn it.");
|
|
}
|
|
|
|
/// <summary>LANTERN Grindylow (2nd creature = A1 repeatability): SciFiSpace Alien body, cold gloam-blue, same path.</summary>
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - Build Grindylow (LANTERN)")]
|
|
public static void BuildGrindylow()
|
|
{
|
|
const string atlas = "Assets/Synty/PolygonSciFiSpace/Textures/PolygonSciFiSpace_Texture_01_A.png";
|
|
const string mat = "Assets/_Project/Materials/M_Enemy_Grindylow_Animated.mat";
|
|
const string synty = "Assets/Synty/PolygonSciFiSpace/Prefabs/Characters/SM_Chr_Alien_01.prefab";
|
|
MakeMat(mat, atlas, new Color(0.30f, 0.62f, 0.72f, 1f)); // cold gloam-blue deep-thing
|
|
AssetDatabase.SaveAssets();
|
|
BuildOne(new Variant { Name = "Grindylow (SciFiSpace Alien)", Template = "Assets/_Project/Prefabs/Enemy.prefab", Synty = synty, Output = "Assets/_Project/Prefabs/EnemyGrindylow.prefab", Material = mat, RootY = -1.13f, Scale = 0f });
|
|
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
|
|
Debug.Log("[EnemyRigTools] Grindylow built -> EnemyGrindylow.prefab.");
|
|
}
|
|
|
|
/// <summary>Bathynaut suit-frame PREVIEW: armored+helmeted soldier body, warm-neutral (ours, not gloam). Built on the
|
|
/// enemy chassis to PROVE the suit body skins+animates via Rukhanka; player-frame integration (swap into Player.prefab)
|
|
/// is a separate step, and the custom brass diving kit is an A3 silhouette upgrade.</summary>
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - Build Bathynaut Suit-Frame (LANTERN)")]
|
|
public static void BuildBathynautFrame()
|
|
{
|
|
const string atlas = "Assets/Synty/PolygonSciFiSpace/Textures/PolygonSciFiSpace_Texture_01_A.png";
|
|
const string mat = "Assets/_Project/Materials/M_SuitFrame_Bathynaut_Animated.mat";
|
|
const string synty = "Assets/Synty/PolygonSciFiSpace/Prefabs/Characters/SM_Chr_SpaceSoldier_HelmetArmor_Male_01.prefab";
|
|
MakeMat(mat, atlas, new Color(0.95f, 0.90f, 0.82f, 1f)); // warm-neutral: our light, not gloam-tinted
|
|
AssetDatabase.SaveAssets();
|
|
BuildOne(new Variant { Name = "Bathynaut Suit-Frame", Template = "Assets/_Project/Prefabs/Enemy.prefab", Synty = synty, Output = "Assets/_Project/Prefabs/SuitFrame_Bathynaut.prefab", Material = mat, RootY = -1.13f, Scale = 0f });
|
|
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
|
|
Debug.Log("[EnemyRigTools] Bathynaut suit-frame preview built -> SuitFrame_Bathynaut.prefab (not wired to the player).");
|
|
}
|
|
|
|
|
|
|
|
// ---- 1. Materials ------------------------------------------------------------------------------------
|
|
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - 1 Build Materials")]
|
|
public static void BuildMaterials()
|
|
{
|
|
MakeMat(MatWerewolf, WerewolfAtlas, null);
|
|
// Undead = the same werewolf atlas with a sickly desaturated-green tint so the Swarmer reads distinct.
|
|
MakeMat(MatWerewolfUndead, WerewolfAtlas, new Color(0.62f, 0.78f, 0.55f, 1f));
|
|
MakeMat(MatKaiju, KaijuAtlas, null);
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Debug.Log("[EnemyRigTools] Materials built.");
|
|
}
|
|
|
|
static void MakeMat(string dst, string atlas, Color? tint)
|
|
{
|
|
var src = AssetDatabase.LoadAssetAtPath<Material>(PlayerMat);
|
|
if (src == null)
|
|
{ Debug.LogError($"[EnemyRigTools] Source material missing: {PlayerMat}"); return; }
|
|
|
|
// 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<Material>(dst);
|
|
if (m == null)
|
|
{
|
|
if (!AssetDatabase.CopyAsset(PlayerMat, dst))
|
|
{ Debug.LogError($"[EnemyRigTools] CopyAsset failed -> {dst}"); return; }
|
|
AssetDatabase.ImportAsset(dst);
|
|
m = AssetDatabase.LoadAssetAtPath<Material>(dst);
|
|
}
|
|
else
|
|
{
|
|
EditorUtility.CopySerialized(src, m);
|
|
}
|
|
|
|
var tex = AssetDatabase.LoadAssetAtPath<Texture>(atlas);
|
|
if (tex == null) Debug.LogWarning($"[EnemyRigTools] Atlas not found: {atlas}");
|
|
if (m.HasProperty("_BaseColorMap") && tex != null) m.SetTexture("_BaseColorMap", tex);
|
|
if (tint.HasValue && m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", tint.Value);
|
|
EditorUtility.SetDirty(m);
|
|
}
|
|
|
|
// ---- 2. Controller + attack clip --------------------------------------------------------------------
|
|
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - 2 Build Controller")]
|
|
public static void BuildControllerAndClip()
|
|
{
|
|
// Attack tell: built FROM the full idle pose (every bone keyed) + a Root YAW coil. 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; 2026-06-11 fix). Idle base = nothing un-keyed; the yaw is
|
|
// height-preserving. Based on the player controller's Idle clip (the enemy controller is forked below).
|
|
var srcAc = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
|
|
AnimationClip idle = null;
|
|
if (srcAc != null) foreach (var st in srcAc.layers[0].stateMachine.states) if (st.state.name == "Idle") idle = st.state.motion as AnimationClip;
|
|
if (idle == null) { Debug.LogError("[EnemyRigTools] No Idle clip to base the attack on."); return; }
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(AttackClip);
|
|
if (clip == null) { clip = new AnimationClip { frameRate = 30f }; AssetDatabase.CreateAsset(clip, AttackClip); }
|
|
var yaw = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.15f, -38f), new Keyframe(0.40f, 0f));
|
|
AnimRigUtil.BuildRootYawOverlayClip(clip, idle, yaw);
|
|
|
|
if (AssetDatabase.LoadAssetAtPath<AnimatorController>(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<AnimatorController>(EnemyController);
|
|
|
|
if (!AnimRigUtil.HasParam(ac, "IsAttacking"))
|
|
ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool);
|
|
|
|
var sm = ac.layers[0].stateMachine;
|
|
var attackClipAsset = AssetDatabase.LoadAssetAtPath<AnimationClip>(AttackClip);
|
|
var attack = sm.AddState("Attack");
|
|
attack.motion = attackClipAsset;
|
|
attack.writeDefaultValues = false; // partial (Root-only) clip: MUST be false, else WDV resets every un-keyed bone (Hips/Spine/legs) to identity and the body collapses into the floor
|
|
|
|
var toAttack = sm.AddAnyStateTransition(attack);
|
|
toAttack.hasExitTime = false;
|
|
toAttack.duration = 0.06f;
|
|
toAttack.canTransitionToSelf = false;
|
|
toAttack.AddCondition(AnimatorConditionMode.If, 0f, "IsAttacking");
|
|
|
|
var fromAttack = attack.AddExitTransition();
|
|
fromAttack.hasExitTime = false;
|
|
fromAttack.duration = 0.12f;
|
|
fromAttack.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsAttacking");
|
|
|
|
EditorUtility.SetDirty(ac);
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log("[EnemyRigTools] AC_EnemyTopDown + EnemyAttackWindup clip built.");
|
|
}
|
|
|
|
// ---- 3. Prefabs --------------------------------------------------------------------------------------
|
|
|
|
[MenuItem("ProjectM/Animation/Enemy Rigs - 3 Build Prefabs")]
|
|
public static void BuildPrefabs()
|
|
{
|
|
foreach (var v in Variants()) BuildOne(v);
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Debug.Log("[EnemyRigTools] Animated enemy prefabs built. Re-point WaveDirector.EnemyPrefabs[] and re-bake.");
|
|
}
|
|
|
|
static void BuildOne(Variant v)
|
|
{
|
|
if (AssetDatabase.LoadAssetAtPath<GameObject>(v.Template) == null)
|
|
{ Debug.LogError($"[EnemyRigTools] Template missing: {v.Template}"); return; }
|
|
if (AssetDatabase.LoadAssetAtPath<GameObject>(v.Synty) == null)
|
|
{ Debug.LogError($"[EnemyRigTools] Synty char prefab missing: {v.Synty}"); return; }
|
|
|
|
// GUID-preserving: COPY the template only on the FIRST build (mints the output + its GUID). On a
|
|
// re-run, modify the existing output IN PLACE so its GUID stays stable -> the gameplay subscene's
|
|
// WaveDirector.EnemyPrefabs[] references never break. (DeleteAsset+CopyAsset would mint a new GUID
|
|
// each run and silently orphan those refs.)
|
|
if (AssetDatabase.LoadAssetAtPath<GameObject>(v.Output) == null)
|
|
{
|
|
if (!AssetDatabase.CopyAsset(v.Template, v.Output))
|
|
{ Debug.LogError($"[EnemyRigTools] CopyAsset failed -> {v.Output}"); return; }
|
|
AssetDatabase.ImportAsset(v.Output);
|
|
}
|
|
|
|
var root = PrefabUtility.LoadPrefabContents(v.Output);
|
|
try
|
|
{
|
|
// Clean slate for idempotent re-runs: drop any previously-built rig (children + rig components)
|
|
// and the primitive capsule visual; keep the root's ghost/authoring components + transform.
|
|
var kids = new List<Transform>();
|
|
foreach (Transform c in root.transform) kids.Add(c);
|
|
foreach (var c in kids) UnityEngine.Object.DestroyImmediate(c.gameObject);
|
|
foreach (var mf in root.GetComponents<MeshFilter>()) UnityEngine.Object.DestroyImmediate(mf);
|
|
foreach (var mr in root.GetComponents<MeshRenderer>()) UnityEngine.Object.DestroyImmediate(mr);
|
|
foreach (var an in root.GetComponents<Animator>()) UnityEngine.Object.DestroyImmediate(an);
|
|
var oldRda = FindType("Rukhanka.Hybrid.RigDefinitionAuthoring");
|
|
if (oldRda != null) { var ex = root.GetComponent(oldRda); if (ex != null) UnityEngine.Object.DestroyImmediate(ex); }
|
|
|
|
// Optional scale override (else keep the template's baked variant scale).
|
|
if (v.Scale > 0f) root.transform.localScale = new Vector3(v.Scale, v.Scale, v.Scale);
|
|
|
|
// Clone the Synty character (unlinked) and flatten its skeleton + SMRs under the ghost root.
|
|
var syntyAsset = AssetDatabase.LoadAssetAtPath<GameObject>(v.Synty);
|
|
var clone = (GameObject)UnityEngine.Object.Instantiate(syntyAsset);
|
|
var srcAnimator = clone.GetComponentInChildren<Animator>();
|
|
var avatar = srcAnimator != null ? srcAnimator.avatar : null;
|
|
|
|
var children = new List<Transform>();
|
|
foreach (Transform c in clone.transform) children.Add(c);
|
|
foreach (var c in children) c.SetParent(root.transform, false); // keep local transforms
|
|
UnityEngine.Object.DestroyImmediate(clone);
|
|
|
|
// Feet-on-ground: offset the un-keyed Root bone (entity origin = capsule center ~1m up).
|
|
var rootBone = root.transform.Find("Root");
|
|
if (rootBone != null)
|
|
{
|
|
var lp = rootBone.localPosition;
|
|
lp.y = v.RootY;
|
|
rootBone.localPosition = lp;
|
|
}
|
|
else Debug.LogWarning($"[EnemyRigTools] No 'Root' bone under {v.Output}; feet offset skipped.");
|
|
|
|
// Deformation material on every SMR slot (else unskinned-static + "does not support skinning").
|
|
var mat = AssetDatabase.LoadAssetAtPath<Material>(v.Material);
|
|
if (mat == null) Debug.LogWarning($"[EnemyRigTools] Material missing: {v.Material}");
|
|
foreach (var smr in root.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
|
{
|
|
var mats = smr.sharedMaterials;
|
|
for (int i = 0; i < mats.Length; i++) mats[i] = mat;
|
|
smr.sharedMaterials = mats;
|
|
}
|
|
|
|
// Animator on the ghost root (the entity that holds EnemyAuthoring + the Rukhanka param buffer).
|
|
var anim = root.GetComponent<Animator>();
|
|
if (anim == null) anim = root.AddComponent<Animator>();
|
|
anim.avatar = avatar;
|
|
anim.runtimeAnimatorController = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(EnemyController);
|
|
anim.applyRootMotion = false;
|
|
|
|
// RigDefinitionAuthoring (Rukhanka.Hybrid) via reflection — CPU engine, match the player rig.
|
|
AddRigDefinition(root);
|
|
|
|
PrefabUtility.SaveAsPrefabAsset(root, v.Output);
|
|
Debug.Log($"[EnemyRigTools] Built {v.Name} -> {v.Output} (rootY={v.RootY}).");
|
|
}
|
|
finally
|
|
{
|
|
PrefabUtility.UnloadPrefabContents(root);
|
|
}
|
|
}
|
|
|
|
static void AddRigDefinition(GameObject root)
|
|
{
|
|
var t = FindType("Rukhanka.Hybrid.RigDefinitionAuthoring");
|
|
if (t == null) { Debug.LogError("[EnemyRigTools] RigDefinitionAuthoring type not found (Rukhanka.Hybrid)."); return; }
|
|
var rda = root.GetComponent(t);
|
|
if (rda == null) rda = root.AddComponent(t);
|
|
SetMember(rda, "applyRootMotion", false);
|
|
SetMember(rda, "boneEntityStrippingMode", 1); // match Player.prefab
|
|
SetMember(rda, "animationEngine", 0); // 0 = CPU
|
|
EditorUtility.SetDirty(rda);
|
|
}
|
|
|
|
static Type FindType(string fullName)
|
|
{
|
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
var t = asm.GetType(fullName);
|
|
if (t != null) return t;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void SetMember(object o, string name, object val)
|
|
{
|
|
const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
|
var f = o.GetType().GetField(name, BF);
|
|
if (f != null)
|
|
{
|
|
object v = f.FieldType.IsEnum ? Enum.ToObject(f.FieldType, Convert.ToInt32(val))
|
|
: Convert.ChangeType(val, f.FieldType);
|
|
f.SetValue(o, v);
|
|
return;
|
|
}
|
|
var p = o.GetType().GetProperty(name, BF);
|
|
if (p != null && p.CanWrite)
|
|
{
|
|
object v = p.PropertyType.IsEnum ? Enum.ToObject(p.PropertyType, Convert.ToInt32(val))
|
|
: Convert.ChangeType(val, p.PropertyType);
|
|
p.SetValue(o, v);
|
|
return;
|
|
}
|
|
Debug.LogWarning($"[EnemyRigTools] Field/prop '{name}' not found on {o.GetType().Name}.");
|
|
}
|
|
|
|
|
|
/// <summary>07-21 hit-react pass (guidelines G5; the Rukhanka-safe flinch replacing the cut vibrate):
|
|
/// adds IsHit/IsHitHeavy params + HitReact (light flinch) and HitStagger (heavy) Any-State states to
|
|
/// AC_EnemyTopDown, pulsed by EnemyAnimationDriveSystem off replicated Health-drop edges. HONESTY GATE:
|
|
/// the light react requires IsAttacking == false so a sub-poise hit never visually cancels a windup the
|
|
/// server is still running (the telegraph must not lie); a HEAVY hit staggers unconditionally, which
|
|
/// aligns with the server's B2 poise break (finisher knockback >= the stagger threshold zeroes the
|
|
/// windup). Idempotent / re-runnable.</summary>
|
|
[MenuItem("ProjectM/Animation/Enemy - Wire Hit Reacts (LANTERN)")]
|
|
public static void WireEnemyHitReacts()
|
|
{
|
|
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(EnemyController);
|
|
if (ac == null) { Debug.LogError($"[EnemyRigTools] Controller missing: {EnemyController}"); return; }
|
|
|
|
AnimationClip Clip(string fbxPath, string clipName)
|
|
{
|
|
foreach (var o in AssetDatabase.LoadAllAssetsAtPath(fbxPath))
|
|
if (o is AnimationClip c && c.name == clipName) return c;
|
|
Debug.LogError($"[EnemyRigTools] Clip missing: {fbxPath} -> {clipName}");
|
|
return null;
|
|
}
|
|
var react = Clip(HitReactFbx, "A_Hit_F_React_Sword");
|
|
var stagger = Clip(HitStaggerFbx, "A_Hit_F_Stagger_Sword");
|
|
if (react == null || stagger == null) return;
|
|
|
|
void EnsureParam(string n)
|
|
{
|
|
foreach (var p in ac.parameters) if (p.name == n) return;
|
|
ac.AddParameter(n, AnimatorControllerParameterType.Bool);
|
|
}
|
|
EnsureParam("IsHit");
|
|
EnsureParam("IsHitHeavy");
|
|
|
|
var sm = ac.layers[0].stateMachine;
|
|
AnimatorState Find(string n)
|
|
{
|
|
foreach (var cs in sm.states) if (cs.state.name == n) return cs.state;
|
|
return null;
|
|
}
|
|
|
|
var hitReact = Find("HitReact");
|
|
if (hitReact == null)
|
|
{
|
|
hitReact = sm.AddState("HitReact");
|
|
var to = sm.AddAnyStateTransition(hitReact);
|
|
to.hasExitTime = false; to.duration = 0.05f; to.canTransitionToSelf = false;
|
|
to.AddCondition(AnimatorConditionMode.If, 0f, "IsHit");
|
|
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsHitHeavy");
|
|
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsAttacking"); // honesty: never cancel a live windup visual
|
|
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsDead");
|
|
var ex = hitReact.AddExitTransition();
|
|
ex.hasExitTime = false; ex.duration = 0.10f;
|
|
ex.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsHit");
|
|
}
|
|
hitReact.motion = react;
|
|
hitReact.speed = 1.4f; // 0.83s clip -> the flinch peak inside the ~0.3s pulse
|
|
|
|
var hitStagger = Find("HitStagger");
|
|
if (hitStagger == null)
|
|
{
|
|
hitStagger = sm.AddState("HitStagger");
|
|
var to = sm.AddAnyStateTransition(hitStagger);
|
|
to.hasExitTime = false; to.duration = 0.05f; to.canTransitionToSelf = false;
|
|
to.AddCondition(AnimatorConditionMode.If, 0f, "IsHit");
|
|
to.AddCondition(AnimatorConditionMode.If, 0f, "IsHitHeavy"); // heavy overrides even a windup (server poise broke it)
|
|
to.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsDead");
|
|
var ex = hitStagger.AddExitTransition();
|
|
ex.hasExitTime = false; ex.duration = 0.12f;
|
|
ex.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsHit");
|
|
}
|
|
hitStagger.motion = stagger;
|
|
hitStagger.speed = 1.2f; // 1.07s clip -> the lurch inside the ~0.55s pulse
|
|
|
|
EditorUtility.SetDirty(ac);
|
|
AssetDatabase.SaveAssets();
|
|
Debug.Log("[EnemyRigTools] Hit reacts wired: HitReact (light, windup-honest) + HitStagger (heavy) on AC_EnemyTopDown.");
|
|
}
|
|
}
|
|
}
|