Files

206 lines
9.2 KiB
C#

using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Enemies
{
// Authored exterior pixels only; this component never changes the damage gate.
[DefaultExecutionOrder(10000)]
public sealed class NecromancerProtectionVisual : MonoBehaviour
{
private static readonly Dictionary<Sprite, Sprite> Masks = new();
private EnemyController controller;
private SpriteRenderer body;
private SpriteRenderer outline;
private SpriteRenderer groggySpiral;
private float orbitTime;
private TextMesh shieldLabel;
private MeshRenderer shieldLabelRenderer;
private static readonly Color GroggyColor = new Color32(255, 238, 182, 255);
private void Awake()
{
controller = GetComponent<EnemyController>();
body = GetComponent<SpriteRenderer>();
var root = new GameObject("Necromancer Protection");
root.transform.SetParent(transform, false);
outline = root.AddComponent<SpriteRenderer>();
outline.enabled = false;
var labelRoot = new GameObject("Artifact Shield Label");
labelRoot.transform.SetParent(transform, false);
shieldLabel = labelRoot.AddComponent<TextMesh>();
shieldLabel.fontSize = 32;
shieldLabel.characterSize = 0.06f;
shieldLabel.anchor = TextAnchor.MiddleCenter;
shieldLabel.alignment = TextAlignment.Center;
Font font = Resources.Load<Font>("Presentation/Fonts/Galmuri9");
shieldLabelRenderer = labelRoot.GetComponent<MeshRenderer>();
if (font != null)
{
shieldLabel.font = font;
shieldLabelRenderer.sharedMaterial = font.material;
}
shieldLabelRenderer.enabled = false;
var marker = new GameObject("Groggy Spiral");
marker.transform.SetParent(transform, false);
groggySpiral = marker.AddComponent<SpriteRenderer>();
groggySpiral.enabled = false;
}
private void LateUpdate()
{
if (controller != null && controller.IsGroggy) orbitTime += Time.deltaTime;
else orbitTime = 0f;
Refresh();
}
public void Refresh()
{
if (outline == null) return;
bool visible = controller != null && controller.isActiveAndEnabled && !controller.IsDead
&& (controller.IsDamageInvulnerable || controller.IsGroggy)
&& body != null && body.enabled && body.sprite != null;
outline.enabled = visible;
RefreshGroggyMarker(visible && controller.IsGroggy);
RefreshShieldLabel(visible && controller.IsDamageInvulnerable && !controller.IsGroggy);
if (!visible) return;
if (!Masks.TryGetValue(body.sprite, out var mask))
{
var candidates = Resources.LoadAll<Sprite>("Enemies/Protection-v2/" + body.sprite.texture.name + "-outline");
foreach (var candidate in candidates)
if (candidate.rect == body.sprite.rect) { mask = candidate; break; }
Masks[body.sprite] = mask;
}
outline.sprite = mask;
outline.enabled = mask != null;
outline.flipX = body.flipX;
outline.flipY = body.flipY;
Color tint = controller.IsGroggy ? GroggyColor : GetShieldColor();
tint.a = body.color.a;
outline.color = tint;
outline.sharedMaterial = body.sharedMaterial;
outline.sortingLayerID = body.sortingLayerID;
outline.sortingOrder = body.sortingOrder + 1;
}
private void OnDisable()
{
if (outline != null) outline.enabled = false;
if (shieldLabelRenderer != null) shieldLabelRenderer.enabled = false;
RefreshGroggyMarker(false);
orbitTime = 0f;
}
private bool HasProtectionSummons => GetComponent<NecromancerBossController>()?.IsDamageBlocked == true;
private Color GetShieldColor()
{
if (HasProtectionSummons) return new Color32(205, 211, 224, 255);
return ActiveArtifactDefinition.GetArtifactPaletteColor(controller.ShieldColor);
}
private void RefreshShieldLabel(bool visible)
{
if (shieldLabelRenderer == null) return;
shieldLabelRenderer.enabled = visible;
if (!visible) return;
string group = controller.ShieldColor == ArtifactColor.Green ? "초록"
: controller.ShieldColor == ArtifactColor.Red ? "빨강" : "파랑";
shieldLabel.text = HasProtectionSummons ? "보호 소환" : group + " 실드 " + controller.ShieldHitsRemaining;
shieldLabel.color = GetShieldColor();
shieldLabel.transform.position = transform.TransformPoint(new Vector3(0, GetHeadHeight(), 0)) + Vector3.up * 0.38f;
Vector3 scale = transform.lossyScale;
shieldLabel.transform.localScale = new Vector3(1f / Mathf.Max(0.001f, Mathf.Abs(scale.x)),
1f / Mathf.Max(0.001f, Mathf.Abs(scale.y)), 1f);
shieldLabelRenderer.sortingLayerID = body.sortingLayerID;
shieldLabelRenderer.sortingOrder = body.sortingOrder + 101;
}
private float GetHeadHeight()
{
return CrowdControlMarkerArt.HeadLocalHeight(body.sprite);
}
private void RefreshGroggyMarker(bool visible)
{
if (groggySpiral == null) return;
groggySpiral.enabled = visible;
if (!visible) return;
groggySpiral.sprite = CrowdControlMarkerArt.Frame(orbitTime, true);
groggySpiral.transform.position = CrowdControlMarkerArt.MarkerWorldPosition(body);
groggySpiral.transform.localScale = CombatFeedback.CalculateStunMarkerBaseLocalScale(transform.lossyScale);
groggySpiral.sortingLayerID = body.sortingLayerID;
groggySpiral.sortingOrder = body.sortingOrder + 100;
groggySpiral.color = new Color(1f, 1f, 1f, body.color.a);
groggySpiral.enabled = groggySpiral.sprite != null;
}
}
// Shared authored CC art and head landmarks. No combat state is modified here.
public static class CrowdControlMarkerArt
{
public const float MarkerScale = 0.5f;
public const float MarkerHalfHeight = 10f / 32f * MarkerScale;
private static Sprite[] stunFrames;
private static Sprite[] groggyFrames;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetFrames()
{
foreach (Sprite[] frames in new[] { stunFrames, groggyFrames })
if (frames != null)
foreach (Sprite frame in frames)
if (frame != null) Object.Destroy(frame);
stunFrames = null;
groggyFrames = null;
}
public static Sprite Frame(float elapsed, bool groggy)
{
Sprite[] frames = groggy ? groggyFrames : stunFrames;
if (frames == null)
{
Texture2D texture = Resources.Load<Texture2D>(
"Artifacts/CrowdControl-v1/CC-Spiral-" + (groggy ? "Groggy" : "Stun") + "-v1");
if (texture == null) return null;
frames = new Sprite[8];
for (int i = 0; i < frames.Length; i++)
frames[i] = Sprite.Create(texture, new Rect(i * 32, 0, 32, 24), new Vector2(.5f, .5f), 32f);
if (groggy) groggyFrames = frames;
else stunFrames = frames;
}
return frames[Mathf.FloorToInt(Mathf.Max(0f, elapsed) / .06f) % frames.Length];
}
public static float HeadLocalHeight(Sprite sprite)
{
if (sprite == null) return 0f;
// First idle frame's central body strip. Excludes weapons, shadows and transparent padding.
string name = sprite.texture.name;
int top = name.StartsWith("ArmoredSkeleton") ? 39
: name.StartsWith("SkeletonArcher") ? 38
: name.StartsWith("GreatswordSkeleton") ? 38
: name.StartsWith("Skeleton") ? 42
: name.StartsWith("Slime") ? 46
: name.StartsWith("Bat") ? 40
: name.StartsWith("NecroGolem") ? 29
: name.StartsWith("Necromancer") ? 30
: name.StartsWith("Necrofire") ? 30
: name.StartsWith("Werebear") ? 40
: name.StartsWith("Werewolf") ? 41
: name.StartsWith("Warlock") ? 34
: name.StartsWith("Lancer") ? 28 : -1;
return top < 0 ? sprite.bounds.max.y
: (sprite.rect.height - top - sprite.pivot.y) / sprite.pixelsPerUnit;
}
public static Vector3 MarkerWorldPosition(SpriteRenderer body, float gap = 4f / 32f)
{
Vector3 head = body.transform.TransformPoint(new Vector3(0f, HeadLocalHeight(body.sprite), 0f));
return head + Vector3.up * (Mathf.Max(0f, gap)
+ MarkerHalfHeight * Mathf.Abs(body.transform.lossyScale.y));
}
}
}