Files
AIGame/AIClient/AgentScripts/BuildEnemyCombatFx.cs
T
udxandClaude Opus 5.5 89e0fa246f Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to
  buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting.
- Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn):
  Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type
  in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes.
- Formal partners for all ten pets in the arena and portraits, including new
  VineSprite, ThunderLeopard and TideWhale.
- Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands,
  configurable Unity run_script timeout, companion static stage.
- Also includes concurrent work from another tool in this working tree
  (FlameLion, enemy combat FX, challenge mode, home explorer, server updates).
- Resources registered in Doc/ResDesc_*.md; verified on isolated :18081.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:23:55 +08:00

119 lines
11 KiB
C#

using System;
using System.IO;
using System.Linq;
using HotUpdate.Gameplay;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>Persistent combat assets, imported sound settings and an isolated review. Run through Pipeline.</summary>
public static class BuildEnemyCombatFx
{
public const string Folder = "Assets/Bundles/Effects/EnemyCombat";
public const string Bank = Folder + "/EnemyCombatPresentation.prefab";
const string Arena = "Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab";
static readonly string[] Names = { "HornletAttack", "DashletAttack", "ShellhornAttack", "HornCaptainAttack", "GreatHornAttack", "HitSoft", "HitArmor", "HitShield" };
static readonly Color[] Colors = { new Color(1,.66f,.23f), new Color(.65f,.9f,1), new Color(1,.7f,.35f), new Color(1,.38f,.14f), new Color(1,.3f,.12f), new Color(1,.94f,.63f), new Color(1,.7f,.28f), new Color(.3f,.85f,1) };
static void Folders(string path)
{
if (AssetDatabase.IsValidFolder(path)) return;
Folders(Path.GetDirectoryName(path).Replace('\\','/')); AssetDatabase.CreateFolder(Path.GetDirectoryName(path).Replace('\\','/'), Path.GetFileName(path));
}
static Material Material()
{
string png = Folder + "/CombatSoft.png";
var tex = new Texture2D(64,64,TextureFormat.RGBA32,false);
for (int y=0;y<64;y++) for (int x=0;x<64;x++)
{ float r = Vector2.Distance(new Vector2(x,y),new Vector2(31.5f,31.5f))/31.5f; tex.SetPixel(x,y,new Color(1,1,1,Mathf.Pow(Mathf.Clamp01(1-r),2))); }
tex.Apply(); File.WriteAllBytes(png,tex.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(tex);
AssetDatabase.ImportAsset(png, ImportAssetOptions.ForceUpdate);
var imp=(TextureImporter)AssetImporter.GetAtPath(png); imp.alphaIsTransparency=true; imp.wrapMode=TextureWrapMode.Clamp; imp.mipmapEnabled=false; imp.SaveAndReimport();
string path=Folder+"/CombatGlow.mat";
var mat=AssetDatabase.LoadAssetAtPath<Material>(path);
if(mat==null) { mat=new Material(Shader.Find("AIGame/EnemyCombatParticle") ?? throw new Exception("Particle shader missing")); AssetDatabase.CreateAsset(mat,path); }
mat.mainTexture=AssetDatabase.LoadAssetAtPath<Texture2D>(png); EditorUtility.SetDirty(mat); return mat;
}
static Mesh Arc(string name, float degrees)
{
const int count=40; var verts=new Vector3[(count+1)*2]; var uv=new Vector2[verts.Length]; var tri=new int[count*6];
for(int i=0;i<=count;i++)
{
float a=(i/(float)count*degrees-degrees/2)*Mathf.Deg2Rad;
float width=.065f*Mathf.Sin(Mathf.PI*i/count)+.006f;
for(int j=0;j<2;j++) { float r=.5f+(j==0?-width:width); verts[i*2+j]=new Vector3(Mathf.Sin(a)*r,Mathf.Cos(a)*r,0); uv[i*2+j]=new Vector2(.5f,j==0?.35f:.65f); }
if(i<count) { int k=i*6,v=i*2; tri[k]=v;tri[k+1]=v+1;tri[k+2]=v+2;tri[k+3]=v+1;tri[k+4]=v+3;tri[k+5]=v+2; }
}
string path=Folder+"/"+name+".asset"; var mesh=AssetDatabase.LoadAssetAtPath<Mesh>(path);
if(mesh==null) { mesh=new Mesh();AssetDatabase.CreateAsset(mesh,path); } else mesh.Clear();
mesh.vertices=verts; mesh.uv=uv; mesh.triangles=tri;mesh.RecalculateBounds();EditorUtility.SetDirty(mesh);return mesh;
}
static ParticleSystem Part(Transform parent,string name,Material mat,Color color,int count,float life,float size,float speed,Mesh mesh=null)
{
var go=new GameObject(name);go.layer=30;if(parent!=null)go.transform.SetParent(parent,false);
var ps=go.AddComponent<ParticleSystem>();var main=ps.main; main.playOnAwake=false;main.loop=false;main.duration=.8f;main.startLifetime=new ParticleSystem.MinMaxCurve(life*.7f,life);main.startSpeed=new ParticleSystem.MinMaxCurve(speed*.5f,speed);main.startSize=new ParticleSystem.MinMaxCurve(size*.65f,size);main.startColor=color;main.maxParticles=48;main.simulationSpace=ParticleSystemSimulationSpace.Local;
var em=ps.emission;em.rateOverTime=0;em.SetBursts(new[]{new ParticleSystem.Burst(0,(short)count)});
var shape=ps.shape;shape.shapeType=ParticleSystemShapeType.Sphere;shape.radius=.055f;
var col=ps.colorOverLifetime;col.enabled=true; var grad=new Gradient();grad.SetKeys(new[]{new GradientColorKey(color,0),new GradientColorKey(color,1)},new[]{new GradientAlphaKey(.95f,0),new GradientAlphaKey(.65f,.25f),new GradientAlphaKey(0,1)});col.color=grad;
var sz=ps.sizeOverLifetime;sz.enabled=true;sz.size=new ParticleSystem.MinMaxCurve(1,AnimationCurve.Linear(0,1,1,.08f));
var rend=ps.GetComponent<ParticleSystemRenderer>();rend.sharedMaterial=mat;
if(mesh!=null) {rend.renderMode=ParticleSystemRenderMode.Mesh;rend.mesh=mesh; main.startRotation3D=true;main.startRotationZ=0;}
else {rend.renderMode=ParticleSystemRenderMode.Stretch;rend.velocityScale=.08f;rend.lengthScale=1.3f;}
return ps;
}
public static string Build()
{
if(EditorApplication.isPlaying)throw new Exception("Author in Edit Mode");
Folders(Folder);var mat=Material();var arc=Arc("CombatArc",200);var ring=Arc("CombatRing",360);
var original=SceneManager.GetActiveScene();var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);
try
{
SceneManager.SetActiveScene(scene);var assets=new GameObject[8];
for(int i=0;i<8;i++)
{
var root=Part(null,"Vfx_Enemy_"+Names[i],mat,Colors[i], i<5?14:10,i==4?.55f:.32f,i==4?.18f:.10f,i==4?2.8f:1.7f);
var glow=Part(root.transform,"ContactFlash",mat,Colors[i],1,.15f,i==4?1.25f:.65f,0);glow.GetComponent<ParticleSystemRenderer>().renderMode=ParticleSystemRenderMode.Billboard;
var sweep=Part(root.transform,i<5?"StrikeArc":"ImpactRing",mat,Colors[i],1,i==4?.46f:.22f,i==4?2.5f:i<5?1.6f:.85f,0,i<5&&i!=2?arc:ring);
var sz=sweep.sizeOverLifetime;sz.size=new ParticleSystem.MinMaxCurve(1,AnimationCurve.Linear(0,.65f,1,1.3f));
if(i==1) {sweep.transform.localScale=new Vector3(.45f,1.5f,1);sweep.transform.localRotation=Quaternion.Euler(0,0,65);}
if(i==3) {var second=Part(root.transform,"CaptainCrossStrike",mat,Colors[i],1,.26f,1.6f,0,arc);second.transform.localRotation=Quaternion.Euler(0,0,95);}
if(i==4) { var shock=Part(root.transform,"GroundShock",mat,Colors[i],1,.55f,3.5f,0,ring);shock.transform.localPosition=Vector3.down*.75f;shock.transform.localRotation=Quaternion.Euler(90,0,0);var s=shock.sizeOverLifetime;s.size=new ParticleSystem.MinMaxCurve(1,AnimationCurve.Linear(0,.1f,1,1.2f)); }
assets[i]=PrefabUtility.SaveAsPrefabAsset(root.gameObject,Folder+"/Vfx_Enemy_"+Names[i]+".prefab");UnityEngine.Object.DestroyImmediate(root.gameObject);
}
var bank=new GameObject("EnemyCombatPresentation");bank.layer=30;var player=bank.AddComponent<EnemyCombatPresentation>();var so=new SerializedObject(player);
var pools=so.FindProperty("_effects");pools.arraySize=8;var clips=so.FindProperty("_clips");clips.arraySize=8;
for(int i=0;i<8;i++)
{
var list=pools.GetArrayElementAtIndex(i).FindPropertyRelative("instances");list.arraySize=4;
for(int n=0;n<4;n++) { var go=(GameObject)PrefabUtility.InstantiatePrefab(assets[i],bank.transform);go.name=Names[i]+"_"+n;list.GetArrayElementAtIndex(n).objectReferenceValue=go.GetComponent<ParticleSystem>(); }
string path="Assets/Bundles/Sound/SFX/EnemyCombat/Enemy_"+Names[i]+".wav";AssetDatabase.ImportAsset(path,ImportAssetOptions.ForceUpdate);
var imp=(AudioImporter)AssetImporter.GetAtPath(path);if(imp==null)throw new Exception("Missing "+path);
var st=imp.defaultSampleSettings;st.loadType=AudioClipLoadType.DecompressOnLoad;st.compressionFormat=AudioCompressionFormat.PCM;st.sampleRateSetting=AudioSampleRateSetting.PreserveSampleRate;st.preloadAudioData=true;imp.defaultSampleSettings=st;imp.forceToMono=false;imp.SaveAndReimport();
clips.GetArrayElementAtIndex(i).objectReferenceValue=AssetDatabase.LoadAssetAtPath<AudioClip>(path);
}
var voices=so.FindProperty("_voices");voices.arraySize=8;
for(int n=0;n<8;n++) {var go=new GameObject("Voice"+n);go.transform.SetParent(bank.transform,false);var source=go.AddComponent<AudioSource>();source.playOnAwake=false;source.spatialBlend=0;source.volume=.40f;source.priority=100;voices.GetArrayElementAtIndex(n).objectReferenceValue=source;}
so.ApplyModifiedPropertiesWithoutUndo();PrefabUtility.SaveAsPrefabAsset(bank,Bank);UnityEngine.Object.DestroyImmediate(bank);
}
finally {SceneManager.SetActiveScene(original);EditorSceneManager.CloseScene(scene,true);}
var arena=PrefabUtility.LoadPrefabContents(Arena);
try {var old=arena.transform.Find("EnemyCombatPresentation");if(old!=null)UnityEngine.Object.DestroyImmediate(old.gameObject);PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Bank),arena.transform);PrefabUtility.SaveAsPrefabAsset(arena,Arena);}
finally {PrefabUtility.UnloadPrefabContents(arena);}
AssetDatabase.SaveAssets();return Verify();
}
public static string Verify()
{
foreach(var name in new[]{"review","battle"})
if(File.Exists("Assets/Screenshots/enemy_combat_fx_"+name+".png"))AssetDatabase.ImportAsset("Assets/Screenshots/enemy_combat_fx_"+name+".png",ImportAssetOptions.ForceUpdate);
var bank=AssetDatabase.LoadAssetAtPath<GameObject>(Bank);var p=bank.GetComponent<EnemyCombatPresentation>();
if(ShaderUtil.ShaderHasError(Shader.Find("AIGame/EnemyCombatParticle")))throw new Exception("Combat shader compilation failed");
if(p.EffectCount!=8||p.VoiceCount!=8)throw new Exception("Missing bank slots");
var clips=Enumerable.Range(0,8).Select(i=> {var c=p.Clip(i);if(c==null||c.length>1||c.frequency!=44100||c.channels!=2)throw new Exception("Invalid clip "+i);var imp=(AudioImporter)AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(c));if(imp.defaultSampleSettings.compressionFormat!=AudioCompressionFormat.PCM||imp.defaultSampleSettings.loadType!=AudioClipLoadType.DecompressOnLoad)throw new Exception("Unexpected importer for "+c.name);return new {c.name,c.length,c.frequency,c.channels};}).ToArray();
if(bank.GetComponentsInChildren<ParticleSystem>().Any(ps=>ps.main.playOnAwake||ps.main.loop))throw new Exception("Loop/autoplay in one shot");
if(AssetDatabase.LoadAssetAtPath<GameObject>(Arena).GetComponentInChildren<EnemyCombatPresentation>(true)==null)throw new Exception("Arena missing bank");
var report=JsonConvert.SerializeObject(new {status="pass",bank=Bank,effects=8,poolInstances=32,voices=8,clips},Formatting.Indented);
string path="../Doc/Design/Sound/EnemyCombat/unity_assets.json";Directory.CreateDirectory(Path.GetDirectoryName(path));File.WriteAllText(path,report);return report;
}
}