using System.Collections; using System.Collections.Generic; using BlockOutReplica.Presentation; using UnityEngine; namespace BlockOutReplica.Gameplay { public sealed partial class BlockOutGameController { public string BoosterPhase { get; private set; } = ""; public float BoosterPlaybackTime { get; private set; } public float FreezeAnimationTime { get; private set; } public int ActiveBoosterParticleCount { get; private set; } public int ActiveFreezeParticleCount { get; private set; } public bool HasNativeFreezeScreen { get; private set; } private Transform CreateAnimationSpace(string name,bool atTop=false) { var space=new GameObject(name).transform; space.SetParent(GetBoardRoot(),false); generated.Add(space.gameObject); float sourceScale=gameCamera.orthographicSize*2/20.48f; space.position=gameCamera.transform.position+gameCamera.transform.forward*3 +gameCamera.transform.up*(atTop?gameCamera.orthographicSize:-gameCamera.orthographicSize); // FreezeGun's animated Root already enters from below. Original // playtest places the gun in the lower half, not over the timer. if(atTop)space.position-=gameCamera.transform.up*gameCamera.orthographicSize; space.rotation=gameCamera.transform.rotation; space.localScale=Vector3.one*sourceScale; return space; } private IEnumerator PlayFreezeBooster() { var data=Resources.Load("OriginalBoosters/Freeze"); if(data==null){Debug.LogError("Missing recovered Freeze animation. Run Recover Original Visual Layers.");yield break;} var space=CreateAnimationSpace("Original Freeze Gun",true); var actor=data.CreateInstance(space,200); var clockData=Resources.Load("OriginalBoosters/FreezeTimerStart"); var endData=Resources.Load("OriginalBoosters/FreezeTimerEnd"); var screenData=Resources.Load("OriginalBoosters/FreezeScreen"); Transform clockSpace=clockData==null?null:CreateAnimationSpace("Original Ice Timer"); var clock=clockData==null?null:clockData.CreateInstance(clockSpace,205); Transform screenSpace=screenData==null?null:CreateAnimationSpace("Original Freeze Screen",true); var screen=screenData==null?null:screenData.CreateInstance(screenSpace,190); HasNativeFreezeScreen=screen!=null; if(screen!=null&&screen.Renderers.TryGetValue("freezefront",out var front)) front.size=new Vector2(gameCamera.aspect*20.48f,20.48f); float clockTime=0,ending=0; float endDuration=endData==null?.8333333f:endData.Duration; for(float t=0;ScreenState==BlockOutScreenState.Playing && (freezeRemaining>0 || ending=data.Duration)Destroy(actor.Root); } if(clock!=null) { clockTime=Mathf.Max(0,t-.55f); if(clockTime<=clockData.Duration)clockData.Sample(clock,clockTime); else clockData.AdvanceParticles(clock,Time.deltaTime); } if(freezeRemaining<=0)ending+=Time.deltaTime; // Original end alpha targets are stripped hashes. Retain the source end // duration, but explicitly reconstruct opacity rather than invent bindings. float fade=1-Mathf.Clamp01(ending/endDuration); if(screen!=null)screenData.SetOverlayOpacity(screen,Mathf.Clamp01(t/.28f)*fade); if(clock!=null&&ending>0)clockData.SetOverlayOpacity(clock,fade); ActiveFreezeParticleCount=(actor.Root==null?0:actor.LiveParticleCount)+(clock==null?0:clock.LiveParticleCount); t+=Time.deltaTime; yield return null; } Destroy(space.gameObject);if(clockSpace!=null)Destroy(clockSpace.gameObject);if(screenSpace!=null)Destroy(screenSpace.gameObject); ActiveFreezeParticleCount=0;HasNativeFreezeScreen=false; } private IEnumerator PlayRecoveredTargetBooster(BoosterType type,List targets) { var data=Resources.Load("OriginalBoosters/"+type); if(data==null) { Debug.LogError("Recovered booster animation missing: "+type); foreach(var item in targets) if(item!=null){SpecialCompleteRemoval(item);Destroy(item.gameObject);activeExits--;} InputLocked=false; CloseUnusedDoors(); yield return FinishBoardIfCleared(); yield break; } var space=CreateAnimationSpace("Original "+type+" FX"); var actor=data.CreateInstance(space,160); var origins=new Vector3[targets.Count]; var scales=new Vector3[targets.Count]; for(int i=0;i=hit&&actor.Transforms.TryGetValue("RocketParent/Rocket",out var rocketImage))rocketImage.gameObject.SetActive(false); if(data.RecoveredParticleSystems==0&&time>=trailAt+.05f && time<.62f){SpawnRocketTrail(space,point);trailAt=time;} } if(time>=hit && !landed) { landed=true; PlayUiSound(type==BoosterType.Rocket?"Rocket_Land":"UFO"); if(type==BoosterType.Rocket) { if(data.RecoveredParticleSystems>0)data.TriggerParticles(actor,"RocketParent/rocket_explosion1"); else SpawnBoosterImpact(origins[0]); } } for(int i=0;i=1){SpecialCompleteRemoval(item);Destroy(item.gameObject);activeExits--;} } time+=Time.deltaTime; yield return null; } // Extremely large target batches are still retired exactly once. foreach(var item in targets)if(item!=null&&item.transform.localScale.sqrMagnitude>.00001f){SpecialCompleteRemoval(item);Destroy(item.gameObject);activeExits--;} Destroy(space.gameObject); InputLocked=false; BoosterPhase=""; BoosterPlaybackTime=0; Status=""; ActiveBoosterParticleCount=0; CloseUnusedDoors(); yield return FinishBoardIfCleared(); } private void SpawnRocketTrail(Transform space,Vector3 point) { // Reconstruction fallback: original particle modules are not yet restored. var cube=GameObject.CreatePrimitive(PrimitiveType.Cube); Destroy(cube.GetComponent()); cube.name="Rocket Exhaust Cube"; cube.transform.SetParent(space,false); cube.transform.localPosition=point-new Vector3(.2f,.7f,0); cube.transform.localScale=Vector3.one*.32f; cube.transform.localRotation=Random.rotation; var m=new Material(Shader.Find("Sprites/Default"));m.color=new Color(1,.65f,.12f); cube.GetComponent().material=m; cube.AddComponent().Initialize(new Vector3(-.7f,0,-1),.4f,this,m); } private void SpawnBoosterImpact(Vector3 position) { for(int i=0;i<12;i++) { var cube=GameObject.CreatePrimitive(PrimitiveType.Cube);Destroy(cube.GetComponent()); cube.name="Rocket Impact Cube";cube.transform.SetParent(GetBoardRoot(),false); cube.transform.position=position+Vector3.up*.4f; cube.transform.localScale=Vector3.one*.15f;generated.Add(cube); var m=new Material(Shader.Find("Sprites/Default"));m.color=i%2==0?Color.white:new Color(1,.65f,.1f); cube.GetComponent().material=m; cube.AddComponent().Initialize(new Vector3(Mathf.Cos(i*2.4f),.6f,Mathf.Sin(i*2.4f))*2,.45f,this,m); } } } internal sealed class BoosterTransient:MonoBehaviour { private Vector3 velocity,initialScale; private float remaining,duration;private BlockOutGameController owner;private Material material; public void Initialize(Vector3 v,float life,BlockOutGameController game,Material m){velocity=v;duration=remaining=life;owner=game;material=m;initialScale=transform.localScale;} private void Update(){if(owner!=null&&owner.IsPaused)return;remaining-=Time.deltaTime;transform.position+=velocity*Time.deltaTime;transform.localScale=initialScale*Mathf.Clamp01(remaining/duration);if(remaining<=0)Destroy(gameObject);} private void OnDestroy(){if(material!=null)Destroy(material);} } }