363 lines
16 KiB
C#
363 lines
16 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
}
|