ThreeKingdomsSlingshot/Assets/Scripts/OutGame/UI/FormationScreen.cs

363 lines
16 KiB
C#
Raw 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.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 편성(img08 · 기획 S12 v3) — 프리셋 3 · 전열 3(윗줄)·후열 2(아랫줄) 자리 = 영웅 목록 카드(축소) · 하단 보유 목록(장수 ▼ 전환 · 같은 카드 4열 · 출전) ·
/// 탭(자리 고르기 → 카드 탭 = 넣기 · 같은 자리 두 번 = 빼기 · 다른 자리 = 맞바꾸기) · 끌기(자리 ↔ 자리 · 카드 → 자리) · 저장 = 그 프리셋을 출전 편성으로.
/// 빈 칸 허용 · 최소 1 · 편성 제약 없음(D4).
/// </summary>
public class FormationScreen : OutGameScreen
{
int preset;
readonly List<string> work = new List<string>();
bool dirty;
int selected = -1;
bool soldiers;
readonly List<Button> presetBtns = new List<Button>();
readonly List<RectTransform> slots = new List<RectTransform>();
RectTransform list;
ScrollRect scroll;
RectTransform ghost;
public IList<string> Working
{
get { return work; }
}
public int Preset
{
get { return preset; }
}
public bool Dirty
{
get { return dirty; }
}
// 자리 카드 = 영웅 목록 카드(320×431) × SeatScale · 보유 목록 카드 × ListScale(PD 2026-11-09 「변경된 영웅 리스트 반영 · 편성 유닛 정보를 목록·상세 UI 참고해 깔끔하게」)
const float SeatScale = 0.70f;
const float ListScale = 0.72f;
const float SeatGap = 24f;
const float BoardTop = 122f;
const float BoardH = 680f;
Text countText;
Text filterText;
public override void Build()
{
int n = Root.Battle.Config.playerSpawnPoints.Length;
int front = Mathf.Clamp(S.Db.Economy.FormationFront, 0, n);
// 바탕 = 영웅 목록과 같음(진남 + 재화 바 뒤 띠) · 헤더 오른쪽 사선 칸 = 출전 n / 칸 수
UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false);
UiKit.Paint(UiKit.TopBand("ListTop", Rt, 0f, OutGameRoot.TopBarH), UiKit.Col("band_top", UiKit.Bg), false);
var head = Header("편성", "", true); // 부제 삭제(설계서 §2-5)
var ob = UiKit.Rect("Oblique", head, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(239f, OutGameRoot.HeaderH - 7f));
UiKit.Skin(ob, "oblique", UiKit.Col("oblique", UiKit.White));
countText = UiKit.Title(UiKit.Label(ob, "", 40, UiKit.Col("count_text", UiKit.Dark), TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(12f, 0f), new Vector2(220f, 80f)));
countText.horizontalOverflow = HorizontalWrapMode.Overflow;
var body = Body();
// 프리셋 3(팩 서브탭) + 제약 칩
for (int i = 0; i < S.Save.presets.Count; i++)
{
int k = i;
presetBtns.Add(SubTab("Preset" + i, body, "프리셋 " + (i + 1), new Vector2(24f + i * 226f, -18f), new Vector2(210f, 84f), () => LoadPreset(k)));
}
var chip = UiKit.Skin(UiKit.Rect("Free", body, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -24f), new Vector2(270f, 72f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
UiKit.Title(UiKit.FillLabel(chip.transform, "편성 제약 없음", 30, UiKit.White), false);
// 자리 판 = 전열(윗줄) · 후열(아랫줄) · 자리 = 목록과 같은 카드
float sw = CollectionCards.CellSize.x * SeatScale, sh = CollectionCards.CellSize.y * SeatScale;
var board = UiKit.TopBand("Board", body, BoardTop, BoardH, 24f, 24f);
UiKit.Skin(board, "card", UiKit.Col("formation_field", UiKit.Paper));
Pill(board, "전열", UiKit.Col("row_front", UiKit.Red), -(20f + sh / 2f));
Pill(board, "후열", UiKit.Col("row_back", UiKit.Dark), -(20f + sh + 20f + sh / 2f));
UiKit.Label(board, "탭·드래그 = 자리 교체 · 같은 자리 두 번 탭 = 빼기", 24, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f, 2f), new Vector2(900f, 34f), false);
for (int i = 0; i < n; i++)
{
bool f = i < front;
int col = f ? i : i - front;
int cnt = f ? front : n - front;
float x = 60f + (col - (cnt - 1) / 2f) * (sw + SeatGap);
float y = f ? -20f : -(20f + sh + 20f);
var s = UiKit.Rect("Slot" + i, board, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(x, y), new Vector2(sw, sh));
var hit = s.gameObject.AddComponent<Image>();
hit.color = Color.clear;
hit.raycastTarget = true;
var td = s.gameObject.AddComponent<TapDrag>();
td.AnyDirection = true;
int k = i;
td.OnTap = () => TapSlot(k);
td.OnDragStart = e => StartGhost(work[k], e);
td.OnDragMove = MoveGhost;
td.OnDragEnd = e => DropFromSlot(k, e);
slots.Add(s);
}
// 보유 목록 머리 = 제목 · 「장수 ▼」(영웅 목록과 같은 전환) · 저장
var row = UiKit.TopBand("ListHead", body, BoardTop + BoardH + 16f, 74f, 24f, 24f);
UiKit.Title(UiKit.Label(row, "보유 영웅", 40, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(4f, 0f), new Vector2(300f, 70f)), true);
var pick = UiKit.Rect("Filter", row, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-244f, 0f), new Vector2(220f, 74f));
pick.gameObject.AddComponent<Image>().color = Color.clear;
pick.gameObject.AddComponent<Button>().onClick.AddListener(() =>
{
soldiers = !soldiers;
Refresh();
});
filterText = UiKit.Title(UiKit.Label(pick, "", 38, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-50f, 2f), new Vector2(160f, 70f)));
UiKit.DropShadow(UiKit.Icon(pick, "arrow_down", new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-6f, 0f), 38f), 3f);
UiKit.Btn("Save", row, "저장", 34, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(210f, 76f), SaveNow);
var cell = CollectionCards.CellSize * ListScale;
float side = (OutGameRoot.Reference.x - cell.x * 4f - 17f * 3f) / 2f;
list = UiKit.ScrollGrid("List", body, side, side, BoardTop + BoardH + 100f, 0f, cell, new Vector2(17f, 16f), 4, out scroll);
}
/// <summary>서브탭(팩 Button_Round10 + 윗면 광택 · 선택 = 붉은 옻칠).</summary>
static Button SubTab(string name, Transform parent, string label, Vector2 pos, Vector2 size, System.Action onClick)
{
var img = UiKit.Skin(UiKit.Rect(name, parent, new Vector2(0f, 1f), new Vector2(0f, 1f), pos, size), UISkin.Has("subtab") ? "subtab" : "button", UiKit.Col("subtab_off", UiKit.Paper), true);
var b = img.gameObject.AddComponent<Button>();
b.onClick.AddListener(() => onClick());
UiKit.Title(UiKit.FillLabel(img.transform, label, 34, UiKit.White, TextAnchor.MiddleCenter, true, 8f), true);
return b;
}
static void Pill(RectTransform board, string label, Color c, float y)
{
// 전열·후열 라벨 = 평행사변형 칩(설계서 §3③)
var p = UISkin.Has("para_orange")
? UiKit.Skin(UiKit.Rect("Row_" + label, board, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(20f, y), new Vector2(140f, 62f)), "para_orange", c)
: UiKit.Box("Row_" + label, board, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(20f, y), new Vector2(140f, 62f), c);
UiKit.Title(UiKit.FillLabel(p.transform, label, 32, UiKit.White), true);
}
public override void OnShow()
{
LoadPreset(S.Save.presetIdx, true);
}
public override bool OnBack()
{
if (dirty) Root.Toast("저장하지 않은 편성 변경은 취소됩니다");
dirty = false;
return false;
}
public void LoadPreset(int idx, bool quiet = false)
{
if (dirty && !quiet) Root.Toast("저장하지 않은 변경 취소");
preset = Mathf.Clamp(idx, 0, S.Save.presets.Count - 1);
work.Clear();
work.AddRange(S.Save.presets[preset].slots);
dirty = false;
selected = -1;
Refresh();
}
int Filled()
{
int n = 0;
foreach (var id in work)
if (!string.IsNullOrEmpty(id)) n++;
return n;
}
public void TapSlot(int i)
{
if (selected < 0)
{
selected = i;
}
else if (selected == i)
{
if (!string.IsNullOrEmpty(work[i]))
{
if (Filled() > 1)
{
work[i] = "";
dirty = true;
}
else Root.Toast("최소 1명은 출전해야 합니다");
}
selected = -1;
}
else
{
Swap(selected, i);
selected = -1;
}
Refresh();
}
public void TapCard(string id)
{
int at = work.IndexOf(id);
if (at >= 0)
{
if (Filled() > 1)
{
work[at] = "";
dirty = true;
}
else Root.Toast("최소 1명은 출전해야 합니다");
}
else
{
int target = selected >= 0 ? selected : work.IndexOf("");
if (target < 0) Root.Toast("빈 자리가 없습니다 — 자리를 먼저 탭해 고른 뒤 카드를 탭(교체)");
else
{
work[target] = id;
dirty = true;
}
}
selected = -1;
Refresh();
}
/// <summary>id 를 자리 slot 에 넣는다(이미 다른 자리에 있으면 맞바꿈).</summary>
public void Place(string id, int slot)
{
if (slot < 0 || slot >= work.Count || string.IsNullOrEmpty(id)) return;
int at = work.IndexOf(id);
if (at >= 0) Swap(at, slot);
else
{
work[slot] = id;
dirty = true;
}
Refresh();
}
public void Swap(int a, int b)
{
if (a == b) return;
var t = work[a];
work[a] = work[b];
work[b] = t;
dirty = true;
}
public void SaveNow()
{
var why = S.SavePreset(preset, work);
if (why != null)
{
Root.Toast(why);
return;
}
dirty = false;
Root.Toast("프리셋 " + (preset + 1) + " 저장 · 출전 편성으로 지정");
Refresh();
}
// ---------- 끌기 ----------
void StartGhost(string id, PointerEventData e)
{
if (string.IsNullOrEmpty(id)) return;
if (ghost != null) Object.Destroy(ghost.gameObject);
ghost = UiKit.Portrait("Ghost", Root.TopRoot, S.Def(id), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(170f, 170f), true, 2.1f, UiKit.Red);
ghost.gameObject.name = "Ghost_" + id;
MoveGhost(e);
}
void MoveGhost(PointerEventData e)
{
if (ghost == null) return;
Vector2 lp;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(Root.TopRoot, e.position, Root.Cam, out lp)) ghost.anchoredPosition = lp;
}
int SlotAt(Vector2 screen)
{
for (int i = 0; i < slots.Count; i++)
if (RectTransformUtility.RectangleContainsScreenPoint(slots[i], screen, Root.Cam)) return i;
return -1;
}
void DropFromSlot(int from, PointerEventData e)
{
EndGhost();
int to = SlotAt(e.position);
if (to < 0 || to == from) return;
Swap(from, to);
selected = -1;
Refresh();
}
void DropCard(string id, PointerEventData e)
{
EndGhost();
int to = SlotAt(e.position);
if (to >= 0) Place(id, to);
}
void EndGhost()
{
if (ghost == null) return;
ghost.gameObject.SetActive(false);
Object.Destroy(ghost.gameObject);
ghost = null;
}
// ---------- 그리기 ----------
public override void Refresh()
{
for (int i = 0; i < presetBtns.Count; i++)
{
bool on = i == preset;
presetBtns[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Col("subtab_off", UiKit.Paper);
var t = UiKit.LabelOf(presetBtns[i]);
t.color = UiKit.White;
t.text = "프리셋 " + (i + 1) + (i == S.Save.presetIdx ? " <size=22>출전</size>" : "") + (on && dirty ? "*" : "");
}
countText.text = "출전 " + Filled() + "/" + work.Count;
// 자리 = 목록과 같은 카드(배율) · 빈 자리 = 빈 카드 · 고른 자리 = 노란 테두리
for (int i = 0; i < slots.Count; i++)
{
var s = slots[i];
UiKit.Clear(s);
var id = i < work.Count ? work[i] : "";
var h = S.Def(id);
if (i == selected) UiKit.Layer(s, "Select", "card_border", UiKit.Col("select_line", UiKit.Star), -7f, -7f, -7f, -7f);
if (h != null) CollectionCards.CardIn(s, S, h, false, null, SeatScale);
else
{
CollectionCards.EmptyIn(s, SeatScale);
UiKit.Title(UiKit.Label(s, "+ 빈 자리", 28, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 24f), new Vector2(200f, 50f)));
}
}
// 보유 목록 = 영웅 목록 카드(배율 · 레벨 높은 순 · 출전 = 이 편성에 든 영웅)
filterText.text = soldiers ? "병사" : "장수";
UiKit.Clear(list);
var owned = new List<HeroDb.Hero>(S.Owned(soldiers));
owned.Sort((a, b) =>
{
int la = S.Save.Level(a.id) != null ? S.Save.Level(a.id).lv : 1, lb = S.Save.Level(b.id) != null ? S.Save.Level(b.id).lv : 1;
return lb.CompareTo(la);
});
var cellSize = CollectionCards.CellSize * ListScale;
foreach (var h in owned)
{
var id = h.id;
var holder = UiKit.Rect("C_" + id, list, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, cellSize);
var card = CollectionCards.CardIn(holder, S, h, work.Contains(id), null, ListScale);
var td = card.gameObject.AddComponent<TapDrag>();
td.OnTap = () => TapCard(id);
td.OnDragStart = e => StartGhost(id, e);
td.OnDragMove = MoveGhost;
td.OnDragEnd = e => DropCard(id, e);
}
}
}
}