Files
tiny_tackle_heroes/Assets/_Project/Scripts/Combat/ArtifactChargeVisual.cs
T

875 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using BumpCombat.Core;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Combat
{
[DisallowMultipleComponent]
[DefaultExecutionOrder(1000)]
public sealed class ArtifactChargeVisual : MonoBehaviour
{
public const int FrameCount = 6;
public const int FlashFrameCount = 4;
public const int CellSize = 64;
public const float PixelsPerUnit = 32f;
public const float ChargeVisualY = 0f;
public const float ReadyFlashDuration = 0.14f;
public const float ReleaseVisualDuration = 0.24f;
public const float SwitchVisualDuration = 0.36f;
private const int AuraSortingOffset = -1;
private const int ReadyAuraSortingOffset = -1;
private const int FlashSortingOffset = 1;
private const int ReleaseSortingOffset = 1;
private const string ResourceRoot = "Artifacts/ThreeColor-v1/Charge/";
private const string AuraSuffix = "-Aura-v1";
private const string ReadyAuraSuffix = "-ReadyAura-v1";
private const string FlashSuffix = "-Flash-v1";
private const string ReleaseSuffix = "-Release-v1";
private static readonly float[] AuraFrameDurations =
{
0.06f, 0.06f, 0.06f, 0.06f, 0.06f, 0.06f,
};
private static readonly float[] ReadyAuraFrameDurations =
{
0.06f, 0.06f, 0.06f, 0.06f, 0.06f, 0.06f,
};
private static readonly float[] FlashFrameDurations =
{
0.03f, 0.04f, 0.04f, 0.03f,
};
private static readonly float[] ReleaseFrameDurations =
{
0.03f, 0.04f, 0.04f, 0.04f, 0.04f, 0.05f,
};
private sealed class CachedFrames
{
public CachedFrames(Sprite[] frames, bool ownsSprites)
{
Frames = frames;
OwnsSprites = ownsSprites;
}
public Sprite[] Frames { get; }
public bool OwnsSprites { get; }
}
private sealed class VisualSlot
{
public VisualSlot(
string objectName,
int sortingOffset,
float[] frameDurations)
{
ObjectName = objectName;
SortingOffset = sortingOffset;
FrameDurations = frameDurations;
}
public string ObjectName { get; }
public int SortingOffset { get; }
public float[] FrameDurations { get; }
public Sprite[] Frames;
public GameObject Root;
public SpriteRenderer Renderer;
public int FrameIndex;
public float FrameElapsed;
public bool Loop;
}
private static readonly Dictionary<string, CachedFrames> FrameCache = new();
private readonly VisualSlot aura = new(
"Artifact Charge Aura",
AuraSortingOffset,
AuraFrameDurations);
private readonly VisualSlot readyAura = new(
"Artifact Charge Ready Aura",
ReadyAuraSortingOffset,
ReadyAuraFrameDurations);
private readonly VisualSlot flash = new(
"Artifact Charge Flash",
FlashSortingOffset,
FlashFrameDurations);
private readonly VisualSlot release = new(
"Artifact Release",
ReleaseSortingOffset,
ReleaseFrameDurations);
private readonly VisualSlot switchPulse = new(
"Artifact Switch",
FlashSortingOffset,
AuraFrameDurations);
private ActiveArtifactController controller;
private readonly Dictionary<string, Sprite[]> equipmentFrames = new();
private readonly Dictionary<Sprite, Sprite> equipmentOriginals = new();
private PlayerHealth playerHealth;
private SpriteRenderer playerRenderer;
private SpriteRenderer guardOutline;
private readonly Dictionary<Sprite, Sprite> guardFrames = new();
private RunManager subscribedRunManager;
private ActiveArtifactDefinition chargingArtifact;
private bool readyFlashPlayed;
private int flashPlayCount;
private int releasePlayCount;
public bool IsAuraVisible => IsVisible(aura);
public bool IsSwitchVisible => IsVisible(switchPulse);
public GameObject SwitchObject => switchPulse.Root;
public bool IsReadyAuraVisible => IsVisible(readyAura);
public bool IsFlashVisible => IsVisible(flash);
public bool IsReleaseVisible => IsVisible(release);
public GameObject AuraObject => aura.Root;
public GameObject ReadyAuraObject => readyAura.Root;
public GameObject FlashObject => flash.Root;
public GameObject ReleaseObject => release.Root;
public int FlashPlayCount => flashPlayCount;
public int ReleasePlayCount => releasePlayCount;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetFrameCache()
{
foreach (CachedFrames cached in FrameCache.Values)
{
if (!cached.OwnsSprites)
{
continue;
}
foreach (Sprite sprite in cached.Frames)
{
if (sprite == null)
{
continue;
}
if (Application.isPlaying)
{
Destroy(sprite);
}
else
{
DestroyImmediate(sprite);
}
}
}
FrameCache.Clear();
}
private void Awake()
{
controller = GetComponent<ActiveArtifactController>();
playerHealth = GetComponent<PlayerHealth>();
playerRenderer = FindPlayerRenderer();
}
private void OnEnable()
{
if (controller == null)
{
controller = GetComponent<ActiveArtifactController>();
}
if (playerHealth == null)
{
playerHealth = GetComponent<PlayerHealth>();
}
if (playerRenderer == null)
{
playerRenderer = FindPlayerRenderer();
}
SubscribeController();
SubscribePlayerHealth();
RefreshRunManagerSubscription();
RestoreCurrentChargeVisuals();
}
private void OnDisable()
{
if (guardOutline != null) guardOutline.enabled = false;
RestoreEquipmentSprite();
UnsubscribeController();
UnsubscribePlayerHealth();
UnsubscribeRunManager();
ClearAllVisuals();
}
private void OnDestroy()
{
if (guardOutline != null) Destroy(guardOutline.gameObject);
UnsubscribeController();
UnsubscribePlayerHealth();
UnsubscribeRunManager();
ClearAllVisuals();
}
private void LateUpdate()
{
RefreshEquipmentSprite();
RefreshGuardOutline();
RefreshRunManagerSubscription();
if (subscribedRunManager != null
&& (subscribedRunManager.IsSelectionOpen
|| subscribedRunManager.IsGameOver
|| subscribedRunManager.IsTitleScreen))
{
ClearAllVisuals();
return;
}
if (controller == null)
{
ClearAllVisuals();
return;
}
if (!controller.isActiveAndEnabled)
{
ClearAllVisuals();
return;
}
if (controller.IsCharging)
{
UpdateChargeVisuals();
}
else
{
ClearChargeHoldVisuals();
}
SyncSlot(aura, true);
SyncSlot(readyAura, true);
SyncSlot(flash, true);
SyncSlot(release, false);
SyncSlot(switchPulse, true);
AdvanceSlot(aura, Time.deltaTime);
AdvanceSlot(readyAura, Time.deltaTime);
AdvanceSlot(flash, Time.deltaTime);
AdvanceSlot(release, Time.deltaTime);
AdvanceSlot(switchPulse, Time.deltaTime);
}
public void RefreshEquipmentSprite()
{
if (playerRenderer == null || playerRenderer.sprite == null) return;
Sprite current = playerRenderer.sprite;
Sprite original = equipmentOriginals.TryGetValue(current, out Sprite source) ? source : current;
if (controller == null || !controller.isActiveAndEnabled || controller.CurrentArtifact == null)
{
playerRenderer.sprite = original;
return;
}
string path = "Artifacts/ThreeColor-v1/Player/" + original.texture.name
+ "-" + controller.CurrentArtifact.ArtifactColor + "-v1";
if (!equipmentFrames.TryGetValue(path, out Sprite[] frames))
{
frames = Resources.LoadAll<Sprite>(path);
equipmentFrames[path] = frames;
}
foreach (Sprite frame in frames)
{
if (frame.rect != original.rect) continue;
equipmentOriginals[frame] = original;
playerRenderer.sprite = frame;
return;
}
playerRenderer.sprite = original;
}
public void RefreshGuardOutline()
{
bool visible = isActiveAndEnabled && playerHealth != null
&& playerHealth.isActiveAndEnabled && playerHealth.CurrentHealth > 0f
&& playerHealth.IsGuarding && playerRenderer != null
&& playerRenderer.enabled && playerRenderer.sprite != null
&& (RunManager.Instance == null || !RunManager.Instance.IsGameOver);
if (!visible)
{
if (guardOutline != null) guardOutline.enabled = false;
return;
}
if (guardOutline == null)
{
var root = new GameObject("Player Guard Outline");
root.transform.SetParent(playerRenderer.transform, false);
guardOutline = root.AddComponent<SpriteRenderer>();
}
Sprite current = playerRenderer.sprite;
Sprite original = equipmentOriginals.TryGetValue(current, out Sprite source) ? source : current;
if (!guardFrames.TryGetValue(original, out Sprite mask))
{
foreach (Sprite frame in Resources.LoadAll<Sprite>("Combat/Guard-v1/" + original.texture.name + "-outline"))
{
if (frame.rect != original.rect) continue;
mask = frame;
break;
}
guardFrames[original] = mask;
}
guardOutline.sprite = mask;
guardOutline.enabled = mask != null;
guardOutline.flipX = playerRenderer.flipX;
guardOutline.flipY = playerRenderer.flipY;
guardOutline.color = new Color(1f, 1f, 1f, playerRenderer.color.a);
guardOutline.sharedMaterial = playerRenderer.sharedMaterial;
guardOutline.sortingLayerID = playerRenderer.sortingLayerID;
guardOutline.sortingOrder = playerRenderer.sortingOrder + 1;
}
private void RestoreEquipmentSprite()
{
if (playerRenderer != null && playerRenderer.sprite != null
&& equipmentOriginals.TryGetValue(playerRenderer.sprite, out Sprite original))
playerRenderer.sprite = original;
}
public void Connect(ActiveArtifactController artifactController)
{
if (controller == artifactController && controller != null)
{
SubscribeController();
return;
}
UnsubscribeController();
controller = artifactController;
SubscribeController();
}
public void NotifyChargeEnded(bool preserveReadyFlash)
{
ClearChargeHoldVisuals();
if (!preserveReadyFlash)
{
ClearSlot(flash);
}
}
public void NotifyArtifactUseFailed()
{
ClearAllVisuals();
}
public void ClearAllVisuals()
{
ClearSlot(aura);
ClearSlot(readyAura);
ClearSlot(flash);
ClearSlot(release);
ClearSlot(switchPulse);
}
private void SubscribeController()
{
if (controller == null)
{
return;
}
controller.OnStateChanged -= HandleControllerStateChanged;
controller.OnArtifactChargeStarted -= HandleArtifactChargeStarted;
controller.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
controller.OnArtifactSwitched -= HandleArtifactSwitched;
controller.OnStateChanged += HandleControllerStateChanged;
controller.OnArtifactChargeStarted += HandleArtifactChargeStarted;
controller.OnArtifactUseSucceeded += HandleArtifactUseSucceeded;
controller.OnArtifactSwitched += HandleArtifactSwitched;
}
private void UnsubscribeController()
{
if (controller == null)
{
return;
}
controller.OnStateChanged -= HandleControllerStateChanged;
controller.OnArtifactChargeStarted -= HandleArtifactChargeStarted;
controller.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
controller.OnArtifactSwitched -= HandleArtifactSwitched;
}
private void SubscribePlayerHealth()
{
if (playerHealth == null)
{
return;
}
playerHealth.OnDamaged -= HandlePlayerDamaged;
playerHealth.OnDied -= HandlePlayerDied;
playerHealth.OnDamaged += HandlePlayerDamaged;
playerHealth.OnDied += HandlePlayerDied;
}
private void UnsubscribePlayerHealth()
{
if (playerHealth == null)
{
return;
}
playerHealth.OnDamaged -= HandlePlayerDamaged;
playerHealth.OnDied -= HandlePlayerDied;
}
private void RefreshRunManagerSubscription()
{
RunManager current = RunManager.Instance;
if (current == subscribedRunManager)
{
return;
}
UnsubscribeRunManager();
subscribedRunManager = current;
if (subscribedRunManager != null)
{
subscribedRunManager.OnSelectionChanged += HandleSelectionChanged;
}
}
private void UnsubscribeRunManager()
{
if (subscribedRunManager != null)
{
subscribedRunManager.OnSelectionChanged -= HandleSelectionChanged;
subscribedRunManager = null;
}
}
private void HandleArtifactChargeStarted(ActiveArtifactDefinition definition)
{
ClearSlot(switchPulse);
chargingArtifact = definition;
readyFlashPlayed = false;
ClearSlot(readyAura);
ClearSlot(flash);
ClearSlot(aura);
if (!Application.isPlaying || definition == null)
{
return;
}
aura.Frames = LoadFrames(
GetResourcePath(definition, AuraSuffix),
FrameCount);
readyAura.Frames = LoadFrames(
GetResourcePath(definition, ReadyAuraSuffix),
FrameCount);
flash.Frames = LoadFrames(
GetResourcePath(definition, FlashSuffix),
FlashFrameCount);
if (aura.Frames.Length == 0)
{
return;
}
CreateSlot(aura, true, Vector2.zero, 1f);
}
private void RestoreCurrentChargeVisuals()
{
if (!Application.isPlaying
|| controller == null
|| !controller.IsCharging)
{
return;
}
chargingArtifact = controller.CurrentArtifact;
if (chargingArtifact == null)
{
return;
}
aura.Frames = LoadFrames(
GetResourcePath(chargingArtifact, AuraSuffix),
FrameCount);
readyAura.Frames = LoadFrames(
GetResourcePath(chargingArtifact, ReadyAuraSuffix),
FrameCount);
flash.Frames = LoadFrames(
GetResourcePath(chargingArtifact, FlashSuffix),
FlashFrameCount);
UpdateChargeVisuals();
}
private void HandleControllerStateChanged()
{
if (controller == null || !controller.IsCharging)
{
ClearChargeHoldVisuals();
return;
}
UpdateChargeVisuals();
}
private void HandleArtifactUseSucceeded(
ActiveArtifactDefinition definition,
bool charged)
{
ClearSlot(switchPulse);
ClearChargeHoldVisuals();
if (!Application.isPlaying || definition == null)
{
return;
}
release.Frames = LoadFrames(
GetResourcePath(definition, ReleaseSuffix),
FrameCount);
if (release.Frames.Length == 0)
{
return;
}
Vector2 origin = controller.LastArtifactUsePosition;
CreateSlot(release, false, origin, charged ? 1.2f : 1f);
releasePlayCount++;
}
private void HandleArtifactSwitched(ActiveArtifactDefinition definition)
{
ClearSlot(switchPulse);
if (!Application.isPlaying || definition == null)
{
return;
}
switchPulse.Frames = LoadFrames(
GetResourcePath(definition, ReleaseSuffix), FrameCount);
if (switchPulse.Frames.Length == 0)
{
return;
}
// Reuse the existing colored outward burst, following the player
// for a single short cycle so movement does not leave it behind.
CreateSlot(switchPulse, true, Vector2.zero, 1f);
}
private void HandlePlayerDamaged(Vector2 _, float __)
{
ClearAllVisuals();
}
private void HandlePlayerDied()
{
ClearAllVisuals();
}
private void HandleSelectionChanged(bool isOpen)
{
if (isOpen)
{
ClearAllVisuals();
}
}
private void UpdateChargeVisuals()
{
if (controller == null
|| !controller.IsCharging
|| chargingArtifact == null)
{
return;
}
if (!controller.IsFullyCharged)
{
EnsureLoopSlot(aura, AuraSuffix, 1f);
ClearSlot(readyAura);
SetAlpha(
aura,
Mathf.Lerp(0.55f, 0.85f, controller.ChargeProgress));
return;
}
EnsureLoopSlot(readyAura, ReadyAuraSuffix, 1f);
ClearSlot(aura);
if (!readyFlashPlayed)
{
readyFlashPlayed = true;
flashPlayCount++;
if (flash.Frames.Length > 0 && Application.isPlaying)
{
CreateSlot(flash, true, Vector2.zero, 1f);
}
}
}
private void EnsureLoopSlot(
VisualSlot slot,
string suffix,
float scale)
{
if (slot.Root != null || chargingArtifact == null)
{
return;
}
slot.Frames = LoadFrames(
GetResourcePath(chargingArtifact, suffix),
FrameCount);
if (slot.Frames.Length == 0 || !Application.isPlaying)
{
return;
}
CreateSlot(slot, true, Vector2.zero, scale);
}
private void CreateSlot(
VisualSlot slot,
bool followPlayer,
Vector2 worldOrigin,
float scale)
{
ClearSlot(slot);
slot.Root = new GameObject(slot.ObjectName);
if (followPlayer)
{
slot.Root.transform.SetParent(transform, false);
slot.Root.transform.localPosition = new Vector3(
0f,
ChargeVisualY,
0f);
}
else
{
slot.Root.transform.position = new Vector3(
worldOrigin.x,
worldOrigin.y + ChargeVisualY,
transform.position.z);
}
slot.Root.transform.localScale = Vector3.one * scale;
slot.Renderer = slot.Root.AddComponent<SpriteRenderer>();
slot.Renderer.color = Color.white;
slot.Renderer.sprite = slot.Frames[0];
slot.Renderer.enabled = true;
slot.FrameIndex = 0;
slot.FrameElapsed = 0f;
slot.Loop = slot == aura || slot == readyAura;
SyncSlot(slot, followPlayer);
}
private void SyncSlot(VisualSlot slot, bool followPlayer)
{
if (slot.Root == null || slot.Renderer == null)
{
return;
}
if (followPlayer)
{
slot.Root.transform.SetParent(transform, false);
slot.Root.transform.localPosition = new Vector3(
0f,
ChargeVisualY,
0f);
}
if (playerRenderer == null)
{
playerRenderer = FindPlayerRenderer();
}
if (playerRenderer != null)
{
slot.Renderer.sortingLayerID = playerRenderer.sortingLayerID;
slot.Renderer.sortingOrder =
playerRenderer.sortingOrder + slot.SortingOffset;
slot.Renderer.sharedMaterial = playerRenderer.sharedMaterial;
}
Color color = Color.white;
if (slot == aura
&& controller != null
&& controller.IsCharging
&& !controller.IsFullyCharged)
{
color.a = Mathf.Lerp(0.55f, 0.85f, controller.ChargeProgress);
}
slot.Renderer.color = color;
slot.Renderer.enabled = true;
}
private static void SetAlpha(VisualSlot slot, float alpha)
{
if (slot.Renderer == null)
{
return;
}
Color color = Color.white;
color.a = Mathf.Clamp01(alpha);
slot.Renderer.color = color;
}
private static void AdvanceSlot(VisualSlot slot, float deltaTime)
{
if (slot.Root == null
|| slot.Renderer == null
|| slot.Frames == null
|| slot.Frames.Length == 0
|| deltaTime <= 0f)
{
return;
}
slot.FrameElapsed += deltaTime;
while (slot.FrameElapsed >= slot.FrameDurations[slot.FrameIndex])
{
slot.FrameElapsed -= slot.FrameDurations[slot.FrameIndex];
int nextFrame = slot.FrameIndex + 1;
if (nextFrame >= slot.Frames.Length)
{
if (!slot.Loop)
{
ClearSlot(slot);
return;
}
nextFrame = 0;
}
slot.FrameIndex = nextFrame;
slot.Renderer.sprite = slot.Frames[slot.FrameIndex];
}
}
private void ClearChargeHoldVisuals()
{
ClearSlot(aura);
ClearSlot(readyAura);
}
private static bool IsVisible(VisualSlot slot)
{
return slot.Root != null
&& slot.Renderer != null
&& slot.Renderer.enabled;
}
private SpriteRenderer FindPlayerRenderer()
{
SpriteRenderer rootRenderer = GetComponent<SpriteRenderer>();
if (rootRenderer != null)
{
return rootRenderer;
}
SpriteRenderer[] renderers = GetComponentsInChildren<SpriteRenderer>(true);
foreach (SpriteRenderer renderer in renderers)
{
if (renderer != null && renderer.transform != transform)
{
return renderer;
}
}
return GetComponent<SpriteRenderer>();
}
private static string GetResourcePath(
ActiveArtifactDefinition definition,
string suffix)
{
return ResourceRoot + definition.Effect + suffix;
}
private static void ClearSlot(VisualSlot slot)
{
if (slot.Root != null)
{
slot.Root.SetActive(false);
if (Application.isPlaying)
{
Destroy(slot.Root);
}
else
{
DestroyImmediate(slot.Root);
}
}
slot.Root = null;
slot.Renderer = null;
slot.FrameIndex = 0;
slot.FrameElapsed = 0f;
}
private static Sprite[] LoadFrames(
string resourcePath,
int expectedFrameCount)
{
if (FrameCache.TryGetValue(resourcePath, out CachedFrames cached))
{
return cached.Frames;
}
Sprite[] importedFrames = Resources.LoadAll<Sprite>(resourcePath);
if (importedFrames != null
&& importedFrames.Length >= expectedFrameCount)
{
Array.Sort(
importedFrames,
(left, right) => String.CompareOrdinal(left.name, right.name));
Sprite[] frames = new Sprite[expectedFrameCount];
Array.Copy(importedFrames, frames, expectedFrameCount);
FrameCache[resourcePath] = new CachedFrames(frames, false);
return frames;
}
Texture2D texture = importedFrames != null
&& importedFrames.Length > 0
? importedFrames[0].texture
: Resources.Load<Texture2D>(resourcePath);
if (texture == null
|| texture.width < CellSize * expectedFrameCount
|| texture.height < CellSize
|| texture.width % expectedFrameCount != 0)
{
Sprite[] missing = Array.Empty<Sprite>();
FrameCache[resourcePath] = new CachedFrames(missing, false);
return missing;
}
int frameWidth = texture.width / expectedFrameCount;
Sprite[] generatedFrames = new Sprite[expectedFrameCount];
for (int i = 0; i < expectedFrameCount; i++)
{
generatedFrames[i] = Sprite.Create(
texture,
new Rect(i * frameWidth, 0f, frameWidth, CellSize),
new Vector2(0.5f, 0.5f),
PixelsPerUnit,
0,
SpriteMeshType.FullRect);
generatedFrames[i].name =
$"{resourcePath} Frame {i + 1}";
generatedFrames[i].hideFlags = HideFlags.HideAndDontSave;
}
FrameCache[resourcePath] = new CachedFrames(generatedFrames, true);
return generatedFrames;
}
}
}