VSClone/Assets/Scripts/View/Art.cs

375 lines
26 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using UnityEngine;
// 코드로 그리는 그림(원작 추출 아트 미사용) — 아이콘 · 방 · 소품 · 스트리머
namespace VS.View
{
/// 간단한 안티에일리어싱 래스터 (거리 기반 커버리지)
public class Raster
{
public readonly int W, H; public readonly Color[] Px;
public Raster(int w, int h) { W = w; H = h; Px = new Color[w * h]; }
public void Blend(int x, int y, Color c, float a)
{
if (x < 0 || y < 0 || x >= W || y >= H || a <= 0f) return;
a = Mathf.Clamp01(a) * c.a;
int i = y * W + x;
var d = Px[i];
float oa = a + d.a * (1 - a);
if (oa <= 0.0001f) return;
Px[i] = new Color((c.r * a + d.r * d.a * (1 - a)) / oa, (c.g * a + d.g * d.a * (1 - a)) / oa, (c.b * a + d.b * d.a * (1 - a)) / oa, oa);
}
// sdf: 음수 = 안쪽
public void Shape(Func<float, float, float> sdf, Color c, float x0, float y0, float x1, float y1)
{
int ax = Mathf.Max(0, (int)x0 - 2), ay = Mathf.Max(0, (int)y0 - 2), bx = Mathf.Min(W - 1, (int)x1 + 2), by = Mathf.Min(H - 1, (int)y1 + 2);
for (int y = ay; y <= by; y++)
for (int x = ax; x <= bx; x++)
{
float d = sdf(x + 0.5f, y + 0.5f);
if (d < 0.75f) Blend(x, y, c, Mathf.Clamp01(0.5f - d));
}
}
public void Circle(float cx, float cy, float r, Color c) => Shape((x, y) => Mathf.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)) - r, c, cx - r, cy - r, cx + r, cy + r);
public void Ring(float cx, float cy, float r, float w, Color c) => Shape((x, y) => Mathf.Abs(Mathf.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)) - r) - w * 0.5f, c, cx - r - w, cy - r - w, cx + r + w, cy + r + w);
public void Ellipse(float cx, float cy, float rx, float ry, Color c) => Shape((x, y) => { float dx = (x - cx) / rx, dy = (y - cy) / ry; return (Mathf.Sqrt(dx * dx + dy * dy) - 1f) * Mathf.Min(rx, ry); }, c, cx - rx, cy - ry, cx + rx, cy + ry);
public void RRect(float x0, float y0, float x1, float y1, float r, Color c)
{
float cx = (x0 + x1) * 0.5f, cy = (y0 + y1) * 0.5f, hx = (x1 - x0) * 0.5f, hy = (y1 - y0) * 0.5f;
r = Mathf.Min(r, Mathf.Min(hx, hy));
Shape((x, y) =>
{
float qx = Mathf.Abs(x - cx) - hx + r, qy = Mathf.Abs(y - cy) - hy + r;
float ox = Mathf.Max(qx, 0), oy = Mathf.Max(qy, 0);
return Mathf.Sqrt(ox * ox + oy * oy) + Mathf.Min(Mathf.Max(qx, qy), 0) - r;
}, c, x0, y0, x1, y1);
}
public void Poly(Vector2[] p, Color c)
{
float minx = float.MaxValue, miny = float.MaxValue, maxx = float.MinValue, maxy = float.MinValue;
foreach (var v in p) { minx = Mathf.Min(minx, v.x); miny = Mathf.Min(miny, v.y); maxx = Mathf.Max(maxx, v.x); maxy = Mathf.Max(maxy, v.y); }
Shape((x, y) =>
{
float d = float.MaxValue; bool inside = false;
for (int i = 0, j = p.Length - 1; i < p.Length; j = i++)
{
var a = p[i]; var b = p[j];
var e = b - a; var w = new Vector2(x, y) - a;
var proj = w - e * Mathf.Clamp01(Vector2.Dot(w, e) / Vector2.Dot(e, e));
d = Mathf.Min(d, proj.sqrMagnitude);
if ((a.y > y) != (b.y > y) && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside = !inside;
}
return (inside ? -1f : 1f) * Mathf.Sqrt(d);
}, c, minx, miny, maxx, maxy);
}
public void Line(Vector2 a, Vector2 b, float w, Color c)
{
Shape((x, y) =>
{
var e = b - a; var p = new Vector2(x, y) - a;
float h = Mathf.Clamp01(Vector2.Dot(p, e) / Mathf.Max(0.0001f, Vector2.Dot(e, e)));
return (p - e * h).magnitude - w * 0.5f;
}, c, Mathf.Min(a.x, b.x) - w, Mathf.Min(a.y, b.y) - w, Mathf.Max(a.x, b.x) + w, Mathf.Max(a.y, b.y) + w);
}
public void Star(float cx, float cy, float r, Color c)
{
var p = new Vector2[10];
for (int i = 0; i < 10; i++) { float ang = Mathf.PI / 2 + i * Mathf.PI / 5; float rr = i % 2 == 0 ? r : r * 0.45f; p[i] = new Vector2(cx + Mathf.Cos(ang) * rr, cy + Mathf.Sin(ang) * rr); }
Poly(p, c);
}
public void Heart(float cx, float cy, float s, Color c)
{
Circle(cx - s * 0.5f, cy + s * 0.25f, s * 0.55f, c);
Circle(cx + s * 0.5f, cy + s * 0.25f, s * 0.55f, c);
Poly(new[] { new Vector2(cx - s * 1.02f, cy + s * 0.1f), new Vector2(cx + s * 1.02f, cy + s * 0.1f), new Vector2(cx, cy - s * 1.05f) }, c);
}
public Texture2D Tex(bool point = false)
{
var t = new Texture2D(W, H, TextureFormat.RGBA32, false) { wrapMode = TextureWrapMode.Clamp, filterMode = point ? FilterMode.Point : FilterMode.Bilinear };
t.SetPixels(Px); t.Apply(false, false);
return t;
}
public Sprite Sprite(Vector4 border = default, float ppu = 100f, bool point = false) => UnityEngine.Sprite.Create(Tex(point), new Rect(0, 0, W, H), new Vector2(0.5f, 0.5f), ppu, 0, SpriteMeshType.FullRect, border);
/// 도트 그림(읽을 수 있는 텍스처)을 최근접으로 (x0,y0)~(x0+w,y0+h) 에 붙인다 — 도트 배경 위 장애물(2026-10-30) · 반투명은 Blend
public void Blit(Texture2D tex, float x0, float y0, float w, float h)
{
if (tex == null || !tex.isReadable || w < 1f || h < 1f) return;
var px = tex.GetPixels32(); int tw = tex.width, th = tex.height;
int ax = Mathf.Max(0, Mathf.FloorToInt(x0)), ay = Mathf.Max(0, Mathf.FloorToInt(y0)), bx = Mathf.Min(W - 1, Mathf.CeilToInt(x0 + w) - 1), by = Mathf.Min(H - 1, Mathf.CeilToInt(y0 + h) - 1);
for (int y = ay; y <= by; y++)
for (int x = ax; x <= bx; x++)
{
int sx = Mathf.Clamp((int)((x + 0.5f - x0) / w * tw), 0, tw - 1), sy = Mathf.Clamp((int)((y + 0.5f - y0) / h * th), 0, th - 1);
var c = px[sy * tw + sx];
if (c.a > 0) Blend(x, y, new Color(c.r / 255f, c.g / 255f, c.b / 255f, 1f), c.a / 255f);
}
}
}
public static class Art
{
static Sprite round, pill, square, circle, softCircle;
public static Sprite Round { get { if (round == null) { var r = new Raster(64, 64); r.RRect(0, 0, 64, 64, 22, Color.white); round = r.Sprite(new Vector4(26, 26, 26, 26)); } return round; } }
public static Sprite Pill { get { if (pill == null) { var r = new Raster(64, 32); r.RRect(0, 0, 64, 32, 16, Color.white); pill = r.Sprite(new Vector4(17, 15, 17, 15)); } return pill; } }
public static Sprite Square { get { if (square == null) { var r = new Raster(4, 4); for (int i = 0; i < 16; i++) r.Px[i] = Color.white; square = r.Sprite(); } return square; } }
public static Sprite Circle { get { if (circle == null) { var r = new Raster(128, 128); r.Circle(64, 64, 62, Color.white); circle = r.Sprite(); } return circle; } }
public static Sprite SoftCircle
{
get
{
if (softCircle == null)
{
var r = new Raster(128, 128);
for (int y = 0; y < 128; y++) for (int x = 0; x < 128; x++) { float d = Vector2.Distance(new Vector2(x, y), new Vector2(64, 64)) / 64f; r.Px[y * 128 + x] = new Color(1, 1, 1, Mathf.Clamp01(1 - d) * Mathf.Clamp01(1 - d)); }
softCircle = r.Sprite();
}
return softCircle;
}
}
static readonly Dictionary<string, Sprite> icons = new Dictionary<string, Sprite>();
/// 아이콘(128×128) — 이름별 코드 그림
public static Sprite Icon(string name)
{
if (icons.TryGetValue(name, out var s)) return s;
var r = new Raster(128, 128);
Color ink = Pal.Ink;
switch (name)
{
case "coin":
r.Circle(64, 64, 56, new Color32(214, 150, 20, 255)); r.Circle(64, 66, 50, Pal.Yellow); r.Ring(64, 66, 38, 5, new Color32(240, 170, 30, 255));
r.RRect(56, 42, 72, 90, 6, new Color32(232, 160, 24, 255)); break;
case "ticket":
r.RRect(10, 30, 118, 98, 14, new Color32(255, 120, 160, 255)); r.Circle(10, 64, 14, Color.clear); r.RRect(16, 36, 112, 92, 10, new Color32(255, 170, 200, 255));
r.Star(64, 64, 22, Color.white); break;
case "heart": r.Heart(64, 70, 30, Pal.Pink); r.Circle(48, 82, 8, new Color(1, 1, 1, 0.6f)); break;
case "follower":
r.Circle(64, 84, 22, Pal.Purple); r.Ellipse(64, 34, 40, 26, Pal.Purple); break;
case "view":
r.Ellipse(64, 64, 56, 32, Color.white); r.Circle(64, 64, 22, Pal.Purple); r.Circle(64, 64, 10, ink); break;
case "house":
r.Poly(new[] { new Vector2(10, 64), new Vector2(64, 116), new Vector2(118, 64) }, Pal.Orange); r.RRect(24, 14, 104, 70, 6, new Color32(255, 220, 150, 255)); r.RRect(54, 14, 76, 48, 4, new Color32(170, 100, 60, 255)); break;
case "closet":
r.Line(new Vector2(64, 104), new Vector2(64, 92), 6, ink); r.Ring(64, 108, 8, 5, ink);
r.Poly(new[] { new Vector2(64, 92), new Vector2(118, 50), new Vector2(10, 50) }, Pal.Pink); r.RRect(10, 40, 118, 54, 6, Pal.PinkDark); break;
case "skill":
r.RRect(48, 50, 80, 112, 16, new Color32(90, 90, 110, 255)); r.RRect(54, 58, 74, 104, 10, new Color32(150, 150, 175, 255)); r.Line(new Vector2(64, 50), new Vector2(64, 22), 8, ink); r.RRect(40, 14, 88, 26, 6, ink); break;
case "x2":
r.Heart(64, 70, 30, Pal.Pink); break;
case "auto":
r.Poly(new[] { new Vector2(70, 120), new Vector2(30, 56), new Vector2(62, 56), new Vector2(54, 8), new Vector2(98, 74), new Vector2(66, 74) }, Pal.Yellow); break;
case "gear":
for (int i = 0; i < 8; i++) { float a = i * Mathf.PI / 4; r.RRect(64 + Mathf.Cos(a) * 44 - 12, 64 + Mathf.Sin(a) * 44 - 12, 64 + Mathf.Cos(a) * 44 + 12, 64 + Mathf.Sin(a) * 44 + 12, 4, new Color32(150, 150, 170, 255)); }
r.Circle(64, 64, 42, new Color32(150, 150, 170, 255)); r.Circle(64, 64, 16, Color.white); break;
case "gift":
r.RRect(18, 14, 110, 80, 8, Pal.Purple); r.RRect(12, 72, 116, 96, 8, Pal.PurpleDark); r.RRect(56, 14, 72, 96, 2, Pal.Yellow);
r.Ellipse(48, 104, 16, 10, Pal.Yellow); r.Ellipse(80, 104, 16, 10, Pal.Yellow); break;
case "task":
r.RRect(22, 10, 106, 116, 12, Color.white); r.RRect(30, 18, 98, 108, 8, new Color32(240, 240, 250, 255));
for (int i = 0; i < 3; i++) { r.Circle(42, 88 - i * 26, 6, Pal.Green); r.RRect(54, 84 - i * 26, 92, 92 - i * 26, 3, Pal.Gray); } break;
case "afk":
r.Circle(64, 64, 52, Color.white); r.Ring(64, 64, 46, 8, Pal.Blue); r.Line(new Vector2(64, 64), new Vector2(64, 96), 8, ink); r.Line(new Vector2(64, 64), new Vector2(88, 54), 8, ink); break;
case "lock":
r.Ring(64, 76, 22, 10, new Color32(120, 120, 140, 255)); r.RRect(30, 14, 98, 74, 10, new Color32(160, 160, 180, 255)); r.Circle(64, 46, 8, ink); break;
case "up":
r.Poly(new[] { new Vector2(64, 118), new Vector2(118, 60), new Vector2(84, 60), new Vector2(84, 10), new Vector2(44, 10), new Vector2(44, 60), new Vector2(10, 60) }, Pal.Green); break;
case "star": r.Star(64, 64, 56, Pal.Yellow); break;
case "fire":
r.Ellipse(64, 46, 36, 38, Pal.Orange); r.Poly(new[] { new Vector2(30, 52), new Vector2(64, 124), new Vector2(98, 52) }, Pal.Orange); r.Ellipse(64, 42, 20, 22, Pal.Yellow); break;
case "chat":
r.RRect(10, 34, 118, 110, 22, Color.white); r.Poly(new[] { new Vector2(30, 40), new Vector2(22, 10), new Vector2(56, 36) }, Color.white);
for (int i = 0; i < 3; i++) r.Circle(38 + i * 26, 72, 8, Pal.Purple); break;
case "shop":
r.RRect(14, 12, 114, 84, 10, Pal.Blue); r.Poly(new[] { new Vector2(8, 84), new Vector2(120, 84), new Vector2(104, 116), new Vector2(24, 116) }, new Color32(255, 110, 120, 255)); break;
case "pk":
r.Line(new Vector2(20, 20), new Vector2(108, 108), 14, new Color32(200, 200, 215, 255)); r.Line(new Vector2(108, 20), new Vector2(20, 108), 14, new Color32(200, 200, 215, 255)); r.Circle(64, 64, 14, Pal.Red); break;
case "calendar":
r.RRect(14, 10, 114, 104, 12, Color.white); r.RRect(14, 78, 114, 104, 12, Pal.Red); r.RRect(30, 24, 98, 70, 6, new Color32(235, 235, 245, 255)); break;
case "hand":
r.RRect(46, 8, 86, 76, 18, new Color32(255, 214, 180, 255)); r.RRect(54, 50, 74, 122, 10, new Color32(255, 214, 180, 255)); break;
default: r.Circle(64, 64, 50, Pal.Gray); break;
}
s = r.Sprite();
icons[name] = s;
return s;
}
// ---------- 방 ----------
static readonly Color[][] HouseTheme =
{
new Color[] { new Color32(255, 214, 230, 255), new Color32(214, 160, 196, 255), new Color32(255, 240, 248, 255) }, // 침실
new Color[] { new Color32(196, 226, 210, 255), new Color32(150, 120, 96, 255), new Color32(236, 250, 240, 255) }, // 교실
new Color[] { new Color32(150, 214, 255, 255), new Color32(244, 220, 160, 255), new Color32(220, 244, 255, 255) }, // 해변
new Color[] { new Color32(255, 236, 196, 255), new Color32(200, 170, 140, 255), new Color32(255, 250, 236, 255) }, // 부엌
new Color[] { new Color32(220, 206, 255, 255), new Color32(170, 160, 200, 255), new Color32(246, 240, 255, 255) }, // 가게
new Color[] { new Color32(196, 206, 255, 255), new Color32(140, 120, 180, 255), new Color32(236, 240, 255, 255) }, // 새 침실
};
static readonly Dictionary<string, Sprite> cache = new Dictionary<string, Sprite>();
/// 방 배경 (house · 방 아이템 시각 단계 0~3 · -1 = 기본)
public static Sprite RoomBg(int house, int stage)
{
string key = $"bg{house}_{stage}";
if (cache.TryGetValue(key, out var s)) return s;
const int W = 540, H = 720;
var r = new Raster(W, H);
var th = HouseTheme[Mathf.Clamp(house, 0, HouseTheme.Length - 1)];
Color wall = th[0], floor = th[1], hi = th[2];
float floorY = H * 0.36f;
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
{
Color c = y < floorY ? Color.Lerp(floor * 0.85f, floor, y / floorY) : Color.Lerp(wall, hi, (y - floorY) / (H - floorY));
c.a = 1; r.Px[y * W + x] = c;
}
if (house == 2)
{
r.RRect(-10, -10, W + 10, floorY, 0, new Color32(244, 214, 150, 255));
r.Ellipse(W * 0.5f, floorY + 6, W * 0.7f, 26, new Color32(80, 170, 240, 255));
r.Circle(W * 0.82f, H * 0.84f, 42, new Color32(255, 240, 140, 255));
}
else
{
// 벽지 무늬 (단계가 오를수록 화려)
int st = Mathf.Max(0, stage);
for (int i = 0; i < 9 + st * 3; i++)
{
float x = (i * 97 + 31) % W, y = floorY + 40 + (i * 173 % (int)(H - floorY - 60));
if (st >= 2) r.Star(x, y, 8 + st * 2, new Color(1, 1, 1, 0.5f)); else r.Circle(x, y, 6 + st * 2, new Color(1, 1, 1, 0.45f));
}
r.RRect(0, floorY - 6, W, floorY + 8, 4, Color.Lerp(floor, Color.white, 0.35f));
if (st >= 1) { r.RRect(W * 0.08f, H * 0.62f, W * 0.34f, H * 0.9f, 10, new Color32(255, 255, 255, 200)); r.RRect(W * 0.1f, H * 0.64f, W * 0.32f, H * 0.88f, 8, new Color32(160, 214, 255, 255)); }
if (st >= 3) for (int i = 0; i < 12; i++) r.Circle(20 + i * 46, H - 26, 7, i % 2 == 0 ? Pal.Yellow : Pal.Pink);
}
s = r.Sprite();
s.texture.filterMode = FilterMode.Bilinear;
cache[key] = s;
return s;
}
static int Kind(string n)
{
n = (n ?? "").ToLowerInvariant();
if (n.Contains("sofa") || n.Contains("bed") || n.Contains("chair") && !n.Contains("beach")) return 0;
if (n.Contains("table") || n.Contains("desk") || n.Contains("counter")) return 1;
if (n.Contains("shelf") || n.Contains("rack") || n.Contains("fridge") || n.Contains("chest") || n.Contains("case") || n == "pc" || n.Contains("cooler")) return 2;
if (n.Contains("monitor") || n.Contains("laptop") || n.Contains("picture") || n.Contains("board") || n.Contains("window") || n.Contains("clock") || n.Contains("sign")) return 3;
if (n.Contains("light") || n.Contains("lamp")) return 4;
if (n.Contains("carpet") || n.Contains("mat") || n.Contains("ring")) return 5;
if (n.Contains("curtain")) return 6;
if (n.Contains("tripod") || n.Contains("camera") || n.Contains("micro") || n.Contains("headphone")) return 7;
return 8; // 둥근 소품 (곰인형·화분·트로피·케이크·컵·모래성·마네킹 …)
}
static readonly Color[] PropColor =
{
new Color32(255, 130, 170, 255), new Color32(120, 190, 255, 255), new Color32(255, 196, 90, 255), new Color32(150, 120, 230, 255),
new Color32(110, 210, 170, 255), new Color32(255, 150, 110, 255), new Color32(200, 140, 255, 255), new Color32(90, 200, 230, 255),
new Color32(250, 120, 120, 255), new Color32(170, 200, 90, 255), new Color32(240, 170, 220, 255),
};
/// 소품 그림 (아이템 이름 → 모양 · 단계 0~3 → 장식)
public static Sprite Prop(int house, int index, string name, int stage)
{
string key = $"p{house}_{index}_{stage}";
if (cache.TryGetValue(key, out var s)) return s;
var r = new Raster(160, 160);
Color c = PropColor[(index + house * 3) % PropColor.Length];
Color dark = Color.Lerp(c, Pal.Ink, 0.35f); Color light = Color.Lerp(c, Color.white, 0.45f);
if (stage >= 2) dark = Color.Lerp(dark, Pal.Yellow, 0.5f);
switch (Kind(name))
{
case 0: r.RRect(14, 60, 146, 110, 18, dark); r.RRect(10, 30, 150, 74, 16, c); r.RRect(4, 26, 30, 96, 12, dark); r.RRect(130, 26, 156, 96, 12, dark); r.RRect(34, 54, 126, 70, 8, light); break;
case 1: r.RRect(10, 86, 150, 106, 8, c); r.RRect(20, 20, 34, 90, 6, dark); r.RRect(126, 20, 140, 90, 6, dark); r.RRect(16, 100, 144, 112, 6, light); break;
case 2: r.RRect(36, 8, 124, 150, 10, dark); r.RRect(44, 16, 116, 142, 6, c); for (int i = 0; i < 3; i++) r.RRect(44, 46 + i * 34, 116, 52 + i * 34, 2, dark); break;
case 3: r.RRect(16, 40, 144, 136, 12, dark); r.RRect(24, 48, 136, 128, 8, light); r.RRect(70, 12, 90, 42, 4, dark); r.RRect(46, 8, 114, 18, 4, dark); break;
case 4: r.Poly(new[] { new Vector2(40, 96), new Vector2(120, 96), new Vector2(100, 146), new Vector2(60, 146) }, c); r.RRect(74, 20, 86, 98, 4, dark); r.Ellipse(80, 16, 32, 10, dark); r.Circle(80, 90, 16, new Color(1, 1, 0.7f, 0.8f)); break;
case 5: r.Ellipse(80, 60, 74, 34, c); r.Ellipse(80, 60, 58, 24, light); r.Ellipse(80, 60, 34, 12, c); break;
case 6: r.RRect(8, 140, 152, 152, 4, dark); r.Poly(new[] { new Vector2(10, 146), new Vector2(64, 146), new Vector2(48, 12), new Vector2(14, 12) }, c); r.Poly(new[] { new Vector2(96, 146), new Vector2(150, 146), new Vector2(146, 12), new Vector2(112, 12) }, c); break;
case 7: r.Line(new Vector2(80, 70), new Vector2(40, 8), 7, dark); r.Line(new Vector2(80, 70), new Vector2(120, 8), 7, dark); r.Line(new Vector2(80, 70), new Vector2(80, 8), 7, dark); r.RRect(48, 70, 112, 120, 14, c); r.Circle(80, 95, 16, light); break;
default: r.Ellipse(80, 64, 48, 52, c); r.Circle(80, 122, 32, c); r.Circle(70, 128, 6, Pal.Ink); r.Circle(90, 128, 6, Pal.Ink); r.Ellipse(80, 60, 30, 34, light); break;
}
if (stage >= 1) r.Star(140, 140, 14, Pal.Yellow);
if (stage >= 2) r.Star(118, 146, 10, Pal.Yellow);
if (stage >= 3) { r.Star(20, 146, 12, Pal.Yellow); r.Ring(80, 80, 74, 3, new Color(1f, 0.9f, 0.4f, 0.6f)); }
s = r.Sprite();
cache[key] = s;
return s;
}
// ---------- 스트리머 ----------
public static readonly Color32[] Hair =
{
new Color32(255, 150, 200, 255), new Color32(120, 80, 60, 255), new Color32(255, 220, 120, 255), new Color32(60, 50, 80, 255),
new Color32(160, 120, 255, 255), new Color32(120, 220, 255, 255), new Color32(255, 120, 90, 255), new Color32(240, 240, 250, 255),
};
public static readonly Color32[] Cloth =
{
new Color32(255, 110, 170, 255), new Color32(110, 170, 255, 255), new Color32(255, 200, 80, 255), new Color32(130, 220, 160, 255),
new Color32(180, 120, 255, 255), new Color32(255, 140, 110, 255), new Color32(80, 80, 110, 255), new Color32(250, 250, 255, 255),
new Color32(255, 90, 110, 255), new Color32(90, 210, 220, 255),
};
/// 프레임: 0 대기 · 1 눈 감기 · 2 탭 포즈 · 3~6 하이프 (원작 SpriteData 순서와 같은 뜻)
public static Sprite Idol(int outfit, int frame)
{
string key = $"idol{outfit}_{frame}";
if (cache.TryGetValue(key, out var s)) return s;
const int W = 300, H = 420;
var r = new Raster(W, H);
int o = outfit < 0 || outfit >= 1000 ? 0 : outfit + 1;
Color hair = Hair[o % Hair.Length], cloth = Cloth[(o * 3 + 1) % Cloth.Length], cloth2 = Color.Lerp(Cloth[(o * 7 + 4) % Cloth.Length], Color.white, 0.2f);
Color skin = new Color32(255, 224, 205, 255), ink = Pal.Ink;
float bob = frame >= 3 ? ((frame % 2 == 0) ? 10 : -4) : (frame == 2 ? 6 : 0);
float cx = W * 0.5f, headY = 300 + bob;
// 뒷머리
r.Ellipse(cx, headY - 20, 92, 120, hair);
// 몸
r.RRect(cx - 70, 40 + bob, cx + 70, 200 + bob, 40, cloth);
r.Poly(new[] { new Vector2(cx - 92, 20 + bob), new Vector2(cx + 92, 20 + bob), new Vector2(cx + 64, 120 + bob), new Vector2(cx - 64, 120 + bob) }, cloth2);
r.RRect(cx - 18, 186 + bob, cx + 18, 216 + bob, 8, skin);
// 팔
Vector2 sl = new Vector2(cx - 64, 170 + bob), sr = new Vector2(cx + 64, 170 + bob);
Vector2 hl, hr;
if (frame == 2) { hl = new Vector2(cx - 120, 250 + bob); hr = new Vector2(cx + 96, 110 + bob); }
else if (frame >= 3) { bool up = frame % 2 == 1; hl = new Vector2(cx - 118, up ? 290 : 220 + bob); hr = new Vector2(cx + 118, up ? 220 : 290 + bob); }
else { hl = new Vector2(cx - 88, 90 + bob); hr = new Vector2(cx + 88, 90 + bob); }
r.Line(sl, hl, 30, cloth); r.Line(sr, hr, 30, cloth); r.Circle(hl.x, hl.y, 17, skin); r.Circle(hr.x, hr.y, 17, skin);
// 머리
r.Circle(cx, headY, 82, skin);
r.Ellipse(cx, headY + 52, 90, 44, hair);
r.Ellipse(cx - 70, headY + 6, 24, 60, hair); r.Ellipse(cx + 70, headY + 6, 24, 60, hair);
// 눈 · 볼 · 입
if (frame == 1) { r.Line(new Vector2(cx - 42, headY - 4), new Vector2(cx - 18, headY - 4), 6, ink); r.Line(new Vector2(cx + 18, headY - 4), new Vector2(cx + 42, headY - 4), 6, ink); }
else if (frame >= 3) { r.Star(cx - 30, headY - 2, 14, ink); r.Star(cx + 30, headY - 2, 14, ink); }
else { r.Ellipse(cx - 30, headY - 2, 10, 15, ink); r.Ellipse(cx + 30, headY - 2, 10, 15, ink); r.Circle(cx - 27, headY + 4, 4, Color.white); r.Circle(cx + 33, headY + 4, 4, Color.white); }
r.Ellipse(cx - 50, headY - 26, 14, 8, new Color(1f, 0.5f, 0.6f, 0.55f)); r.Ellipse(cx + 50, headY - 26, 14, 8, new Color(1f, 0.5f, 0.6f, 0.55f));
if (frame >= 2) r.Ellipse(cx, headY - 40, 16, 12, new Color32(200, 60, 90, 255)); else r.Line(new Vector2(cx - 12, headY - 40), new Vector2(cx + 12, headY - 40), 5, new Color32(200, 60, 90, 255));
// 의상 장식(의상마다 다름)
int deco = o % 4;
if (deco == 1) r.Star(cx + 56, headY + 70, 22, Pal.Yellow);
else if (deco == 2) { r.Ellipse(cx + 60, headY + 66, 26, 14, Pal.Pink); r.Ellipse(cx + 84, headY + 66, 26, 14, Pal.Pink); r.Circle(cx + 72, headY + 66, 9, Pal.PinkDark); }
else if (deco == 3) r.Poly(new[] { new Vector2(cx - 50, headY + 92), new Vector2(cx - 30, headY + 140), new Vector2(cx, headY + 100), new Vector2(cx + 30, headY + 140), new Vector2(cx + 50, headY + 92) }, Pal.Yellow);
// 마이크
if (frame != 2) { r.Line(new Vector2(hr.x, hr.y), new Vector2(hr.x + 6, hr.y + 40), 8, new Color32(70, 70, 90, 255)); r.Circle(hr.x + 6, hr.y + 46, 14, new Color32(110, 110, 130, 255)); }
s = r.Sprite();
cache[key] = s;
return s;
}
}
}