206 lines
9.2 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|