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

324 lines
13 KiB
C#
Raw Normal View History

아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 편성(img08 · 기획 S12 v3) — 프리셋 3 · 전열 3(윗줄)·후열 2(아랫줄) 원형 자리 · 하단 보유 목록(장수|병사 · 2줄 카드 · 출전 중) ·
/// 탭(자리 고르기 → 카드 탭 = 넣기 · 같은 자리 두 번 = 빼기 · 다른 자리 = 맞바꾸기) · 끌기(자리 ↔ 자리 · 카드 → 자리) · 저장 = 그 프리셋을 출전 편성으로.
/// 빈 칸 허용 · 최소 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>();
Button tabHero;
Button tabSoldier;
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; }
}
public override void Build()
{
int n = Root.Battle.Config.playerSpawnPoints.Length;
int front = Mathf.Clamp(S.Db.Economy.FormationFront, 0, n);
Header("편성", "출정 좌측 · 출전 " + n + "칸", true);
var body = Body();
for (int i = 0; i < S.Save.presets.Count; i++)
{
int k = i;
presetBtns.Add(UiKit.Btn("Preset" + i, body, "프리셋 " + (i + 1), 34, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 226f, -18f), new Vector2(210f, 84f), () => LoadPreset(k)));
}
var chip = UiKit.Box("Free", body, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -24f), new Vector2(270f, 72f), UiKit.Dark);
UiKit.FillLabel(chip.transform, "편성 제약 없음", 30, UiKit.White);
var board = UiKit.TopBand("Board", body, 122f, 600f, 24f, 24f);
var bimg = UiKit.Paint(board, UiKit.Paper);
UiKit.Border(bimg, UiKit.Line, 3f);
Pill(board, "전열", UiKit.Red, -116f);
Pill(board, "후열", UiKit.Dark, -380f);
UiKit.Label(board, "탭·드래그 = 자리 교체 · 같은 자리 두 번 탭 = 빼기", 24, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f, 8f), new Vector2(900f, 40f), 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 gap = 264f;
float x = 70f + (col - (cnt - 1) / 2f) * gap;
float y = f ? -30f : -290f;
var s = UiKit.Rect("Slot" + i, board, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(x, y), new Vector2(230f, 250f));
var hit = s.gameObject.AddComponent<Image>();
hit.color = new Color(1f, 1f, 1f, 0f);
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, 738f, 84f, 24f, 24f);
UiKit.Label(row, "보유 영웅", 42, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(300f, 70f));
tabHero = UiKit.Btn("TabHero", row, "장수", 32, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-440f, 0f), new Vector2(190f, 76f), () => { soldiers = false; Refresh(); });
tabSoldier = UiKit.Btn("TabSoldier", row, "병사", 32, UiKit.Paper, UiKit.Ink, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-240f, 0f), new Vector2(190f, 76f), () => { soldiers = true; Refresh(); });
UiKit.Btn("Save", row, "저장", 34, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(220f, 76f), SaveNow);
list = UiKit.ScrollGrid("List", body, 24f, 24f, 836f, 12f, new Vector2(506f, 150f), new Vector2(20f, 18f), 2, out scroll);
}
static void Pill(RectTransform board, string label, Color c, float y)
{
var p = UiKit.Box("Row_" + label, board, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(20f, y), new Vector2(140f, 62f), c);
UiKit.FillLabel(p.transform, label, 32, UiKit.White);
}
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.Paper;
var t = UiKit.LabelOf(presetBtns[i]);
t.color = on ? UiKit.White : UiKit.Ink;
t.text = "프리셋 " + (i + 1) + (i == S.Save.presetIdx ? " <size=22>출전</size>" : "") + (on && dirty ? "*" : "");
}
int front = S.Db.Economy.FormationFront;
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.Dot("Select", s, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), 236f, UiKit.Star);
Color ring = i < front ? UiKit.Red : UiKit.Dark;
UiKit.Dot("Ring", s, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, 0f), 212f, ring);
if (h != null)
{
UiKit.Portrait("Face", s, h, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -10f), new Vector2(192f, 192f), true, 2.1f, new Color(0.94f, 0.91f, 0.85f));
var tag = UiKit.Box("Name", s, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 4f), new Vector2(170f, 50f), UiKit.Dark);
UiKit.FillLabel(tag.transform, h.nameKo, 28, UiKit.White);
}
else
{
UiKit.Dot("Empty", s, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -10f), 192f, new Color(0.9f, 0.87f, 0.82f));
UiKit.Label(s, "+\n<size=26>빈 칸</size>", 60, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -40f), new Vector2(200f, 140f));
}
}
tabHero.GetComponent<Image>().color = soldiers ? UiKit.Paper : UiKit.Red;
UiKit.LabelOf(tabHero).color = soldiers ? UiKit.Ink : UiKit.White;
tabSoldier.GetComponent<Image>().color = soldiers ? UiKit.Red : UiKit.Paper;
UiKit.LabelOf(tabSoldier).color = soldiers ? UiKit.White : UiKit.Ink;
UiKit.Clear(list);
foreach (var h in S.Owned(soldiers))
{
var id = h.id;
var card = HeroCards.Card(list, S, h, work.Contains(id), true, null);
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);
}
}
}
}