diff --git a/Assets/Editor/EditorRender.Systems.cs b/Assets/Editor/EditorRender.Systems.cs index 468096c..85468f2 100644 --- a/Assets/Editor/EditorRender.Systems.cs +++ b/Assets/Editor/EditorRender.Systems.cs @@ -154,14 +154,26 @@ namespace VS.EditorTools var g = app.G; var us = g.BC.units; int slots0 = g.S.battleSlots; g.S.battleSlots = 3; app.SwitchTab(2); UnitCheck(app, "units_deck"); Shot(app, "units_deck"); + app.ScrollUnits(0.5f); Shot(app, "units_locked"); app.ScrollUnits(1f); app.OpenUnitDetail(us[0].id); Shot(app, "unit_detail"); app.OpenUnitDetail(us[us.Count - 1].id); Shot(app, "unit_detail_last"); app.CloseUnitDetail(); - app.SelectPreset(1); g.DeckRemove(us[0].id); app.ShowUnits(); Shot(app, "units_preset2"); - app.BeginUnitSwap(us[0].id); Shot(app, "units_swap"); - app.PickDeckSlot(3); - if (g.ActiveDeck[3] != us[0].id || g.DeckSel != 1) { fails++; notes.Add("units_swap: 고른 칸에 안 들어감"); } - app.SelectPreset(0); g.S.battleSlots = slots0; + // 편성 — 「편성」 → 유닛 누름 = 덱 칸마다 화살표 → 덱 칸 누름 = 그 칸으로 교체(프리셋 2 · 빈 칸 하나) + app.SelectPreset(1); g.DeckRemove(us[0].id); + app.ToggleUnitEdit(); EditCheck(app, "units_edit", false); Shot(app, "units_edit"); + app.TapUnitCard(us[0].id); EditCheck(app, "units_edit_pick", true); Shot(app, "units_edit_pick"); + app.PickDeckSlot(3); EditCheck(app, "units_edit_put", false); + if (g.ActiveDeck[3] != us[0].id || g.DeckSel != 1) { fails++; notes.Add("units_edit: 고른 칸에 안 들어감"); } + app.ToggleUnitEdit(); app.SelectPreset(0); g.S.battleSlots = slots0; + } + + /// 편성 중 — 유닛을 고르면 덱 칸마다 화살표 · 넣고 나면 화살표 없음 · 편성 버튼 = 「완료」 + static void EditCheck(GameApp app, string name, bool arrows) + { + var content = app.UnitRoot.Find("List/Viewport/Content"); int n = 0; + foreach (Transform ch in content.Find("DeckGrid")) if (ch.gameObject.activeSelf && ch.Find("Arrow") != null) n++; + var label = content.Find("CollHead/Edit/Label")?.GetComponent(); + if (n != (arrows ? app.G.BC.slotMax : 0) || label == null || label.text != "완료") { fails++; notes.Add($"{name}: 편성 화면 이상 (화살표 {n} · 버튼 {label?.text})"); } } /// 아래 메뉴 = 연구 · 전투 · 유닛 · 유닛 화면 = 덱 칸 slotMax 개 · 목록 = 유닛 수 · 프리셋 버튼 = deckPresets 개 @@ -173,8 +185,12 @@ namespace VS.EditorTools foreach (Transform ch in content.Find("DeckHead")) if (ch.name.StartsWith("P")) presets++; string tabs = ""; if (bar != null) foreach (Transform ch in bar) { var t = ch.Find("Label")?.GetComponent(); if (t != null) tabs += t.text + "|"; } - bool ok = root.gameObject.activeSelf && bar != null && bar.gameObject.activeSelf && tabs == "연구|전투|유닛|" && deck == g.BC.slotMax && coll == g.BC.units.Count && presets == g.DeckPresets; - if (!ok) { fails++; notes.Add($"{name}: 유닛 화면 이상 (메뉴 {tabs} · 덱 {deck} · 목록 {coll} · 프리셋 {presets})"); } + // 미보유 = 목록 그림 중 보유 유닛이 쓰지 않는 것 · 「편성」 버튼 = 목록 머리줄 오른쪽 + var ownedArts = new HashSet(); foreach (var u in g.BC.units) { int a = UnitArt.ArtOf(u.id); if (a > 0) ownedArts.Add(a); } + int want = 0; foreach (var r in g.BC.roster) if (!ownedArts.Contains(r.art)) want++; + int locked = content.Find("LockGrid").childCount; bool edit = content.Find("CollHead/Edit") != null; + bool ok = root.gameObject.activeSelf && bar != null && bar.gameObject.activeSelf && tabs == "연구|전투|유닛|" && deck == g.BC.slotMax && coll == g.BC.units.Count && presets == g.DeckPresets && want > 0 && locked == want && edit; + if (!ok) { fails++; notes.Add($"{name}: 유닛 화면 이상 (메뉴 {tabs} · 덱 {deck} · 보유 {coll} · 미보유 {locked}/{want} · 프리셋 {presets} · 편성 {edit})"); } } static List L(params string[] s) => new List(s); diff --git a/Assets/Editor/HeadlessRun.Battle.cs b/Assets/Editor/HeadlessRun.Battle.cs index 958d0c3..22e426d 100644 --- a/Assets/Editor/HeadlessRun.Battle.cs +++ b/Assets/Editor/HeadlessRun.Battle.cs @@ -155,6 +155,12 @@ namespace VS.EditorTools for (int i = 0; grades && i < gradeNames.Length; i++) grades &= c.grades[i].id == i + 1 && c.grades[i].name == gradeNames[i] && !string.IsNullOrEmpty(c.grades[i].color); foreach (var u in us) grades &= c.Grade(u.grade) != null; Check("B12.grades", grades && c.Grade(7).color == "rainbow"); + // B13 유닛 목록 = PD 가 고른 유닛 그림(battle.json roster) · 번호 겹침 없음 · 이름 · 그림 파일 있음 · 보유 유닛 그림도 그 안(고른 그림만 씀) + var artFiles = System.IO.Directory.GetFiles("Assets/Resources/Art/Units", "px_*.png"); + var rosterArts = new HashSet(); bool rosterOk = c.roster != null && c.roster.Count > 0 && c.roster.Count <= artFiles.Length; + if (c.roster != null) foreach (var r in c.roster) rosterOk &= rosterArts.Add(r.art) && !string.IsNullOrEmpty(r.name) && System.IO.File.Exists($"Assets/Resources/Art/Units/px_{r.art}.png") && VS.View.UnitArt.Get(r.art) != null; + int ownedArts = 0; foreach (var u in us) { int a = VS.View.UnitArt.ArtOf(u.id); if (a > 0) { ownedArts++; rosterOk &= rosterArts.Contains(a); } } + Check("B13.roster", rosterOk && ownedArts > 0, $"files={artFiles.Length} roster={c.roster?.Count} owned art={ownedArts}"); MapTests(c); CastleTests(c); diff --git a/Assets/Resources/Data/battle.json b/Assets/Resources/Data/battle.json index ee5dae4..a46e89e 100644 --- a/Assets/Resources/Data/battle.json +++ b/Assets/Resources/Data/battle.json @@ -101,6 +101,15 @@ { "id": 7, "name": "신화", "color": "rainbow" } ], "deckPresets": 3, + "rosterSource": "유닛 목록 = 우리 게임 유닛 그림(Resources/Art/Units/px_) 중 PD 가 고른 28종(2026-10-30 PD 「X표 제외 · 나머지만 사용」 + 「307 도 포함」 = 52종 제외 · 처음 「제작된 리소스 유닛을 모두 목록에 · 초기 보유 외 = 미보유(회색 딤드 + 자물쇠 + 미보유)」) · 초기 보유 = 전투 유닛 6종(units · 그림 = unit_art.json) · 나머지 = 미보유 · 이름 = 그림 모습으로 붙인 임시 이름(확정 전)", + "roster": [ + { "art": 101, "name": "금발 엘프" }, { "art": 102, "name": "은발 검객" }, { "art": 105, "name": "자이언트" }, { "art": 106, "name": "금발 소녀" }, { "art": 110, "name": "앨리스" }, + { "art": 112, "name": "그림자 도적" }, { "art": 116, "name": "숲 사냥꾼" }, { "art": 117, "name": "메이드" }, { "art": 120, "name": "비행사" }, { "art": 202, "name": "은발 수도사" }, + { "art": 206, "name": "노검사" }, { "art": 208, "name": "리본 소녀" }, { "art": 209, "name": "기계 기사" }, { "art": 212, "name": "사신" }, { "art": 218, "name": "흡혈귀" }, + { "art": 302, "name": "붉은 십자" }, { "art": 307, "name": "초록 마녀" }, { "art": 308, "name": "금발 전사" }, { "art": 314, "name": "백발 마도사" }, { "art": 319, "name": "사막 무희" }, { "art": 407, "name": "초록 요정" }, + { "art": 409, "name": "기사" }, { "art": 410, "name": "어둠 인형" }, { "art": 412, "name": "붉은 사제" }, { "art": 414, "name": "기계공" }, { "art": 416, "name": "궁수" }, + { "art": 418, "name": "금발 기사" }, { "art": 419, "name": "붉은 악마" } + ], "view": { "source": "전투 화면 비율 — 클래시 로얄 실제 전투 화면을 재서 맞춤(2026-10-29 PD 「유닛 크기를 클래시 로얄과 비슷하게 · 배경·전체 비율도 클래시 로얄 화면 비율에 맞게」). 잰 값: 맵 가로 18칸 = 화면 폭의 약 90~96% · 칸 세로 길이 ≈ 가로 × 0.81(비스듬히 내려다보는 시점) · 병사 키 ≈ 1.5칸 · 아래 카드 칸 높이 ≈ 화면 폭 × 0.34 → 전투 중에는 아래 메뉴가 없어(2026-10-29 PD) 화면 전체 1080×1920 기준 칸 가로 56 · 세로 46 px(맵 = 화면 폭의 93% · 좌우 0.65칸 = 맵 밖 장식) · 카드 칸 360 px · 유닛 그림 한 변 = 반지름 × 4칸(검사 101 px · 몸 키 약 1.6칸 · 자이언트 179 px) · 나는 유닛 = 0.8칸 위 (확정 전)", "tileX": 56, "tileY": 46, diff --git a/Assets/Resources/Data/unit_art.json b/Assets/Resources/Data/unit_art.json index 1f37487..6d656e8 100644 --- a/Assets/Resources/Data/unit_art.json +++ b/Assets/Resources/Data/unit_art.json @@ -1,10 +1,10 @@ { - "source": "전투 유닛 → 도트 그림(Resources/Art/Units/px_.png) · 그림 = PD 원화 시트 80종(101~120 · 201~220 · 301~320 · 401~420 = 시트·행·열) · 매핑 = PM 제안(2026-10-28 · PD 확정 전) · 드래곤(6)은 맞는 원화가 없어 코드 그림 유지", + "source": "전투 유닛 → 도트 그림(Resources/Art/Units/px_.png) · 그림 = PD 원화 시트 80종(101~120 · 201~220 · 301~320 · 401~420 = 시트·행·열) · 매핑 = PM 제안(2026-10-28 · PD 확정 전) · 드래곤(6)은 맞는 원화가 없어 코드 그림 유지 · 2026-10-30 PD 선별(X표 52종 제외 · 28종만 사용)로 검사 402 → 102(은발 검객) · 마법사 417 → 314(백발 마도사)(확정 전)", "units": [ - { "id": 1, "art": 402 }, + { "id": 1, "art": 102 }, { "id": 2, "art": 416 }, { "id": 3, "art": 409 }, - { "id": 4, "art": 417 }, + { "id": 4, "art": 314 }, { "id": 5, "art": 105 } ] } diff --git a/Assets/Scripts/Sim/Battle.cs b/Assets/Scripts/Sim/Battle.cs index 533f820..28f9868 100644 --- a/Assets/Scripts/Sim/Battle.cs +++ b/Assets/Scripts/Sim/Battle.cs @@ -13,6 +13,8 @@ namespace VS.Sim [Serializable] public class BUnitDef { public int id; public string name; public int cost, grade; public float hp, damage, interval, range, speed, radius; public bool flying, projectile; } /// 유닛 등급 — 일반 → 고급 → 희귀 → 에픽 → 전설 → 불멸 → 신화(색 = "#RRGGBB" · 신화 = "rainbow") [Serializable] public class BGradeDef { public int id; public string name, color; } + /// 유닛 목록 한 칸 = 유닛 그림(px_) + 이름 — 전투 유닛(units)이 쓰는 그림 = 보유 · 나머지 = 미보유 + [Serializable] public class BRosterDef { public int art; public string name; } /// regen = 마지막으로 맞은 뒤 regenDelay 초가 지나면 regenInterval 초마다 최대 체력의 regenPct % [Serializable] public class BCastleDef { public float y, xMin, xMax, radius, hp, hpGrowth, atk, atkGrowth, summonGrowth, enrageHpRatio, enrageIntervalMul, regenDelay, regenPct, regenInterval; } /// 성 투사체(normal·splash·multi·slow·laser) · 성 스킬(fireball·lightning) — 피해 = 성 공격력 × dmgPct% · 사거리 = 성 가장자리부터 @@ -45,7 +47,7 @@ namespace VS.Sim public List shots, skills; public List summons; public List stageLoadouts; public float[] testRangeMuls; public BViewDef view; - public List grades; public int deckPresets; + public List grades; public int deckPresets; public List roster; public BUnitDef UnitDef(int id) => units?.Find(u => u.id == id); public BGradeDef Grade(int id) => grades?.Find(g => g.id == id); diff --git a/Assets/Scripts/View/GameApp.Battle.cs b/Assets/Scripts/View/GameApp.Battle.cs index 2b1a87e..520683c 100644 --- a/Assets/Scripts/View/GameApp.Battle.cs +++ b/Assets/Scripts/View/GameApp.Battle.cs @@ -75,7 +75,7 @@ namespace VS.View if (UnitRoot != null) UnitRoot.gameObject.SetActive(t == 2); for (int i = 0; i < tabs.Count; i++) tabs[i].bg.color = Skin.On ? (i == t ? Pal.NavyLight : new Color(0, 0, 0, 0)) : (i == t ? Pal.Pink : (Color)new Color32(84, 64, 120, 255)); if (t == 1 && !fighting) ShowStage(G.StageFocus); - if (t == 2) ShowUnits(); + if (t == 2) OpenUnits(); UpdateTabBar(); CloseAllPopups(); } diff --git a/Assets/Scripts/View/GameApp.Units.cs b/Assets/Scripts/View/GameApp.Units.cs index 9f8442a..a181e81 100644 --- a/Assets/Scripts/View/GameApp.Units.cs +++ b/Assets/Scripts/View/GameApp.Units.cs @@ -2,24 +2,32 @@ using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Events; +using UnityEngine.EventSystems; using UnityEngine.UI; using VS.Sim; -// 유닛 메뉴(2026-10-29 PD) — 클래시 로얄 「전투 덱」처럼 위 = 덱(프리셋 1·2·3 · 6칸) · 아래 = 유닛 목록(등급 색 카드 · 참고 = 수집 카드 화면) -// 카드를 누르면 유닛 상세(등급 · 이름 · 그림 · 능력치 · 특징 · 덱에 넣기/빼기) · 덱에 빈 칸이 없으면 바꿀 칸을 고른다 +// 유닛 메뉴(2026-10-29~30 PD) — 클래시 로얄 「전투 덱」처럼 위 = 덱(프리셋 1·2·3 · 6칸) · 아래 = 유닛 목록(등급 색 카드 · 참고 = 수집 카드 화면) +// 목록 = 유닛 그림 80종 전부 · 보유(전투 유닛) 먼저 · 나머지 = 미보유(회색 딤드 + 자물쇠 + 「미보유」) +// 목록 머리줄 오른쪽 끝 「편성」 = 편성 상태: 유닛을 누르거나 끌면 덱 칸에 화살표 → 덱 칸을 누르거나 그 칸에 놓으면 교체 · 편성 상태가 아니면 카드 = 유닛 상세 // 전투에는 열린 칸(전투 「강화」의 칸 열기)만 나감 · 등급 = battle.json grades(일반 → 고급 → 희귀 → 에픽 → 전설 → 불멸 → 신화 · 별 수 = 등급) namespace VS.View { public partial class GameApp { public RectTransform UnitRoot; - RectTransform unitDeckGrid, unitCollGrid, unitDetail, unitSwapBar; - Text unitDeckInfo, unitCollTitle, unitSwapText; + RectTransform unitDeckGrid, unitCollGrid, unitLockGrid, unitDetail, unitSwapBar, unitDragGhost; + ScrollRect unitScroll; + Text unitDeckInfo, unitCollTitle, unitSwapText, unitEditLabel; + Button unitEditBtn; readonly List