Project_WL/Assets/WL/Equip/WLEquipGui.Compose.cs

320 lines
20 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLEquipGui.Compose.cs — 합성 화면(레퍼런스 2 · 「대장간」 대신 「합성」) · FR ComposePopup 흐름 이식 (2026-10-12)
//
// FR 원본(ComposePopup.cs): 그리드에서 본 아이템을 누르면 합성 슬롯으로 올라가고(RequiredItemCount 만큼 재료 칸이 열림) →
// 같은 부위·등급의 재료만 누를 수 있고(나머지는 잠김) → 재료가 다 차면 「합성」 버튼 → 결과 = 상위 등급(레벨 유지) · 「전부 합성」 = 가능한 조합 전부.
// WL 판: 위 = 결과 미리보기(큰 칸) ← 초록 화살표 ← 본 아이템 + 재료 칸 N · 오른쪽 = 합성 장인 초상(Layer Lab 데모 캐릭터 임시) ·
// 말풍선이 다음 할 일을 안내 · 하단 [필터] [전부 합성] [합성] · 그리드(본 아이템 = 글로우 · 재료 후보 = 체크 · 그 외 = 어둡게+자물쇠 · 장착 중 = 「장착됨」).
// 본 아이템을 고르면 PlanCompose 가 재료를 자동으로 채워 주고, 재료 칸을 눌러 빼거나 다른 후보를 눌러 바꿀 수 있다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace WL.Equip
{
public partial class WLEquipGui
{
RectTransform _cView, _cGridContent;
ScrollRect _cScroll;
TextMeshProUGUI _cBubble, _cInfo, _cResultLabel, _cBaseLabel, _cMessage, _cFilterLabel;
Cell _cResult, _cBase;
readonly List<Cell> _cMats = new List<Cell>();
readonly List<Cell> _cCells = new List<Cell>();
Button _cFilterBtn, _cAllBtn, _cDoBtn, _cBackBtn;
Image _cArrow;
int _cTab;
int _cBaseId = -1;
readonly List<int> _cMatIds = new List<int>();
public static int Composes;
void BuildCompose()
{
_cView = NewRT(_root, "Compose"); Stretch(_cView);
if (H.lowerPanelTop > 0f) AddBand(_cView, "LowerPanel", C.gridTop - H.gridPad, 0f, "lowerPanel"); // 캐주얼 톤 — 그리드 뒤 남색 패널
// 말풍선
var bubble = NewImage(_cView, "Bubble", Spr("bubble"), Spr("bubble") != null ? Col("bubbleBg", new Color(0.93f, 0.91f, 0.95f, 1f)) : new Color(0.93f, 0.91f, 0.95f, 1f));
Place(bubble.rectTransform, new Vector2(0.5f, 1f), new Vector2(0f, -C.bubbleTop), new Vector2(C.bubbleW, C.bubbleH), new Vector2(0.5f, 1f));
_cBubble = NewText(bubble.rectTransform, "Text", C.bubbleFontPx, TextAlignmentOptions.Midline, Col("bubbleText", new Color(0.2f, 0.16f, 0.12f, 1f)));
_cBubble.fontStyle = FontStyles.Bold; _cBubble.enableWordWrapping = true; Stretch(_cBubble.rectTransform, 30f, 10f, 30f, 10f);
// 무대 — 결과(큰 칸) ← 화살표 ← 본 아이템 + 재료 칸 · 오른쪽 초상
var stage = NewRT(_cView, "Stage");
stage.anchorMin = new Vector2(0f, 1f); stage.anchorMax = new Vector2(1f, 1f); stage.pivot = new Vector2(0.5f, 1f);
stage.anchoredPosition = new Vector2(0f, -C.stageTop); stage.sizeDelta = new Vector2(0f, C.stageH);
// 세로: 결과(큰 칸) + 라벨 → 화살표 → [본 장비] [재료]… 한 줄 + 라벨 → 안내 글(무대 바닥)
var center = new Vector2(0.5f, 0.5f);
float cx = C.resultX + C.resultSize * 0.5f;
float y = -(C.resultY + C.resultSize * 0.5f);
_cResult = NewCell(stage, "Result", C.resultSize, L.cellIconInset, C.slotFontPx + 4f, false, true, null);
Place(_cResult.root, new Vector2(0f, 1f), new Vector2(cx, y), new Vector2(C.resultSize, C.resultSize), center);
_cResultLabel = NewText(stage, "ResultLabel", C.slotFontPx, TextAlignmentOptions.Top, Col("textDim", Color.white));
Place(_cResultLabel.rectTransform, new Vector2(0f, 1f), new Vector2(cx, y - C.resultSize * 0.5f - 2f), new Vector2(C.resultSize + 160f, C.slotFontPx + 8f), new Vector2(0.5f, 1f));
y -= C.resultSize * 0.5f + C.slotFontPx + 8f + C.arrowGap + C.arrowSize * 0.5f;
_cArrow = NewImage(stage, "Arrow", Spr("upArrow"), Col("upBadge", new Color(0.35f, 0.85f, 0.3f, 1f)), Image.Type.Simple); _cArrow.preserveAspect = true;
Place(_cArrow.rectTransform, new Vector2(0f, 1f), new Vector2(cx, y), new Vector2(C.arrowSize, C.arrowSize), center);
y -= C.arrowSize * 0.5f + C.arrowGap + C.baseSize * 0.5f;
float bx = C.resultX + C.baseSize * 0.5f;
_cBase = NewCell(stage, "Base", C.baseSize, L.cellIconInset, C.slotFontPx, false, true, () => { _cBaseId = -1; _cMatIds.Clear(); RefreshCompose(); });
Place(_cBase.root, new Vector2(0f, 1f), new Vector2(bx, y), new Vector2(C.baseSize, C.baseSize), center);
_cBaseLabel = NewText(stage, "BaseLabel", C.slotFontPx, TextAlignmentOptions.TopLeft, Col("textDim", Color.white));
Place(_cBaseLabel.rectTransform, new Vector2(0f, 1f), new Vector2(C.resultX, y - C.baseSize * 0.5f - 2f), new Vector2(C.infoW, C.slotFontPx + 8f), new Vector2(0f, 1f));
for (int i = 0; i < 3; i++)
{
int mi = i;
var m = NewCell(stage, "Mat" + i, C.matSize, L.cellIconInset, C.slotFontPx, false, true, () => OnMatSlotClick(mi));
_cMats.Add(m);
}
_cMatRowY = y;
LayoutMatSlots(2);
var portrait = NewImage(stage, "Portrait", WLEquipGuiAssets.Get(C.portrait), Color.white, Image.Type.Simple); portrait.preserveAspect = true;
Place(portrait.rectTransform, new Vector2(1f, 1f), new Vector2(C.portraitX, -C.portraitY), new Vector2(C.portraitW, C.portraitH), new Vector2(1f, 1f));
if (portrait.sprite == null) portrait.color = new Color(1f, 1f, 1f, 0f);
_cInfo = NewText(stage, "Info", C.infoFontPx, TextAlignmentOptions.BottomLeft, TextCol()); _cInfo.enableWordWrapping = true;
Place(_cInfo.rectTransform, new Vector2(0f, 0f), new Vector2(C.resultX, 6f), new Vector2(C.infoW, C.infoH), new Vector2(0f, 0f));
// 버튼 줄
var row = NewRT(_cView, "Buttons");
row.anchorMin = new Vector2(0f, 1f); row.anchorMax = new Vector2(1f, 1f); row.pivot = new Vector2(0.5f, 1f);
row.anchoredPosition = new Vector2(0f, -C.btnRowY); row.sizeDelta = new Vector2(0f, C.btnH);
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
hl.padding = new RectOffset((int)H.edge, (int)H.edge, 0, 0); hl.spacing = C.btnGap;
hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = false; hl.childForceExpandHeight = true;
_cFilterBtn = NewButton(row, "필터: 전체", Spr("btnBlue"), Color.white, C.filterBtnW, C.btnH, CycleComposeFilter, C.btnFontPx * 0.8f, TextCol());
_cFilterLabel = _cFilterBtn.GetComponentInChildren<TextMeshProUGUI>();
var spacer = NewRT(row, "Spacer"); Flex(spacer, 1f);
_cAllBtn = NewButton(row, "전부 합성", Spr("btnSecondary"), Color.white, C.allBtnW, C.btnH, () => { Act(() => WLInventory.ComposeAll() > 0); _cBaseId = -1; _cMatIds.Clear(); RefreshCompose(); }, C.btnFontPx * 0.85f, TextCol());
_cDoBtn = NewButton(row, "합성", Spr("btnPrimary"), Color.white, C.composeBtnW, C.btnH, DoCompose, C.btnFontPx, DarkCol());
// 그리드 + 뒤로 가기 + 메시지
_cScroll = BuildGrid(_cView, "Grid", C.gridTop, C.gridBottom, out _cGridContent);
_cBackBtn = NewBackButton(_cView, ShowHub);
_cMessage = NewText(_cView, "Message", H.messageFontPx, TextAlignmentOptions.MidlineLeft, Col("textDim", Color.white));
_cMessage.enableWordWrapping = true; _cMessage.overflowMode = TextOverflowModes.Ellipsis;
var mrt = _cMessage.rectTransform; mrt.anchorMin = new Vector2(0f, 0f); mrt.anchorMax = new Vector2(1f, 0f); mrt.pivot = new Vector2(0.5f, 0f);
mrt.offsetMin = new Vector2(BackRight() + 14f, H.backY); mrt.offsetMax = new Vector2(-H.edge, H.backY + H.backBtn);
_cView.gameObject.SetActive(false);
}
float _cMatRowY;
/// <summary>재료 칸 n 개를 본 장비 오른쪽에 한 줄로(세로 중심 = 본 장비 중심).</summary>
void LayoutMatSlots(int n)
{
n = Mathf.Clamp(n, 1, _cMats.Count);
float x0 = C.resultX + C.baseSize + C.matGap + 20f + C.matSize * 0.5f;
for (int i = 0; i < _cMats.Count; i++)
{
bool on = i < n;
_cMats[i].root.gameObject.SetActive(on);
if (!on) continue;
float x = x0 + i * (C.matSize + C.matGap);
Place(_cMats[i].root, new Vector2(0f, 1f), new Vector2(x, _cMatRowY), new Vector2(C.matSize, C.matSize), new Vector2(0.5f, 0.5f));
}
}
/// <summary>허브 「합성」 버튼 · 상세 팝업 「합성」 → 합성 화면. baseId ≥ 0 이면 그 장비를 본 아이템으로 올려 둔다.</summary>
public void OpenCompose(int baseId)
{
_popup.SetActive(false);
_hub.gameObject.SetActive(false);
_cView.gameObject.SetActive(true);
_cBaseId = -1; _cMatIds.Clear();
if (baseId >= 0) SetComposeBase(baseId);
RefreshCompose();
}
void SetComposeBase(int id)
{
var it = WLInventory.FindItem(id);
ItemComposeJson c; ItemJson r;
_cMatIds.Clear();
if (it == null || !WLInventory.ComposeInfo(it, out c, out r)) { _cBaseId = -1; _cMessage.text = "합성 불가 — 최고 등급이거나 합성표에 없는 장비"; return; }
_cBaseId = id;
_cTab = (int)it.itemJson.EquipmentType;
var plan = WLInventory.PlanCompose(it);
if (plan != null) for (int i = 0; i < plan.materials.Count; i++) _cMatIds.Add(plan.materials[i].rawItem._id);
_cMessage.text = "";
}
int CurrentMatCount()
{
var mats = new List<Item>();
for (int i = 0; i < _cMatIds.Count; i++) { var m = WLInventory.FindItem(_cMatIds[i]); if (m != null) mats.Add(m); }
return WLInventory.MaterialCount(mats);
}
void OnComposeCellClick(int idx)
{
if (idx < 0 || idx >= _cCells.Count) return;
int id = _cCells[idx].id;
if (id < 0) return;
if (_cBaseId < 0) { SetComposeBase(id); RefreshCompose(); return; }
if (id == _cBaseId) { _cBaseId = -1; _cMatIds.Clear(); RefreshCompose(); return; }
var src = WLInventory.FindItem(_cBaseId); var it = WLInventory.FindItem(id);
ItemComposeJson c; ItemJson r;
if (src == null || it == null || !WLInventory.ComposeInfo(src, out c, out r)) { _cBaseId = -1; _cMatIds.Clear(); RefreshCompose(); return; }
if (WLInventory.IsEquipped(id) || !WLInventory.IsMaterialItem(it, src, c)) { _cMessage.text = "재료로 쓸 수 없는 장비입니다 — 같은 부위·같은 등급만"; RefreshCompose(); return; }
if (_cMatIds.Contains(id)) _cMatIds.Remove(id);
else
{
if (CurrentMatCount() >= c.RequiredItemCount && _cMatIds.Count > 0) _cMatIds.RemoveAt(_cMatIds.Count - 1); // 꽉 찼으면 마지막 재료를 바꾼다
_cMatIds.Add(id);
}
RefreshCompose();
}
void OnMatSlotClick(int i)
{
if (i < _cMatIds.Count) { _cMatIds.RemoveAt(i); RefreshCompose(); }
}
void CycleComposeFilter()
{
var tabs = _g.tabs;
if (tabs == null || tabs.Count == 0) return;
int pos = 0;
for (int i = 0; i < tabs.Count; i++) if (tabs[i].id == _cTab) { pos = i; break; }
_cTab = tabs[(pos + 1) % tabs.Count].id;
RefreshCompose();
}
void DoCompose()
{
if (_cBaseId < 0) { _cMessage.text = "합성할 장비를 먼저 고르세요"; return; }
int keep = _cBaseId;
var mats = new List<int>(_cMatIds);
bool ok = false;
try { ok = WLInventory.ComposeWith(keep, mats); }
catch (System.Exception ex) { Debug.LogError("[WL-846] 합성 예외 — " + ex); }
if (ok) Composes++;
// 결과 장비를 그대로 본 아이템으로 두면 바로 다음 등급 재료를 이어서 고를 수 있다(없으면 비운다)
_cMatIds.Clear();
var src = WLInventory.FindItem(keep);
ItemComposeJson c; ItemJson r;
if (ok && src != null && WLInventory.ComposeInfo(src, out c, out r)) SetComposeBase(keep); else _cBaseId = ok ? -1 : _cBaseId;
string msg = WLInventory.LastMessage;
Refresh(); // Notify 가 Changed 로 이미 Refresh 를 불렀을 수 있다 — 한 번 더 그려도 싸다
_cMessage.text = msg;
}
void RefreshCompose()
{
if (_cView == null || !_cView.gameObject.activeSelf) return;
var src = _cBaseId >= 0 ? WLInventory.FindItem(_cBaseId) : null;
if (_cBaseId >= 0 && src == null) { _cBaseId = -1; _cMatIds.Clear(); }
ItemComposeJson c = null; ItemJson r = null;
bool ok = src != null && WLInventory.ComposeInfo(src, out c, out r);
if (src != null && !ok) { _cBaseId = -1; src = null; _cMatIds.Clear(); }
// 유효하지 않게 된 재료 정리
for (int i = _cMatIds.Count - 1; i >= 0; i--)
{
var m = WLInventory.FindItem(_cMatIds[i]);
if (m == null || src == null || m == src || WLInventory.IsEquipped(m.rawItem._id) || !WLInventory.IsMaterialItem(m, src, c)) _cMatIds.RemoveAt(i);
}
int need = ok ? c.RequiredItemCount : 0;
int have = ok ? CurrentMatCount() : 0;
bool ready = ok && have >= need;
_cBubble.text = src == null ? "합성할 장비를 선택하세요." : (ready ? "준비 완료! 합성 버튼을 누르세요." : "같은 부위 · 같은 등급의 재료를 " + (need - have) + "개 더 고르세요.");
// 결과 · 본 아이템 · 재료 칸
if (ok) { BindCellJson(_cResult, r, src.rawItem.level > r.ItemMaxLv ? r.ItemMaxLv : src.rawItem.level); _cResultLabel.text = WLEquipGrades.Label(r, true); }
else { BindCellJson(_cResult, null, 1); if (_cResult.empty != null) { SetOn(_cResult.empty, true); _cResult.empty.text = "?"; } _cResultLabel.text = "합성 결과"; }
BindCell(_cBase, src, false);
if (src == null) { if (_cBase.empty != null) { SetOn(_cBase.empty, true); _cBase.empty.text = "본 장비"; } _cBaseLabel.text = ""; }
else _cBaseLabel.text = src.itemJson.Item_name + " " + WLEquipGrades.Label(src.itemJson, true) + " Lv" + src.rawItem.level + " + 재료 " + have + "/" + need;
LayoutMatSlots(Mathf.Max(1, need > 0 ? need : 2));
var matItems = new List<Item>();
for (int i = 0; i < _cMatIds.Count; i++) { var m = WLInventory.FindItem(_cMatIds[i]); if (m != null) matItems.Add(m); }
for (int i = 0; i < _cMats.Count; i++)
{
if (!_cMats[i].root.gameObject.activeSelf) continue;
var m = i < matItems.Count ? matItems[i] : null;
BindCell(_cMats[i], m, false);
if (m == null && _cMats[i].empty != null) { SetOn(_cMats[i].empty, true); _cMats[i].empty.text = src == null ? "" : "재료"; }
if (m != null) SetOn(_cMats[i].check, true);
}
_cArrow.color = ready ? Col("upBadge", Color.green) : new Color(0.6f, 0.6f, 0.6f, 0.6f);
_cInfo.text = src == null ? "본 장비 1 + 같은 등급 재료 → 한 단계 위 등급 (레벨 유지 · 재료에 쓴 강화석은 돌려받음)"
: "결과: " + (ok ? WLEquipGrades.Label(r, true) : "-") + " · Lv " + src.rawItem.level + " 유지 · 재료 칸을 누르면 빠지고, 체크된 장비를 누르면 바뀝니다";
SetEnabled(_cDoBtn, ready, "btnPrimary");
SetEnabled(_cAllBtn, AnyComposable(), "btnSecondary");
string tabName = "전체";
var tabs = _g.tabs; if (tabs != null) for (int i = 0; i < tabs.Count; i++) if (tabs[i].id == _cTab) tabName = tabs[i].label;
if (_cFilterLabel != null) _cFilterLabel.text = "필터: " + tabName;
// 그리드 — 본 아이템을 골랐으면 그 부위만 · 재료 후보 먼저
var items = WLInventory.SortedItems();
var shown = new List<Item>();
var later = new List<Item>();
int tab = src != null ? (int)src.itemJson.EquipmentType : _cTab;
for (int i = 0; i < items.Count; i++)
{
var it = items[i];
if (!it.itemJson.IsEquipment) continue;
if (it == src) continue; // 본 장비는 아래에서 맨 앞에 한 번만 넣는다(2026-10-12 캡처: 두 번 나오던 결함)
if (tab != 0 && (int)it.itemJson.EquipmentType != tab) continue;
if (src != null && it != src && !WLInventory.IsEquipped(it.rawItem._id) && WLInventory.IsMaterialItem(it, src, c)) shown.Add(it);
else later.Add(it);
}
if (src != null) { shown.Insert(0, src); }
shown.AddRange(later);
int n = 0;
for (int i = 0; i < shown.Count; i++)
{
var it = shown[i];
if (n >= _cCells.Count)
{
int idx = n;
_cCells.Add(NewCell(_cGridContent, "CCell" + n, H.gridCell, H.cellIconInset, H.cellFontPx, true, false, () => OnComposeCellClick(idx)));
}
var cell = _cCells[n];
cell.root.gameObject.SetActive(true);
BindCell(cell, it, true);
if (src != null)
{
bool isBase = it == src;
bool cand = !isBase && !WLInventory.IsEquipped(it.rawItem._id) && WLInventory.IsMaterialItem(it, src, c);
SetOn(cell.glow, isBase);
SetOn(cell.check, cand && _cMatIds.Contains(it.rawItem._id));
SetOn(cell.dim, !isBase && !cand);
}
n++;
if (n >= _cfg.maxListRows) break;
}
for (int i = n; i < _cCells.Count; i++) _cCells[i].root.gameObject.SetActive(false);
}
bool AnyComposable()
{
foreach (var it in WLInventory.SortedItems())
{
if (!it.itemJson.IsEquipment) continue;
if (WLInventory.PlanCompose(it) != null) return true;
}
return false;
}
// 프로브용
public void OpenComposeForProbe(int baseId) { OpenCompose(baseId); }
public bool ComposeForProbe() { int before = Composes; DoCompose(); return Composes > before; }
public bool ComposeVisibleForProbe { get { return _cView != null && _cView.gameObject.activeSelf; } }
public string ComposeStateForProbe
{
get
{
var src = _cBaseId >= 0 ? WLInventory.FindItem(_cBaseId) : null;
int n = 0; for (int i = 0; i < _cCells.Count; i++) if (_cCells[i].root.gameObject.activeSelf) n++;
return "base=" + _cBaseId + (src != null ? "(" + src.itemJson.Item_name + " " + WLEquipGrades.Label(src.itemJson, false) + " Lv" + src.rawItem.level + ")" : "") + " mats=" + _cMatIds.Count + " have=" + (src != null ? CurrentMatCount() : 0) + " cells=" + n + " ready=" + (_cDoBtn != null && _cDoBtn.interactable) + " bubble=" + (_cBubble != null ? _cBubble.text : "");
}
}
}
}