375 lines
26 KiB
C#
375 lines
26 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|